diff --git a/.axiom/semantic_index/index.json b/.axiom/semantic_index/index.json index 22a81965..d8ec59b2 100644 --- a/.axiom/semantic_index/index.json +++ b/.axiom/semantic_index/index.json @@ -1,16 +1,16 @@ { - "generated_at": "2026-05-12T16:38:31.173905832Z", + "generated_at": "2026-05-14T08:38:38.419481874Z", "root_path": "/home/busya/dev/ss-tools", - "contract_count": 3052, - "edge_count": 2543, - "file_count": 583, + "contract_count": 2516, + "edge_count": 1984, + "file_count": 548, "contracts": [ { "contract_id": "DeleteRunningTasksUtil", "contract_type": "Module", "file_path": "backend/delete_running_tasks.py", "start_line": 2, - "end_line": 46, + "end_line": 48, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57,14 +57,14 @@ } ], "anchor_syntax": "def", - "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\nfrom src.core.database import TasksSessionLocal\nfrom src.models.task import TaskRecord\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": "# [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" }, { "contract_id": "delete_running_tasks", "contract_type": "Function", "file_path": "backend/delete_running_tasks.py", - "start_line": 13, - "end_line": 42, + "start_line": 15, + "end_line": 44, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -103,7 +103,7 @@ } ], "anchor_syntax": "def", - "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": "# [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" }, { "contract_id": "SrcRoot", @@ -128,8 +128,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SrcRoot:Module]\n# @PURPOSE: Canonical backend package root for application, scripts, and tests.\n# [/DEF:SrcRoot:Module]\n" + "anchor_syntax": "region", + "body": "# #region SrcRoot [TYPE Module] [SEMANTICS root, package]\n# @BRIEF Canonical backend package root for application, scripts, and tests.\n# #endregion SrcRoot\n" }, { "contract_id": "src.api", @@ -172,29 +172,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:src.api:Package]\n# @PURPOSE: Backend API package root.\n# [/DEF:src.api:Package]\n" + "anchor_syntax": "region", + "body": "# #region src.api [TYPE Package] [SEMANTICS api, package, init]\n# @BRIEF Backend API package root.\n# #endregion src.api\n" }, { "contract_id": "AuthApi", "contract_type": "Module", "file_path": "backend/src/api/auth.py", "start_line": 1, - "end_line": 156, + "end_line": 141, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "All auth endpoints must return consistent error codes.", "LAYER": "API", - "PURPOSE": "Authentication API endpoints.", - "SEMANTICS": [ - "api", - "auth", - "routes", - "login", - "logout" - ] + "PURPOSE": "Authentication API endpoints." }, "relations": [ { @@ -233,25 +226,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthApi:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: api, auth, routes, login, logout\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy.orm import Session\nfrom ..core.database import get_auth_db\nfrom ..services.auth_service import AuthService\nfrom ..schemas.auth import Token, User as UserSchema\nfrom ..dependencies import get_current_user\nfrom ..core.auth.oauth import oauth, is_adfs_configured\nfrom ..core.auth.logger import log_security_event\nfrom ..core.logger import belief_scope\nimport starlette.requests\n# [/SECTION]\n\n# [DEF:router:Variable]\n# @RELATION: DEPENDS_ON -> [fastapi.APIRouter]\n# @COMPLEXITY: 1\n# @PURPOSE: APIRouter instance for authentication routes.\nrouter = APIRouter(prefix=\"/api/auth\", tags=[\"auth\"])\n# [/DEF:router:Variable]\n\n\n# [DEF:login_for_access_token:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @THROW: HTTPException 401 if authentication fails.\n# @PARAM: form_data (OAuth2PasswordRequestForm) - Login credentials.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: Token - The generated JWT token.\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# [/DEF:login_for_access_token:Function]\n\n\n# [DEF:read_users_me:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Retrieves the profile of the currently authenticated user.\n# @PRE: Valid JWT token provided.\n# @POST: Returns the current user's data.\n# @PARAM: current_user (UserSchema) - The user extracted from the token.\n# @RETURN: UserSchema - The current user profile.\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# [/DEF:read_users_me:Function]\n\n\n# [DEF:logout:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Logs out the current user (placeholder for session revocation).\n# @PRE: Valid JWT token provided.\n# @POST: Returns success message.\n# @PARAM: current_user (UserSchema) - The user extracted from the token.\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# [/DEF:logout:Function]\n\n\n# [DEF:login_adfs:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:login_adfs:Function]\n\n\n# [DEF:auth_callback_adfs:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:auth_callback_adfs:Function]\n\n# [/DEF:AuthApi:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "login_for_access_token", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 35, - "end_line": 66, + "start_line": 32, + "end_line": 58, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "Returns a Token object on success.", "PRE": "form_data contains username and password.", - "PURPOSE": "Authenticates a user and returns a JWT access token.", - "RETURN": "Token - The generated JWT token.", - "THROW": "HTTPException 401 if authentication fails." + "PURPOSE": "Authenticates a user and returns a JWT access token." }, "relations": [ { @@ -268,12 +258,6 @@ } ], "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", @@ -291,38 +275,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:login_for_access_token:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @THROW: HTTPException 401 if authentication fails.\n# @PARAM: form_data (OAuth2PasswordRequestForm) - Login credentials.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: Token - The generated JWT token.\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# [/DEF:login_for_access_token:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "read_users_me", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 69, - "end_line": 83, + "start_line": 61, + "end_line": 72, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "current_user (UserSchema) - The user extracted from the token.", + "COMPLEXITY": 3, "POST": "Returns the current user's data.", "PRE": "Valid JWT token provided.", - "PURPOSE": "Retrieves the profile of the currently authenticated user.", - "RETURN": "UserSchema - The current user profile." + "PURPOSE": "Retrieves the profile of the currently authenticated user." }, "relations": [ { @@ -334,11 +304,50 @@ ], "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 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", + "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" + }, + { + "contract_id": "logout", + "contract_type": "Function", + "file_path": "backend/src/api/auth.py", + "start_line": 75, + "end_line": 89, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Returns success message.", + "PRE": "Valid JWT token provided.", + "PURPOSE": "Logs out the current user (placeholder for session revocation)." + }, + "relations": [ + { + "source_id": "logout", + "relation_type": "DEPENDS_ON", + "target_id": "get_current_user", + "target_ref": "[get_current_user]" + } + ], + "schema_warnings": [ { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -356,27 +365,21 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:read_users_me:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Retrieves the profile of the currently authenticated user.\n# @PRE: Valid JWT token provided.\n# @POST: Returns the current user's data.\n# @PARAM: current_user (UserSchema) - The user extracted from the token.\n# @RETURN: UserSchema - The current user profile.\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# [/DEF:read_users_me:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "login_adfs", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 105, - "end_line": 122, + "start_line": 92, + "end_line": 108, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Redirects the user to ADFS.", "PURPOSE": "Initiates the ADFS OIDC login flow." }, @@ -417,19 +420,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:login_adfs:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:login_adfs:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "auth_callback_adfs", "contract_type": "Function", "file_path": "backend/src/api/auth.py", - "start_line": 125, - "end_line": 154, + "start_line": 111, + "end_line": 139, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Provisions user JIT and returns session token.", "PURPOSE": "Handles the callback from ADFS after successful authentication." }, @@ -464,38 +467,33 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:auth_callback_adfs:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:auth_callback_adfs:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ApiRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/__init__.py", "start_line": 1, - "end_line": 60, + "end_line": 56, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Only names listed in __all__ are importable via __getattr__.", "LAYER": "API", - "PURPOSE": "Provide lazy route module loading to avoid heavyweight imports during tests.", - "SEMANTICS": [ - "routes", - "lazy-import", - "module-registry" - ] + "PURPOSE": "Provide lazy route module loading to avoid heavyweight imports during tests." }, "relations": [ { "source_id": "ApiRoutesModule", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "ApiRoutesGetAttr", "target_ref": "[ApiRoutesGetAttr]" }, { "source_id": "ApiRoutesModule", - "relation_type": "[BINDS_TO]", + "relation_type": "BINDS_TO", "target_id": "Route_Group_Contracts", "target_ref": "[Route_Group_Contracts]" } @@ -509,57 +507,21 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [BINDS_TO] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[BINDS_TO]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ApiRoutesModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: routes, lazy-import, module-registry\n# @PURPOSE: 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# [DEF:Route_Group_Contracts:Block]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 \"plugins\",\n \"tasks\",\n \"settings\",\n \"connections\",\n \"environments\",\n \"mappings\",\n \"migration\",\n \"git\",\n \"storage\",\n \"admin\",\n \"reports\",\n \"assistant\",\n \"clean_release\",\n \"clean_release_v2\",\n \"profile\",\n \"dataset_review\",\n \"llm\",\n \"dashboards\",\n \"datasets\",\n \"health\",\n \"translate\",\n]\n# [/DEF:Route_Group_Contracts:Block]\n\n\n# [DEF:ApiRoutesGetAttr:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:ApiRoutesGetAttr:Function]\n# [/DEF:ApiRoutesModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "Route_Group_Contracts", "contract_type": "Block", "file_path": "backend/src/api/routes/__init__.py", - "start_line": 10, - "end_line": 42, + "start_line": 8, + "end_line": 39, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Declare the canonical route-module registry used by lazy imports and app router inclusion." }, "relations": [ @@ -601,19 +563,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:Route_Group_Contracts:Block]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 \"plugins\",\n \"tasks\",\n \"settings\",\n \"connections\",\n \"environments\",\n \"mappings\",\n \"migration\",\n \"git\",\n \"storage\",\n \"admin\",\n \"reports\",\n \"assistant\",\n \"clean_release\",\n \"clean_release_v2\",\n \"profile\",\n \"dataset_review\",\n \"llm\",\n \"dashboards\",\n \"datasets\",\n \"health\",\n \"translate\",\n]\n# [/DEF:Route_Group_Contracts:Block]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ApiRoutesGetAttr", "contract_type": "Function", "file_path": "backend/src/api/routes/__init__.py", - "start_line": 45, - "end_line": 59, + "start_line": 42, + "end_line": 55, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Returns imported submodule or raises AttributeError.", "PRE": "name is module candidate exposed in __all__.", "PURPOSE": "Lazily import route module by attribute name." @@ -621,7 +583,7 @@ "relations": [ { "source_id": "ApiRoutesGetAttr", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ApiRoutesModule", "target_ref": "[ApiRoutesModule]" } @@ -644,126 +606,29 @@ "actual_complexity": 3, "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ApiRoutesGetAttr:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:ApiRoutesGetAttr:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { - "contract_id": "RoutesTestsConftest", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/conftest.py", - "start_line": 1, - "end_line": 45, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Shared low-fidelity test doubles for API route test modules." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:RoutesTestsConftest:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: Shared low-fidelity test doubles for API route test modules.\n\n\nclass FakeQuery:\n \"\"\"Shared chainable query stub for route tests.\n\n WARNING: filter() is predicate-blind — all ownership and permission filters are\n ignored. Tests using FakeQuery cannot verify scoped data access. This is a\n known limitation; do not use for permission-sensitive test paths without a\n spec-guarded replacement.\n \"\"\"\n\n def __init__(self, rows):\n self._rows = list(rows)\n self._seen_predicates = []\n\n def filter(self, *args, **kwargs):\n # Predicate-aware bookkeeping only; no predicate evaluation is performed.\n self._seen_predicates.append((args, kwargs))\n return self\n\n def order_by(self, *args, **kwargs):\n return self\n\n def limit(self, limit):\n self._rows = self._rows[:limit]\n return self\n\n def offset(self, offset):\n self._rows = self._rows[offset:]\n return self\n\n def first(self):\n return self._rows[0] if self._rows else None\n\n def all(self):\n return list(self._rows)\n\n def count(self):\n return len(self._rows)\n\n\n# [/DEF:RoutesTestsConftest:Module]\n" - }, - { - "contract_id": "AssistantApiTests", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 4, - "end_line": 562, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Every test clears assistant in-memory state before execution.", - "PURPOSE": "Validate assistant API endpoint logic via direct async handler invocation.", - "SEMANTICS": [ - "tests", - "assistant", - "api" - ] - }, - "relations": [ - { - "source_id": "AssistantApiTests", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantApi", - "target_ref": "[AssistantApi]" - } - ], - "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" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantApiTests:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, assistant, api\n# @PURPOSE: Validate assistant API endpoint logic via direct async handler invocation.\n# @RELATION: DEPENDS_ON -> [AssistantApi]\n# @INVARIANT: Every test clears assistant in-memory state before execution.\n\nimport asyncio\nimport uuid\nfrom datetime import datetime, timedelta\nfrom typing import Any, Dict, List, Optional, Tuple\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom fastapi import HTTPException\nfrom pydantic import BaseModel\n\nfrom src.api.routes import assistant as assistant_routes\nfrom src.schemas.auth import User\nfrom src.models.assistant import AssistantMessageRecord\nfrom src.models.dataset_review import (\n ApprovalState,\n CandidateStatus,\n DatasetReviewSession,\n ExecutionMapping,\n ImportedFilter,\n MappingMethod,\n SemanticCandidate,\n SemanticFieldEntry,\n ReadinessState,\n RecommendedAction,\n SessionPhase,\n SessionStatus,\n)\n\n\n# [DEF:_run_async:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\ndef _run_async(coro):\n return asyncio.run(coro)\n\n\n# [/DEF:_run_async:Function]\n\n\n# [DEF:_FakeTask:Class]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 1\n# @PURPOSE: Lightweight task model stub used as return value from _FakeTaskManager.create_task in assistant route tests.\n# @INVARIANT: status is a bare string not a TaskStatus enum; callers must not depend on enum semantics.\nclass _FakeTask:\n def __init__(\n self,\n id,\n status=\"SUCCESS\",\n plugin_id=\"unknown\",\n params=None,\n result=None,\n user_id=None,\n ):\n self.id = id\n self.status = status\n self.plugin_id = plugin_id\n self.params = params or {}\n self.result = result or {}\n self.user_id = user_id\n self.started_at = datetime.utcnow()\n self.finished_at = datetime.utcnow()\n\n\n# [/DEF:_FakeTask:Class]\n\n\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# [DEF:_FakeTaskManager:Class]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 2\n# @PURPOSE: In-memory task manager stub that records created tasks for route-level assertions.\n# @INVARIANT: create_task stores tasks retrievable by get_task/get_tasks without external side effects.\nclass _FakeTaskManager:\n def __init__(self):\n self.tasks = {}\n\n async def create_task(self, plugin_id, params, user_id=None):\n task_id = f\"task-{uuid.uuid4().hex[:8]}\"\n task = _FakeTask(\n task_id,\n status=\"STARTED\",\n plugin_id=plugin_id,\n params=params,\n user_id=user_id,\n )\n self.tasks[task_id] = task\n return task\n\n def get_task(self, task_id):\n return self.tasks.get(task_id)\n\n def get_tasks(self, limit=20, offset=0):\n return sorted(self.tasks.values(), key=lambda t: t.id, reverse=True)[\n offset : offset + limit\n ]\n\n def get_all_tasks(self):\n return list(self.tasks.values())\n\n\n# [/DEF:_FakeTaskManager:Class]\n\n\n# [DEF:_FakeConfigManager:Class]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 2\n# @PURPOSE: Deterministic config stub providing hardcoded dev/prod environments and minimal settings shape for assistant route tests.\n# @INVARIANT: get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access.\nclass _FakeConfigManager:\n class _Env:\n def __init__(self, id, name):\n self.id = id\n self.name = name\n\n def get_environments(self):\n return [self._Env(\"dev\", \"Development\"), self._Env(\"prod\", \"Production\")]\n\n def get_config(self):\n class _Settings:\n default_environment_id = \"dev\"\n llm = {}\n\n class _Config:\n settings = _Settings()\n environments = []\n\n return _Config()\n\n\n# [/DEF:_FakeConfigManager:Class]\n\n\n# [DEF:_admin_user:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 1\n# @PURPOSE: Build admin principal with spec=User for assistant route authorization tests.\ndef _admin_user():\n user = MagicMock(spec=User)\n user.id = \"u-admin\"\n user.username = \"admin\"\n role = MagicMock()\n role.name = \"Admin\"\n user.roles = [role]\n return user\n\n\n# [/DEF:_admin_user:Function]\n\n\n# [DEF:_limited_user:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 1\n# @PURPOSE: Build limited user principal with empty roles for assistant route denial tests.\ndef _limited_user():\n user = MagicMock(spec=User)\n user.id = \"u-limited\"\n user.username = \"limited\"\n user.roles = []\n return user\n\n\n# [/DEF:_limited_user:Function]\n\n\n# [DEF:_FakeQuery:Class]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 2\n# @PURPOSE: Chainable SQLAlchemy-like query stub returning fixed item lists for assistant message persistence paths.\n# @INVARIANT: filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated.\nclass _FakeQuery:\n def __init__(self, items):\n self.items = items\n\n def outerjoin(self, *args, **kwargs):\n return self\n\n def options(self, *args, **kwargs):\n return self\n\n def filter(self, *args, **kwargs):\n # @INVARIANT: filter() is predicate-blind; returns all records regardless of user_id scope\n return self\n\n def order_by(self, *args, **kwargs):\n return self\n\n def limit(self, n):\n self.items = self.items[:n]\n return self\n\n def offset(self, n):\n self.items = self.items[n:]\n return self\n\n def first(self):\n return self.items[0] if self.items else None\n\n def all(self):\n return self.items\n\n def count(self):\n return len(self.items)\n\n\n# [/DEF:_FakeQuery:Class]\n\n\n# [DEF:_FakeDb:Class]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 2\n# @PURPOSE: Explicit in-memory DB session double limited to assistant message persistence paths.\n# @INVARIANT: query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior.\nclass _FakeDb:\n def __init__(self):\n self.added = []\n self.dataset_sessions = {}\n\n def query(self, model):\n if model == AssistantMessageRecord:\n return _FakeQuery([])\n if model == DatasetReviewSession:\n return _FakeQuery(list(self.dataset_sessions.values()))\n return _FakeQuery([])\n\n def add(self, obj):\n self.added.append(obj)\n\n def commit(self):\n pass\n\n def rollback(self):\n pass\n\n def merge(self, obj):\n return obj\n\n def refresh(self, obj):\n pass\n\n\n# [/DEF:_FakeDb:Class]\n\n\n# [DEF:_clear_assistant_state:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\ndef _clear_assistant_state():\n assistant_routes.CONVERSATIONS.clear()\n assistant_routes.USER_ACTIVE_CONVERSATION.clear()\n assistant_routes.CONFIRMATIONS.clear()\n assistant_routes.ASSISTANT_AUDIT.clear()\n\n\n# [/DEF:_clear_assistant_state:Function]\n\n\n# [DEF:_dataset_review_session:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 1\n# @PURPOSE: Build minimal owned dataset-review session fixture for assistant scoped routing tests.\ndef _dataset_review_session():\n session = DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"u-admin\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n version=3,\n readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED,\n recommended_action=RecommendedAction.APPROVE_MAPPING,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.MAPPING_REVIEW,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n last_activity_at=datetime.utcnow(),\n )\n session.findings = []\n session.previews = []\n session.imported_filters = [\n ImportedFilter(\n filter_id=\"filter-1\",\n session_id=\"sess-1\",\n filter_name=\"email\",\n display_name=\"Email\",\n raw_value=\"john.doe@example.com\",\n raw_value_masked=False,\n normalized_value=\"john.doe@example.com\",\n source=\"manual\",\n confidence_state=\"confirmed\",\n requires_confirmation=False,\n recovery_status=\"recovered\",\n notes=None,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n )\n ]\n session.execution_mappings = [\n ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=\"john.doe@example.com\",\n effective_value=\"john.doe@example.com\",\n transformation_note=None,\n warning_level=None,\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n )\n ]\n session.semantic_fields = []\n session.semantic_fields = [\n SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"customer_name\",\n field_kind=\"dimension\",\n verbose_name=\"Customer name\",\n description=\"Current semantic label\",\n display_format=\"text\",\n provenance=\"unresolved\",\n source_id=None,\n source_version=None,\n confidence_rank=None,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"system\",\n )\n ]\n session.semantic_fields[0].candidates = [\n SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=None,\n candidate_rank=1,\n match_type=\"exact\",\n confidence_score=0.99,\n proposed_verbose_name=\"Customer legal name\",\n proposed_description=\"Approved semantic wording\",\n proposed_display_format=\"text\",\n status=CandidateStatus.PROPOSED,\n )\n ]\n session.template_variables = []\n session.clarification_sessions = []\n session.run_contexts = []\n return session\n\n\n# [/DEF:_dataset_review_session:Function]\n\n\n# [DEF:_await_none:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 1\n# @PURPOSE: Async helper returning None for planner fallback tests.\nasync def _await_none(*args, **kwargs):\n return None\n\n\n# [/DEF:_await_none:Function]\n\n\n# [DEF:test_unknown_command_returns_needs_clarification:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Unknown command should return clarification state and unknown intent.\ndef test_unknown_command_returns_needs_clarification(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"some random gibberish\")\n\n # We mock LLM planner to return low confidence\n monkeypatch.setattr(assistant_routes, \"_plan_intent_with_llm\", lambda *a, **k: None)\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n\n assert resp.state == \"needs_clarification\"\n assert \"уточните\" in resp.text.lower() or \"неоднозначна\" in resp.text.lower()\n\n\n# [/DEF:test_unknown_command_returns_needs_clarification:Function]\n\n\n# [DEF:test_capabilities_question_returns_successful_help:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Capability query should return deterministic help response.\ndef test_capabilities_question_returns_successful_help(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"что ты умеешь?\")\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n\n assert resp.state == \"success\"\n assert \"я могу сделать\" in resp.text.lower()\n\n\n# [/DEF:test_capabilities_question_returns_successful_help:Function]\n\n\n# [DEF:test_assistant_message_request_accepts_dataset_review_session_binding:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Assistant request schema should accept active dataset review session binding for scoped orchestration.\ndef test_assistant_message_request_accepts_dataset_review_session_binding():\n request = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n\n assert request.dataset_review_session_id == \"sess-1\"\n\n\n# [/DEF:test_assistant_message_request_accepts_dataset_review_session_binding:Function]\n\n\n# [DEF:test_dataset_review_scoped_message_uses_masked_filter_context:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Session-scoped assistant context should mask imported-filter raw values before assistant-visible metadata is persisted.\ndef test_dataset_review_scoped_message_uses_masked_filter_context(monkeypatch):\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"show filters\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n\n async def _fake_dispatch_dataset_review_intent(\n intent, current_user, config_manager, db\n ):\n return str(intent[\"entities\"][\"summary\"]), None, []\n\n monkeypatch.setattr(\n assistant_routes,\n \"_dispatch_dataset_review_intent\",\n _fake_dispatch_dataset_review_intent,\n )\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n\n assert resp.state == \"success\"\n persisted_assistant = [\n item for item in db.added if getattr(item, \"role\", None) == \"assistant\"\n ][-1]\n imported_filters = persisted_assistant.payload[\"dataset_review_context\"][\n \"imported_filters\"\n ]\n assert imported_filters[0][\"raw_value\"] == \"***@example.com\"\n assert imported_filters[0][\"raw_value_masked\"] is True\n\n\n# [/DEF:test_dataset_review_scoped_message_uses_masked_filter_context:Function]\n\n\n# [DEF:test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Session-scoped assistant commands should route dataset-review mapping approvals into confirmation workflow with bound session metadata.\ndef test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_approve_mappings\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"session_version\"] == 3\n assert resp.intent[\"entities\"][\"mapping_ids\"] == [\"map-1\"]\n\n\n# [/DEF:test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval:Function]\n\n\n# [DEF:test_dataset_review_scoped_command_routes_field_semantics_update:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Session-scoped assistant commands should route semantic field updates through explicit confirmation metadata.\ndef test_dataset_review_scoped_command_routes_field_semantics_update():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message='set field semantics target=field:field-1 desc=\"Approved semantic wording\" lock',\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_set_field_semantics\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"field_id\"] == \"field-1\"\n assert resp.intent[\"entities\"][\"description\"] == \"Approved semantic wording\"\n assert resp.intent[\"entities\"][\"lock_field\"] is True\n\n\n# [/DEF:test_dataset_review_scoped_command_routes_field_semantics_update:Function]\n\n\n# [/DEF:AssistantApiTests:Module]\n" - }, - { - "contract_id": "_dataset_review_session", + "contract_id": "_run_async", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 266, - "end_line": 367, + "start_line": 32, + "end_line": 36, "tier": "TIER_1", "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Build minimal owned dataset-review session fixture for assistant scoped routing tests." - }, + "metadata": {}, "relations": [ { - "source_id": "_dataset_review_session", + "source_id": "_run_async", "relation_type": "BINDS_TO", "target_id": "AssistantApiTests", "target_ref": "[AssistantApiTests]" } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -774,39 +639,27 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_dataset_review_session:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 1\n# @PURPOSE: Build minimal owned dataset-review session fixture for assistant scoped routing tests.\ndef _dataset_review_session():\n session = DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"u-admin\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n version=3,\n readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED,\n recommended_action=RecommendedAction.APPROVE_MAPPING,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.MAPPING_REVIEW,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n last_activity_at=datetime.utcnow(),\n )\n session.findings = []\n session.previews = []\n session.imported_filters = [\n ImportedFilter(\n filter_id=\"filter-1\",\n session_id=\"sess-1\",\n filter_name=\"email\",\n display_name=\"Email\",\n raw_value=\"john.doe@example.com\",\n raw_value_masked=False,\n normalized_value=\"john.doe@example.com\",\n source=\"manual\",\n confidence_state=\"confirmed\",\n requires_confirmation=False,\n recovery_status=\"recovered\",\n notes=None,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n )\n ]\n session.execution_mappings = [\n ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=\"john.doe@example.com\",\n effective_value=\"john.doe@example.com\",\n transformation_note=None,\n warning_level=None,\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n )\n ]\n session.semantic_fields = []\n session.semantic_fields = [\n SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"customer_name\",\n field_kind=\"dimension\",\n verbose_name=\"Customer name\",\n description=\"Current semantic label\",\n display_format=\"text\",\n provenance=\"unresolved\",\n source_id=None,\n source_version=None,\n confidence_rank=None,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"system\",\n )\n ]\n session.semantic_fields[0].candidates = [\n SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=None,\n candidate_rank=1,\n match_type=\"exact\",\n confidence_score=0.99,\n proposed_verbose_name=\"Customer legal name\",\n proposed_description=\"Approved semantic wording\",\n proposed_display_format=\"text\",\n status=CandidateStatus.PROPOSED,\n )\n ]\n session.template_variables = []\n session.clarification_sessions = []\n session.run_contexts = []\n return session\n\n\n# [/DEF:_dataset_review_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region _run_async [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\ndef _run_async(coro):\n return asyncio.run(coro)\n# #endregion _run_async\n" }, { - "contract_id": "_await_none", + "contract_id": "_clear_assistant_state", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 370, - "end_line": 378, + "start_line": 184, + "end_line": 191, "tier": "TIER_1", "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Async helper returning None for planner fallback tests." - }, + "metadata": {}, "relations": [ { - "source_id": "_await_none", + "source_id": "_clear_assistant_state", "relation_type": "BINDS_TO", "target_id": "AssistantApiTests", "target_ref": "[AssistantApiTests]" } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -817,15 +670,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_await_none:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @COMPLEXITY: 1\n# @PURPOSE: Async helper returning None for planner fallback tests.\nasync def _await_none(*args, **kwargs):\n return None\n\n\n# [/DEF:_await_none:Function]\n" + "anchor_syntax": "region", + "body": "# #region _clear_assistant_state [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\ndef _clear_assistant_state():\n assistant_routes.CONVERSATIONS.clear()\n assistant_routes.USER_ACTIVE_CONVERSATION.clear()\n assistant_routes.CONFIRMATIONS.clear()\n assistant_routes.ASSISTANT_AUDIT.clear()\n# #endregion _clear_assistant_state\n" }, { "contract_id": "test_unknown_command_returns_needs_clarification", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 381, - "end_line": 405, + "start_line": 297, + "end_line": 316, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -850,15 +703,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_unknown_command_returns_needs_clarification:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Unknown command should return clarification state and unknown intent.\ndef test_unknown_command_returns_needs_clarification(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"some random gibberish\")\n\n # We mock LLM planner to return low confidence\n monkeypatch.setattr(assistant_routes, \"_plan_intent_with_llm\", lambda *a, **k: None)\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n\n assert resp.state == \"needs_clarification\"\n assert \"уточните\" in resp.text.lower() or \"неоднозначна\" in resp.text.lower()\n\n\n# [/DEF:test_unknown_command_returns_needs_clarification:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_unknown_command_returns_needs_clarification [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Unknown command should return clarification state and unknown intent.\ndef test_unknown_command_returns_needs_clarification(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"some random gibberish\")\n # We mock LLM planner to return low confidence\n monkeypatch.setattr(assistant_routes, \"_plan_intent_with_llm\", lambda *a, **k: None)\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert resp.state == \"needs_clarification\"\n assert \"уточните\" in resp.text.lower() or \"неоднозначна\" in resp.text.lower()\n# #endregion test_unknown_command_returns_needs_clarification\n" }, { "contract_id": "test_capabilities_question_returns_successful_help", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 408, - "end_line": 429, + "start_line": 317, + "end_line": 334, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -883,15 +736,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_capabilities_question_returns_successful_help:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Capability query should return deterministic help response.\ndef test_capabilities_question_returns_successful_help(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"что ты умеешь?\")\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n\n assert resp.state == \"success\"\n assert \"я могу сделать\" in resp.text.lower()\n\n\n# [/DEF:test_capabilities_question_returns_successful_help:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_capabilities_question_returns_successful_help [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Capability query should return deterministic help response.\ndef test_capabilities_question_returns_successful_help(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"что ты умеешь?\")\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert resp.state == \"success\"\n assert \"я могу сделать\" in resp.text.lower()\n# #endregion test_capabilities_question_returns_successful_help\n" }, { "contract_id": "test_assistant_message_request_accepts_dataset_review_session_binding", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 432, - "end_line": 444, + "start_line": 335, + "end_line": 344, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -916,15 +769,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_assistant_message_request_accepts_dataset_review_session_binding:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Assistant request schema should accept active dataset review session binding for scoped orchestration.\ndef test_assistant_message_request_accepts_dataset_review_session_binding():\n request = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n\n assert request.dataset_review_session_id == \"sess-1\"\n\n\n# [/DEF:test_assistant_message_request_accepts_dataset_review_session_binding:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_assistant_message_request_accepts_dataset_review_session_binding [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Assistant request schema should accept active dataset review session binding for scoped orchestration.\ndef test_assistant_message_request_accepts_dataset_review_session_binding():\n request = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n assert request.dataset_review_session_id == \"sess-1\"\n# #endregion test_assistant_message_request_accepts_dataset_review_session_binding\n" }, { "contract_id": "test_dataset_review_scoped_message_uses_masked_filter_context", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 447, - "end_line": 492, + "start_line": 345, + "end_line": 384, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -949,15 +802,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_dataset_review_scoped_message_uses_masked_filter_context:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Session-scoped assistant context should mask imported-filter raw values before assistant-visible metadata is persisted.\ndef test_dataset_review_scoped_message_uses_masked_filter_context(monkeypatch):\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"show filters\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n\n async def _fake_dispatch_dataset_review_intent(\n intent, current_user, config_manager, db\n ):\n return str(intent[\"entities\"][\"summary\"]), None, []\n\n monkeypatch.setattr(\n assistant_routes,\n \"_dispatch_dataset_review_intent\",\n _fake_dispatch_dataset_review_intent,\n )\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n\n assert resp.state == \"success\"\n persisted_assistant = [\n item for item in db.added if getattr(item, \"role\", None) == \"assistant\"\n ][-1]\n imported_filters = persisted_assistant.payload[\"dataset_review_context\"][\n \"imported_filters\"\n ]\n assert imported_filters[0][\"raw_value\"] == \"***@example.com\"\n assert imported_filters[0][\"raw_value_masked\"] is True\n\n\n# [/DEF:test_dataset_review_scoped_message_uses_masked_filter_context:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_dataset_review_scoped_message_uses_masked_filter_context [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant context should mask imported-filter raw values before assistant-visible metadata is persisted.\ndef test_dataset_review_scoped_message_uses_masked_filter_context(monkeypatch):\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"show filters\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n async def _fake_dispatch_dataset_review_intent(\n intent, current_user, config_manager, db\n ):\n return str(intent[\"entities\"][\"summary\"]), None, []\n monkeypatch.setattr(\n assistant_routes,\n \"_dispatch_dataset_review_intent\",\n _fake_dispatch_dataset_review_intent,\n )\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"success\"\n persisted_assistant = [\n item for item in db.added if getattr(item, \"role\", None) == \"assistant\"\n ][-1]\n imported_filters = persisted_assistant.payload[\"dataset_review_context\"][\n \"imported_filters\"\n ]\n assert imported_filters[0][\"raw_value\"] == \"***@example.com\"\n assert imported_filters[0][\"raw_value_masked\"] is True\n# #endregion test_dataset_review_scoped_message_uses_masked_filter_context\n" }, { "contract_id": "test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 495, - "end_line": 525, + "start_line": 385, + "end_line": 411, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -982,15 +835,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Session-scoped assistant commands should route dataset-review mapping approvals into confirmation workflow with bound session metadata.\ndef test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_approve_mappings\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"session_version\"] == 3\n assert resp.intent[\"entities\"][\"mapping_ids\"] == [\"map-1\"]\n\n\n# [/DEF:test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant commands should route dataset-review mapping approvals into confirmation workflow with bound session metadata.\ndef test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_approve_mappings\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"session_version\"] == 3\n assert resp.intent[\"entities\"][\"mapping_ids\"] == [\"map-1\"]\n# #endregion test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval\n" }, { "contract_id": "test_dataset_review_scoped_command_routes_field_semantics_update", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_api.py", - "start_line": 528, - "end_line": 559, + "start_line": 412, + "end_line": 439, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1015,490 +868,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_dataset_review_scoped_command_routes_field_semantics_update:Function]\n# @RELATION: BINDS_TO -> [AssistantApiTests]\n# @PURPOSE: Session-scoped assistant commands should route semantic field updates through explicit confirmation metadata.\ndef test_dataset_review_scoped_command_routes_field_semantics_update():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message='set field semantics target=field:field-1 desc=\"Approved semantic wording\" lock',\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_set_field_semantics\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"field_id\"] == \"field-1\"\n assert resp.intent[\"entities\"][\"description\"] == \"Approved semantic wording\"\n assert resp.intent[\"entities\"][\"lock_field\"] is True\n\n\n# [/DEF:test_dataset_review_scoped_command_routes_field_semantics_update:Function]\n" - }, - { - "contract_id": "TestAssistantAuthz", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 4, - "end_line": 360, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Security-sensitive flows fail closed for unauthorized actors.", - "LAYER": "UI (API Tests)", - "PURPOSE": "Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.", - "SEMANTICS": [ - "tests", - "assistant", - "authz", - "confirmation", - "rbac" - ] - }, - "relations": [ - { - "source_id": "TestAssistantAuthz", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantApi", - "target_ref": "AssistantApi" - } - ], - "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" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestAssistantAuthz:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, assistant, authz, confirmation, rbac\n# @PURPOSE: Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.\n# @LAYER: UI (API Tests)\n\n# @RELATION: DEPENDS_ON -> AssistantApi\n# @INVARIANT: Security-sensitive flows fail closed for unauthorized actors.\n\nimport os\nimport asyncio\nfrom datetime import datetime, timedelta\nfrom types import SimpleNamespace\n\nimport pytest\nfrom fastapi import HTTPException\n\n# Force isolated sqlite databases for test module before dependencies import.\nos.environ.setdefault(\"DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz.db\")\nos.environ.setdefault(\n \"TASKS_DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz_tasks.db\"\n)\nos.environ.setdefault(\n \"AUTH_DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz_auth.db\"\n)\n\nfrom src.api.routes import assistant as assistant_module\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantConfirmationRecord,\n AssistantMessageRecord,\n)\n\n\n# [DEF:_run_async:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Execute async endpoint handler in synchronous test context.\n# @PRE: coroutine is awaitable endpoint invocation.\n# @POST: Returns coroutine result or raises propagated exception.\ndef _run_async(coroutine):\n return asyncio.run(coroutine)\n\n\n# [/DEF:_run_async:Function]\n\n\n# [DEF:_FakeTask:Class]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Lightweight task model used for assistant authz tests.\n# @PRE: task_id is non-empty string.\n# @POST: Returns task with provided id, status, and user_id accessible as attributes.\nclass _FakeTask:\n def __init__(self, task_id: str, status: str = \"RUNNING\", user_id: str = \"u-admin\"):\n self.id = task_id\n self.status = status\n self.user_id = user_id\n\n\n# [/DEF:_FakeTask:Class]\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# [DEF:_FakeTaskManager:Class]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 2\n# @PURPOSE: In-memory task manager double that records assistant-created tasks deterministically.\n# @INVARIANT: Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated.\nclass _FakeTaskManager:\n def __init__(self):\n self._created = []\n\n async def create_task(self, plugin_id, params, user_id=None):\n task_id = f\"task-{len(self._created) + 1}\"\n task = _FakeTask(task_id=task_id, status=\"RUNNING\", user_id=user_id)\n self._created.append((plugin_id, params, user_id, task))\n return task\n\n def get_task(self, task_id):\n for _, _, _, task in self._created:\n if task.id == task_id:\n return task\n return None\n\n def get_tasks(self, limit=20, offset=0):\n return [x[3] for x in self._created][offset : offset + limit]\n\n def get_all_tasks(self):\n raise NotImplementedError(\n \"get_all_tasks not implemented in authz FakeTaskManager\"\n )\n\n\n# [/DEF:_FakeTaskManager:Class]\n# @CONTRACT: Partial ConfigManager stub for authz tests. Missing: get_config().\n# [DEF:_FakeConfigManager:Class]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Provide deterministic environment aliases required by intent parsing.\n# @PRE: No external config or DB state is required.\n# @POST: get_environments() returns two deterministic SimpleNamespace stubs with id/name.\n# @INVARIANT: get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake.\nclass _FakeConfigManager:\n def get_environments(self):\n return [\n SimpleNamespace(id=\"dev\", name=\"Development\"),\n SimpleNamespace(id=\"prod\", name=\"Production\"),\n ]\n\n def get_config(self):\n raise NotImplementedError(\n \"get_config not implemented in authz fake — add if route under test requires it\"\n )\n\n\n# [/DEF:_FakeConfigManager:Class]\n# [DEF:_admin_user:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Build admin principal fixture.\n# @PRE: Test requires privileged principal for risky operations.\n# @POST: Returns admin-like user stub with Admin role.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(id=\"u-admin\", username=\"admin\", roles=[role])\n\n\n# [/DEF:_admin_user:Function]\n# [DEF:_other_admin_user:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Build second admin principal fixture for ownership tests.\n# @PRE: Ownership mismatch scenario needs distinct authenticated actor.\n# @POST: Returns alternate admin-like user stub.\ndef _other_admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(id=\"u-admin-2\", username=\"admin2\", roles=[role])\n\n\n# [/DEF:_other_admin_user:Function]\n# [DEF:_limited_user:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Build limited principal without required assistant execution privileges.\n# @PRE: Permission denial scenario needs non-admin actor.\n# @POST: Returns restricted user stub.\ndef _limited_user():\n role = SimpleNamespace(name=\"Operator\", permissions=[])\n return SimpleNamespace(id=\"u-limited\", username=\"limited\", roles=[role])\n\n\n# [/DEF:_limited_user:Function]\n\n\n# [DEF:_FakeQuery:Class]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal chainable query object for fake DB interactions.\n# @INVARIANT: filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation.\nclass _FakeQuery:\n def __init__(self, rows):\n self._rows = list(rows)\n\n def filter(self, *args, **kwargs):\n # @INVARIANT: filter() is predicate-blind; returns all records regardless of user_id scope\n return self\n\n def order_by(self, *args, **kwargs):\n return self\n\n def first(self):\n return self._rows[0] if self._rows else None\n\n def all(self):\n return list(self._rows)\n\n def limit(self, limit):\n self._rows = self._rows[:limit]\n return self\n\n def offset(self, offset):\n self._rows = self._rows[offset:]\n return self\n\n def count(self):\n return len(self._rows)\n\n\n# [/DEF:_FakeQuery:Class]\n# [DEF:_FakeDb:Class]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 2\n# @PURPOSE: In-memory DB session double constrained to assistant message/confirmation/audit persistence paths.\n# @INVARIANT: query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.\nclass _FakeDb:\n def __init__(self):\n self._messages = []\n self._confirmations = []\n self._audit = []\n\n def add(self, row):\n table = getattr(row, \"__tablename__\", \"\")\n if table == \"assistant_messages\":\n self._messages.append(row)\n elif table == \"assistant_confirmations\":\n self._confirmations.append(row)\n elif table == \"assistant_audit\":\n self._audit.append(row)\n\n def merge(self, row):\n if getattr(row, \"__tablename__\", \"\") != \"assistant_confirmations\":\n self.add(row)\n return row\n\n for i, existing in enumerate(self._confirmations):\n if getattr(existing, \"id\", None) == getattr(row, \"id\", None):\n self._confirmations[i] = row\n return row\n self._confirmations.append(row)\n return row\n\n def query(self, model):\n if model is AssistantMessageRecord:\n return _FakeQuery(self._messages)\n if model is AssistantConfirmationRecord:\n return _FakeQuery(self._confirmations)\n if model is AssistantAuditRecord:\n return _FakeQuery(self._audit)\n return _FakeQuery([])\n\n def commit(self):\n return None\n\n def rollback(self):\n return None\n\n\n# [/DEF:_FakeDb:Class]\n# [DEF:_clear_assistant_state:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Reset assistant process-local state between test cases.\n# @PRE: Assistant globals may contain state from prior tests.\n# @POST: Assistant in-memory state dictionaries are cleared.\ndef _clear_assistant_state():\n assistant_module.CONVERSATIONS.clear()\n assistant_module.USER_ACTIVE_CONVERSATION.clear()\n assistant_module.CONFIRMATIONS.clear()\n assistant_module.ASSISTANT_AUDIT.clear()\n\n\n# [/DEF:_clear_assistant_state:Function]\n\n\n# [DEF:test_confirmation_owner_mismatch_returns_403:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @PURPOSE: Confirm endpoint should reject requests from user that does not own the confirmation token.\n# @PRE: Confirmation token is created by first admin actor.\n# @POST: Second actor receives 403 on confirm operation.\ndef test_confirmation_owner_mismatch_returns_403():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 18\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert create.state == \"needs_confirmation\"\n\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_other_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 403\n\n\n# [/DEF:test_confirmation_owner_mismatch_returns_403:Function]\n\n\n# [DEF:test_expired_confirmation_cannot_be_confirmed:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @PURPOSE: Expired confirmation token should be rejected and not create task.\n# @PRE: Confirmation token exists and is manually expired before confirm request.\n# @POST: Confirm endpoint raises 400 and no task is created.\ndef test_expired_confirmation_cannot_be_confirmed():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 19\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = (\n datetime.utcnow() - timedelta(minutes=1)\n )\n\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 400\n assert task_manager.get_tasks(limit=10, offset=0) == []\n\n\n# [/DEF:test_expired_confirmation_cannot_be_confirmed:Function]\n\n\n# [DEF:test_limited_user_cannot_launch_restricted_operation:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @PURPOSE: Limited user should receive denied state for privileged operation.\n# @PRE: Restricted user attempts dangerous deploy command.\n# @POST: Assistant returns denied state and does not execute operation.\ndef test_limited_user_cannot_launch_restricted_operation():\n _clear_assistant_state()\n response = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"задеплой дашборд 88 в production\"\n ),\n current_user=_limited_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert response.state == \"denied\"\n\n\n# [/DEF:test_limited_user_cannot_launch_restricted_operation:Function]\n# [/DEF:TestAssistantAuthz:Module]\n" - }, - { - "contract_id": "_run_async", - "contract_type": "Function", - "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 38, - "end_line": 48, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns coroutine result or raises propagated exception.", - "PRE": "coroutine is awaitable endpoint invocation.", - "PURPOSE": "Execute async endpoint handler in synchronous test context." - }, - "relations": [ - { - "source_id": "_run_async", - "relation_type": "BINDS_TO", - "target_id": "TestAssistantAuthz", - "target_ref": "[TestAssistantAuthz]" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_run_async:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Execute async endpoint handler in synchronous test context.\n# @PRE: coroutine is awaitable endpoint invocation.\n# @POST: Returns coroutine result or raises propagated exception.\ndef _run_async(coroutine):\n return asyncio.run(coroutine)\n\n\n# [/DEF:_run_async:Function]\n" - }, - { - "contract_id": "_FakeTask", - "contract_type": "Class", - "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 51, - "end_line": 64, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns task with provided id, status, and user_id accessible as attributes.", - "PRE": "task_id is non-empty string.", - "PURPOSE": "Lightweight task model used for assistant authz tests." - }, - "relations": [ - { - "source_id": "_FakeTask", - "relation_type": "BINDS_TO", - "target_id": "TestAssistantAuthz", - "target_ref": "[TestAssistantAuthz]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_FakeTask:Class]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Lightweight task model used for assistant authz tests.\n# @PRE: task_id is non-empty string.\n# @POST: Returns task with provided id, status, and user_id accessible as attributes.\nclass _FakeTask:\n def __init__(self, task_id: str, status: str = \"RUNNING\", user_id: str = \"u-admin\"):\n self.id = task_id\n self.status = status\n self.user_id = user_id\n\n\n# [/DEF:_FakeTask:Class]\n" - }, - { - "contract_id": "_FakeConfigManager", - "contract_type": "Class", - "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 98, - "end_line": 118, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake.", - "POST": "get_environments() returns two deterministic SimpleNamespace stubs with id/name.", - "PRE": "No external config or DB state is required.", - "PURPOSE": "Provide deterministic environment aliases required by intent parsing." - }, - "relations": [ - { - "source_id": "_FakeConfigManager", - "relation_type": "BINDS_TO", - "target_id": "TestAssistantAuthz", - "target_ref": "[TestAssistantAuthz]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_FakeConfigManager:Class]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Provide deterministic environment aliases required by intent parsing.\n# @PRE: No external config or DB state is required.\n# @POST: get_environments() returns two deterministic SimpleNamespace stubs with id/name.\n# @INVARIANT: get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake.\nclass _FakeConfigManager:\n def get_environments(self):\n return [\n SimpleNamespace(id=\"dev\", name=\"Development\"),\n SimpleNamespace(id=\"prod\", name=\"Production\"),\n ]\n\n def get_config(self):\n raise NotImplementedError(\n \"get_config not implemented in authz fake — add if route under test requires it\"\n )\n\n\n# [/DEF:_FakeConfigManager:Class]\n" - }, - { - "contract_id": "_other_admin_user", - "contract_type": "Function", - "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 131, - "end_line": 142, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns alternate admin-like user stub.", - "PRE": "Ownership mismatch scenario needs distinct authenticated actor.", - "PURPOSE": "Build second admin principal fixture for ownership tests." - }, - "relations": [ - { - "source_id": "_other_admin_user", - "relation_type": "BINDS_TO", - "target_id": "TestAssistantAuthz", - "target_ref": "[TestAssistantAuthz]" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_other_admin_user:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Build second admin principal fixture for ownership tests.\n# @PRE: Ownership mismatch scenario needs distinct authenticated actor.\n# @POST: Returns alternate admin-like user stub.\ndef _other_admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(id=\"u-admin-2\", username=\"admin2\", roles=[role])\n\n\n# [/DEF:_other_admin_user:Function]\n" - }, - { - "contract_id": "_limited_user", - "contract_type": "Function", - "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 143, - "end_line": 154, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns restricted user stub.", - "PRE": "Permission denial scenario needs non-admin actor.", - "PURPOSE": "Build limited principal without required assistant execution privileges." - }, - "relations": [ - { - "source_id": "_limited_user", - "relation_type": "BINDS_TO", - "target_id": "TestAssistantAuthz", - "target_ref": "[TestAssistantAuthz]" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_limited_user:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Build limited principal without required assistant execution privileges.\n# @PRE: Permission denial scenario needs non-admin actor.\n# @POST: Returns restricted user stub.\ndef _limited_user():\n role = SimpleNamespace(name=\"Operator\", permissions=[])\n return SimpleNamespace(id=\"u-limited\", username=\"limited\", roles=[role])\n\n\n# [/DEF:_limited_user:Function]\n" - }, - { - "contract_id": "_FakeDb", - "contract_type": "Class", - "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 192, - "end_line": 240, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "INVARIANT": "query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.", - "PURPOSE": "In-memory DB session double constrained to assistant message/confirmation/audit persistence paths." - }, - "relations": [ - { - "source_id": "_FakeDb", - "relation_type": "BINDS_TO", - "target_id": "TestAssistantAuthz", - "target_ref": "[TestAssistantAuthz]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_FakeDb:Class]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 2\n# @PURPOSE: In-memory DB session double constrained to assistant message/confirmation/audit persistence paths.\n# @INVARIANT: query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.\nclass _FakeDb:\n def __init__(self):\n self._messages = []\n self._confirmations = []\n self._audit = []\n\n def add(self, row):\n table = getattr(row, \"__tablename__\", \"\")\n if table == \"assistant_messages\":\n self._messages.append(row)\n elif table == \"assistant_confirmations\":\n self._confirmations.append(row)\n elif table == \"assistant_audit\":\n self._audit.append(row)\n\n def merge(self, row):\n if getattr(row, \"__tablename__\", \"\") != \"assistant_confirmations\":\n self.add(row)\n return row\n\n for i, existing in enumerate(self._confirmations):\n if getattr(existing, \"id\", None) == getattr(row, \"id\", None):\n self._confirmations[i] = row\n return row\n self._confirmations.append(row)\n return row\n\n def query(self, model):\n if model is AssistantMessageRecord:\n return _FakeQuery(self._messages)\n if model is AssistantConfirmationRecord:\n return _FakeQuery(self._confirmations)\n if model is AssistantAuditRecord:\n return _FakeQuery(self._audit)\n return _FakeQuery([])\n\n def commit(self):\n return None\n\n def rollback(self):\n return None\n\n\n# [/DEF:_FakeDb:Class]\n" - }, - { - "contract_id": "_clear_assistant_state", - "contract_type": "Function", - "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 241, - "end_line": 254, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Assistant in-memory state dictionaries are cleared.", - "PRE": "Assistant globals may contain state from prior tests.", - "PURPOSE": "Reset assistant process-local state between test cases." - }, - "relations": [ - { - "source_id": "_clear_assistant_state", - "relation_type": "BINDS_TO", - "target_id": "TestAssistantAuthz", - "target_ref": "[TestAssistantAuthz]" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_clear_assistant_state:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @COMPLEXITY: 1\n# @PURPOSE: Reset assistant process-local state between test cases.\n# @PRE: Assistant globals may contain state from prior tests.\n# @POST: Assistant in-memory state dictionaries are cleared.\ndef _clear_assistant_state():\n assistant_module.CONVERSATIONS.clear()\n assistant_module.USER_ACTIVE_CONVERSATION.clear()\n assistant_module.CONFIRMATIONS.clear()\n assistant_module.ASSISTANT_AUDIT.clear()\n\n\n# [/DEF:_clear_assistant_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_dataset_review_scoped_command_routes_field_semantics_update [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant commands should route semantic field updates through explicit confirmation metadata.\ndef test_dataset_review_scoped_command_routes_field_semantics_update():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message='set field semantics target=field:field-1 desc=\"Approved semantic wording\" lock',\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_set_field_semantics\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"field_id\"] == \"field-1\"\n assert resp.intent[\"entities\"][\"description\"] == \"Approved semantic wording\"\n assert resp.intent[\"entities\"][\"lock_field\"] is True\n# #endregion test_dataset_review_scoped_command_routes_field_semantics_update\n" }, { "contract_id": "test_confirmation_owner_mismatch_returns_403", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 257, - "end_line": 293, + "start_line": 198, + "end_line": 230, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1543,15 +921,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_confirmation_owner_mismatch_returns_403:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @PURPOSE: Confirm endpoint should reject requests from user that does not own the confirmation token.\n# @PRE: Confirmation token is created by first admin actor.\n# @POST: Second actor receives 403 on confirm operation.\ndef test_confirmation_owner_mismatch_returns_403():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 18\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert create.state == \"needs_confirmation\"\n\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_other_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 403\n\n\n# [/DEF:test_confirmation_owner_mismatch_returns_403:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_confirmation_owner_mismatch_returns_403 [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Confirm endpoint should reject requests from user that does not own the confirmation token.\n# @PRE: Confirmation token is created by first admin actor.\n# @POST: Second actor receives 403 on confirm operation.\ndef test_confirmation_owner_mismatch_returns_403():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 18\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert create.state == \"needs_confirmation\"\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_other_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 403\n# #endregion test_confirmation_owner_mismatch_returns_403\n" }, { "contract_id": "test_expired_confirmation_cannot_be_confirmed", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 296, - "end_line": 335, + "start_line": 231, + "end_line": 266, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1596,15 +974,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_expired_confirmation_cannot_be_confirmed:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @PURPOSE: Expired confirmation token should be rejected and not create task.\n# @PRE: Confirmation token exists and is manually expired before confirm request.\n# @POST: Confirm endpoint raises 400 and no task is created.\ndef test_expired_confirmation_cannot_be_confirmed():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 19\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = (\n datetime.utcnow() - timedelta(minutes=1)\n )\n\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 400\n assert task_manager.get_tasks(limit=10, offset=0) == []\n\n\n# [/DEF:test_expired_confirmation_cannot_be_confirmed:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Expired confirmation token should be rejected and not create task.\n# @PRE: Confirmation token exists and is manually expired before confirm request.\n# @POST: Confirm endpoint raises 400 and no task is created.\ndef test_expired_confirmation_cannot_be_confirmed():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 19\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = (\n datetime.utcnow() - timedelta(minutes=1)\n )\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 400\n assert task_manager.get_tasks(limit=10, offset=0) == []\n# #endregion test_expired_confirmation_cannot_be_confirmed\n" }, { "contract_id": "test_limited_user_cannot_launch_restricted_operation", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_assistant_authz.py", - "start_line": 338, - "end_line": 359, + "start_line": 267, + "end_line": 286, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1649,76 +1027,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_limited_user_cannot_launch_restricted_operation:Function]\n# @RELATION: BINDS_TO -> [TestAssistantAuthz]\n# @PURPOSE: Limited user should receive denied state for privileged operation.\n# @PRE: Restricted user attempts dangerous deploy command.\n# @POST: Assistant returns denied state and does not execute operation.\ndef test_limited_user_cannot_launch_restricted_operation():\n _clear_assistant_state()\n response = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"задеплой дашборд 88 в production\"\n ),\n current_user=_limited_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert response.state == \"denied\"\n\n\n# [/DEF:test_limited_user_cannot_launch_restricted_operation:Function]\n" - }, - { - "contract_id": "TestCleanReleaseApi", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_clean_release_api.py", - "start_line": 1, - "end_line": 193, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "API returns deterministic payload shapes for checks and reports.", - "LAYER": "Domain", - "PURPOSE": "Contract tests for clean release checks and reports endpoints.", - "SEMANTICS": [ - "tests", - "api", - "clean-release", - "checks", - "reports" - ] - }, - "relations": [ - { - "source_id": "TestCleanReleaseApi", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "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 BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestCleanReleaseApi:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, api, clean-release, checks, reports\n# @PURPOSE: Contract tests for clean release checks and reports endpoints.\n# @LAYER: Domain\n# @INVARIANT: API returns deterministic payload shapes for checks and reports.\n\nfrom datetime import datetime, timezone\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.dependencies import get_clean_release_repository\nfrom src.models.clean_release import (\n CleanProfilePolicy,\n ProfileType,\n ReleaseCandidate,\n ReleaseCandidateStatus,\n ResourceSourceEntry,\n ResourceSourceRegistry,\n ComplianceReport,\n CheckFinalStatus,\n)\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_repo_with_seed_data:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\ndef _repo_with_seed_data() -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n repo.save_candidate(\n ReleaseCandidate(\n candidate_id=\"2026.03.03-rc1\",\n version=\"2026.03.03\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n created_at=datetime.now(timezone.utc),\n created_by=\"tester\",\n source_snapshot_ref=\"git:abc123\",\n status=ReleaseCandidateStatus.PREPARED,\n )\n )\n repo.save_registry(\n ResourceSourceRegistry(\n registry_id=\"registry-internal-v1\",\n name=\"Internal\",\n entries=[\n ResourceSourceEntry(\n source_id=\"src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n )\n ],\n updated_at=datetime.now(timezone.utc),\n updated_by=\"tester\",\n status=\"active\",\n )\n )\n repo.save_policy(\n CleanProfilePolicy(\n policy_id=\"policy-enterprise-clean-v1\",\n policy_version=\"1.0.0\",\n active=True,\n prohibited_artifact_categories=[\"test-data\"],\n required_system_categories=[\"system-init\"],\n external_source_forbidden=True,\n internal_source_registry_ref=\"registry-internal-v1\",\n effective_from=datetime.now(timezone.utc),\n profile=ProfileType.ENTERPRISE_CLEAN,\n )\n )\n return repo\n\n\n# [/DEF:_repo_with_seed_data:Function]\n\n\n# [DEF:test_start_check_and_get_status_contract:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\n# @PURPOSE: Validate checks start endpoint returns expected identifiers and status endpoint reflects the same run.\ndef test_start_check_and_get_status_contract():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n\n start = client.post(\n \"/api/clean-release/checks\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"profile\": \"enterprise-clean\",\n \"execution_mode\": \"tui\",\n \"triggered_by\": \"tester\",\n },\n )\n assert start.status_code == 202\n payload = start.json()\n assert set([\"check_run_id\", \"candidate_id\", \"status\", \"started_at\"]).issubset(\n payload.keys()\n )\n\n check_run_id = payload[\"check_run_id\"]\n status_resp = client.get(f\"/api/clean-release/checks/{check_run_id}\")\n assert status_resp.status_code == 200\n status_payload = status_resp.json()\n assert status_payload[\"check_run_id\"] == check_run_id\n assert \"final_status\" in status_payload\n assert \"checks\" in status_payload\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_start_check_and_get_status_contract:Function]\n\n\n# [DEF:test_get_report_not_found_returns_404:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\n# @PURPOSE: Validate reports endpoint returns 404 for an unknown report identifier.\ndef test_get_report_not_found_returns_404():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n resp = client.get(\"/api/clean-release/reports/unknown-report\")\n assert resp.status_code == 404\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_report_not_found_returns_404:Function]\n\n\n# [DEF:test_get_report_success:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\n# @PURPOSE: Validate reports endpoint returns persisted report payload for an existing report identifier.\ndef test_get_report_success():\n repo = _repo_with_seed_data()\n report = ComplianceReport(\n report_id=\"rep-1\",\n check_run_id=\"run-1\",\n candidate_id=\"2026.03.03-rc1\",\n generated_at=datetime.now(timezone.utc),\n final_status=CheckFinalStatus.COMPLIANT,\n operator_summary=\"all systems go\",\n structured_payload_ref=\"manifest-1\",\n violations_count=0,\n blocking_violations_count=0,\n )\n repo.save_report(report)\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n resp = client.get(\"/api/clean-release/reports/rep-1\")\n assert resp.status_code == 200\n assert resp.json()[\"report_id\"] == \"rep-1\"\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_report_success:Function]\n\n\n# [DEF:test_prepare_candidate_api_success:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\n# @PURPOSE: Validate candidate preparation endpoint returns prepared status and manifest identifier on valid input.\ndef test_prepare_candidate_api_success():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"artifacts\": [\n {\"path\": \"file1.txt\", \"category\": \"system-init\", \"reason\": \"core\"}\n ],\n \"sources\": [\"repo.intra.company.local\"],\n \"operator_id\": \"operator-1\",\n },\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"prepared\"\n assert \"manifest_id\" in data\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_prepare_candidate_api_success:Function]\n# [/DEF:TestCleanReleaseApi:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_limited_user_cannot_launch_restricted_operation [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Limited user should receive denied state for privileged operation.\n# @PRE: Restricted user attempts dangerous deploy command.\n# @POST: Assistant returns denied state and does not execute operation.\ndef test_limited_user_cannot_launch_restricted_operation():\n _clear_assistant_state()\n response = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"задеплой дашборд 88 в production\"\n ),\n current_user=_limited_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert response.state == \"denied\"\n# #endregion test_limited_user_cannot_launch_restricted_operation\n" }, { "contract_id": "test_start_check_and_get_status_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_api.py", - "start_line": 80, - "end_line": 115, + "start_line": 73, + "end_line": 104, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1743,15 +1060,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_start_check_and_get_status_contract:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\n# @PURPOSE: Validate checks start endpoint returns expected identifiers and status endpoint reflects the same run.\ndef test_start_check_and_get_status_contract():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n\n start = client.post(\n \"/api/clean-release/checks\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"profile\": \"enterprise-clean\",\n \"execution_mode\": \"tui\",\n \"triggered_by\": \"tester\",\n },\n )\n assert start.status_code == 202\n payload = start.json()\n assert set([\"check_run_id\", \"candidate_id\", \"status\", \"started_at\"]).issubset(\n payload.keys()\n )\n\n check_run_id = payload[\"check_run_id\"]\n status_resp = client.get(f\"/api/clean-release/checks/{check_run_id}\")\n assert status_resp.status_code == 200\n status_payload = status_resp.json()\n assert status_payload[\"check_run_id\"] == check_run_id\n assert \"final_status\" in status_payload\n assert \"checks\" in status_payload\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_start_check_and_get_status_contract:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_start_check_and_get_status_contract [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseApi\n# @BRIEF Validate checks start endpoint returns expected identifiers and status endpoint reflects the same run.\ndef test_start_check_and_get_status_contract():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n start = client.post(\n \"/api/clean-release/checks\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"profile\": \"enterprise-clean\",\n \"execution_mode\": \"tui\",\n \"triggered_by\": \"tester\",\n },\n )\n assert start.status_code == 202\n payload = start.json()\n assert set([\"check_run_id\", \"candidate_id\", \"status\", \"started_at\"]).issubset(\n payload.keys()\n )\n check_run_id = payload[\"check_run_id\"]\n status_resp = client.get(f\"/api/clean-release/checks/{check_run_id}\")\n assert status_resp.status_code == 200\n status_payload = status_resp.json()\n assert status_payload[\"check_run_id\"] == check_run_id\n assert \"final_status\" in status_payload\n assert \"checks\" in status_payload\n finally:\n app.dependency_overrides.clear()\n# #endregion test_start_check_and_get_status_contract\n" }, { "contract_id": "test_get_report_not_found_returns_404", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_api.py", - "start_line": 118, - "end_line": 132, + "start_line": 105, + "end_line": 117, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1776,15 +1093,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_report_not_found_returns_404:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\n# @PURPOSE: Validate reports endpoint returns 404 for an unknown report identifier.\ndef test_get_report_not_found_returns_404():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n resp = client.get(\"/api/clean-release/reports/unknown-report\")\n assert resp.status_code == 404\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_report_not_found_returns_404:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_report_not_found_returns_404 [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseApi\n# @BRIEF Validate reports endpoint returns 404 for an unknown report identifier.\ndef test_get_report_not_found_returns_404():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n resp = client.get(\"/api/clean-release/reports/unknown-report\")\n assert resp.status_code == 404\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_report_not_found_returns_404\n" }, { "contract_id": "test_get_report_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_api.py", - "start_line": 135, - "end_line": 162, + "start_line": 118, + "end_line": 143, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1809,15 +1126,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_report_success:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\n# @PURPOSE: Validate reports endpoint returns persisted report payload for an existing report identifier.\ndef test_get_report_success():\n repo = _repo_with_seed_data()\n report = ComplianceReport(\n report_id=\"rep-1\",\n check_run_id=\"run-1\",\n candidate_id=\"2026.03.03-rc1\",\n generated_at=datetime.now(timezone.utc),\n final_status=CheckFinalStatus.COMPLIANT,\n operator_summary=\"all systems go\",\n structured_payload_ref=\"manifest-1\",\n violations_count=0,\n blocking_violations_count=0,\n )\n repo.save_report(report)\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n resp = client.get(\"/api/clean-release/reports/rep-1\")\n assert resp.status_code == 200\n assert resp.json()[\"report_id\"] == \"rep-1\"\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_report_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_report_success [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseApi\n# @BRIEF Validate reports endpoint returns persisted report payload for an existing report identifier.\ndef test_get_report_success():\n repo = _repo_with_seed_data()\n report = ComplianceReport(\n report_id=\"rep-1\",\n check_run_id=\"run-1\",\n candidate_id=\"2026.03.03-rc1\",\n generated_at=datetime.now(UTC),\n final_status=CheckFinalStatus.COMPLIANT,\n operator_summary=\"all systems go\",\n structured_payload_ref=\"manifest-1\",\n violations_count=0,\n blocking_violations_count=0,\n )\n repo.save_report(report)\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n resp = client.get(\"/api/clean-release/reports/rep-1\")\n assert resp.status_code == 200\n assert resp.json()[\"report_id\"] == \"rep-1\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_report_success\n" }, { "contract_id": "test_prepare_candidate_api_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_api.py", - "start_line": 165, - "end_line": 192, + "start_line": 144, + "end_line": 169, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1842,59 +1159,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_prepare_candidate_api_success:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseApi\n# @PURPOSE: Validate candidate preparation endpoint returns prepared status and manifest identifier on valid input.\ndef test_prepare_candidate_api_success():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"artifacts\": [\n {\"path\": \"file1.txt\", \"category\": \"system-init\", \"reason\": \"core\"}\n ],\n \"sources\": [\"repo.intra.company.local\"],\n \"operator_id\": \"operator-1\",\n },\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"prepared\"\n assert \"manifest_id\" in data\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_prepare_candidate_api_success:Function]\n" - }, - { - "contract_id": "TestCleanReleaseLegacyCompat", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py", - "start_line": 1, - "end_line": 187, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Tests", - "PURPOSE": "Compatibility tests for legacy clean-release API paths retained during v2 migration." - }, - "relations": [ - { - "source_id": "TestCleanReleaseLegacyCompat", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestCleanReleaseLegacyCompat:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @PURPOSE: Compatibility tests for legacy clean-release API paths retained during v2 migration.\n# @LAYER: Tests\n\nfrom __future__ import annotations\n\nimport os\nfrom datetime import datetime, timezone\n\nfrom fastapi.testclient import TestClient\n\nos.environ.setdefault(\"DATABASE_URL\", \"sqlite:///./test_clean_release_legacy_compat.db\")\nos.environ.setdefault(\n \"AUTH_DATABASE_URL\", \"sqlite:///./test_clean_release_legacy_auth.db\"\n)\n\nfrom src.app import app\nfrom src.dependencies import get_clean_release_repository\nfrom src.models.clean_release import (\n CleanProfilePolicy,\n DistributionManifest,\n ProfileType,\n ReleaseCandidate,\n ReleaseCandidateStatus,\n ResourceSourceEntry,\n ResourceSourceRegistry,\n)\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_legacy_repo:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat\n# @PURPOSE: Seed in-memory repository with minimum trusted data for legacy endpoint contracts.\n# @PRE: Repository is empty.\n# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.\ndef _seed_legacy_repo() -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n now = datetime.now(timezone.utc)\n\n repo.save_candidate(\n ReleaseCandidate(\n id=\"legacy-rc-001\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:legacy-001\",\n created_at=now,\n created_by=\"compat-tester\",\n status=ReleaseCandidateStatus.DRAFT,\n )\n )\n\n registry = ResourceSourceRegistry(\n registry_id=\"legacy-reg-1\",\n name=\"Legacy Internal Registry\",\n entries=[\n ResourceSourceEntry(\n source_id=\"legacy-src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n )\n ],\n updated_at=now,\n updated_by=\"compat-tester\",\n status=\"ACTIVE\",\n )\n setattr(registry, \"immutable\", True)\n setattr(registry, \"allowed_hosts\", [\"repo.intra.company.local\"])\n setattr(registry, \"allowed_schemes\", [\"https\"])\n setattr(registry, \"allowed_source_types\", [\"artifact-repo\"])\n repo.save_registry(registry)\n\n policy = CleanProfilePolicy(\n policy_id=\"legacy-pol-1\",\n policy_version=\"1.0.0\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=\"legacy-reg-1\",\n prohibited_artifact_categories=[\"test-data\"],\n required_system_categories=[\"core\"],\n effective_from=now,\n )\n setattr(policy, \"immutable\", True)\n setattr(\n policy,\n \"content_json\",\n {\n \"profile\": \"enterprise-clean\",\n \"prohibited_artifact_categories\": [\"test-data\"],\n \"required_system_categories\": [\"core\"],\n \"external_source_forbidden\": True,\n },\n )\n repo.save_policy(policy)\n\n repo.save_manifest(\n DistributionManifest(\n id=\"legacy-manifest-1\",\n candidate_id=\"legacy-rc-001\",\n manifest_version=1,\n manifest_digest=\"sha256:legacy-manifest\",\n artifacts_digest=\"sha256:legacy-artifacts\",\n created_at=now,\n created_by=\"compat-tester\",\n source_snapshot_ref=\"git:legacy-001\",\n content_json={\n \"items\": [],\n \"summary\": {\"included_count\": 0, \"prohibited_detected_count\": 0},\n },\n immutable=True,\n )\n )\n\n return repo\n\n\n# [/DEF:_seed_legacy_repo:Function]\n\n\n# [DEF:test_legacy_prepare_endpoint_still_available:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat\n# @PURPOSE: Verify legacy prepare endpoint remains reachable and returns a status payload.\ndef test_legacy_prepare_endpoint_still_available() -> None:\n repo = _seed_legacy_repo()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"legacy-rc-001\",\n \"artifacts\": [\n {\"path\": \"src/main.py\", \"category\": \"core\", \"reason\": \"required\"}\n ],\n \"sources\": [\"repo.intra.company.local\"],\n \"operator_id\": \"compat-tester\",\n },\n )\n assert response.status_code == 200\n payload = response.json()\n assert \"status\" in payload\n assert payload[\"status\"] in {\"prepared\", \"blocked\", \"PREPARED\", \"BLOCKED\"}\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_legacy_prepare_endpoint_still_available:Function]\n\n\n# [DEF:test_legacy_checks_endpoints_still_available:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat\n# @PURPOSE: Verify legacy checks start/status endpoints remain available during v2 transition.\ndef test_legacy_checks_endpoints_still_available() -> None:\n repo = _seed_legacy_repo()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n start_response = client.post(\n \"/api/clean-release/checks\",\n json={\n \"candidate_id\": \"legacy-rc-001\",\n \"profile\": \"enterprise-clean\",\n \"execution_mode\": \"api\",\n \"triggered_by\": \"compat-tester\",\n },\n )\n assert start_response.status_code == 202\n start_payload = start_response.json()\n assert \"check_run_id\" in start_payload\n assert start_payload[\"candidate_id\"] == \"legacy-rc-001\"\n\n status_response = client.get(\n f\"/api/clean-release/checks/{start_payload['check_run_id']}\"\n )\n assert status_response.status_code == 200\n status_payload = status_response.json()\n assert status_payload[\"check_run_id\"] == start_payload[\"check_run_id\"]\n assert \"final_status\" in status_payload\n assert \"checks\" in status_payload\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_legacy_checks_endpoints_still_available:Function]\n# [/DEF:TestCleanReleaseLegacyCompat:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_prepare_candidate_api_success [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseApi\n# @BRIEF Validate candidate preparation endpoint returns prepared status and manifest identifier on valid input.\ndef test_prepare_candidate_api_success():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"artifacts\": [\n {\"path\": \"file1.txt\", \"category\": \"system-init\", \"reason\": \"core\"}\n ],\n \"sources\": [\"repo.intra.company.local\"],\n \"operator_id\": \"operator-1\",\n },\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"prepared\"\n assert \"manifest_id\" in data\n finally:\n app.dependency_overrides.clear()\n# #endregion test_prepare_candidate_api_success\n" }, { "contract_id": "_seed_legacy_repo", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py", - "start_line": 33, - "end_line": 119, + "start_line": 30, + "end_line": 100, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -1948,15 +1221,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_seed_legacy_repo:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat\n# @PURPOSE: Seed in-memory repository with minimum trusted data for legacy endpoint contracts.\n# @PRE: Repository is empty.\n# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.\ndef _seed_legacy_repo() -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n now = datetime.now(timezone.utc)\n\n repo.save_candidate(\n ReleaseCandidate(\n id=\"legacy-rc-001\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:legacy-001\",\n created_at=now,\n created_by=\"compat-tester\",\n status=ReleaseCandidateStatus.DRAFT,\n )\n )\n\n registry = ResourceSourceRegistry(\n registry_id=\"legacy-reg-1\",\n name=\"Legacy Internal Registry\",\n entries=[\n ResourceSourceEntry(\n source_id=\"legacy-src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n )\n ],\n updated_at=now,\n updated_by=\"compat-tester\",\n status=\"ACTIVE\",\n )\n setattr(registry, \"immutable\", True)\n setattr(registry, \"allowed_hosts\", [\"repo.intra.company.local\"])\n setattr(registry, \"allowed_schemes\", [\"https\"])\n setattr(registry, \"allowed_source_types\", [\"artifact-repo\"])\n repo.save_registry(registry)\n\n policy = CleanProfilePolicy(\n policy_id=\"legacy-pol-1\",\n policy_version=\"1.0.0\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=\"legacy-reg-1\",\n prohibited_artifact_categories=[\"test-data\"],\n required_system_categories=[\"core\"],\n effective_from=now,\n )\n setattr(policy, \"immutable\", True)\n setattr(\n policy,\n \"content_json\",\n {\n \"profile\": \"enterprise-clean\",\n \"prohibited_artifact_categories\": [\"test-data\"],\n \"required_system_categories\": [\"core\"],\n \"external_source_forbidden\": True,\n },\n )\n repo.save_policy(policy)\n\n repo.save_manifest(\n DistributionManifest(\n id=\"legacy-manifest-1\",\n candidate_id=\"legacy-rc-001\",\n manifest_version=1,\n manifest_digest=\"sha256:legacy-manifest\",\n artifacts_digest=\"sha256:legacy-artifacts\",\n created_at=now,\n created_by=\"compat-tester\",\n source_snapshot_ref=\"git:legacy-001\",\n content_json={\n \"items\": [],\n \"summary\": {\"included_count\": 0, \"prohibited_detected_count\": 0},\n },\n immutable=True,\n )\n )\n\n return repo\n\n\n# [/DEF:_seed_legacy_repo:Function]\n" + "anchor_syntax": "region", + "body": "# #region _seed_legacy_repo [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseLegacyCompat\n# @BRIEF Seed in-memory repository with minimum trusted data for legacy endpoint contracts.\n# @PRE: Repository is empty.\n# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.\ndef _seed_legacy_repo() -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n now = datetime.now(UTC)\n repo.save_candidate(\n ReleaseCandidate(\n id=\"legacy-rc-001\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:legacy-001\",\n created_at=now,\n created_by=\"compat-tester\",\n status=ReleaseCandidateStatus.DRAFT,\n )\n )\n registry = ResourceSourceRegistry(\n registry_id=\"legacy-reg-1\",\n name=\"Legacy Internal Registry\",\n entries=[\n ResourceSourceEntry(\n source_id=\"legacy-src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n )\n ],\n updated_at=now,\n updated_by=\"compat-tester\",\n status=\"ACTIVE\",\n )\n registry.immutable = True\n registry.allowed_hosts = [\"repo.intra.company.local\"]\n registry.allowed_schemes = [\"https\"]\n registry.allowed_source_types = [\"artifact-repo\"]\n repo.save_registry(registry)\n policy = CleanProfilePolicy(\n policy_id=\"legacy-pol-1\",\n policy_version=\"1.0.0\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n active=True,\n internal_source_registry_ref=\"legacy-reg-1\",\n prohibited_artifact_categories=[\"test-data\"],\n required_system_categories=[\"core\"],\n effective_from=now,\n )\n policy.immutable = True\n policy.content_json = {\"profile\": \"enterprise-clean\", \"prohibited_artifact_categories\": [\"test-data\"], \"required_system_categories\": [\"core\"], \"external_source_forbidden\": True}\n repo.save_policy(policy)\n repo.save_manifest(\n DistributionManifest(\n id=\"legacy-manifest-1\",\n candidate_id=\"legacy-rc-001\",\n manifest_version=1,\n manifest_digest=\"sha256:legacy-manifest\",\n artifacts_digest=\"sha256:legacy-artifacts\",\n created_at=now,\n created_by=\"compat-tester\",\n source_snapshot_ref=\"git:legacy-001\",\n content_json={\n \"items\": [],\n \"summary\": {\"included_count\": 0, \"prohibited_detected_count\": 0},\n },\n immutable=True,\n )\n )\n return repo\n# #endregion _seed_legacy_repo\n" }, { "contract_id": "test_legacy_prepare_endpoint_still_available", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py", - "start_line": 122, - "end_line": 149, + "start_line": 101, + "end_line": 126, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -1981,15 +1254,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_legacy_prepare_endpoint_still_available:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat\n# @PURPOSE: Verify legacy prepare endpoint remains reachable and returns a status payload.\ndef test_legacy_prepare_endpoint_still_available() -> None:\n repo = _seed_legacy_repo()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"legacy-rc-001\",\n \"artifacts\": [\n {\"path\": \"src/main.py\", \"category\": \"core\", \"reason\": \"required\"}\n ],\n \"sources\": [\"repo.intra.company.local\"],\n \"operator_id\": \"compat-tester\",\n },\n )\n assert response.status_code == 200\n payload = response.json()\n assert \"status\" in payload\n assert payload[\"status\"] in {\"prepared\", \"blocked\", \"PREPARED\", \"BLOCKED\"}\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_legacy_prepare_endpoint_still_available:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_legacy_prepare_endpoint_still_available [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseLegacyCompat\n# @BRIEF Verify legacy prepare endpoint remains reachable and returns a status payload.\ndef test_legacy_prepare_endpoint_still_available() -> None:\n repo = _seed_legacy_repo()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"legacy-rc-001\",\n \"artifacts\": [\n {\"path\": \"src/main.py\", \"category\": \"core\", \"reason\": \"required\"}\n ],\n \"sources\": [\"repo.intra.company.local\"],\n \"operator_id\": \"compat-tester\",\n },\n )\n assert response.status_code == 200\n payload = response.json()\n assert \"status\" in payload\n assert payload[\"status\"] in {\"prepared\", \"blocked\", \"PREPARED\", \"BLOCKED\"}\n finally:\n app.dependency_overrides.clear()\n# #endregion test_legacy_prepare_endpoint_still_available\n" }, { "contract_id": "test_legacy_checks_endpoints_still_available", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py", - "start_line": 152, - "end_line": 186, + "start_line": 127, + "end_line": 158, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2014,75 +1287,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_legacy_checks_endpoints_still_available:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseLegacyCompat\n# @PURPOSE: Verify legacy checks start/status endpoints remain available during v2 transition.\ndef test_legacy_checks_endpoints_still_available() -> None:\n repo = _seed_legacy_repo()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n start_response = client.post(\n \"/api/clean-release/checks\",\n json={\n \"candidate_id\": \"legacy-rc-001\",\n \"profile\": \"enterprise-clean\",\n \"execution_mode\": \"api\",\n \"triggered_by\": \"compat-tester\",\n },\n )\n assert start_response.status_code == 202\n start_payload = start_response.json()\n assert \"check_run_id\" in start_payload\n assert start_payload[\"candidate_id\"] == \"legacy-rc-001\"\n\n status_response = client.get(\n f\"/api/clean-release/checks/{start_payload['check_run_id']}\"\n )\n assert status_response.status_code == 200\n status_payload = status_response.json()\n assert status_payload[\"check_run_id\"] == start_payload[\"check_run_id\"]\n assert \"final_status\" in status_payload\n assert \"checks\" in status_payload\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_legacy_checks_endpoints_still_available:Function]\n" - }, - { - "contract_id": "TestCleanReleaseSourcePolicy", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_clean_release_source_policy.py", - "start_line": 1, - "end_line": 114, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "External endpoints must produce blocking violation entries.", - "LAYER": "Domain", - "PURPOSE": "Validate API behavior for source isolation violations in clean release preparation.", - "SEMANTICS": [ - "tests", - "api", - "clean-release", - "source-policy" - ] - }, - "relations": [ - { - "source_id": "TestCleanReleaseSourcePolicy", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "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 BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestCleanReleaseSourcePolicy:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, api, clean-release, source-policy\n# @PURPOSE: Validate API behavior for source isolation violations in clean release preparation.\n# @LAYER: Domain\n# @INVARIANT: External endpoints must produce blocking violation entries.\n\nfrom datetime import datetime, timezone\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.dependencies import get_clean_release_repository\nfrom src.models.clean_release import (\n CleanProfilePolicy,\n ProfileType,\n ReleaseCandidate,\n ReleaseCandidateStatus,\n ResourceSourceEntry,\n ResourceSourceRegistry,\n)\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_repo_with_seed_data:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseSourcePolicy\n# @PURPOSE: Seed repository with candidate, registry, and active policy for source isolation test flow.\ndef _repo_with_seed_data() -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n\n repo.save_candidate(\n ReleaseCandidate(\n candidate_id=\"2026.03.03-rc1\",\n version=\"2026.03.03\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n created_at=datetime.now(timezone.utc),\n created_by=\"tester\",\n source_snapshot_ref=\"git:abc123\",\n status=ReleaseCandidateStatus.DRAFT,\n )\n )\n\n repo.save_registry(\n ResourceSourceRegistry(\n registry_id=\"registry-internal-v1\",\n name=\"Internal\",\n entries=[\n ResourceSourceEntry(\n source_id=\"src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n )\n ],\n updated_at=datetime.now(timezone.utc),\n updated_by=\"tester\",\n status=\"active\",\n )\n )\n\n repo.save_policy(\n CleanProfilePolicy(\n policy_id=\"policy-enterprise-clean-v1\",\n policy_version=\"1.0.0\",\n active=True,\n prohibited_artifact_categories=[\"test-data\"],\n required_system_categories=[\"system-init\"],\n external_source_forbidden=True,\n internal_source_registry_ref=\"registry-internal-v1\",\n effective_from=datetime.now(timezone.utc),\n profile=ProfileType.ENTERPRISE_CLEAN,\n )\n )\n return repo\n\n\n# [/DEF:_repo_with_seed_data:Function]\n\n\n# [DEF:test_prepare_candidate_blocks_external_source:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseSourcePolicy\n# @PURPOSE: Verify candidate preparation is blocked when at least one source host is external to the trusted registry.\ndef test_prepare_candidate_blocks_external_source():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"artifacts\": [\n {\n \"path\": \"cfg/system.yaml\",\n \"category\": \"system-init\",\n \"reason\": \"required\",\n }\n ],\n \"sources\": [\"repo.intra.company.local\", \"pypi.org\"],\n \"operator_id\": \"release-manager\",\n },\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"blocked\"\n assert any(v[\"category\"] == \"external-source\" for v in data[\"violations\"])\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_prepare_candidate_blocks_external_source:Function]\n# [/DEF:TestCleanReleaseSourcePolicy:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_legacy_checks_endpoints_still_available [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseLegacyCompat\n# @BRIEF Verify legacy checks start/status endpoints remain available during v2 transition.\ndef test_legacy_checks_endpoints_still_available() -> None:\n repo = _seed_legacy_repo()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n start_response = client.post(\n \"/api/clean-release/checks\",\n json={\n \"candidate_id\": \"legacy-rc-001\",\n \"profile\": \"enterprise-clean\",\n \"execution_mode\": \"api\",\n \"triggered_by\": \"compat-tester\",\n },\n )\n assert start_response.status_code == 202\n start_payload = start_response.json()\n assert \"check_run_id\" in start_payload\n assert start_payload[\"candidate_id\"] == \"legacy-rc-001\"\n status_response = client.get(\n f\"/api/clean-release/checks/{start_payload['check_run_id']}\"\n )\n assert status_response.status_code == 200\n status_payload = status_response.json()\n assert status_payload[\"check_run_id\"] == start_payload[\"check_run_id\"]\n assert \"final_status\" in status_payload\n assert \"checks\" in status_payload\n finally:\n app.dependency_overrides.clear()\n# #endregion test_legacy_checks_endpoints_still_available\n" }, { "contract_id": "_repo_with_seed_data", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_source_policy.py", - "start_line": 25, - "end_line": 78, + "start_line": 23, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -2116,15 +1329,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_repo_with_seed_data:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseSourcePolicy\n# @PURPOSE: Seed repository with candidate, registry, and active policy for source isolation test flow.\ndef _repo_with_seed_data() -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n\n repo.save_candidate(\n ReleaseCandidate(\n candidate_id=\"2026.03.03-rc1\",\n version=\"2026.03.03\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n created_at=datetime.now(timezone.utc),\n created_by=\"tester\",\n source_snapshot_ref=\"git:abc123\",\n status=ReleaseCandidateStatus.DRAFT,\n )\n )\n\n repo.save_registry(\n ResourceSourceRegistry(\n registry_id=\"registry-internal-v1\",\n name=\"Internal\",\n entries=[\n ResourceSourceEntry(\n source_id=\"src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n )\n ],\n updated_at=datetime.now(timezone.utc),\n updated_by=\"tester\",\n status=\"active\",\n )\n )\n\n repo.save_policy(\n CleanProfilePolicy(\n policy_id=\"policy-enterprise-clean-v1\",\n policy_version=\"1.0.0\",\n active=True,\n prohibited_artifact_categories=[\"test-data\"],\n required_system_categories=[\"system-init\"],\n external_source_forbidden=True,\n internal_source_registry_ref=\"registry-internal-v1\",\n effective_from=datetime.now(timezone.utc),\n profile=ProfileType.ENTERPRISE_CLEAN,\n )\n )\n return repo\n\n\n# [/DEF:_repo_with_seed_data:Function]\n" + "anchor_syntax": "region", + "body": "# #region _repo_with_seed_data [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseSourcePolicy\n# @BRIEF Seed repository with candidate, registry, and active policy for source isolation test flow.\ndef _repo_with_seed_data() -> CleanReleaseRepository:\n repo = CleanReleaseRepository()\n repo.save_candidate(\n ReleaseCandidate(\n candidate_id=\"2026.03.03-rc1\",\n version=\"2026.03.03\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n created_at=datetime.now(UTC),\n created_by=\"tester\",\n source_snapshot_ref=\"git:abc123\",\n status=ReleaseCandidateStatus.DRAFT,\n )\n )\n repo.save_registry(\n ResourceSourceRegistry(\n registry_id=\"registry-internal-v1\",\n name=\"Internal\",\n entries=[\n ResourceSourceEntry(\n source_id=\"src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n )\n ],\n updated_at=datetime.now(UTC),\n updated_by=\"tester\",\n status=\"active\",\n )\n )\n repo.save_policy(\n CleanProfilePolicy(\n policy_id=\"policy-enterprise-clean-v1\",\n policy_version=\"1.0.0\",\n active=True,\n prohibited_artifact_categories=[\"test-data\"],\n required_system_categories=[\"system-init\"],\n external_source_forbidden=True,\n internal_source_registry_ref=\"registry-internal-v1\",\n effective_from=datetime.now(UTC),\n profile=ProfileType.ENTERPRISE_CLEAN,\n )\n )\n return repo\n# #endregion _repo_with_seed_data\n" }, { "contract_id": "test_prepare_candidate_blocks_external_source", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_source_policy.py", - "start_line": 81, - "end_line": 113, + "start_line": 72, + "end_line": 101, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2149,40 +1362,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_prepare_candidate_blocks_external_source:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseSourcePolicy\n# @PURPOSE: Verify candidate preparation is blocked when at least one source host is external to the trusted registry.\ndef test_prepare_candidate_blocks_external_source():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"artifacts\": [\n {\n \"path\": \"cfg/system.yaml\",\n \"category\": \"system-init\",\n \"reason\": \"required\",\n }\n ],\n \"sources\": [\"repo.intra.company.local\", \"pypi.org\"],\n \"operator_id\": \"release-manager\",\n },\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"blocked\"\n assert any(v[\"category\"] == \"external-source\" for v in data[\"violations\"])\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_prepare_candidate_blocks_external_source:Function]\n" - }, - { - "contract_id": "CleanReleaseV2ApiTests", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_clean_release_v2_api.py", - "start_line": 1, - "end_line": 115, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Domain", - "PURPOSE": "API contract tests for redesigned clean release endpoints." - }, - "relations": [ - { - "source_id": "CleanReleaseV2ApiTests", - "relation_type": "DEPENDS_ON", - "target_id": "CleanReleaseV2Api", - "target_ref": "[CleanReleaseV2Api]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseV2ApiTests:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: API contract tests for redesigned clean release endpoints.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [CleanReleaseV2Api]\n\nfrom datetime import datetime, timezone\nfrom types import SimpleNamespace\nfrom uuid import uuid4\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.dependencies import get_clean_release_repository, get_config_manager\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.enums import CandidateStatus\n\nclient = TestClient(app)\n\n\n# [REASON] Implementing API contract tests for candidate/artifact/manifest endpoints (T012).\n# [DEF:test_candidate_registration_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests\n# @PURPOSE: Validate candidate registration endpoint creates a draft candidate with expected identifier contract.\ndef test_candidate_registration_contract():\n \"\"\"\n @TEST_SCENARIO: candidate_registration -> Should return 201 and candidate DTO.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates -> CandidateDTO\n \"\"\"\n payload = {\n \"id\": \"rc-test-001\",\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n response = client.post(\"/api/v2/clean-release/candidates\", json=payload)\n assert response.status_code == 201\n data = response.json()\n assert data[\"id\"] == \"rc-test-001\"\n assert data[\"status\"] == CandidateStatus.DRAFT.value\n\n\n# [/DEF:test_candidate_registration_contract:Function]\n\n\n# [DEF:test_artifact_import_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests\n# @PURPOSE: Validate artifact import endpoint accepts candidate artifacts and returns success status payload.\ndef test_artifact_import_contract():\n \"\"\"\n @TEST_SCENARIO: artifact_import -> Should return 200 and success status.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/artifacts -> SuccessDTO\n \"\"\"\n candidate_id = \"rc-test-001-art\"\n bootstrap_candidate = {\n \"id\": candidate_id,\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n create_response = client.post(\n \"/api/v2/clean-release/candidates\", json=bootstrap_candidate\n )\n assert create_response.status_code == 201\n\n payload = {\n \"artifacts\": [\n {\"id\": \"art-1\", \"path\": \"bin/app.exe\", \"sha256\": \"hash123\", \"size\": 1024}\n ]\n }\n response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/artifacts\", json=payload\n )\n assert response.status_code == 200\n assert response.json()[\"status\"] == \"success\"\n\n\n# [/DEF:test_artifact_import_contract:Function]\n\n\n# [DEF:test_manifest_build_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests\n# @PURPOSE: Validate manifest build endpoint produces manifest payload linked to the target candidate.\ndef test_manifest_build_contract():\n \"\"\"\n @TEST_SCENARIO: manifest_build -> Should return 201 and manifest DTO.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/manifests -> ManifestDTO\n \"\"\"\n candidate_id = \"rc-test-001-manifest\"\n bootstrap_candidate = {\n \"id\": candidate_id,\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n create_response = client.post(\n \"/api/v2/clean-release/candidates\", json=bootstrap_candidate\n )\n assert create_response.status_code == 201\n\n response = client.post(f\"/api/v2/clean-release/candidates/{candidate_id}/manifests\")\n assert response.status_code == 201\n data = response.json()\n assert \"manifest_digest\" in data\n assert data[\"candidate_id\"] == candidate_id\n\n\n# [/DEF:test_manifest_build_contract:Function]\n# [/DEF:CleanReleaseV2ApiTests:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_prepare_candidate_blocks_external_source [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseSourcePolicy\n# @BRIEF Verify candidate preparation is blocked when at least one source host is external to the trusted registry.\ndef test_prepare_candidate_blocks_external_source():\n repo = _repo_with_seed_data()\n app.dependency_overrides[get_clean_release_repository] = lambda: repo\n try:\n client = TestClient(app)\n response = client.post(\n \"/api/clean-release/candidates/prepare\",\n json={\n \"candidate_id\": \"2026.03.03-rc1\",\n \"artifacts\": [\n {\n \"path\": \"cfg/system.yaml\",\n \"category\": \"system-init\",\n \"reason\": \"required\",\n }\n ],\n \"sources\": [\"repo.intra.company.local\", \"pypi.org\"],\n \"operator_id\": \"release-manager\",\n },\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"blocked\"\n assert any(v[\"category\"] == \"external-source\" for v in data[\"violations\"])\n finally:\n app.dependency_overrides.clear()\n# #endregion test_prepare_candidate_blocks_external_source\n" }, { "contract_id": "test_candidate_registration_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_v2_api.py", - "start_line": 28, - "end_line": 49, + "start_line": 12, + "end_line": 31, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2207,15 +1395,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_candidate_registration_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests\n# @PURPOSE: Validate candidate registration endpoint creates a draft candidate with expected identifier contract.\ndef test_candidate_registration_contract():\n \"\"\"\n @TEST_SCENARIO: candidate_registration -> Should return 201 and candidate DTO.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates -> CandidateDTO\n \"\"\"\n payload = {\n \"id\": \"rc-test-001\",\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n response = client.post(\"/api/v2/clean-release/candidates\", json=payload)\n assert response.status_code == 201\n data = response.json()\n assert data[\"id\"] == \"rc-test-001\"\n assert data[\"status\"] == CandidateStatus.DRAFT.value\n\n\n# [/DEF:test_candidate_registration_contract:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_candidate_registration_contract [TYPE Function]\n# @RELATION BINDS_TO -> CleanReleaseV2ApiTests\n# @BRIEF Validate candidate registration endpoint creates a draft candidate with expected identifier contract.\ndef test_candidate_registration_contract():\n \"\"\"\n @TEST_SCENARIO: candidate_registration -> Should return 201 and candidate DTO.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates -> CandidateDTO\n \"\"\"\n payload = {\n \"id\": \"rc-test-001\",\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n response = client.post(\"/api/v2/clean-release/candidates\", json=payload)\n assert response.status_code == 201\n data = response.json()\n assert data[\"id\"] == \"rc-test-001\"\n assert data[\"status\"] == CandidateStatus.DRAFT.value\n# #endregion test_candidate_registration_contract\n" }, { "contract_id": "test_artifact_import_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_v2_api.py", - "start_line": 52, - "end_line": 84, + "start_line": 32, + "end_line": 61, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2240,15 +1428,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_artifact_import_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests\n# @PURPOSE: Validate artifact import endpoint accepts candidate artifacts and returns success status payload.\ndef test_artifact_import_contract():\n \"\"\"\n @TEST_SCENARIO: artifact_import -> Should return 200 and success status.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/artifacts -> SuccessDTO\n \"\"\"\n candidate_id = \"rc-test-001-art\"\n bootstrap_candidate = {\n \"id\": candidate_id,\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n create_response = client.post(\n \"/api/v2/clean-release/candidates\", json=bootstrap_candidate\n )\n assert create_response.status_code == 201\n\n payload = {\n \"artifacts\": [\n {\"id\": \"art-1\", \"path\": \"bin/app.exe\", \"sha256\": \"hash123\", \"size\": 1024}\n ]\n }\n response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/artifacts\", json=payload\n )\n assert response.status_code == 200\n assert response.json()[\"status\"] == \"success\"\n\n\n# [/DEF:test_artifact_import_contract:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_artifact_import_contract [TYPE Function]\n# @RELATION BINDS_TO -> CleanReleaseV2ApiTests\n# @BRIEF Validate artifact import endpoint accepts candidate artifacts and returns success status payload.\ndef test_artifact_import_contract():\n \"\"\"\n @TEST_SCENARIO: artifact_import -> Should return 200 and success status.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/artifacts -> SuccessDTO\n \"\"\"\n candidate_id = \"rc-test-001-art\"\n bootstrap_candidate = {\n \"id\": candidate_id,\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n create_response = client.post(\n \"/api/v2/clean-release/candidates\", json=bootstrap_candidate\n )\n assert create_response.status_code == 201\n payload = {\n \"artifacts\": [\n {\"id\": \"art-1\", \"path\": \"bin/app.exe\", \"sha256\": \"hash123\", \"size\": 1024}\n ]\n }\n response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/artifacts\", json=payload\n )\n assert response.status_code == 200\n assert response.json()[\"status\"] == \"success\"\n# #endregion test_artifact_import_contract\n" }, { "contract_id": "test_manifest_build_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_v2_api.py", - "start_line": 87, - "end_line": 114, + "start_line": 62, + "end_line": 86, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2273,40 +1461,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_manifest_build_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ApiTests\n# @PURPOSE: Validate manifest build endpoint produces manifest payload linked to the target candidate.\ndef test_manifest_build_contract():\n \"\"\"\n @TEST_SCENARIO: manifest_build -> Should return 201 and manifest DTO.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/manifests -> ManifestDTO\n \"\"\"\n candidate_id = \"rc-test-001-manifest\"\n bootstrap_candidate = {\n \"id\": candidate_id,\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n create_response = client.post(\n \"/api/v2/clean-release/candidates\", json=bootstrap_candidate\n )\n assert create_response.status_code == 201\n\n response = client.post(f\"/api/v2/clean-release/candidates/{candidate_id}/manifests\")\n assert response.status_code == 201\n data = response.json()\n assert \"manifest_digest\" in data\n assert data[\"candidate_id\"] == candidate_id\n\n\n# [/DEF:test_manifest_build_contract:Function]\n" - }, - { - "contract_id": "CleanReleaseV2ReleaseApiTests", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py", - "start_line": 1, - "end_line": 127, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Domain", - "PURPOSE": "API contract test scaffolding for clean release approval and publication endpoints." - }, - "relations": [ - { - "source_id": "CleanReleaseV2ReleaseApiTests", - "relation_type": "DEPENDS_ON", - "target_id": "CleanReleaseV2Api", - "target_ref": "[CleanReleaseV2Api]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseV2ReleaseApiTests:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: API contract test scaffolding for clean release approval and publication endpoints.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [CleanReleaseV2Api]\n\n\"\"\"Contract tests for redesigned approval/publication API endpoints.\"\"\"\n\nfrom datetime import datetime, timezone\nfrom uuid import uuid4\n\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\n\nfrom src.api.routes.clean_release_v2 import router as clean_release_v2_router\nfrom src.dependencies import get_clean_release_repository\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision\n\n\ntest_app = FastAPI()\ntest_app.include_router(clean_release_v2_router)\nclient = TestClient(test_app)\n\n\n# [DEF:_seed_candidate_and_passed_report:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @PURPOSE: Seed repository with approvable candidate and passed report for release endpoint contracts.\ndef _seed_candidate_and_passed_report() -> tuple[str, str]:\n repository = get_clean_release_repository()\n candidate_id = f\"api-release-candidate-{uuid4()}\"\n report_id = f\"api-release-report-{uuid4()}\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-api-release\",\n created_by=\"api-test\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n return candidate_id, report_id\n\n\n# [/DEF:_seed_candidate_and_passed_report:Function]\n\n\n# [DEF:test_release_approve_and_publish_revoke_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @PURPOSE: Verify approve, publish, and revoke endpoints preserve expected release lifecycle contract.\ndef test_release_approve_and_publish_revoke_contract() -> None:\n \"\"\"Contract for approve -> publish -> revoke lifecycle endpoints.\"\"\"\n candidate_id, report_id = _seed_candidate_and_passed_report()\n\n approve_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/approve\",\n json={\"report_id\": report_id, \"decided_by\": \"api-test\", \"comment\": \"approved\"},\n )\n assert approve_response.status_code == 200\n approve_payload = approve_response.json()\n assert approve_payload[\"status\"] == \"ok\"\n assert approve_payload[\"decision\"] == \"APPROVED\"\n\n publish_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/publish\",\n json={\n \"report_id\": report_id,\n \"published_by\": \"api-test\",\n \"target_channel\": \"stable\",\n \"publication_ref\": \"rel-api-001\",\n },\n )\n assert publish_response.status_code == 200\n publish_payload = publish_response.json()\n assert publish_payload[\"status\"] == \"ok\"\n assert publish_payload[\"publication\"][\"status\"] == \"ACTIVE\"\n\n publication_id = publish_payload[\"publication\"][\"id\"]\n revoke_response = client.post(\n f\"/api/v2/clean-release/publications/{publication_id}/revoke\",\n json={\"revoked_by\": \"api-test\", \"comment\": \"rollback\"},\n )\n assert revoke_response.status_code == 200\n revoke_payload = revoke_response.json()\n assert revoke_payload[\"status\"] == \"ok\"\n assert revoke_payload[\"publication\"][\"status\"] == \"REVOKED\"\n\n\n# [/DEF:test_release_approve_and_publish_revoke_contract:Function]\n\n\n# [DEF:test_release_reject_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @PURPOSE: Verify reject endpoint returns successful rejection decision payload.\ndef test_release_reject_contract() -> None:\n \"\"\"Contract for reject endpoint.\"\"\"\n candidate_id, report_id = _seed_candidate_and_passed_report()\n\n reject_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/reject\",\n json={\"report_id\": report_id, \"decided_by\": \"api-test\", \"comment\": \"rejected\"},\n )\n assert reject_response.status_code == 200\n payload = reject_response.json()\n assert payload[\"status\"] == \"ok\"\n assert payload[\"decision\"] == \"REJECTED\"\n\n\n# [/DEF:test_release_reject_contract:Function]\n# [/DEF:CleanReleaseV2ReleaseApiTests:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_manifest_build_contract [TYPE Function]\n# @RELATION BINDS_TO -> CleanReleaseV2ApiTests\n# @BRIEF Validate manifest build endpoint produces manifest payload linked to the target candidate.\ndef test_manifest_build_contract():\n \"\"\"\n @TEST_SCENARIO: manifest_build -> Should return 201 and manifest DTO.\n @TEST_CONTRACT: POST /api/v2/clean-release/candidates/{id}/manifests -> ManifestDTO\n \"\"\"\n candidate_id = \"rc-test-001-manifest\"\n bootstrap_candidate = {\n \"id\": candidate_id,\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:sha123\",\n \"created_by\": \"test-user\",\n }\n create_response = client.post(\n \"/api/v2/clean-release/candidates\", json=bootstrap_candidate\n )\n assert create_response.status_code == 201\n response = client.post(f\"/api/v2/clean-release/candidates/{candidate_id}/manifests\")\n assert response.status_code == 201\n data = response.json()\n assert \"manifest_digest\" in data\n assert data[\"candidate_id\"] == candidate_id\n# #endregion test_manifest_build_contract\n" }, { "contract_id": "_seed_candidate_and_passed_report", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py", - "start_line": 26, - "end_line": 62, + "start_line": 20, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -2340,15 +1503,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_seed_candidate_and_passed_report:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @PURPOSE: Seed repository with approvable candidate and passed report for release endpoint contracts.\ndef _seed_candidate_and_passed_report() -> tuple[str, str]:\n repository = get_clean_release_repository()\n candidate_id = f\"api-release-candidate-{uuid4()}\"\n report_id = f\"api-release-report-{uuid4()}\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-api-release\",\n created_by=\"api-test\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n return candidate_id, report_id\n\n\n# [/DEF:_seed_candidate_and_passed_report:Function]\n" + "anchor_syntax": "region", + "body": "# #region _seed_candidate_and_passed_report [TYPE Function]\n# @RELATION BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @BRIEF Seed repository with approvable candidate and passed report for release endpoint contracts.\ndef _seed_candidate_and_passed_report() -> tuple[str, str]:\n repository = get_clean_release_repository()\n candidate_id = f\"api-release-candidate-{uuid4()}\"\n report_id = f\"api-release-report-{uuid4()}\"\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-api-release\",\n created_by=\"api-test\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return candidate_id, report_id\n# #endregion _seed_candidate_and_passed_report\n" }, { "contract_id": "test_release_approve_and_publish_revoke_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py", - "start_line": 65, - "end_line": 106, + "start_line": 54, + "end_line": 90, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2373,15 +1536,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_release_approve_and_publish_revoke_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @PURPOSE: Verify approve, publish, and revoke endpoints preserve expected release lifecycle contract.\ndef test_release_approve_and_publish_revoke_contract() -> None:\n \"\"\"Contract for approve -> publish -> revoke lifecycle endpoints.\"\"\"\n candidate_id, report_id = _seed_candidate_and_passed_report()\n\n approve_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/approve\",\n json={\"report_id\": report_id, \"decided_by\": \"api-test\", \"comment\": \"approved\"},\n )\n assert approve_response.status_code == 200\n approve_payload = approve_response.json()\n assert approve_payload[\"status\"] == \"ok\"\n assert approve_payload[\"decision\"] == \"APPROVED\"\n\n publish_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/publish\",\n json={\n \"report_id\": report_id,\n \"published_by\": \"api-test\",\n \"target_channel\": \"stable\",\n \"publication_ref\": \"rel-api-001\",\n },\n )\n assert publish_response.status_code == 200\n publish_payload = publish_response.json()\n assert publish_payload[\"status\"] == \"ok\"\n assert publish_payload[\"publication\"][\"status\"] == \"ACTIVE\"\n\n publication_id = publish_payload[\"publication\"][\"id\"]\n revoke_response = client.post(\n f\"/api/v2/clean-release/publications/{publication_id}/revoke\",\n json={\"revoked_by\": \"api-test\", \"comment\": \"rollback\"},\n )\n assert revoke_response.status_code == 200\n revoke_payload = revoke_response.json()\n assert revoke_payload[\"status\"] == \"ok\"\n assert revoke_payload[\"publication\"][\"status\"] == \"REVOKED\"\n\n\n# [/DEF:test_release_approve_and_publish_revoke_contract:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_release_approve_and_publish_revoke_contract [TYPE Function]\n# @RELATION BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @BRIEF Verify approve, publish, and revoke endpoints preserve expected release lifecycle contract.\ndef test_release_approve_and_publish_revoke_contract() -> None:\n \"\"\"Contract for approve -> publish -> revoke lifecycle endpoints.\"\"\"\n candidate_id, report_id = _seed_candidate_and_passed_report()\n approve_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/approve\",\n json={\"report_id\": report_id, \"decided_by\": \"api-test\", \"comment\": \"approved\"},\n )\n assert approve_response.status_code == 200\n approve_payload = approve_response.json()\n assert approve_payload[\"status\"] == \"ok\"\n assert approve_payload[\"decision\"] == \"APPROVED\"\n publish_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/publish\",\n json={\n \"report_id\": report_id,\n \"published_by\": \"api-test\",\n \"target_channel\": \"stable\",\n \"publication_ref\": \"rel-api-001\",\n },\n )\n assert publish_response.status_code == 200\n publish_payload = publish_response.json()\n assert publish_payload[\"status\"] == \"ok\"\n assert publish_payload[\"publication\"][\"status\"] == \"ACTIVE\"\n publication_id = publish_payload[\"publication\"][\"id\"]\n revoke_response = client.post(\n f\"/api/v2/clean-release/publications/{publication_id}/revoke\",\n json={\"revoked_by\": \"api-test\", \"comment\": \"rollback\"},\n )\n assert revoke_response.status_code == 200\n revoke_payload = revoke_response.json()\n assert revoke_payload[\"status\"] == \"ok\"\n assert revoke_payload[\"publication\"][\"status\"] == \"REVOKED\"\n# #endregion test_release_approve_and_publish_revoke_contract\n" }, { "contract_id": "test_release_reject_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py", - "start_line": 109, - "end_line": 126, + "start_line": 91, + "end_line": 105, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2406,40 +1569,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_release_reject_contract:Function]\n# @RELATION: BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @PURPOSE: Verify reject endpoint returns successful rejection decision payload.\ndef test_release_reject_contract() -> None:\n \"\"\"Contract for reject endpoint.\"\"\"\n candidate_id, report_id = _seed_candidate_and_passed_report()\n\n reject_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/reject\",\n json={\"report_id\": report_id, \"decided_by\": \"api-test\", \"comment\": \"rejected\"},\n )\n assert reject_response.status_code == 200\n payload = reject_response.json()\n assert payload[\"status\"] == \"ok\"\n assert payload[\"decision\"] == \"REJECTED\"\n\n\n# [/DEF:test_release_reject_contract:Function]\n" - }, - { - "contract_id": "ConnectionsRoutesTests", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_connections_routes.py", - "start_line": 1, - "end_line": 87, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "API", - "PURPOSE": "Verifies connection routes bootstrap their table before CRUD access." - }, - "relations": [ - { - "source_id": "ConnectionsRoutesTests", - "relation_type": "DEPENDS_ON", - "target_id": "ConnectionsRouter", - "target_ref": "ConnectionsRouter" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ConnectionsRoutesTests:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Verifies connection routes bootstrap their table before CRUD access.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> ConnectionsRouter\n\nimport os\nimport sys\nimport asyncio\nfrom pathlib import Path\n\nimport pytest\nfrom sqlalchemy import create_engine, inspect\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import StaticPool\n\n# Force SQLite in-memory for database module imports.\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"TASKS_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"AUTH_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"ENVIRONMENT\"] = \"testing\"\n\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\n\n@pytest.fixture\ndef db_session():\n engine = create_engine(\n \"sqlite:///:memory:\",\n connect_args={\"check_same_thread\": False},\n poolclass=StaticPool,\n )\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n\n\n# [DEF:test_list_connections_bootstraps_missing_table:Function]\n# @RELATION: BINDS_TO -> ConnectionsRoutesTests\n# @PURPOSE: Ensure listing connections auto-creates missing table and returns empty payload.\ndef test_list_connections_bootstraps_missing_table(db_session):\n from src.api.routes.connections import list_connections\n\n result = asyncio.run(list_connections(db=db_session))\n\n inspector = inspect(db_session.get_bind())\n assert result == []\n assert \"connection_configs\" in inspector.get_table_names()\n\n\n# [/DEF:test_list_connections_bootstraps_missing_table:Function]\n\n\n# [DEF:test_create_connection_bootstraps_missing_table:Function]\n# @RELATION: BINDS_TO -> ConnectionsRoutesTests\n# @PURPOSE: Ensure connection creation bootstraps table and persists returned connection fields.\ndef test_create_connection_bootstraps_missing_table(db_session):\n from src.api.routes.connections import ConnectionCreate, create_connection\n\n payload = ConnectionCreate(\n name=\"Analytics Warehouse\",\n type=\"postgres\",\n host=\"warehouse.internal\",\n port=5432,\n database=\"analytics\",\n username=\"reporter\",\n password=\"secret\",\n )\n\n created = asyncio.run(create_connection(connection=payload, db=db_session))\n\n inspector = inspect(db_session.get_bind())\n assert created.name == \"Analytics Warehouse\"\n assert created.host == \"warehouse.internal\"\n assert \"connection_configs\" in inspector.get_table_names()\n\n\n# [/DEF:test_create_connection_bootstraps_missing_table:Function]\n# [/DEF:ConnectionsRoutesTests:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_release_reject_contract [TYPE Function]\n# @RELATION BINDS_TO -> CleanReleaseV2ReleaseApiTests\n# @BRIEF Verify reject endpoint returns successful rejection decision payload.\ndef test_release_reject_contract() -> None:\n \"\"\"Contract for reject endpoint.\"\"\"\n candidate_id, report_id = _seed_candidate_and_passed_report()\n reject_response = client.post(\n f\"/api/v2/clean-release/candidates/{candidate_id}/reject\",\n json={\"report_id\": report_id, \"decided_by\": \"api-test\", \"comment\": \"rejected\"},\n )\n assert reject_response.status_code == 200\n payload = reject_response.json()\n assert payload[\"status\"] == \"ok\"\n assert payload[\"decision\"] == \"REJECTED\"\n# #endregion test_release_reject_contract\n" }, { "contract_id": "test_list_connections_bootstraps_missing_table", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_connections_routes.py", - "start_line": 46, - "end_line": 59, + "start_line": 39, + "end_line": 48, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2464,15 +1602,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_list_connections_bootstraps_missing_table:Function]\n# @RELATION: BINDS_TO -> ConnectionsRoutesTests\n# @PURPOSE: Ensure listing connections auto-creates missing table and returns empty payload.\ndef test_list_connections_bootstraps_missing_table(db_session):\n from src.api.routes.connections import list_connections\n\n result = asyncio.run(list_connections(db=db_session))\n\n inspector = inspect(db_session.get_bind())\n assert result == []\n assert \"connection_configs\" in inspector.get_table_names()\n\n\n# [/DEF:test_list_connections_bootstraps_missing_table:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_list_connections_bootstraps_missing_table [TYPE Function]\n# @RELATION BINDS_TO -> ConnectionsRoutesTests\n# @BRIEF Ensure listing connections auto-creates missing table and returns empty payload.\ndef test_list_connections_bootstraps_missing_table(db_session):\n from src.api.routes.connections import list_connections\n result = asyncio.run(list_connections(db=db_session))\n inspector = inspect(db_session.get_bind())\n assert result == []\n assert \"connection_configs\" in inspector.get_table_names()\n# #endregion test_list_connections_bootstraps_missing_table\n" }, { "contract_id": "test_create_connection_bootstraps_missing_table", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_connections_routes.py", - "start_line": 62, - "end_line": 86, + "start_line": 49, + "end_line": 68, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2497,40 +1635,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_create_connection_bootstraps_missing_table:Function]\n# @RELATION: BINDS_TO -> ConnectionsRoutesTests\n# @PURPOSE: Ensure connection creation bootstraps table and persists returned connection fields.\ndef test_create_connection_bootstraps_missing_table(db_session):\n from src.api.routes.connections import ConnectionCreate, create_connection\n\n payload = ConnectionCreate(\n name=\"Analytics Warehouse\",\n type=\"postgres\",\n host=\"warehouse.internal\",\n port=5432,\n database=\"analytics\",\n username=\"reporter\",\n password=\"secret\",\n )\n\n created = asyncio.run(create_connection(connection=payload, db=db_session))\n\n inspector = inspect(db_session.get_bind())\n assert created.name == \"Analytics Warehouse\"\n assert created.host == \"warehouse.internal\"\n assert \"connection_configs\" in inspector.get_table_names()\n\n\n# [/DEF:test_create_connection_bootstraps_missing_table:Function]\n" - }, - { - "contract_id": "DashboardsApiTests", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 1, - "end_line": 1051, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "API", - "PURPOSE": "Unit tests for dashboards API endpoints." - }, - "relations": [ - { - "source_id": "DashboardsApiTests", - "relation_type": "DEPENDS_ON", - "target_id": "DashboardsApi", - "target_ref": "[DashboardsApi]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DashboardsApiTests:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for dashboards API endpoints.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DashboardsApi]\n\nimport pytest\nfrom unittest.mock import MagicMock, patch, AsyncMock\nfrom datetime import datetime, timezone\nfrom fastapi.testclient import TestClient\nfrom src.app import app\nfrom src.api.routes.dashboards import DashboardsResponse\nfrom src.dependencies import (\n get_current_user,\n has_permission,\n get_config_manager,\n get_task_manager,\n get_resource_service,\n get_mapping_service,\n)\nfrom src.core.database import get_db\nfrom src.services.profile_service import ProfileService as DomainProfileService\n\n# Global mock user for get_current_user dependency overrides\nmock_user = MagicMock()\nmock_user.id = \"u-1\"\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture(autouse=True)\ndef mock_deps():\n config_manager = MagicMock()\n task_manager = MagicMock()\n resource_service = MagicMock()\n mapping_service = MagicMock()\n\n db = MagicMock()\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n app.dependency_overrides[get_mapping_service] = lambda: mapping_service\n app.dependency_overrides[get_current_user] = lambda: mock_user\n app.dependency_overrides[get_db] = lambda: db\n\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:migration\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:backup\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"tasks\", \"READ\")] = lambda: mock_user\n\n yield {\n \"config\": config_manager,\n \"task\": task_manager,\n \"resource\": resource_service,\n \"mapping\": mapping_service,\n \"db\": db,\n }\n app.dependency_overrides.clear()\n\n\nclient = TestClient(app)\n\n\n# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns a populated response that satisfies the schema contract.\n# @TEST: GET /api/dashboards returns 200 and valid schema\n# @PRE: env_id exists\n# @POST: Response matches DashboardsResponse schema\ndef test_get_dashboards_success(mock_deps):\n \"\"\"Uses @TEST_FIXTURE: dashboard_list_happy data.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n # @TEST_FIXTURE: dashboard_list_happy -> {\"id\": 1, \"title\": \"Main Revenue\"}\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Main Revenue\",\n \"slug\": \"main-revenue\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": {\"task_id\": \"task-1\", \"status\": \"SUCCESS\"},\n }\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod\")\n\n assert response.status_code == 200\n data = response.json()\n # exhaustive @POST assertions\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Main Revenue\"\n assert data[\"total\"] == 1\n assert \"page\" in data\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_success:Function]\n\n\n# [DEF:test_get_dashboards_with_search:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing applies the search filter and returns only matching rows.\n# @TEST: GET /api/dashboards filters by search term\n# @PRE: search parameter provided\n# @POST: Only matching dashboards returned\ndef test_get_dashboards_with_search(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n async def mock_get_dashboards(env, tasks, include_git_status=False):\n return [\n {\n \"id\": 1,\n \"title\": \"Sales Report\",\n \"slug\": \"sales\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"title\": \"Marketing Dashboard\",\n \"slug\": \"marketing\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n ]\n\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=mock_get_dashboards\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&search=sales\")\n\n assert response.status_code == 200\n data = response.json()\n # @POST: Filtered result count must match search\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales Report\"\n\n\n# [/DEF:test_get_dashboards_with_search:Function]\n\n\n# [DEF:test_get_dashboards_empty:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns an empty payload for an environment without dashboards.\n# @TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\ndef test_get_dashboards_empty(mock_deps):\n \"\"\"@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"empty_env\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(return_value=[])\n\n response = client.get(\"/api/dashboards?env_id=empty_env\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert len(data[\"dashboards\"]) == 0\n assert data[\"total_pages\"] == 1\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_empty:Function]\n\n\n# [DEF:test_get_dashboards_superset_failure:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing surfaces a 503 contract when Superset access fails.\n# @TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\ndef test_get_dashboards_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n\n response = client.get(\"/api/dashboards?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch dashboards\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_superset_failure:Function]\n\n\n# [DEF:test_get_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns 404 when the requested environment does not exist.\n# @TEST: GET /api/dashboards returns 404 if env_id missing\n# @PRE: env_id does not exist\n# @POST: Returns 404 error\ndef test_get_dashboards_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards?env_id=nonexistent\")\n\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_env_not_found:Function]\n\n\n# [DEF:test_get_dashboards_invalid_pagination:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing rejects invalid pagination parameters with 400 responses.\n# @TEST: GET /api/dashboards returns 400 for invalid page/page_size\n# @PRE: page < 1 or page_size > 100\n# @POST: Returns 400 error\ndef test_get_dashboards_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n # Invalid page\n response = client.get(\"/api/dashboards?env_id=prod&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n\n # Invalid page_size\n response = client.get(\"/api/dashboards?env_id=prod&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_invalid_pagination:Function]\n\n\n# [DEF:test_get_dashboard_detail_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard detail returns charts and datasets for an existing dashboard.\n# @TEST: GET /api/dashboards/{id} returns dashboard detail with charts and datasets\ndef test_get_dashboard_detail_success(mock_deps):\n with patch(\"src.api.routes.dashboards._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n mock_client.get_dashboard_detail.return_value = {\n \"id\": 42,\n \"title\": \"Revenue Dashboard\",\n \"slug\": \"revenue-dashboard\",\n \"url\": \"/superset/dashboard/42/\",\n \"description\": \"Overview\",\n \"last_modified\": \"2026-02-20T10:00:00+00:00\",\n \"published\": True,\n \"charts\": [\n {\n \"id\": 100,\n \"title\": \"Revenue by Month\",\n \"viz_type\": \"line\",\n \"dataset_id\": 7,\n \"last_modified\": \"2026-02-19T10:00:00+00:00\",\n \"overview\": \"line\",\n }\n ],\n \"datasets\": [\n {\n \"id\": 7,\n \"table_name\": \"fact_revenue\",\n \"schema\": \"mart\",\n \"database\": \"Analytics\",\n \"last_modified\": \"2026-02-18T10:00:00+00:00\",\n \"overview\": \"mart.fact_revenue\",\n }\n ],\n \"chart_count\": 1,\n \"dataset_count\": 1,\n }\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42?env_id=prod\")\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"id\"] == 42\n assert payload[\"chart_count\"] == 1\n assert payload[\"dataset_count\"] == 1\n\n\n# [/DEF:test_get_dashboard_detail_success:Function]\n\n\n# [DEF:test_get_dashboard_detail_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard detail returns 404 when the requested environment is missing.\n# @TEST: GET /api/dashboards/{id} returns 404 for missing environment\ndef test_get_dashboard_detail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n\n response = client.get(\"/api/dashboards/42?env_id=missing\")\n\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboard_detail_env_not_found:Function]\n\n\n# [DEF:test_migrate_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/migrate creates migration task\n# @PRE: Valid source_env_id, target_env_id, dashboard_ids\n# @PURPOSE: Validate dashboard migration request creates an async task and returns its identifier.\n# @POST: Returns task_id and create_task was called\ndef test_migrate_dashboards_success(mock_deps):\n mock_source = MagicMock()\n mock_source.id = \"source\"\n mock_target = MagicMock()\n mock_target.id = \"target\"\n mock_deps[\"config\"].get_environments.return_value = [mock_source, mock_target]\n\n mock_task = MagicMock()\n mock_task.id = \"task-migrate-123\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\n \"source_env_id\": \"source\",\n \"target_env_id\": \"target\",\n \"dashboard_ids\": [1, 2, 3],\n \"db_mappings\": {\"old_db\": \"new_db\"},\n },\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_migrate_dashboards_success:Function]\n\n\n# [DEF:test_migrate_dashboards_no_ids:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/migrate returns 400 for empty dashboard_ids\n# @PRE: dashboard_ids is empty\n# @PURPOSE: Validate dashboard migration rejects empty dashboard identifier lists.\n# @POST: Returns 400 error\ndef test_migrate_dashboards_no_ids(mock_deps):\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\n \"source_env_id\": \"source\",\n \"target_env_id\": \"target\",\n \"dashboard_ids\": [],\n },\n )\n\n assert response.status_code == 400\n assert \"At least one dashboard ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_no_ids:Function]\n\n\n# [DEF:test_migrate_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate migration creation returns 404 when the source environment cannot be resolved.\n# @PRE: source_env_id and target_env_id are valid environment IDs\ndef test_migrate_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id and target_env_id are valid environment IDs.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"ghost\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 404\n assert \"Source environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_env_not_found:Function]\n\n\n# [DEF:test_backup_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/backup creates backup task\n# @PRE: Valid env_id, dashboard_ids\n# @PURPOSE: Validate dashboard backup request creates an async backup task and returns its identifier.\n# @POST: Returns task_id and create_task was called\ndef test_backup_dashboards_success(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_task = MagicMock()\n mock_task.id = \"task-backup-456\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/dashboards/backup\",\n json={\"env_id\": \"prod\", \"dashboard_ids\": [1, 2, 3], \"schedule\": \"0 0 * * *\"},\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_backup_dashboards_success:Function]\n\n\n# [DEF:test_backup_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate backup task creation returns 404 when the target environment is missing.\n# @PRE: env_id is a valid environment ID\ndef test_backup_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: env_id is a valid environment ID.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"ghost\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_backup_dashboards_env_not_found:Function]\n\n\n# [DEF:test_get_database_mappings_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards/db-mappings returns mapping suggestions\n# @PRE: Valid source_env_id, target_env_id\n# @PURPOSE: Validate database mapping suggestions are returned for valid source and target environments.\n# @POST: Returns list of database mappings\ndef test_get_database_mappings_success(mock_deps):\n mock_source = MagicMock()\n mock_source.id = \"prod\"\n mock_target = MagicMock()\n mock_target.id = \"staging\"\n mock_deps[\"config\"].get_environments.return_value = [mock_source, mock_target]\n\n mock_deps[\"mapping\"].get_suggestions = AsyncMock(\n return_value=[\n {\n \"source_db\": \"old_sales\",\n \"target_db\": \"new_sales\",\n \"source_db_uuid\": \"uuid-1\",\n \"target_db_uuid\": \"uuid-2\",\n \"confidence\": 0.95,\n }\n ]\n )\n\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=prod&target_env_id=staging\"\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"mappings\" in data\n assert len(data[\"mappings\"]) == 1\n assert data[\"mappings\"][0][\"confidence\"] == 0.95\n\n\n# [/DEF:test_get_database_mappings_success:Function]\n\n\n# [DEF:test_get_database_mappings_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate database mapping suggestions return 404 when either environment is missing.\n# @PRE: source_env_id and target_env_id are valid environment IDs\ndef test_get_database_mappings_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id must be a valid environment.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t\"\n )\n assert response.status_code == 404\n\n\n# [/DEF:test_get_database_mappings_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_tasks_history_filters_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard task history returns only related backup and LLM tasks.\n# @TEST: GET /api/dashboards/{id}/tasks returns backup and llm tasks for dashboard\ndef test_get_dashboard_tasks_history_filters_success(mock_deps):\n now = datetime.now(timezone.utc)\n\n llm_task = MagicMock()\n llm_task.id = \"task-llm-1\"\n llm_task.plugin_id = \"llm_dashboard_validation\"\n llm_task.status = \"SUCCESS\"\n llm_task.started_at = now\n llm_task.finished_at = now\n llm_task.params = {\"dashboard_id\": \"42\", \"environment_id\": \"prod\"}\n llm_task.result = {\"summary\": \"LLM validation complete\"}\n\n backup_task = MagicMock()\n backup_task.id = \"task-backup-1\"\n backup_task.plugin_id = \"superset-backup\"\n backup_task.status = \"RUNNING\"\n backup_task.started_at = now\n backup_task.finished_at = None\n backup_task.params = {\"env\": \"prod\", \"dashboards\": [42]}\n backup_task.result = {}\n\n other_task = MagicMock()\n other_task.id = \"task-other\"\n other_task.plugin_id = \"superset-backup\"\n other_task.status = \"SUCCESS\"\n other_task.started_at = now\n other_task.finished_at = now\n other_task.params = {\"env\": \"prod\", \"dashboards\": [777]}\n other_task.result = {}\n\n mock_deps[\"task\"].get_all_tasks.return_value = [other_task, llm_task, backup_task]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod&limit=10\")\n\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 2\n assert {item[\"plugin_id\"] for item in data[\"items\"]} == {\n \"llm_dashboard_validation\",\n \"superset-backup\",\n }\n\n\n# [/DEF:test_get_dashboard_tasks_history_filters_success:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard thumbnail endpoint proxies image bytes and content type from Superset.\n# @TEST: GET /api/dashboards/{id}/thumbnail proxies image bytes from Superset\ndef test_get_dashboard_thumbnail_success(mock_deps):\n with patch(\"src.api.routes.dashboards._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n mock_response = MagicMock()\n mock_response.status_code = 200\n mock_response.content = b\"fake-image-bytes\"\n mock_response.headers = {\"Content-Type\": \"image/png\"}\n\n def _network_request(method, endpoint, **kwargs):\n if method == \"POST\":\n return {\"image_url\": \"/api/v1/dashboard/42/screenshot/abc123/\"}\n return mock_response\n\n mock_client.network.request.side_effect = _network_request\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n\n assert response.status_code == 200\n assert response.content == b\"fake-image-bytes\"\n assert response.headers[\"content-type\"].startswith(\"image/png\")\n\n\n# [/DEF:test_get_dashboard_thumbnail_success:Function]\n\n\n# [DEF:_build_profile_preference_stub:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Creates profile preference payload stub for dashboards filter contract tests.\n# @PRE: username can be empty; enabled indicates profile-default toggle state.\n# @POST: Returns object compatible with ProfileService.get_my_preference contract.\ndef _build_profile_preference_stub(username: str, enabled: bool):\n preference = MagicMock()\n preference.superset_username = username\n preference.superset_username_normalized = (\n str(username or \"\").strip().lower() or None\n )\n preference.show_only_my_dashboards = bool(enabled)\n\n payload = MagicMock()\n payload.preference = preference\n return payload\n\n\n# [/DEF:_build_profile_preference_stub:Function]\n\n\n# [DEF:_matches_actor_case_insensitive:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Applies trim + case-insensitive owners OR modified_by matching used by route contract tests.\n# @PRE: owners can be None or list-like values.\n# @POST: Returns True when bound username matches any owner or modified_by.\ndef _matches_actor_case_insensitive(bound_username, owners, modified_by):\n normalized_bound = str(bound_username or \"\").strip().lower()\n if not normalized_bound:\n return False\n\n owner_tokens = []\n for owner in owners or []:\n token = str(owner or \"\").strip().lower()\n if token:\n owner_tokens.append(token)\n\n modified_token = str(modified_by or \"\").strip().lower()\n return normalized_bound in owner_tokens or bool(\n modified_token and modified_token == normalized_bound\n )\n\n\n# [/DEF:_matches_actor_case_insensitive:Function]\n\n\n# [DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards applies profile-default filter with owners OR modified_by trim+case-insensitive semantics.\n# @PURPOSE: Validate profile-default filtering matches owner and modifier aliases using normalized Superset actor values.\n# @PRE: Current user has enabled profile-default preference and bound username.\n# @POST: Response includes only matching dashboards and effective_profile_filter metadata.\ndef test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Owner Match\",\n \"slug\": \"owner-match\",\n \"owners\": [\" John_Doe \"],\n \"modified_by\": \"someone_else\",\n },\n {\n \"id\": 2,\n \"title\": \"Modifier Match\",\n \"slug\": \"modifier-match\",\n \"owners\": [\"analytics-team\"],\n \"modified_by\": \" JOHN_DOE \",\n },\n {\n \"id\": 3,\n \"title\": \"No Match\",\n \"slug\": \"no-match\",\n \"owners\": [\"another-user\"],\n \"modified_by\": \"nobody\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\" JOHN_DOE \",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] == \"john_doe\"\n assert payload[\"effective_profile_filter\"][\"match_logic\"] == \"owners_or_modified_by\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function]\n\n\n# [DEF:test_get_dashboards_override_show_all_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards honors override_show_all and disables profile-default filter for current page.\n# @PURPOSE: Validate override_show_all bypasses profile-default filtering without changing dashboard list semantics.\n# @PRE: Profile-default preference exists but override_show_all=true query is provided.\n# @POST: Response remains unfiltered and effective_profile_filter.applied is false.\ndef test_get_dashboards_override_show_all_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Dash A\",\n \"slug\": \"dash-a\",\n \"owners\": [\"john_doe\"],\n \"modified_by\": \"john_doe\",\n },\n {\n \"id\": 2,\n \"title\": \"Dash B\",\n \"slug\": \"dash-b\",\n \"owners\": [\"other\"],\n \"modified_by\": \"other\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true&override_show_all=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is False\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is True\n assert payload[\"effective_profile_filter\"][\"username\"] is None\n assert payload[\"effective_profile_filter\"][\"match_logic\"] is None\n profile_service.matches_dashboard_actor.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_override_show_all_contract:Function]\n\n\n# [DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards returns empty result set when profile-default filter is active and no dashboard actors match.\n# @PURPOSE: Validate profile-default filtering returns an empty dashboard page when no actor aliases match the bound user.\n# @PRE: Profile-default preference is enabled with bound username and all dashboards are non-matching.\n# @POST: Response total is 0 with deterministic pagination and active effective_profile_filter metadata.\ndef test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 101,\n \"title\": \"Team Dashboard\",\n \"slug\": \"team-dashboard\",\n \"owners\": [\"analytics-team\"],\n \"modified_by\": \"someone_else\",\n },\n {\n \"id\": 102,\n \"title\": \"Ops Dashboard\",\n \"slug\": \"ops-dashboard\",\n \"owners\": [\"ops-user\"],\n \"modified_by\": \"ops-user\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 0\n assert payload[\"dashboards\"] == []\n assert payload[\"page\"] == 1\n assert payload[\"page_size\"] == 10\n assert payload[\"total_pages\"] == 1\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] == \"john_doe\"\n assert payload[\"effective_profile_filter\"][\"match_logic\"] == \"owners_or_modified_by\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function]\n\n\n# [DEF:test_get_dashboards_page_context_other_disables_profile_default:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards does not auto-apply profile-default filter outside dashboards_main page context.\n# @PURPOSE: Validate non-dashboard page contexts suppress profile-default filtering and preserve unfiltered results.\n# @PRE: Profile-default preference exists but page_context=other query is provided.\n# @POST: Response remains unfiltered and metadata reflects source_page=other.\ndef test_get_dashboards_page_context_other_disables_profile_default(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Dash A\",\n \"slug\": \"dash-a\",\n \"owners\": [\"john_doe\"],\n \"modified_by\": \"john_doe\",\n },\n {\n \"id\": 2,\n \"title\": \"Dash B\",\n \"slug\": \"dash-b\",\n \"owners\": [\"other\"],\n \"modified_by\": \"other\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=other&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is False\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"other\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] is None\n assert payload[\"effective_profile_filter\"][\"match_logic\"] is None\n profile_service.matches_dashboard_actor.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_page_context_other_disables_profile_default:Function]\n\n\n# [DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards resolves Superset display-name alias once and filters without per-dashboard detail calls.\n# @PURPOSE: Validate profile-default filtering reuses resolved Superset display aliases without triggering per-dashboard detail fanout.\n# @PRE: Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.\n# @POST: Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.\ndef test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout(\n mock_deps,\n):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 5,\n \"title\": \"Alias Match\",\n \"slug\": \"alias-match\",\n \"owners\": [],\n \"created_by\": None,\n \"modified_by\": \"Superset Admin\",\n },\n {\n \"id\": 6,\n \"title\": \"Alias No Match\",\n \"slug\": \"alias-no-match\",\n \"owners\": [],\n \"created_by\": None,\n \"modified_by\": \"Other User\",\n },\n ]\n )\n\n with (\n patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls,\n patch(\"src.api.routes.dashboards._projection.SupersetClient\") as superset_client_cls,\n patch(\n \"src.api.routes.dashboards._projection.SupersetAccountLookupAdapter\"\n ) as lookup_adapter_cls,\n ):\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"admin\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n superset_client = MagicMock()\n superset_client_cls.return_value = superset_client\n\n lookup_adapter = MagicMock()\n lookup_adapter.get_users_page.return_value = {\n \"items\": [\n {\n \"environment_id\": \"prod\",\n \"username\": \"admin\",\n \"display_name\": \"Superset Admin\",\n \"email\": \"admin@example.com\",\n \"is_active\": True,\n }\n ],\n \"total\": 1,\n }\n lookup_adapter_cls.return_value = lookup_adapter\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 1\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {5}\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n lookup_adapter.get_users_page.assert_called_once()\n superset_client.get_dashboard.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function]\n\n\n# [DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards profile-default filter matches Superset owner object payloads.\n# @PURPOSE: Validate profile-default filtering accepts owner object payloads once aliases resolve to the bound Superset username.\n# @PRE: Profile-default preference is enabled and owners list contains dict payloads.\n# @POST: Response keeps dashboards where owner object resolves to bound username alias.\ndef test_get_dashboards_profile_filter_matches_owner_object_payload_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 701,\n \"title\": \"Featured Charts\",\n \"slug\": \"featured-charts\",\n \"owners\": [\n {\n \"id\": 11,\n \"first_name\": \"user\",\n \"last_name\": \"1\",\n \"username\": None,\n \"email\": \"user_1@example.local\",\n }\n ],\n \"modified_by\": \"another_user\",\n },\n {\n \"id\": 702,\n \"title\": \"Other Dashboard\",\n \"slug\": \"other-dashboard\",\n \"owners\": [\n {\n \"id\": 12,\n \"first_name\": \"other\",\n \"last_name\": \"user\",\n \"username\": None,\n \"email\": \"other@example.local\",\n }\n ],\n \"modified_by\": \"other_user\",\n },\n ]\n )\n\n with (\n patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls,\n patch(\n \"src.api.routes.dashboards._projection._resolve_profile_actor_aliases\",\n return_value=[\"user_1\"],\n ),\n ):\n profile_service = MagicMock(spec=DomainProfileService)\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"user_1\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n lambda bound_username, owners, modified_by: any(\n str(owner.get(\"email\", \"\")).split(\"@\", 1)[0].strip().lower()\n == str(bound_username).strip().lower()\n for owner in (owners or [])\n if isinstance(owner, dict)\n )\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 1\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {701}\n assert payload[\"dashboards\"][0][\"title\"] == \"Featured Charts\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function]\n\n\n# [/DEF:DashboardsApiTests:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_create_connection_bootstraps_missing_table [TYPE Function]\n# @RELATION BINDS_TO -> ConnectionsRoutesTests\n# @BRIEF Ensure connection creation bootstraps table and persists returned connection fields.\ndef test_create_connection_bootstraps_missing_table(db_session):\n from src.api.routes.connections import ConnectionCreate, create_connection\n payload = ConnectionCreate(\n name=\"Analytics Warehouse\",\n type=\"postgres\",\n host=\"warehouse.internal\",\n port=5432,\n database=\"analytics\",\n username=\"reporter\",\n password=\"secret\",\n )\n created = asyncio.run(create_connection(connection=payload, db=db_session))\n inspector = inspect(db_session.get_bind())\n assert created.name == \"Analytics Warehouse\"\n assert created.host == \"warehouse.internal\"\n assert \"connection_configs\" in inspector.get_table_names()\n# #endregion test_create_connection_bootstraps_missing_table\n" }, { "contract_id": "test_get_dashboards_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 74, - "end_line": 113, + "start_line": 64, + "end_line": 98, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2603,15 +1716,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns a populated response that satisfies the schema contract.\n# @TEST: GET /api/dashboards returns 200 and valid schema\n# @PRE: env_id exists\n# @POST: Response matches DashboardsResponse schema\ndef test_get_dashboards_success(mock_deps):\n \"\"\"Uses @TEST_FIXTURE: dashboard_list_happy data.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n # @TEST_FIXTURE: dashboard_list_happy -> {\"id\": 1, \"title\": \"Main Revenue\"}\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Main Revenue\",\n \"slug\": \"main-revenue\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": {\"task_id\": \"task-1\", \"status\": \"SUCCESS\"},\n }\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod\")\n\n assert response.status_code == 200\n data = response.json()\n # exhaustive @POST assertions\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Main Revenue\"\n assert data[\"total\"] == 1\n assert \"page\" in data\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_success [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboards listing returns a populated response that satisfies the schema contract.\n# @TEST: GET /api/dashboards returns 200 and valid schema\n# @PRE: env_id exists\n# @POST: Response matches DashboardsResponse schema\ndef test_get_dashboards_success(mock_deps):\n \"\"\"Uses @TEST_FIXTURE: dashboard_list_happy data.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n # @TEST_FIXTURE: dashboard_list_happy -> {\"id\": 1, \"title\": \"Main Revenue\"}\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Main Revenue\",\n \"slug\": \"main-revenue\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": {\"task_id\": \"task-1\", \"status\": \"SUCCESS\"},\n }\n ]\n )\n response = client.get(\"/api/dashboards?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n # exhaustive @POST assertions\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Main Revenue\"\n assert data[\"total\"] == 1\n assert \"page\" in data\n DashboardsResponse(**data)\n# #endregion test_get_dashboards_success\n" }, { "contract_id": "test_get_dashboards_with_search", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 116, - "end_line": 159, + "start_line": 99, + "end_line": 136, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2663,15 +1776,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_with_search:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing applies the search filter and returns only matching rows.\n# @TEST: GET /api/dashboards filters by search term\n# @PRE: search parameter provided\n# @POST: Only matching dashboards returned\ndef test_get_dashboards_with_search(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n async def mock_get_dashboards(env, tasks, include_git_status=False):\n return [\n {\n \"id\": 1,\n \"title\": \"Sales Report\",\n \"slug\": \"sales\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"title\": \"Marketing Dashboard\",\n \"slug\": \"marketing\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n ]\n\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=mock_get_dashboards\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&search=sales\")\n\n assert response.status_code == 200\n data = response.json()\n # @POST: Filtered result count must match search\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales Report\"\n\n\n# [/DEF:test_get_dashboards_with_search:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_with_search [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboards listing applies the search filter and returns only matching rows.\n# @TEST: GET /api/dashboards filters by search term\n# @PRE: search parameter provided\n# @POST: Only matching dashboards returned\ndef test_get_dashboards_with_search(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n async def mock_get_dashboards(env, tasks, include_git_status=False):\n return [\n {\n \"id\": 1,\n \"title\": \"Sales Report\",\n \"slug\": \"sales\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"title\": \"Marketing Dashboard\",\n \"slug\": \"marketing\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n ]\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=mock_get_dashboards\n )\n response = client.get(\"/api/dashboards?env_id=prod&search=sales\")\n assert response.status_code == 200\n data = response.json()\n # @POST: Filtered result count must match search\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales Report\"\n# #endregion test_get_dashboards_with_search\n" }, { "contract_id": "test_get_dashboards_empty", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 162, - "end_line": 183, + "start_line": 137, + "end_line": 155, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2697,15 +1810,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_empty:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns an empty payload for an environment without dashboards.\n# @TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\ndef test_get_dashboards_empty(mock_deps):\n \"\"\"@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"empty_env\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(return_value=[])\n\n response = client.get(\"/api/dashboards?env_id=empty_env\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert len(data[\"dashboards\"]) == 0\n assert data[\"total_pages\"] == 1\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_empty:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_empty [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboards listing returns an empty payload for an environment without dashboards.\n# @TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\ndef test_get_dashboards_empty(mock_deps):\n \"\"\"@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"empty_env\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(return_value=[])\n response = client.get(\"/api/dashboards?env_id=empty_env\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert len(data[\"dashboards\"]) == 0\n assert data[\"total_pages\"] == 1\n DashboardsResponse(**data)\n# #endregion test_get_dashboards_empty\n" }, { "contract_id": "test_get_dashboards_superset_failure", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 186, - "end_line": 205, + "start_line": 156, + "end_line": 172, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2731,15 +1844,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_superset_failure:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing surfaces a 503 contract when Superset access fails.\n# @TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\ndef test_get_dashboards_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n\n response = client.get(\"/api/dashboards?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch dashboards\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_superset_failure:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_superset_failure [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboards listing surfaces a 503 contract when Superset access fails.\n# @TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\ndef test_get_dashboards_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n response = client.get(\"/api/dashboards?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch dashboards\" in response.json()[\"detail\"]\n# #endregion test_get_dashboards_superset_failure\n" }, { "contract_id": "test_get_dashboards_env_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 208, - "end_line": 222, + "start_line": 173, + "end_line": 184, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2791,15 +1904,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing returns 404 when the requested environment does not exist.\n# @TEST: GET /api/dashboards returns 404 if env_id missing\n# @PRE: env_id does not exist\n# @POST: Returns 404 error\ndef test_get_dashboards_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards?env_id=nonexistent\")\n\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_env_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_env_not_found [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboards listing returns 404 when the requested environment does not exist.\n# @TEST: GET /api/dashboards returns 404 if env_id missing\n# @PRE: env_id does not exist\n# @POST: Returns 404 error\ndef test_get_dashboards_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards?env_id=nonexistent\")\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n# #endregion test_get_dashboards_env_not_found\n" }, { "contract_id": "test_get_dashboards_invalid_pagination", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 225, - "end_line": 246, + "start_line": 185, + "end_line": 203, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2851,15 +1964,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_invalid_pagination:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboards listing rejects invalid pagination parameters with 400 responses.\n# @TEST: GET /api/dashboards returns 400 for invalid page/page_size\n# @PRE: page < 1 or page_size > 100\n# @POST: Returns 400 error\ndef test_get_dashboards_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n # Invalid page\n response = client.get(\"/api/dashboards?env_id=prod&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n\n # Invalid page_size\n response = client.get(\"/api/dashboards?env_id=prod&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_invalid_pagination:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_invalid_pagination [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboards listing rejects invalid pagination parameters with 400 responses.\n# @TEST: GET /api/dashboards returns 400 for invalid page/page_size\n# @PRE: page < 1 or page_size > 100\n# @POST: Returns 400 error\ndef test_get_dashboards_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n # Invalid page\n response = client.get(\"/api/dashboards?env_id=prod&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n # Invalid page_size\n response = client.get(\"/api/dashboards?env_id=prod&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n# #endregion test_get_dashboards_invalid_pagination\n" }, { "contract_id": "test_get_dashboard_detail_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 249, - "end_line": 302, + "start_line": 204, + "end_line": 252, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2891,15 +2004,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboard_detail_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard detail returns charts and datasets for an existing dashboard.\n# @TEST: GET /api/dashboards/{id} returns dashboard detail with charts and datasets\ndef test_get_dashboard_detail_success(mock_deps):\n with patch(\"src.api.routes.dashboards._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n mock_client.get_dashboard_detail.return_value = {\n \"id\": 42,\n \"title\": \"Revenue Dashboard\",\n \"slug\": \"revenue-dashboard\",\n \"url\": \"/superset/dashboard/42/\",\n \"description\": \"Overview\",\n \"last_modified\": \"2026-02-20T10:00:00+00:00\",\n \"published\": True,\n \"charts\": [\n {\n \"id\": 100,\n \"title\": \"Revenue by Month\",\n \"viz_type\": \"line\",\n \"dataset_id\": 7,\n \"last_modified\": \"2026-02-19T10:00:00+00:00\",\n \"overview\": \"line\",\n }\n ],\n \"datasets\": [\n {\n \"id\": 7,\n \"table_name\": \"fact_revenue\",\n \"schema\": \"mart\",\n \"database\": \"Analytics\",\n \"last_modified\": \"2026-02-18T10:00:00+00:00\",\n \"overview\": \"mart.fact_revenue\",\n }\n ],\n \"chart_count\": 1,\n \"dataset_count\": 1,\n }\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42?env_id=prod\")\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"id\"] == 42\n assert payload[\"chart_count\"] == 1\n assert payload[\"dataset_count\"] == 1\n\n\n# [/DEF:test_get_dashboard_detail_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboard_detail_success [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboard detail returns charts and datasets for an existing dashboard.\n# @TEST: GET /api/dashboards/{id} returns dashboard detail with charts and datasets\ndef test_get_dashboard_detail_success(mock_deps):\n with patch(\"src.api.routes.dashboards._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_client = MagicMock()\n mock_client.get_dashboard_detail.return_value = {\n \"id\": 42,\n \"title\": \"Revenue Dashboard\",\n \"slug\": \"revenue-dashboard\",\n \"url\": \"/superset/dashboard/42/\",\n \"description\": \"Overview\",\n \"last_modified\": \"2026-02-20T10:00:00+00:00\",\n \"published\": True,\n \"charts\": [\n {\n \"id\": 100,\n \"title\": \"Revenue by Month\",\n \"viz_type\": \"line\",\n \"dataset_id\": 7,\n \"last_modified\": \"2026-02-19T10:00:00+00:00\",\n \"overview\": \"line\",\n }\n ],\n \"datasets\": [\n {\n \"id\": 7,\n \"table_name\": \"fact_revenue\",\n \"schema\": \"mart\",\n \"database\": \"Analytics\",\n \"last_modified\": \"2026-02-18T10:00:00+00:00\",\n \"overview\": \"mart.fact_revenue\",\n }\n ],\n \"chart_count\": 1,\n \"dataset_count\": 1,\n }\n mock_client_cls.return_value = mock_client\n response = client.get(\"/api/dashboards/42?env_id=prod\")\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"id\"] == 42\n assert payload[\"chart_count\"] == 1\n assert payload[\"dataset_count\"] == 1\n# #endregion test_get_dashboard_detail_success\n" }, { "contract_id": "test_get_dashboard_detail_env_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 305, - "end_line": 318, + "start_line": 253, + "end_line": 262, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -2931,19 +2044,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboard_detail_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard detail returns 404 when the requested environment is missing.\n# @TEST: GET /api/dashboards/{id} returns 404 for missing environment\ndef test_get_dashboard_detail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n\n response = client.get(\"/api/dashboards/42?env_id=missing\")\n\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboard_detail_env_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboard_detail_env_not_found [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboard detail returns 404 when the requested environment is missing.\n# @TEST: GET /api/dashboards/{id} returns 404 for missing environment\ndef test_get_dashboard_detail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards/42?env_id=missing\")\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n# #endregion test_get_dashboard_detail_env_not_found\n" }, { "contract_id": "test_migrate_dashboards_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 321, - "end_line": 355, + "start_line": 263, + "end_line": 292, "tier": "TIER_1", "complexity": 2, "metadata": { - "POST": "Returns task_id and create_task was called\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n@POST/@SIDE_EFFECT: create_task was called", + "POST": "Returns task_id and create_task was called\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n@POST/@SIDE_EFFECT: create_task was called", "PRE": "Valid source_env_id, target_env_id, dashboard_ids", "PURPOSE": "Validate dashboard migration request creates an async task and returns its identifier.", "TEST": "POST /api/dashboards/migrate creates migration task" @@ -2991,15 +2104,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_migrate_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/migrate creates migration task\n# @PRE: Valid source_env_id, target_env_id, dashboard_ids\n# @PURPOSE: Validate dashboard migration request creates an async task and returns its identifier.\n# @POST: Returns task_id and create_task was called\ndef test_migrate_dashboards_success(mock_deps):\n mock_source = MagicMock()\n mock_source.id = \"source\"\n mock_target = MagicMock()\n mock_target.id = \"target\"\n mock_deps[\"config\"].get_environments.return_value = [mock_source, mock_target]\n\n mock_task = MagicMock()\n mock_task.id = \"task-migrate-123\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\n \"source_env_id\": \"source\",\n \"target_env_id\": \"target\",\n \"dashboard_ids\": [1, 2, 3],\n \"db_mappings\": {\"old_db\": \"new_db\"},\n },\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_migrate_dashboards_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_migrate_dashboards_success [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/migrate creates migration task\n# @PRE: Valid source_env_id, target_env_id, dashboard_ids\n# @BRIEF Validate dashboard migration request creates an async task and returns its identifier.\n# @POST: Returns task_id and create_task was called\ndef test_migrate_dashboards_success(mock_deps):\n mock_source = MagicMock()\n mock_source.id = \"source\"\n mock_target = MagicMock()\n mock_target.id = \"target\"\n mock_deps[\"config\"].get_environments.return_value = [mock_source, mock_target]\n mock_task = MagicMock()\n mock_task.id = \"task-migrate-123\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\n \"source_env_id\": \"source\",\n \"target_env_id\": \"target\",\n \"dashboard_ids\": [1, 2, 3],\n \"db_mappings\": {\"old_db\": \"new_db\"},\n },\n )\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n# #endregion test_migrate_dashboards_success\n" }, { "contract_id": "test_migrate_dashboards_no_ids", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 358, - "end_line": 378, + "start_line": 293, + "end_line": 310, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3051,15 +2164,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_migrate_dashboards_no_ids:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/migrate returns 400 for empty dashboard_ids\n# @PRE: dashboard_ids is empty\n# @PURPOSE: Validate dashboard migration rejects empty dashboard identifier lists.\n# @POST: Returns 400 error\ndef test_migrate_dashboards_no_ids(mock_deps):\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\n \"source_env_id\": \"source\",\n \"target_env_id\": \"target\",\n \"dashboard_ids\": [],\n },\n )\n\n assert response.status_code == 400\n assert \"At least one dashboard ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_no_ids:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_migrate_dashboards_no_ids [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/migrate returns 400 for empty dashboard_ids\n# @PRE: dashboard_ids is empty\n# @BRIEF Validate dashboard migration rejects empty dashboard identifier lists.\n# @POST: Returns 400 error\ndef test_migrate_dashboards_no_ids(mock_deps):\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\n \"source_env_id\": \"source\",\n \"target_env_id\": \"target\",\n \"dashboard_ids\": [],\n },\n )\n assert response.status_code == 400\n assert \"At least one dashboard ID must be provided\" in response.json()[\"detail\"]\n# #endregion test_migrate_dashboards_no_ids\n" }, { "contract_id": "test_migrate_dashboards_env_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 381, - "end_line": 396, + "start_line": 311, + "end_line": 324, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3094,19 +2207,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_migrate_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate migration creation returns 404 when the source environment cannot be resolved.\n# @PRE: source_env_id and target_env_id are valid environment IDs\ndef test_migrate_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id and target_env_id are valid environment IDs.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"ghost\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 404\n assert \"Source environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_env_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_migrate_dashboards_env_not_found [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate migration creation returns 404 when the source environment cannot be resolved.\n# @PRE: source_env_id and target_env_id are valid environment IDs\ndef test_migrate_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id and target_env_id are valid environment IDs.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"ghost\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 404\n assert \"Source environment not found\" in response.json()[\"detail\"]\n# #endregion test_migrate_dashboards_env_not_found\n" }, { "contract_id": "test_backup_dashboards_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 399, - "end_line": 426, + "start_line": 325, + "end_line": 347, "tier": "TIER_1", "complexity": 2, "metadata": { - "POST": "Returns task_id and create_task was called\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n@POST/@SIDE_EFFECT: create_task was called", + "POST": "Returns task_id and create_task was called\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n@POST/@SIDE_EFFECT: create_task was called", "PRE": "Valid env_id, dashboard_ids", "PURPOSE": "Validate dashboard backup request creates an async backup task and returns its identifier.", "TEST": "POST /api/dashboards/backup creates backup task" @@ -3154,15 +2267,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_backup_dashboards_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/backup creates backup task\n# @PRE: Valid env_id, dashboard_ids\n# @PURPOSE: Validate dashboard backup request creates an async backup task and returns its identifier.\n# @POST: Returns task_id and create_task was called\ndef test_backup_dashboards_success(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_task = MagicMock()\n mock_task.id = \"task-backup-456\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/dashboards/backup\",\n json={\"env_id\": \"prod\", \"dashboard_ids\": [1, 2, 3], \"schedule\": \"0 0 * * *\"},\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_backup_dashboards_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_backup_dashboards_success [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: POST /api/dashboards/backup creates backup task\n# @PRE: Valid env_id, dashboard_ids\n# @BRIEF Validate dashboard backup request creates an async backup task and returns its identifier.\n# @POST: Returns task_id and create_task was called\ndef test_backup_dashboards_success(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_task = MagicMock()\n mock_task.id = \"task-backup-456\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n response = client.post(\n \"/api/dashboards/backup\",\n json={\"env_id\": \"prod\", \"dashboard_ids\": [1, 2, 3], \"schedule\": \"0 0 * * *\"},\n )\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n# #endregion test_backup_dashboards_success\n" }, { "contract_id": "test_backup_dashboards_env_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 429, - "end_line": 443, + "start_line": 348, + "end_line": 360, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3197,15 +2310,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_backup_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate backup task creation returns 404 when the target environment is missing.\n# @PRE: env_id is a valid environment ID\ndef test_backup_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: env_id is a valid environment ID.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"ghost\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_backup_dashboards_env_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_backup_dashboards_env_not_found [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate backup task creation returns 404 when the target environment is missing.\n# @PRE: env_id is a valid environment ID\ndef test_backup_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: env_id is a valid environment ID.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"ghost\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n# #endregion test_backup_dashboards_env_not_found\n" }, { "contract_id": "test_get_database_mappings_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 446, - "end_line": 482, + "start_line": 361, + "end_line": 392, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3257,15 +2370,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_database_mappings_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards/db-mappings returns mapping suggestions\n# @PRE: Valid source_env_id, target_env_id\n# @PURPOSE: Validate database mapping suggestions are returned for valid source and target environments.\n# @POST: Returns list of database mappings\ndef test_get_database_mappings_success(mock_deps):\n mock_source = MagicMock()\n mock_source.id = \"prod\"\n mock_target = MagicMock()\n mock_target.id = \"staging\"\n mock_deps[\"config\"].get_environments.return_value = [mock_source, mock_target]\n\n mock_deps[\"mapping\"].get_suggestions = AsyncMock(\n return_value=[\n {\n \"source_db\": \"old_sales\",\n \"target_db\": \"new_sales\",\n \"source_db_uuid\": \"uuid-1\",\n \"target_db_uuid\": \"uuid-2\",\n \"confidence\": 0.95,\n }\n ]\n )\n\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=prod&target_env_id=staging\"\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"mappings\" in data\n assert len(data[\"mappings\"]) == 1\n assert data[\"mappings\"][0][\"confidence\"] == 0.95\n\n\n# [/DEF:test_get_database_mappings_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_database_mappings_success [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards/db-mappings returns mapping suggestions\n# @PRE: Valid source_env_id, target_env_id\n# @BRIEF Validate database mapping suggestions are returned for valid source and target environments.\n# @POST: Returns list of database mappings\ndef test_get_database_mappings_success(mock_deps):\n mock_source = MagicMock()\n mock_source.id = \"prod\"\n mock_target = MagicMock()\n mock_target.id = \"staging\"\n mock_deps[\"config\"].get_environments.return_value = [mock_source, mock_target]\n mock_deps[\"mapping\"].get_suggestions = AsyncMock(\n return_value=[\n {\n \"source_db\": \"old_sales\",\n \"target_db\": \"new_sales\",\n \"source_db_uuid\": \"uuid-1\",\n \"target_db_uuid\": \"uuid-2\",\n \"confidence\": 0.95,\n }\n ]\n )\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=prod&target_env_id=staging\"\n )\n assert response.status_code == 200\n data = response.json()\n assert \"mappings\" in data\n assert len(data[\"mappings\"]) == 1\n assert data[\"mappings\"][0][\"confidence\"] == 0.95\n# #endregion test_get_database_mappings_success\n" }, { "contract_id": "test_get_database_mappings_env_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 485, - "end_line": 498, + "start_line": 393, + "end_line": 404, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3300,15 +2413,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_database_mappings_env_not_found:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate database mapping suggestions return 404 when either environment is missing.\n# @PRE: source_env_id and target_env_id are valid environment IDs\ndef test_get_database_mappings_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id must be a valid environment.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t\"\n )\n assert response.status_code == 404\n\n\n# [/DEF:test_get_database_mappings_env_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_database_mappings_env_not_found [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate database mapping suggestions return 404 when either environment is missing.\n# @PRE: source_env_id and target_env_id are valid environment IDs\ndef test_get_database_mappings_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id must be a valid environment.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t\"\n )\n assert response.status_code == 404\n# #endregion test_get_database_mappings_env_not_found\n" }, { "contract_id": "test_get_dashboard_tasks_history_filters_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 501, - "end_line": 549, + "start_line": 405, + "end_line": 445, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3340,15 +2453,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboard_tasks_history_filters_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard task history returns only related backup and LLM tasks.\n# @TEST: GET /api/dashboards/{id}/tasks returns backup and llm tasks for dashboard\ndef test_get_dashboard_tasks_history_filters_success(mock_deps):\n now = datetime.now(timezone.utc)\n\n llm_task = MagicMock()\n llm_task.id = \"task-llm-1\"\n llm_task.plugin_id = \"llm_dashboard_validation\"\n llm_task.status = \"SUCCESS\"\n llm_task.started_at = now\n llm_task.finished_at = now\n llm_task.params = {\"dashboard_id\": \"42\", \"environment_id\": \"prod\"}\n llm_task.result = {\"summary\": \"LLM validation complete\"}\n\n backup_task = MagicMock()\n backup_task.id = \"task-backup-1\"\n backup_task.plugin_id = \"superset-backup\"\n backup_task.status = \"RUNNING\"\n backup_task.started_at = now\n backup_task.finished_at = None\n backup_task.params = {\"env\": \"prod\", \"dashboards\": [42]}\n backup_task.result = {}\n\n other_task = MagicMock()\n other_task.id = \"task-other\"\n other_task.plugin_id = \"superset-backup\"\n other_task.status = \"SUCCESS\"\n other_task.started_at = now\n other_task.finished_at = now\n other_task.params = {\"env\": \"prod\", \"dashboards\": [777]}\n other_task.result = {}\n\n mock_deps[\"task\"].get_all_tasks.return_value = [other_task, llm_task, backup_task]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod&limit=10\")\n\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 2\n assert {item[\"plugin_id\"] for item in data[\"items\"]} == {\n \"llm_dashboard_validation\",\n \"superset-backup\",\n }\n\n\n# [/DEF:test_get_dashboard_tasks_history_filters_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboard_tasks_history_filters_success [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboard task history returns only related backup and LLM tasks.\n# @TEST: GET /api/dashboards/{id}/tasks returns backup and llm tasks for dashboard\ndef test_get_dashboard_tasks_history_filters_success(mock_deps):\n now = datetime.now(UTC)\n llm_task = MagicMock()\n llm_task.id = \"task-llm-1\"\n llm_task.plugin_id = \"llm_dashboard_validation\"\n llm_task.status = \"SUCCESS\"\n llm_task.started_at = now\n llm_task.finished_at = now\n llm_task.params = {\"dashboard_id\": \"42\", \"environment_id\": \"prod\"}\n llm_task.result = {\"summary\": \"LLM validation complete\"}\n backup_task = MagicMock()\n backup_task.id = \"task-backup-1\"\n backup_task.plugin_id = \"superset-backup\"\n backup_task.status = \"RUNNING\"\n backup_task.started_at = now\n backup_task.finished_at = None\n backup_task.params = {\"env\": \"prod\", \"dashboards\": [42]}\n backup_task.result = {}\n other_task = MagicMock()\n other_task.id = \"task-other\"\n other_task.plugin_id = \"superset-backup\"\n other_task.status = \"SUCCESS\"\n other_task.started_at = now\n other_task.finished_at = now\n other_task.params = {\"env\": \"prod\", \"dashboards\": [777]}\n other_task.result = {}\n mock_deps[\"task\"].get_all_tasks.return_value = [other_task, llm_task, backup_task]\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod&limit=10\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 2\n assert {item[\"plugin_id\"] for item in data[\"items\"]} == {\n \"llm_dashboard_validation\",\n \"superset-backup\",\n }\n# #endregion test_get_dashboard_tasks_history_filters_success\n" }, { "contract_id": "test_get_dashboard_thumbnail_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 552, - "end_line": 583, + "start_line": 446, + "end_line": 470, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3380,15 +2493,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboard_thumbnail_success:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Validate dashboard thumbnail endpoint proxies image bytes and content type from Superset.\n# @TEST: GET /api/dashboards/{id}/thumbnail proxies image bytes from Superset\ndef test_get_dashboard_thumbnail_success(mock_deps):\n with patch(\"src.api.routes.dashboards._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n mock_response = MagicMock()\n mock_response.status_code = 200\n mock_response.content = b\"fake-image-bytes\"\n mock_response.headers = {\"Content-Type\": \"image/png\"}\n\n def _network_request(method, endpoint, **kwargs):\n if method == \"POST\":\n return {\"image_url\": \"/api/v1/dashboard/42/screenshot/abc123/\"}\n return mock_response\n\n mock_client.network.request.side_effect = _network_request\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n\n assert response.status_code == 200\n assert response.content == b\"fake-image-bytes\"\n assert response.headers[\"content-type\"].startswith(\"image/png\")\n\n\n# [/DEF:test_get_dashboard_thumbnail_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboard_thumbnail_success [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Validate dashboard thumbnail endpoint proxies image bytes and content type from Superset.\n# @TEST: GET /api/dashboards/{id}/thumbnail proxies image bytes from Superset\ndef test_get_dashboard_thumbnail_success(mock_deps):\n with patch(\"src.api.routes.dashboards._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_client = MagicMock()\n mock_response = MagicMock()\n mock_response.status_code = 200\n mock_response.content = b\"fake-image-bytes\"\n mock_response.headers = {\"Content-Type\": \"image/png\"}\n def _network_request(method, endpoint, **kwargs):\n if method == \"POST\":\n return {\"image_url\": \"/api/v1/dashboard/42/screenshot/abc123/\"}\n return mock_response\n mock_client.network.request.side_effect = _network_request\n mock_client_cls.return_value = mock_client\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n assert response.status_code == 200\n assert response.content == b\"fake-image-bytes\"\n assert response.headers[\"content-type\"].startswith(\"image/png\")\n# #endregion test_get_dashboard_thumbnail_success\n" }, { "contract_id": "_build_profile_preference_stub", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 586, - "end_line": 604, + "start_line": 471, + "end_line": 486, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -3442,15 +2555,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_build_profile_preference_stub:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Creates profile preference payload stub for dashboards filter contract tests.\n# @PRE: username can be empty; enabled indicates profile-default toggle state.\n# @POST: Returns object compatible with ProfileService.get_my_preference contract.\ndef _build_profile_preference_stub(username: str, enabled: bool):\n preference = MagicMock()\n preference.superset_username = username\n preference.superset_username_normalized = (\n str(username or \"\").strip().lower() or None\n )\n preference.show_only_my_dashboards = bool(enabled)\n\n payload = MagicMock()\n payload.preference = preference\n return payload\n\n\n# [/DEF:_build_profile_preference_stub:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_profile_preference_stub [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Creates profile preference payload stub for dashboards filter contract tests.\n# @PRE: username can be empty; enabled indicates profile-default toggle state.\n# @POST: Returns object compatible with ProfileService.get_my_preference contract.\ndef _build_profile_preference_stub(username: str, enabled: bool):\n preference = MagicMock()\n preference.superset_username = username\n preference.superset_username_normalized = (\n str(username or \"\").strip().lower() or None\n )\n preference.show_only_my_dashboards = bool(enabled)\n payload = MagicMock()\n payload.preference = preference\n return payload\n# #endregion _build_profile_preference_stub\n" }, { "contract_id": "_matches_actor_case_insensitive", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 607, - "end_line": 629, + "start_line": 487, + "end_line": 505, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -3504,15 +2617,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_matches_actor_case_insensitive:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @PURPOSE: Applies trim + case-insensitive owners OR modified_by matching used by route contract tests.\n# @PRE: owners can be None or list-like values.\n# @POST: Returns True when bound username matches any owner or modified_by.\ndef _matches_actor_case_insensitive(bound_username, owners, modified_by):\n normalized_bound = str(bound_username or \"\").strip().lower()\n if not normalized_bound:\n return False\n\n owner_tokens = []\n for owner in owners or []:\n token = str(owner or \"\").strip().lower()\n if token:\n owner_tokens.append(token)\n\n modified_token = str(modified_by or \"\").strip().lower()\n return normalized_bound in owner_tokens or bool(\n modified_token and modified_token == normalized_bound\n )\n\n\n# [/DEF:_matches_actor_case_insensitive:Function]\n" + "anchor_syntax": "region", + "body": "# #region _matches_actor_case_insensitive [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @BRIEF Applies trim + case-insensitive owners OR modified_by matching used by route contract tests.\n# @PRE: owners can be None or list-like values.\n# @POST: Returns True when bound username matches any owner or modified_by.\ndef _matches_actor_case_insensitive(bound_username, owners, modified_by):\n normalized_bound = str(bound_username or \"\").strip().lower()\n if not normalized_bound:\n return False\n owner_tokens = []\n for owner in owners or []:\n token = str(owner or \"\").strip().lower()\n if token:\n owner_tokens.append(token)\n modified_token = str(modified_by or \"\").strip().lower()\n return normalized_bound in owner_tokens or bool(\n modified_token and modified_token == normalized_bound\n )\n# #endregion _matches_actor_case_insensitive\n" }, { "contract_id": "test_get_dashboards_profile_filter_contract_owners_or_modified_by", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 632, - "end_line": 696, + "start_line": 506, + "end_line": 564, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3564,15 +2677,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards applies profile-default filter with owners OR modified_by trim+case-insensitive semantics.\n# @PURPOSE: Validate profile-default filtering matches owner and modifier aliases using normalized Superset actor values.\n# @PRE: Current user has enabled profile-default preference and bound username.\n# @POST: Response includes only matching dashboards and effective_profile_filter metadata.\ndef test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Owner Match\",\n \"slug\": \"owner-match\",\n \"owners\": [\" John_Doe \"],\n \"modified_by\": \"someone_else\",\n },\n {\n \"id\": 2,\n \"title\": \"Modifier Match\",\n \"slug\": \"modifier-match\",\n \"owners\": [\"analytics-team\"],\n \"modified_by\": \" JOHN_DOE \",\n },\n {\n \"id\": 3,\n \"title\": \"No Match\",\n \"slug\": \"no-match\",\n \"owners\": [\"another-user\"],\n \"modified_by\": \"nobody\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\" JOHN_DOE \",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] == \"john_doe\"\n assert payload[\"effective_profile_filter\"][\"match_logic\"] == \"owners_or_modified_by\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_profile_filter_contract_owners_or_modified_by [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards applies profile-default filter with owners OR modified_by trim+case-insensitive semantics.\n# @BRIEF Validate profile-default filtering matches owner and modifier aliases using normalized Superset actor values.\n# @PRE: Current user has enabled profile-default preference and bound username.\n# @POST: Response includes only matching dashboards and effective_profile_filter metadata.\ndef test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Owner Match\",\n \"slug\": \"owner-match\",\n \"owners\": [\" John_Doe \"],\n \"modified_by\": \"someone_else\",\n },\n {\n \"id\": 2,\n \"title\": \"Modifier Match\",\n \"slug\": \"modifier-match\",\n \"owners\": [\"analytics-team\"],\n \"modified_by\": \" JOHN_DOE \",\n },\n {\n \"id\": 3,\n \"title\": \"No Match\",\n \"slug\": \"no-match\",\n \"owners\": [\"another-user\"],\n \"modified_by\": \"nobody\",\n },\n ]\n )\n with patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\" JOHN_DOE \",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] == \"john_doe\"\n assert payload[\"effective_profile_filter\"][\"match_logic\"] == \"owners_or_modified_by\"\n# #endregion test_get_dashboards_profile_filter_contract_owners_or_modified_by\n" }, { "contract_id": "test_get_dashboards_override_show_all_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 699, - "end_line": 757, + "start_line": 565, + "end_line": 617, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3624,15 +2737,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_override_show_all_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards honors override_show_all and disables profile-default filter for current page.\n# @PURPOSE: Validate override_show_all bypasses profile-default filtering without changing dashboard list semantics.\n# @PRE: Profile-default preference exists but override_show_all=true query is provided.\n# @POST: Response remains unfiltered and effective_profile_filter.applied is false.\ndef test_get_dashboards_override_show_all_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Dash A\",\n \"slug\": \"dash-a\",\n \"owners\": [\"john_doe\"],\n \"modified_by\": \"john_doe\",\n },\n {\n \"id\": 2,\n \"title\": \"Dash B\",\n \"slug\": \"dash-b\",\n \"owners\": [\"other\"],\n \"modified_by\": \"other\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true&override_show_all=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is False\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is True\n assert payload[\"effective_profile_filter\"][\"username\"] is None\n assert payload[\"effective_profile_filter\"][\"match_logic\"] is None\n profile_service.matches_dashboard_actor.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_override_show_all_contract:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_override_show_all_contract [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards honors override_show_all and disables profile-default filter for current page.\n# @BRIEF Validate override_show_all bypasses profile-default filtering without changing dashboard list semantics.\n# @PRE: Profile-default preference exists but override_show_all=true query is provided.\n# @POST: Response remains unfiltered and effective_profile_filter.applied is false.\ndef test_get_dashboards_override_show_all_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Dash A\",\n \"slug\": \"dash-a\",\n \"owners\": [\"john_doe\"],\n \"modified_by\": \"john_doe\",\n },\n {\n \"id\": 2,\n \"title\": \"Dash B\",\n \"slug\": \"dash-b\",\n \"owners\": [\"other\"],\n \"modified_by\": \"other\",\n },\n ]\n )\n with patch(\"src.api.routes.dashboards.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true&override_show_all=true\"\n )\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is False\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is True\n assert payload[\"effective_profile_filter\"][\"username\"] is None\n assert payload[\"effective_profile_filter\"][\"match_logic\"] is None\n profile_service.matches_dashboard_actor.assert_not_called()\n# #endregion test_get_dashboards_override_show_all_contract\n" }, { "contract_id": "test_get_dashboards_profile_filter_no_match_results_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 760, - "end_line": 820, + "start_line": 618, + "end_line": 672, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3684,15 +2797,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards returns empty result set when profile-default filter is active and no dashboard actors match.\n# @PURPOSE: Validate profile-default filtering returns an empty dashboard page when no actor aliases match the bound user.\n# @PRE: Profile-default preference is enabled with bound username and all dashboards are non-matching.\n# @POST: Response total is 0 with deterministic pagination and active effective_profile_filter metadata.\ndef test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 101,\n \"title\": \"Team Dashboard\",\n \"slug\": \"team-dashboard\",\n \"owners\": [\"analytics-team\"],\n \"modified_by\": \"someone_else\",\n },\n {\n \"id\": 102,\n \"title\": \"Ops Dashboard\",\n \"slug\": \"ops-dashboard\",\n \"owners\": [\"ops-user\"],\n \"modified_by\": \"ops-user\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 0\n assert payload[\"dashboards\"] == []\n assert payload[\"page\"] == 1\n assert payload[\"page_size\"] == 10\n assert payload[\"total_pages\"] == 1\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] == \"john_doe\"\n assert payload[\"effective_profile_filter\"][\"match_logic\"] == \"owners_or_modified_by\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_profile_filter_no_match_results_contract [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards returns empty result set when profile-default filter is active and no dashboard actors match.\n# @BRIEF Validate profile-default filtering returns an empty dashboard page when no actor aliases match the bound user.\n# @PRE: Profile-default preference is enabled with bound username and all dashboards are non-matching.\n# @POST: Response total is 0 with deterministic pagination and active effective_profile_filter metadata.\ndef test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 101,\n \"title\": \"Team Dashboard\",\n \"slug\": \"team-dashboard\",\n \"owners\": [\"analytics-team\"],\n \"modified_by\": \"someone_else\",\n },\n {\n \"id\": 102,\n \"title\": \"Ops Dashboard\",\n \"slug\": \"ops-dashboard\",\n \"owners\": [\"ops-user\"],\n \"modified_by\": \"ops-user\",\n },\n ]\n )\n with patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 0\n assert payload[\"dashboards\"] == []\n assert payload[\"page\"] == 1\n assert payload[\"page_size\"] == 10\n assert payload[\"total_pages\"] == 1\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"dashboards_main\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] == \"john_doe\"\n assert payload[\"effective_profile_filter\"][\"match_logic\"] == \"owners_or_modified_by\"\n# #endregion test_get_dashboards_profile_filter_no_match_results_contract\n" }, { "contract_id": "test_get_dashboards_page_context_other_disables_profile_default", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 823, - "end_line": 881, + "start_line": 673, + "end_line": 725, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3744,15 +2857,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_page_context_other_disables_profile_default:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards does not auto-apply profile-default filter outside dashboards_main page context.\n# @PURPOSE: Validate non-dashboard page contexts suppress profile-default filtering and preserve unfiltered results.\n# @PRE: Profile-default preference exists but page_context=other query is provided.\n# @POST: Response remains unfiltered and metadata reflects source_page=other.\ndef test_get_dashboards_page_context_other_disables_profile_default(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Dash A\",\n \"slug\": \"dash-a\",\n \"owners\": [\"john_doe\"],\n \"modified_by\": \"john_doe\",\n },\n {\n \"id\": 2,\n \"title\": \"Dash B\",\n \"slug\": \"dash-b\",\n \"owners\": [\"other\"],\n \"modified_by\": \"other\",\n },\n ]\n )\n\n with patch(\"src.api.routes.dashboards.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=other&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is False\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"other\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] is None\n assert payload[\"effective_profile_filter\"][\"match_logic\"] is None\n profile_service.matches_dashboard_actor.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_page_context_other_disables_profile_default:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_page_context_other_disables_profile_default [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards does not auto-apply profile-default filter outside dashboards_main page context.\n# @BRIEF Validate non-dashboard page contexts suppress profile-default filtering and preserve unfiltered results.\n# @PRE: Profile-default preference exists but page_context=other query is provided.\n# @POST: Response remains unfiltered and metadata reflects source_page=other.\ndef test_get_dashboards_page_context_other_disables_profile_default(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Dash A\",\n \"slug\": \"dash-a\",\n \"owners\": [\"john_doe\"],\n \"modified_by\": \"john_doe\",\n },\n {\n \"id\": 2,\n \"title\": \"Dash B\",\n \"slug\": \"dash-b\",\n \"owners\": [\"other\"],\n \"modified_by\": \"other\",\n },\n ]\n )\n with patch(\"src.api.routes.dashboards.ProfileService\") as profile_service_cls:\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"john_doe\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=other&apply_profile_default=true\"\n )\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 2\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {1, 2}\n assert payload[\"effective_profile_filter\"][\"applied\"] is False\n assert payload[\"effective_profile_filter\"][\"source_page\"] == \"other\"\n assert payload[\"effective_profile_filter\"][\"override_show_all\"] is False\n assert payload[\"effective_profile_filter\"][\"username\"] is None\n assert payload[\"effective_profile_filter\"][\"match_logic\"] is None\n profile_service.matches_dashboard_actor.assert_not_called()\n# #endregion test_get_dashboards_page_context_other_disables_profile_default\n" }, { "contract_id": "test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 884, - "end_line": 966, + "start_line": 726, + "end_line": 801, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3804,15 +2917,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards resolves Superset display-name alias once and filters without per-dashboard detail calls.\n# @PURPOSE: Validate profile-default filtering reuses resolved Superset display aliases without triggering per-dashboard detail fanout.\n# @PRE: Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.\n# @POST: Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.\ndef test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout(\n mock_deps,\n):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 5,\n \"title\": \"Alias Match\",\n \"slug\": \"alias-match\",\n \"owners\": [],\n \"created_by\": None,\n \"modified_by\": \"Superset Admin\",\n },\n {\n \"id\": 6,\n \"title\": \"Alias No Match\",\n \"slug\": \"alias-no-match\",\n \"owners\": [],\n \"created_by\": None,\n \"modified_by\": \"Other User\",\n },\n ]\n )\n\n with (\n patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls,\n patch(\"src.api.routes.dashboards._projection.SupersetClient\") as superset_client_cls,\n patch(\n \"src.api.routes.dashboards._projection.SupersetAccountLookupAdapter\"\n ) as lookup_adapter_cls,\n ):\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"admin\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n\n superset_client = MagicMock()\n superset_client_cls.return_value = superset_client\n\n lookup_adapter = MagicMock()\n lookup_adapter.get_users_page.return_value = {\n \"items\": [\n {\n \"environment_id\": \"prod\",\n \"username\": \"admin\",\n \"display_name\": \"Superset Admin\",\n \"email\": \"admin@example.com\",\n \"is_active\": True,\n }\n ],\n \"total\": 1,\n }\n lookup_adapter_cls.return_value = lookup_adapter\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 1\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {5}\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n lookup_adapter.get_users_page.assert_called_once()\n superset_client.get_dashboard.assert_not_called()\n\n\n# [/DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards resolves Superset display-name alias once and filters without per-dashboard detail calls.\n# @BRIEF Validate profile-default filtering reuses resolved Superset display aliases without triggering per-dashboard detail fanout.\n# @PRE: Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.\n# @POST: Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.\ndef test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout(\n mock_deps,\n):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 5,\n \"title\": \"Alias Match\",\n \"slug\": \"alias-match\",\n \"owners\": [],\n \"created_by\": None,\n \"modified_by\": \"Superset Admin\",\n },\n {\n \"id\": 6,\n \"title\": \"Alias No Match\",\n \"slug\": \"alias-no-match\",\n \"owners\": [],\n \"created_by\": None,\n \"modified_by\": \"Other User\",\n },\n ]\n )\n with (\n patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls,\n patch(\"src.api.routes.dashboards._projection.SupersetClient\") as superset_client_cls,\n patch(\n \"src.api.routes.dashboards._projection.SupersetAccountLookupAdapter\"\n ) as lookup_adapter_cls,\n ):\n profile_service = MagicMock()\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"admin\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n _matches_actor_case_insensitive\n )\n profile_service_cls.return_value = profile_service\n superset_client = MagicMock()\n superset_client_cls.return_value = superset_client\n lookup_adapter = MagicMock()\n lookup_adapter.get_users_page.return_value = {\n \"items\": [\n {\n \"environment_id\": \"prod\",\n \"username\": \"admin\",\n \"display_name\": \"Superset Admin\",\n \"email\": \"admin@example.com\",\n \"is_active\": True,\n }\n ],\n \"total\": 1,\n }\n lookup_adapter_cls.return_value = lookup_adapter\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 1\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {5}\n assert payload[\"effective_profile_filter\"][\"applied\"] is True\n lookup_adapter.get_users_page.assert_called_once()\n superset_client.get_dashboard.assert_not_called()\n# #endregion test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout\n" }, { "contract_id": "test_get_dashboards_profile_filter_matches_owner_object_payload_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dashboards.py", - "start_line": 969, - "end_line": 1048, + "start_line": 802, + "end_line": 876, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -3864,91 +2977,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function]\n# @RELATION: BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards profile-default filter matches Superset owner object payloads.\n# @PURPOSE: Validate profile-default filtering accepts owner object payloads once aliases resolve to the bound Superset username.\n# @PRE: Profile-default preference is enabled and owners list contains dict payloads.\n# @POST: Response keeps dashboards where owner object resolves to bound username alias.\ndef test_get_dashboards_profile_filter_matches_owner_object_payload_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 701,\n \"title\": \"Featured Charts\",\n \"slug\": \"featured-charts\",\n \"owners\": [\n {\n \"id\": 11,\n \"first_name\": \"user\",\n \"last_name\": \"1\",\n \"username\": None,\n \"email\": \"user_1@example.local\",\n }\n ],\n \"modified_by\": \"another_user\",\n },\n {\n \"id\": 702,\n \"title\": \"Other Dashboard\",\n \"slug\": \"other-dashboard\",\n \"owners\": [\n {\n \"id\": 12,\n \"first_name\": \"other\",\n \"last_name\": \"user\",\n \"username\": None,\n \"email\": \"other@example.local\",\n }\n ],\n \"modified_by\": \"other_user\",\n },\n ]\n )\n\n with (\n patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls,\n patch(\n \"src.api.routes.dashboards._projection._resolve_profile_actor_aliases\",\n return_value=[\"user_1\"],\n ),\n ):\n profile_service = MagicMock(spec=DomainProfileService)\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"user_1\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n lambda bound_username, owners, modified_by: any(\n str(owner.get(\"email\", \"\")).split(\"@\", 1)[0].strip().lower()\n == str(bound_username).strip().lower()\n for owner in (owners or [])\n if isinstance(owner, dict)\n )\n )\n profile_service_cls.return_value = profile_service\n\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 1\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {701}\n assert payload[\"dashboards\"][0][\"title\"] == \"Featured Charts\"\n\n\n# [/DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function]\n" - }, - { - "contract_id": "DatasetReviewApiTests", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1, - "end_line": 1766, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "API", - "PURPOSE": "Verify backend US1 dataset review lifecycle, export, parsing, and dictionary-resolution contracts.", - "SEMANTICS": [ - "dataset_review", - "api", - "tests", - "lifecycle", - "exports", - "orchestration" - ] - }, - "relations": [ - { - "source_id": "DatasetReviewApiTests", - "relation_type": "[BINDS_TO]", - "target_id": "DatasetReviewApi", - "target_ref": "[DatasetReviewApi]" - }, - { - "source_id": "DatasetReviewApiTests", - "relation_type": "[BINDS_TO]", - "target_id": "DatasetReviewOrchestrator", - "target_ref": "[DatasetReviewOrchestrator]" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [BINDS_TO] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[BINDS_TO]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [BINDS_TO] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[BINDS_TO]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewApiTests:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: dataset_review, api, tests, lifecycle, exports, orchestration\n# @PURPOSE: Verify backend US1 dataset review lifecycle, export, parsing, and dictionary-resolution contracts.\n# @LAYER: API\n# @RELATION: [BINDS_TO] ->[DatasetReviewApi]\n# @RELATION: [BINDS_TO] ->[DatasetReviewOrchestrator]\n\nfrom datetime import datetime, timezone\nimport json\nfrom types import SimpleNamespace\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.api.routes.dataset_review import (\n _get_clarification_engine,\n _get_orchestrator,\n _get_repository,\n)\nfrom src.core.config_models import Environment, GlobalSettings, AppConfig\nfrom src.core.utils.superset_context_extractor import SupersetContextExtractor\nfrom src.dependencies import get_config_manager, get_current_user, get_task_manager\nfrom src.models.dataset_review import (\n AnswerKind,\n ApprovalState,\n BusinessSummarySource,\n CandidateMatchType,\n CandidateStatus,\n ClarificationOption,\n ClarificationQuestion,\n ClarificationSession,\n ClarificationStatus,\n CompiledPreview,\n ConfidenceState,\n DatasetReviewSession,\n LaunchStatus,\n ExecutionMapping,\n FieldKind,\n FieldProvenance,\n FindingArea,\n FindingSeverity,\n MappingMethod,\n PreviewStatus,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n SemanticCandidate,\n SemanticFieldEntry,\n SemanticSource,\n SessionPhase,\n SessionStatus,\n SemanticSourceStatus,\n SemanticSourceType,\n TrustLevel,\n)\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n LaunchDatasetResult,\n PreparePreviewResult,\n StartSessionCommand,\n)\nfrom src.services.dataset_review.semantic_resolver import SemanticSourceResolver\nfrom src.services.dataset_review.event_logger import SessionEventLogger\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionVersionConflictError,\n)\n\n\nclient = TestClient(app)\n\n\n# [DEF:_make_user:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_user():\n permissions = [\n SimpleNamespace(resource=\"dataset:session\", action=\"READ\"),\n SimpleNamespace(resource=\"dataset:session\", action=\"MANAGE\"),\n SimpleNamespace(resource=\"dataset:execution:launch\", action=\"EXECUTE\"),\n ]\n dataset_review_role = SimpleNamespace(\n name=\"DatasetReviewOperator\", permissions=permissions\n )\n return SimpleNamespace(id=\"user-1\", username=\"tester\", roles=[dataset_review_role])\n\n\n# [/DEF:_make_user:Function]\n\n\n# [DEF:_make_config_manager:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_config_manager():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n config = AppConfig(environments=[env], settings=GlobalSettings())\n manager = MagicMock()\n manager.get_environment.side_effect = (\n lambda env_id: env if env_id == \"env-1\" else None\n )\n manager.get_config.return_value = config\n return manager\n\n\n# [/DEF:_make_config_manager:Function]\n\n\n# [DEF:_make_session:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_session():\n now = datetime.now(timezone.utc)\n return DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"user-1\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n version=0,\n readiness_state=ReadinessState.REVIEW_READY,\n recommended_action=RecommendedAction.REVIEW_DOCUMENTATION,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.REVIEW,\n created_at=now,\n updated_at=now,\n last_activity_at=now,\n )\n\n\n# [/DEF:_make_session:Function]\n\n\n# [DEF:_make_us2_session:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_us2_session():\n now = datetime.now(timezone.utc)\n session = _make_session()\n session.readiness_state = ReadinessState.CLARIFICATION_NEEDED\n session.recommended_action = RecommendedAction.START_CLARIFICATION\n session.current_phase = SessionPhase.CLARIFICATION\n\n field = SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"revenue\",\n field_kind=FieldKind.COLUMN,\n verbose_name=\"Revenue\",\n description=\"AI-generated revenue description\",\n display_format=\"$,.2f\",\n provenance=FieldProvenance.AI_GENERATED,\n source_id=\"source-ai\",\n source_version=None,\n confidence_rank=1,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"agent\",\n user_feedback=None,\n created_at=now,\n updated_at=now,\n )\n candidate = SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=\"dict-1\",\n candidate_rank=1,\n match_type=CandidateMatchType.EXACT,\n confidence_score=1.0,\n proposed_verbose_name=\"Recognized Revenue\",\n proposed_description=\"Trusted dictionary description\",\n proposed_display_format=\"$,.2f\",\n status=CandidateStatus.PROPOSED,\n created_at=now,\n )\n field.candidates = [candidate]\n\n clarification_session = ClarificationSession(\n clarification_session_id=\"clar-1\",\n session_id=\"sess-1\",\n status=ClarificationStatus.PENDING,\n current_question_id=None,\n resolved_count=0,\n remaining_count=1,\n summary_delta=None,\n started_at=now,\n updated_at=now,\n completed_at=None,\n )\n question = ClarificationQuestion(\n question_id=\"q-1\",\n clarification_session_id=\"clar-1\",\n topic_ref=\"dataset.business_purpose\",\n question_text=\"Which business concept does this dataset represent?\",\n why_it_matters=\"This determines how downstream users interpret revenue KPIs.\",\n current_guess=\"Revenue reporting\",\n priority=100,\n state=QuestionState.OPEN,\n created_at=now,\n updated_at=now,\n )\n question.options = [\n ClarificationOption(\n option_id=\"opt-1\",\n question_id=\"q-1\",\n label=\"Revenue reporting\",\n value=\"Revenue reporting\",\n is_recommended=True,\n display_order=1,\n ),\n ClarificationOption(\n option_id=\"opt-2\",\n question_id=\"q-1\",\n label=\"Margin analysis\",\n value=\"Margin analysis\",\n is_recommended=False,\n display_order=2,\n ),\n ]\n question.answer = None\n clarification_session.questions = [question]\n\n session.findings = []\n session.collaborators = []\n session.semantic_sources = [\n SemanticSource(\n source_id=\"dict-1\",\n session_id=\"sess-1\",\n source_type=SemanticSourceType.CONNECTED_DICTIONARY,\n source_ref=\"dict://finance\",\n source_version=\"2026.03\",\n display_name=\"Finance Dictionary\",\n trust_level=TrustLevel.TRUSTED,\n schema_overlap_score=1.0,\n status=SemanticSourceStatus.AVAILABLE,\n created_at=now,\n )\n ]\n session.semantic_fields = [field]\n session.imported_filters = []\n session.template_variables = []\n session.execution_mappings = []\n session.clarification_sessions = [clarification_session]\n session.previews = []\n session.run_contexts = []\n return session\n\n\n# [/DEF:_make_us2_session:Function]\n\n\n# [DEF:_make_us3_session:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_us3_session():\n \"\"\"Fake session factory for US3 flow tests.\n\n `imported_filter` and `template_variable` are bare MagicMocks without spec;\n ORM attribute access is unchecked.\n \"\"\"\n now = datetime.now(timezone.utc)\n session = _make_session()\n session.readiness_state = ReadinessState.MAPPING_REVIEW_NEEDED\n session.recommended_action = RecommendedAction.APPROVE_MAPPING\n session.current_phase = SessionPhase.MAPPING_REVIEW\n\n imported_filter = MagicMock() # @RISK: No spec= guard — enum/field contract changes are undetectable at test time.\n imported_filter.filter_id = \"filter-1\"\n imported_filter.session_id = \"sess-1\"\n imported_filter.filter_name = \"country\"\n imported_filter.display_name = \"Country\"\n imported_filter.raw_value = \"DE\"\n imported_filter.normalized_value = \"DE\"\n imported_filter.source = \"superset_url\"\n imported_filter.confidence_state = \"imported\"\n imported_filter.requires_confirmation = False\n imported_filter.recovery_status = \"recovered\"\n imported_filter.notes = \"Recovered from URL state\"\n\n template_variable = MagicMock() # @RISK: No spec= guard — enum/field contract changes are undetectable at test time.\n template_variable.variable_id = \"var-1\"\n template_variable.session_id = \"sess-1\"\n template_variable.variable_name = \"country\"\n template_variable.expression_source = \"{{ filter_values('country') }}\"\n template_variable.variable_kind = \"native_filter\"\n template_variable.is_required = True\n template_variable.default_value = None\n template_variable.mapping_status = \"unmapped\"\n mapping = ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=\"direct_match\",\n raw_input_value=\"DE\",\n effective_value=\"DE\",\n transformation_note=\"Trimmed imported value\",\n warning_level=\"medium\",\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=now,\n updated_at=now,\n )\n\n session.findings = []\n session.collaborators = []\n session.semantic_sources = []\n session.semantic_fields = []\n session.imported_filters = [imported_filter]\n session.template_variables = [template_variable]\n session.execution_mappings = [mapping]\n session.clarification_sessions = []\n session.previews = []\n session.run_contexts = []\n return session\n\n\n# [/DEF:_make_us3_session:Function]\n\n\n# [DEF:_make_preview_ready_session:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_preview_ready_session():\n session = _make_us3_session()\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n session.current_phase = SessionPhase.PREVIEW\n return session\n\n\n# [/DEF:_make_preview_ready_session:Function]\n\n\n# [DEF:dataset_review_api_dependencies:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n@pytest.fixture(autouse=True)\ndef dataset_review_api_dependencies():\n mock_user = _make_user()\n config_manager = _make_config_manager()\n task_manager = MagicMock()\n\n app.dependency_overrides[get_current_user] = lambda: mock_user\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n\n yield {\n \"user\": mock_user,\n \"config_manager\": config_manager,\n \"task_manager\": task_manager,\n }\n app.dependency_overrides.clear()\n\n\n# [/DEF:dataset_review_api_dependencies:Function]\n\n\n# [DEF:test_parse_superset_link_dashboard_partial_recovery:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify dashboard links recover dataset context and preserve explicit partial-recovery markers.\ndef test_parse_superset_link_dashboard_partial_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_detail.return_value = {\n \"id\": 10,\n \"datasets\": [{\"id\": 42}, {\"id\": 77}],\n }\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://superset.local/dashboard/10/?native_filters=%5B%7B%22name%22%3A%22country%22%2C%22value%22%3A%22DE%22%7D%5D\"\n )\n\n assert result.dataset_id == 42\n assert result.dashboard_id == 10\n assert result.dataset_ref == \"public.sales\"\n assert result.partial_recovery is True\n assert \"multiple_dashboard_datasets\" in result.unresolved_references\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n\n\n# [/DEF:test_parse_superset_link_dashboard_partial_recovery:Function]\n\n\n# [DEF:test_parse_superset_link_dashboard_slug_recovery:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify dashboard slug links resolve through dashboard detail endpoints and recover dataset context.\ndef test_parse_superset_link_dashboard_slug_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_detail.return_value = {\n \"id\": 15,\n \"datasets\": [{\"id\": 42}],\n }\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"https://ss-dev.bebesh.ru/superset/dashboard/slack/?native_filters_key=8ZLV4M-UXOM\"\n )\n\n assert result.dataset_id == 42\n assert result.dashboard_id == 15\n assert result.dataset_ref == \"public.sales\"\n assert result.partial_recovery is False\n assert result.query_state[\"native_filters_key\"] == \"8ZLV4M-UXOM\"\n fake_client.get_dashboard_detail.assert_called_once_with(\"slack\")\n\n\n# [/DEF:test_parse_superset_link_dashboard_slug_recovery:Function]\n\n\n# [DEF:test_parse_superset_link_dashboard_permalink_partial_recovery:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify dashboard permalink links no longer fail parsing and preserve permalink filter state for partial recovery.\ndef test_parse_superset_link_dashboard_permalink_partial_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_permalink_state.return_value = {\n \"state\": {\n \"dataMask\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"country\",\n \"filterState\": {\n \"label\": \"Country\",\n \"value\": [\"DE\"],\n },\n \"extraFormData\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n },\n }\n }\n }\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://ss-dev.bebesh.ru/superset/dashboard/p/QabXy6wG30Z/\"\n )\n\n assert result.resource_type == \"dashboard\"\n assert result.dataset_id is None\n assert result.dashboard_id is None\n assert result.dataset_ref == \"dashboard_permalink:QabXy6wG30Z\"\n assert result.partial_recovery is True\n assert (\n \"dashboard_permalink_dataset_binding_unresolved\" in result.unresolved_references\n )\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n assert result.imported_filters[0][\"raw_value\"] == [\"DE\"]\n fake_client.get_dashboard_permalink_state.assert_called_once_with(\"QabXy6wG30Z\")\n\n\n# [DEF:test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify permalink state with nested dashboard id recovers dataset binding and keeps imported filters.\ndef test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_permalink_state.return_value = {\n \"state\": {\n \"form_data\": {\"dashboardId\": 22},\n \"dataMask\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"country\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n }\n },\n }\n }\n fake_client.get_dashboard_detail.return_value = {\"id\": 22, \"datasets\": [{\"id\": 42}]}\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://ss-dev.bebesh.ru/superset/dashboard/p/QabXy6wG30Z/\"\n )\n\n assert result.dashboard_id == 22\n assert result.dataset_id == 42\n assert result.dataset_ref == \"public.sales\"\n assert (\n \"dashboard_permalink_dataset_binding_unresolved\"\n not in result.unresolved_references\n )\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n\n\n# [/DEF:test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state:Function]\n# [/DEF:test_parse_superset_link_dashboard_permalink_partial_recovery:Function]\n\n\n# [DEF:test_resolve_from_dictionary_prefers_exact_match:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify trusted dictionary exact matches outrank fuzzy candidates and unresolved fields stay explicit.\ndef test_resolve_from_dictionary_prefers_exact_match():\n resolver = SemanticSourceResolver()\n result = resolver.resolve_from_dictionary(\n {\n \"source_ref\": \"dict://finance\",\n \"rows\": [\n {\n \"field_name\": \"revenue\",\n \"verbose_name\": \"Revenue\",\n \"description\": \"Recognized revenue amount\",\n \"display_format\": \"$,.2f\",\n },\n {\n \"field_name\": \"revnue\",\n \"verbose_name\": \"Revenue typo\",\n \"description\": \"Fuzzy variant\",\n },\n ],\n },\n [\n {\"field_name\": \"revenue\", \"is_locked\": False},\n {\"field_name\": \"margin\", \"is_locked\": False},\n ],\n )\n\n resolved_exact = next(\n item for item in result.resolved_fields if item[\"field_name\"] == \"revenue\"\n )\n unresolved = next(\n item for item in result.resolved_fields if item[\"field_name\"] == \"margin\"\n )\n\n assert resolved_exact[\"applied_candidate\"][\"match_type\"] == \"exact\"\n assert resolved_exact[\"provenance\"] == \"dictionary_exact\"\n assert unresolved[\"status\"] == \"unresolved\"\n assert \"margin\" in result.unresolved_fields\n assert result.partial_recovery is True\n\n\n# [/DEF:test_resolve_from_dictionary_prefers_exact_match:Function]\n\n\n# [DEF:test_orchestrator_start_session_preserves_partial_recovery:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify session start persists usable recovery-required state when Superset intake is partial.\ndef test_orchestrator_start_session_preserves_partial_recovery(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n created_session = _make_session()\n created_session.readiness_state = ReadinessState.RECOVERY_REQUIRED\n created_session.current_phase = SessionPhase.RECOVERY\n\n repository.create_session.return_value = created_session\n repository.save_profile_and_findings.return_value = created_session\n repository.save_recovery_state.return_value = created_session\n repository.db = MagicMock()\n\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n\n parsed_context = SimpleNamespace(\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n chart_id=None,\n partial_recovery=True,\n unresolved_references=[\"dashboard_dataset_binding_missing\"],\n imported_filters=[],\n )\n\n fake_extractor = MagicMock()\n fake_extractor.parse_superset_link.return_value = parsed_context\n fake_extractor.recover_imported_filters.return_value = []\n fake_extractor.client.get_dataset_detail.return_value = {\n \"id\": 42,\n \"sql\": \"\",\n \"columns\": [],\n \"metrics\": [],\n }\n fake_extractor.discover_template_variables.return_value = []\n\n with patch(\n \"src.services.dataset_review.orchestrator.SupersetContextExtractor\",\n side_effect=[fake_extractor, fake_extractor],\n ):\n result = orchestrator.start_session(\n StartSessionCommand(\n user=dataset_review_api_dependencies[\"user\"],\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n )\n )\n\n assert result.session.readiness_state == ReadinessState.RECOVERY_REQUIRED\n assert result.findings\n assert result.findings[0].severity.value == \"warning\"\n repository.create_session.assert_called_once()\n repository.save_profile_and_findings.assert_called_once()\n\n\n# [/DEF:test_orchestrator_start_session_preserves_partial_recovery:Function]\n\n\n# [DEF:test_orchestrator_start_session_bootstraps_recovery_state:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify session start persists recovered filters, template variables, and initial execution mappings for review workspace bootstrap.\ndef test_orchestrator_start_session_bootstraps_recovery_state(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n created_session = _make_session()\n created_session.readiness_state = ReadinessState.RECOVERY_REQUIRED\n created_session.current_phase = SessionPhase.RECOVERY\n\n repository.create_session.return_value = created_session\n repository.save_profile_and_findings.return_value = created_session\n repository.save_recovery_state.return_value = created_session\n repository.db = MagicMock()\n\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n\n parsed_context = SimpleNamespace(\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n chart_id=None,\n partial_recovery=True,\n unresolved_references=[\"dashboard_dataset_binding_missing\"],\n imported_filters=[{\"filter_name\": \"country\", \"raw_value\": [\"DE\"]}],\n )\n\n fake_extractor = MagicMock()\n fake_extractor.parse_superset_link.return_value = parsed_context\n fake_extractor.recover_imported_filters.return_value = [\n {\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"raw_value\": [\"DE\"],\n \"normalized_value\": {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n \"source\": \"superset_url\",\n \"confidence_state\": \"imported\",\n \"requires_confirmation\": False,\n \"recovery_status\": \"recovered\",\n \"notes\": \"Recovered from permalink state\",\n }\n ]\n fake_extractor.client.get_dataset_detail.return_value = {\n \"id\": 42,\n \"sql\": \"select * from sales where country in {{ filter_values('country') }}\",\n \"columns\": [],\n \"metrics\": [],\n }\n fake_extractor.discover_template_variables.return_value = [\n {\n \"variable_name\": \"country\",\n \"expression_source\": \"{{ filter_values('country') }}\",\n \"variable_kind\": \"native_filter\",\n \"is_required\": True,\n \"default_value\": None,\n \"mapping_status\": \"unmapped\",\n }\n ]\n\n with patch(\n \"src.services.dataset_review.orchestrator.SupersetContextExtractor\",\n side_effect=[fake_extractor, fake_extractor],\n ):\n result = orchestrator.start_session(\n StartSessionCommand(\n user=dataset_review_api_dependencies[\"user\"],\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n )\n )\n\n assert result.session.readiness_state == ReadinessState.RECOVERY_REQUIRED\n repository.save_recovery_state.assert_called_once()\n saved_filters = repository.save_recovery_state.call_args.args[2]\n saved_variables = repository.save_recovery_state.call_args.args[3]\n saved_mappings = repository.save_recovery_state.call_args.args[4]\n assert len(saved_filters) == 1\n assert saved_filters[0].filter_name == \"country\"\n assert saved_filters[0].normalized_value == {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n }\n assert len(saved_variables) == 1\n assert saved_variables[0].variable_name == \"country\"\n assert len(saved_mappings) == 1\n assert saved_mappings[0].raw_input_value == [\"DE\"]\n\n\n# [/DEF:test_orchestrator_start_session_bootstraps_recovery_state:Function]\n\n\n# [DEF:test_start_session_endpoint_returns_created_summary:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify POST session lifecycle endpoint returns a persisted ownership-scoped summary.\ndef test_start_session_endpoint_returns_created_summary(\n dataset_review_api_dependencies,\n):\n session = _make_session()\n orchestrator = MagicMock()\n orchestrator.start_session.return_value = SimpleNamespace(\n session=session, findings=[], parsed_context=None\n )\n\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n\n response = client.post(\n \"/api/dataset-orchestration/sessions\",\n json={\n \"source_kind\": \"superset_link\",\n \"source_input\": \"http://superset.local/dashboard/10\",\n \"environment_id\": \"env-1\",\n },\n )\n\n assert response.status_code == 201\n payload = response.json()\n assert payload[\"session_id\"] == \"sess-1\"\n assert payload[\"dataset_ref\"] == \"public.sales\"\n assert payload[\"environment_id\"] == \"env-1\"\n\n\n# [/DEF:test_start_session_endpoint_returns_created_summary:Function]\n\n\n# [DEF:test_get_session_detail_export_and_lifecycle_endpoints:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify lifecycle get/patch/delete plus documentation and validation exports remain ownership-scoped and usable.\ndef test_get_session_detail_export_and_lifecycle_endpoints(\n dataset_review_api_dependencies,\n):\n now = datetime.now(timezone.utc)\n session = MagicMock(spec=DatasetReviewSession)\n session.session_id = \"sess-1\"\n session.user_id = \"user-1\"\n session.environment_id = \"env-1\"\n session.source_kind = \"superset_link\"\n session.source_input = \"http://superset.local/dashboard/10\"\n session.dataset_ref = \"public.sales\"\n session.dataset_id = 42\n session.dashboard_id = 10\n session.readiness_state = ReadinessState.REVIEW_READY\n session.recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n session.status = SessionStatus.ACTIVE\n session.current_phase = SessionPhase.REVIEW\n session.created_at = now\n session.updated_at = now\n session.last_activity_at = now\n session.profile = SimpleNamespace(\n dataset_name=\"sales\",\n business_summary=\"Summary text\",\n confidence_state=ConfidenceState.MOSTLY_CONFIRMED,\n dataset_type=\"unknown\",\n schema_name=None,\n database_name=None,\n business_summary_source=BusinessSummarySource.IMPORTED,\n description=None,\n is_sqllab_view=False,\n completeness_score=None,\n has_blocking_findings=False,\n has_warning_findings=True,\n manual_summary_locked=False,\n created_at=now,\n updated_at=now,\n profile_id=\"profile-1\",\n session_id=\"sess-1\",\n )\n session.findings = [\n SimpleNamespace(\n finding_id=\"f-1\",\n session_id=\"sess-1\",\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"PARTIAL_SUPERSET_RECOVERY\",\n title=\"Partial\",\n message=\"Some filters require review\",\n resolution_state=ResolutionState.OPEN,\n resolution_note=None,\n caused_by_ref=None,\n created_at=now,\n resolved_at=None,\n )\n ]\n session.collaborators = []\n session.semantic_sources = []\n session.semantic_fields = []\n session.imported_filters = []\n session.template_variables = []\n session.execution_mappings = []\n session.clarification_sessions = []\n session.previews = []\n session.run_contexts = []\n\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.list_sessions_for_user.return_value = [session]\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-0\"\n )\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n detail_response = client.get(\"/api/dataset-orchestration/sessions/sess-1\")\n assert detail_response.status_code == 200\n assert detail_response.json()[\"session_id\"] == \"sess-1\"\n\n patch_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1\",\n json={\"status\": \"paused\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n assert patch_response.status_code == 200\n assert patch_response.json()[\"status\"] == \"paused\"\n\n doc_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/exports/documentation?format=json\"\n )\n assert doc_response.status_code == 200\n assert doc_response.json()[\"artifact_type\"] == \"documentation\"\n\n validation_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/exports/validation?format=markdown\"\n )\n assert validation_response.status_code == 200\n assert validation_response.json()[\"artifact_type\"] == \"validation_report\"\n assert \"Validation Report\" in validation_response.json()[\"content\"][\"markdown\"]\n\n delete_response = client.delete(\n \"/api/dataset-orchestration/sessions/sess-1\",\n headers={\"X-Session-Version\": \"1\"},\n )\n assert delete_response.status_code == 204\n\n\n# [/DEF:test_get_session_detail_export_and_lifecycle_endpoints:Function]\n\n\n# [DEF:test_get_clarification_state_returns_empty_payload_when_session_has_no_record:Function]\n# @PURPOSE: Clarification state endpoint should return a non-blocking empty payload when the session has no clarification aggregate yet.\ndef test_get_clarification_state_returns_empty_payload_when_session_has_no_record(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n response = client.get(\"/api/dataset-orchestration/sessions/sess-1/clarification\")\n\n assert response.status_code == 200\n assert response.json() == {\n \"clarification_session\": None,\n \"current_question\": None,\n }\n\n\n# [/DEF:test_get_clarification_state_returns_empty_payload_when_session_has_no_record:Function]\n\n\n# [DEF:test_us2_clarification_endpoints_persist_answer_and_feedback:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Clarification endpoints should expose one current question, persist the answer before advancement, and store feedback on the answer audit record.\ndef test_us2_clarification_endpoints_persist_answer_and_feedback(\n dataset_review_api_dependencies,\n):\n session = _make_us2_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n\n def _add_side_effect(obj):\n if obj.__class__.__name__ == \"ClarificationAnswer\":\n session.clarification_sessions[0].questions[0].answer = obj\n\n repository.db.add.side_effect = _add_side_effect\n repository.db.flush.side_effect = lambda: None\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n state_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/clarification\"\n )\n assert state_response.status_code == 200\n state_payload = state_response.json()\n assert (\n state_payload[\"current_question\"][\"why_it_matters\"]\n == \"This determines how downstream users interpret revenue KPIs.\"\n )\n assert state_payload[\"current_question\"][\"current_guess\"] == \"Revenue reporting\"\n assert len(state_payload[\"current_question\"][\"options\"]) == 2\n\n answer_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/clarification/answers\",\n json={\n \"question_id\": \"q-1\",\n \"answer_kind\": \"selected\",\n \"answer_value\": \"Revenue reporting\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert answer_response.status_code == 200\n answer_payload = answer_response.json()\n assert answer_payload[\"session\"][\"readiness_state\"] == \"review_ready\"\n assert answer_payload[\"clarification_state\"][\"current_question\"] is None\n assert answer_payload[\"changed_findings\"][0][\"resolution_state\"] == \"resolved\"\n assert (\n session.clarification_sessions[0].questions[0].answer.answer_value\n == \"Revenue reporting\"\n )\n\n feedback_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/clarification/questions/q-1/feedback\",\n json={\"feedback\": \"up\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n assert feedback_response.status_code == 200\n assert feedback_response.json() == {\"target_id\": \"q-1\", \"feedback\": \"up\"}\n assert session.clarification_sessions[0].questions[0].answer.user_feedback == \"up\"\n\n\n# [/DEF:test_us2_clarification_endpoints_persist_answer_and_feedback:Function]\n\n\n# [DEF:test_us2_field_semantic_override_lock_unlock_and_feedback:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Semantic field endpoints should apply manual overrides with lock/provenance invariants and persist feedback independently.\ndef test_us2_field_semantic_override_lock_unlock_and_feedback(\n dataset_review_api_dependencies,\n):\n session = _make_us2_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n repository.db.add.side_effect = lambda obj: None\n repository.db.flush.side_effect = lambda: None\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-1\"\n )\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n override_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic\",\n json={\n \"verbose_name\": \"Confirmed Revenue\",\n \"description\": \"Manual business-approved description\",\n \"display_format\": \"$,.0f\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert override_response.status_code == 200\n override_payload = override_response.json()\n assert override_payload[\"provenance\"] == \"manual_override\"\n assert override_payload[\"is_locked\"] is True\n\n unlock_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/unlock\",\n headers={\"X-Session-Version\": \"1\"},\n )\n assert unlock_response.status_code == 200\n assert unlock_response.json()[\"is_locked\"] is False\n\n candidate_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic\",\n json={\"candidate_id\": \"cand-1\", \"lock_field\": True},\n headers={\"X-Session-Version\": \"2\"},\n )\n assert candidate_response.status_code == 200\n candidate_payload = candidate_response.json()\n assert candidate_payload[\"verbose_name\"] == \"Recognized Revenue\"\n assert candidate_payload[\"provenance\"] == \"dictionary_exact\"\n assert candidate_payload[\"is_locked\"] is True\n\n batch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/semantic/approve-batch\",\n json={\n \"items\": [\n {\"field_id\": \"field-1\", \"candidate_id\": \"cand-1\", \"lock_field\": False}\n ]\n },\n headers={\"X-Session-Version\": \"3\"},\n )\n assert batch_response.status_code == 200\n assert batch_response.json()[0][\"field_id\"] == \"field-1\"\n\n feedback_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/feedback\",\n json={\"feedback\": \"down\"},\n headers={\"X-Session-Version\": \"4\"},\n )\n assert feedback_response.status_code == 200\n assert feedback_response.json() == {\"target_id\": \"field-1\", \"feedback\": \"down\"}\n assert session.semantic_fields[0].user_feedback == \"down\"\n\n\n# [/DEF:test_us2_field_semantic_override_lock_unlock_and_feedback:Function]\n\n\n# [DEF:test_us3_mapping_patch_approval_preview_and_launch_endpoints:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: US3 execution endpoints should persist manual overrides, preserve explicit approval semantics, return contract-shaped preview truth, and expose audited launch handoff.\ndef test_us3_mapping_patch_approval_preview_and_launch_endpoints(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n latest_preview = CompiledPreview(\n preview_id=\"preview-old\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'FR'\",\n preview_fingerprint=\"fingerprint-old\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(timezone.utc),\n created_at=datetime.now(timezone.utc),\n )\n session.previews = [latest_preview]\n\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-2\"\n )\n\n preview = SimpleNamespace(\n preview_id=\"preview-1\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'DE'\",\n preview_fingerprint=\"fingerprint-1\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(timezone.utc),\n created_at=datetime.now(timezone.utc),\n )\n run_context = SimpleNamespace(\n run_context_id=\"run-1\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-1\",\n sql_lab_session_ref=\"sql-lab-77\",\n effective_filters=[{\"mapping_id\": \"map-1\", \"effective_value\": \"EU\"}],\n template_params={\"country\": \"EU\"},\n approved_mapping_ids=[\"map-1\"],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(timezone.utc),\n )\n orchestrator = MagicMock()\n orchestrator.prepare_launch_preview.return_value = PreparePreviewResult(\n session=session,\n preview=preview,\n blocked_reasons=[],\n )\n orchestrator.launch_dataset.return_value = LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n\n def _assert_expected_preview_version(command):\n assert command.expected_version == 3\n return PreparePreviewResult(\n session=session, preview=preview, blocked_reasons=[]\n )\n\n def _assert_expected_launch_version(command):\n assert command.expected_version == 5\n return LaunchDatasetResult(\n session=session, run_context=run_context, blocked_reasons=[]\n )\n\n orchestrator.prepare_launch_preview.side_effect = _assert_expected_preview_version\n orchestrator.launch_dataset.side_effect = _assert_expected_launch_version\n\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n\n patch_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1\",\n json={\n \"effective_value\": \"EU\",\n \"mapping_method\": \"manual_override\",\n \"transformation_note\": \"Manual override for SQL Lab launch\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert patch_response.status_code == 200\n patch_payload = patch_response.json()\n assert patch_payload[\"mapping_id\"] == \"map-1\"\n assert patch_payload[\"mapping_method\"] == \"manual_override\"\n assert patch_payload[\"effective_value\"] == \"EU\"\n assert patch_payload[\"approval_state\"] == \"approved\"\n assert patch_payload[\"approved_by_user_id\"] == \"user-1\"\n assert session.execution_mappings[0].mapping_method == MappingMethod.MANUAL_OVERRIDE\n assert (\n session.execution_mappings[0].transformation_note\n == \"Manual override for SQL Lab launch\"\n )\n assert session.execution_mappings[0].effective_value == \"EU\"\n assert session.recommended_action == RecommendedAction.GENERATE_SQL_PREVIEW\n assert latest_preview.preview_status == PreviewStatus.STALE\n\n approve_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1/approve\",\n json={\"approval_note\": \"Approved after reviewing transformation\"},\n headers={\"X-Session-Version\": \"1\"},\n )\n assert approve_response.status_code == 200\n approve_payload = approve_response.json()\n assert approve_payload[\"mapping_id\"] == \"map-1\"\n assert approve_payload[\"approval_state\"] == \"approved\"\n assert approve_payload[\"approved_by_user_id\"] == \"user-1\"\n assert (\n session.execution_mappings[0].transformation_note\n == \"Approved after reviewing transformation\"\n )\n\n batch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/approve-batch\",\n json={\"mapping_ids\": [\"map-1\"]},\n headers={\"X-Session-Version\": \"2\"},\n )\n assert batch_response.status_code == 200\n assert batch_response.json()[0][\"mapping_id\"] == \"map-1\"\n\n list_response = client.get(\"/api/dataset-orchestration/sessions/sess-1/mappings\")\n assert list_response.status_code == 200\n assert list_response.json()[\"items\"][0][\"mapping_id\"] == \"map-1\"\n\n preview_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"3\"},\n )\n assert preview_response.status_code == 200\n preview_payload = preview_response.json()\n assert preview_payload[\"preview_id\"] == \"preview-1\"\n assert preview_payload[\"preview_status\"] == \"ready\"\n assert preview_payload[\"compiled_by\"] == \"superset\"\n assert \"SELECT * FROM sales\" in preview_payload[\"compiled_sql\"]\n\n def _assert_expected_pending_preview_version(command):\n assert command.expected_version == 4\n return PreparePreviewResult(\n session=session,\n preview=SimpleNamespace(\n preview_id=\"preview-pending\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.PENDING,\n compiled_sql=None,\n preview_fingerprint=\"fingerprint-pending\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=None,\n created_at=datetime.now(timezone.utc),\n ),\n blocked_reasons=[],\n )\n\n orchestrator.prepare_launch_preview.side_effect = (\n _assert_expected_pending_preview_version\n )\n preview_enqueue_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"4\"},\n )\n assert preview_enqueue_response.status_code == 202\n assert preview_enqueue_response.json() == {\n \"session_id\": \"sess-1\",\n \"session_version\": 3,\n \"preview_status\": \"pending\",\n \"task_id\": None,\n }\n\n launch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/launch\",\n headers={\"X-Session-Version\": \"5\"},\n )\n assert launch_response.status_code == 201\n launch_payload = launch_response.json()\n assert launch_payload[\"run_context\"][\"run_context_id\"] == \"run-1\"\n assert launch_payload[\"run_context\"][\"sql_lab_session_ref\"] == \"sql-lab-77\"\n assert launch_payload[\"run_context\"][\"launch_status\"] == \"started\"\n assert (\n launch_payload[\"redirect_url\"]\n == \"http://superset.local/superset/sqllab?queryId=sql-lab-77\"\n )\n\n\n# [/DEF:test_us3_mapping_patch_approval_preview_and_launch_endpoints:Function]\n\n\n# [DEF:test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Preview response should expose the refreshed session version so the normal preview-then-launch UI flow can satisfy optimistic locking without a forced full reload.\ndef test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n session.version = 4\n\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n\n def _require_session_version(current, expected):\n if int(getattr(current, \"version\", 0) or 0) != expected:\n raise DatasetReviewSessionVersionConflictError(\n current.session_id,\n expected,\n int(getattr(current, \"version\", 0) or 0),\n )\n return current\n\n repository.require_session_version.side_effect = _require_session_version\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n preview = SimpleNamespace(\n preview_id=\"preview-2\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'FR'\",\n preview_fingerprint=\"fingerprint-2\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(timezone.utc),\n created_at=datetime.now(timezone.utc),\n )\n run_context = SimpleNamespace(\n run_context_id=\"run-2\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-2\",\n sql_lab_session_ref=\"sql-lab-88\",\n effective_filters=[{\"mapping_id\": \"map-1\", \"effective_value\": \"FR\"}],\n template_params={\"country\": \"FR\"},\n approved_mapping_ids=[],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(timezone.utc),\n )\n orchestrator = MagicMock()\n\n def _prepare_preview(command):\n assert command.expected_version == 4\n session.version = 5\n return PreparePreviewResult(\n session=session,\n preview=preview,\n blocked_reasons=[],\n )\n\n def _launch_dataset(command):\n assert command.expected_version == 5\n return LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n\n orchestrator.prepare_launch_preview.side_effect = _prepare_preview\n orchestrator.launch_dataset.side_effect = _launch_dataset\n\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n\n preview_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"4\"},\n )\n\n assert preview_response.status_code == 200\n preview_payload = preview_response.json()\n assert preview_payload[\"session_version\"] == 5\n\n launch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/launch\",\n headers={\"X-Session-Version\": str(preview_payload[\"session_version\"])},\n )\n\n assert launch_response.status_code == 201\n assert launch_response.json()[\"run_context\"][\"run_context_id\"] == \"run-2\"\n\n\n# [/DEF:test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up:Function]\n\n\n# [DEF:test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Preview endpoint should preserve API contract and surface generic upstream preview failures without fabricating dashboard-not-found semantics for non-dashboard 404s.\ndef test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n failed_preview = SimpleNamespace(\n preview_id=\"preview-failed\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=\"fingerprint-failed\",\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=\"RuntimeError: [API_FAILURE] API resource not found at endpoint '/chart/data' | Context: {'status_code': 404, 'endpoint': '/chart/data', 'subtype': 'not_found'}\",\n compiled_at=None,\n created_at=datetime.now(timezone.utc),\n )\n orchestrator = MagicMock()\n orchestrator.prepare_launch_preview.return_value = PreparePreviewResult(\n session=session,\n preview=failed_preview,\n blocked_reasons=[],\n )\n\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n\n response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"0\"},\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"preview_id\"] == \"preview-failed\"\n assert payload[\"preview_status\"] == \"failed\"\n assert payload[\"compiled_sql\"] is None\n assert payload[\"compiled_by\"] == \"superset\"\n assert payload[\"error_code\"] == \"superset_preview_failed\"\n assert \"/chart/data\" in payload[\"error_details\"]\n assert \"API resource not found\" in payload[\"error_details\"]\n assert \"Dashboard not found\" not in payload[\"error_details\"]\n\n\n# [/DEF:test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift:Function]\n\n\n# [DEF:test_mutation_endpoints_surface_session_version_conflict_payload:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Dataset review mutation endpoints should return deterministic 409 conflict semantics when optimistic-lock versions are stale.\ndef test_mutation_endpoints_surface_session_version_conflict_payload(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = (\n DatasetReviewSessionVersionConflictError(\n session_id=\"sess-1\",\n expected_version=2,\n actual_version=5,\n )\n )\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1\",\n json={\n \"effective_value\": \"EU\",\n \"mapping_method\": \"manual_override\",\n },\n headers={\"X-Session-Version\": \"2\"},\n )\n\n assert response.status_code == 409\n payload = response.json()[\"detail\"]\n assert payload[\"error_code\"] == \"session_version_conflict\"\n assert payload[\"expected_version\"] == 2\n assert payload[\"actual_version\"] == 5\n\n\n# [/DEF:test_mutation_endpoints_surface_session_version_conflict_payload:Function]\n\n\n# [DEF:test_update_session_surfaces_commit_time_session_version_conflict_payload:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Session lifecycle mutation should return deterministic 409 conflict semantics when commit-time optimistic locking rejects a stale write.\ndef test_update_session_surfaces_commit_time_session_version_conflict_payload(\n dataset_review_api_dependencies,\n):\n session = _make_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.return_value = session\n repository.commit_session_mutation.side_effect = (\n DatasetReviewSessionVersionConflictError(\n session_id=\"sess-1\",\n expected_version=0,\n actual_version=1,\n )\n )\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1\",\n json={\"status\": \"paused\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n\n assert response.status_code == 409\n payload = response.json()[\"detail\"]\n assert payload[\"error_code\"] == \"session_version_conflict\"\n assert payload[\"expected_version\"] == 0\n assert payload[\"actual_version\"] == 1\n\n\n# [/DEF:test_update_session_surfaces_commit_time_session_version_conflict_payload:Function]\n\n\n# [DEF:test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Recovered imported filters with values should flow into preview filter context even when no template variable mapping exists.\ndef test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n\n recovered_filter = MagicMock()\n recovered_filter.filter_id = \"filter-country\"\n recovered_filter.filter_name = \"country\"\n recovered_filter.display_name = \"Country\"\n recovered_filter.raw_value = [\"DE\", \"FR\"]\n recovered_filter.normalized_value = [\"DE\", \"FR\"]\n recovered_filter.requires_confirmation = False\n recovered_filter.recovery_status = \"recovered\"\n\n session.imported_filters = [recovered_filter]\n session.template_variables = []\n session.execution_mappings = []\n session.semantic_fields = []\n\n snapshot = orchestrator._build_execution_snapshot(session)\n\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"preview_blockers\"] == []\n recovered_filter.normalized_value = {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n }\n\n snapshot = orchestrator._build_execution_snapshot(session)\n\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"preview_blockers\"] == []\n assert snapshot[\"effective_filters\"] == [\n {\n \"filter_id\": \"filter-country\",\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\", \"FR\"],\n \"raw_input_value\": [\"DE\", \"FR\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n }\n ]\n\n\n# [/DEF:test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping:Function]\n\n\n# [DEF:test_execution_snapshot_preserves_mapped_template_variables_and_filter_context:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Mapped template variables should still populate template params while contributing their effective filter context.\ndef test_execution_snapshot_preserves_mapped_template_variables_and_filter_context(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n\n snapshot = orchestrator._build_execution_snapshot(session)\n\n assert snapshot[\"template_params\"] == {\"country\": \"DE\"}\n assert snapshot[\"preview_blockers\"] == []\n assert snapshot[\"effective_filters\"] == [\n {\n \"mapping_id\": \"map-1\",\n \"filter_id\": \"filter-1\",\n \"filter_name\": \"country\",\n \"variable_id\": \"var-1\",\n \"variable_name\": \"country\",\n \"effective_value\": \"DE\",\n \"raw_input_value\": \"DE\",\n }\n ]\n assert snapshot[\"open_warning_refs\"] == [\"map-1\"]\n\n\n# [/DEF:test_execution_snapshot_preserves_mapped_template_variables_and_filter_context:Function]\n\n\n# [DEF:test_execution_snapshot_skips_partial_imported_filters_without_values:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Partial imported filters without raw or normalized values must not emit bogus active preview filters.\ndef test_execution_snapshot_skips_partial_imported_filters_without_values(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n\n unresolved_filter = MagicMock()\n unresolved_filter.filter_id = \"filter-region\"\n unresolved_filter.filter_name = \"region\"\n unresolved_filter.display_name = \"Region\"\n unresolved_filter.raw_value = None\n unresolved_filter.normalized_value = None\n unresolved_filter.requires_confirmation = True\n unresolved_filter.recovery_status = \"partial\"\n\n session.imported_filters = [unresolved_filter]\n session.template_variables = []\n session.execution_mappings = []\n session.semantic_fields = []\n\n snapshot = orchestrator._build_execution_snapshot(session)\n\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"effective_filters\"] == []\n assert snapshot[\"preview_blockers\"] == []\n\n\n# [/DEF:test_execution_snapshot_skips_partial_imported_filters_without_values:Function]\n\n\n# [DEF:test_us3_launch_endpoint_requires_launch_permission:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Launch endpoint should enforce the contract RBAC permission instead of the generic session-manage permission.\ndef test_us3_launch_endpoint_requires_launch_permission(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n run_context = SimpleNamespace(\n run_context_id=\"run-1\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-1\",\n sql_lab_session_ref=\"sql-lab-77\",\n effective_filters=[],\n template_params={},\n approved_mapping_ids=[\"map-1\"],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(timezone.utc),\n )\n orchestrator = MagicMock()\n orchestrator.launch_dataset.return_value = LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n dataset_review_api_dependencies[\"user\"].roles = [\n SimpleNamespace(\n name=\"SessionManager\",\n permissions=[SimpleNamespace(resource=\"dataset:session\", action=\"MANAGE\")],\n )\n ]\n\n response = client.post(\"/api/dataset-orchestration/sessions/sess-1/launch\")\n\n assert response.status_code == 403\n assert (\n response.json()[\"detail\"]\n == \"Permission denied for dataset:execution:launch:EXECUTE\"\n )\n\n\n# [/DEF:test_us3_launch_endpoint_requires_launch_permission:Function]\n\n\n# [DEF:test_semantic_source_version_propagation_preserves_locked_fields:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Updated semantic source versions should mark unlocked fields reviewable while preserving locked manual values.\ndef test_semantic_source_version_propagation_preserves_locked_fields():\n resolver = SemanticSourceResolver()\n source = SimpleNamespace(source_id=\"src-1\", source_version=\"2026.04\")\n\n unlocked_field = SimpleNamespace(\n source_id=\"src-1\",\n source_version=\"2026.03\",\n is_locked=False,\n provenance=FieldProvenance.DICTIONARY_EXACT,\n needs_review=False,\n has_conflict=False,\n )\n locked_field = SimpleNamespace(\n source_id=\"src-1\",\n source_version=\"2026.03\",\n is_locked=True,\n provenance=FieldProvenance.MANUAL_OVERRIDE,\n needs_review=False,\n has_conflict=False,\n )\n\n result = resolver.propagate_source_version_update(\n source, [unlocked_field, locked_field]\n )\n\n assert result[\"propagated\"] == 1\n assert result[\"preserved_locked\"] == 1\n assert unlocked_field.source_version == \"2026.04\"\n assert unlocked_field.needs_review is True\n assert locked_field.source_version == \"2026.03\"\n assert locked_field.needs_review is False\n\n\n# [/DEF:test_semantic_source_version_propagation_preserves_locked_fields:Function]\n\n# [/DEF:DatasetReviewApiTests:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_get_dashboards_profile_filter_matches_owner_object_payload_contract [TYPE Function]\n# @RELATION BINDS_TO -> DashboardsApiTests\n# @TEST: GET /api/dashboards profile-default filter matches Superset owner object payloads.\n# @BRIEF Validate profile-default filtering accepts owner object payloads once aliases resolve to the bound Superset username.\n# @PRE: Profile-default preference is enabled and owners list contains dict payloads.\n# @POST: Response keeps dashboards where owner object resolves to bound username alias.\ndef test_get_dashboards_profile_filter_matches_owner_object_payload_contract(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 701,\n \"title\": \"Featured Charts\",\n \"slug\": \"featured-charts\",\n \"owners\": [\n {\n \"id\": 11,\n \"first_name\": \"user\",\n \"last_name\": \"1\",\n \"username\": None,\n \"email\": \"user_1@example.local\",\n }\n ],\n \"modified_by\": \"another_user\",\n },\n {\n \"id\": 702,\n \"title\": \"Other Dashboard\",\n \"slug\": \"other-dashboard\",\n \"owners\": [\n {\n \"id\": 12,\n \"first_name\": \"other\",\n \"last_name\": \"user\",\n \"username\": None,\n \"email\": \"other@example.local\",\n }\n ],\n \"modified_by\": \"other_user\",\n },\n ]\n )\n with (\n patch(\"src.api.routes.dashboards._listing_routes.ProfileService\") as profile_service_cls,\n patch(\n \"src.api.routes.dashboards._projection._resolve_profile_actor_aliases\",\n return_value=[\"user_1\"],\n ),\n ):\n profile_service = MagicMock(spec=DomainProfileService)\n profile_service.get_my_preference.return_value = _build_profile_preference_stub(\n username=\"user_1\",\n enabled=True,\n )\n profile_service.matches_dashboard_actor.side_effect = (\n lambda bound_username, owners, modified_by: any(\n str(owner.get(\"email\", \"\")).split(\"@\", 1)[0].strip().lower()\n == str(bound_username).strip().lower()\n for owner in (owners or [])\n if isinstance(owner, dict)\n )\n )\n profile_service_cls.return_value = profile_service\n response = client.get(\n \"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true\"\n )\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"total\"] == 1\n assert {item[\"id\"] for item in payload[\"dashboards\"]} == {701}\n assert payload[\"dashboards\"][0][\"title\"] == \"Featured Charts\"\n# #endregion test_get_dashboards_profile_filter_matches_owner_object_payload_contract\n" }, { "contract_id": "_make_user", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 76, - "end_line": 90, + "start_line": 70, + "end_line": 82, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -3971,15 +3008,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_user:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_user():\n permissions = [\n SimpleNamespace(resource=\"dataset:session\", action=\"READ\"),\n SimpleNamespace(resource=\"dataset:session\", action=\"MANAGE\"),\n SimpleNamespace(resource=\"dataset:execution:launch\", action=\"EXECUTE\"),\n ]\n dataset_review_role = SimpleNamespace(\n name=\"DatasetReviewOperator\", permissions=permissions\n )\n return SimpleNamespace(id=\"user-1\", username=\"tester\", roles=[dataset_review_role])\n\n\n# [/DEF:_make_user:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_user [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\ndef _make_user():\n permissions = [\n SimpleNamespace(resource=\"dataset:session\", action=\"READ\"),\n SimpleNamespace(resource=\"dataset:session\", action=\"MANAGE\"),\n SimpleNamespace(resource=\"dataset:execution:launch\", action=\"EXECUTE\"),\n ]\n dataset_review_role = SimpleNamespace(\n name=\"DatasetReviewOperator\", permissions=permissions\n )\n return SimpleNamespace(id=\"user-1\", username=\"tester\", roles=[dataset_review_role])\n# #endregion _make_user\n" }, { "contract_id": "_make_config_manager", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 93, - "end_line": 112, + "start_line": 83, + "end_line": 100, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -4002,15 +3039,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_config_manager:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_config_manager():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n config = AppConfig(environments=[env], settings=GlobalSettings())\n manager = MagicMock()\n manager.get_environment.side_effect = (\n lambda env_id: env if env_id == \"env-1\" else None\n )\n manager.get_config.return_value = config\n return manager\n\n\n# [/DEF:_make_config_manager:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_config_manager [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\ndef _make_config_manager():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n config = AppConfig(environments=[env], settings=GlobalSettings())\n manager = MagicMock()\n manager.get_environment.side_effect = (\n lambda env_id: env if env_id == \"env-1\" else None\n )\n manager.get_config.return_value = config\n return manager\n# #endregion _make_config_manager\n" }, { "contract_id": "_make_session", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 115, - "end_line": 139, + "start_line": 101, + "end_line": 123, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -4033,15 +3070,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_session:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_session():\n now = datetime.now(timezone.utc)\n return DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"user-1\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n version=0,\n readiness_state=ReadinessState.REVIEW_READY,\n recommended_action=RecommendedAction.REVIEW_DOCUMENTATION,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.REVIEW,\n created_at=now,\n updated_at=now,\n last_activity_at=now,\n )\n\n\n# [/DEF:_make_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_session [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\ndef _make_session():\n now = datetime.now(UTC)\n return DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"user-1\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n version=0,\n readiness_state=ReadinessState.REVIEW_READY,\n recommended_action=RecommendedAction.REVIEW_DOCUMENTATION,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.REVIEW,\n created_at=now,\n updated_at=now,\n last_activity_at=now,\n )\n# #endregion _make_session\n" }, { "contract_id": "_make_us2_session", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 142, - "end_line": 257, + "start_line": 124, + "end_line": 234, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -4064,15 +3101,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_us2_session:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_us2_session():\n now = datetime.now(timezone.utc)\n session = _make_session()\n session.readiness_state = ReadinessState.CLARIFICATION_NEEDED\n session.recommended_action = RecommendedAction.START_CLARIFICATION\n session.current_phase = SessionPhase.CLARIFICATION\n\n field = SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"revenue\",\n field_kind=FieldKind.COLUMN,\n verbose_name=\"Revenue\",\n description=\"AI-generated revenue description\",\n display_format=\"$,.2f\",\n provenance=FieldProvenance.AI_GENERATED,\n source_id=\"source-ai\",\n source_version=None,\n confidence_rank=1,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"agent\",\n user_feedback=None,\n created_at=now,\n updated_at=now,\n )\n candidate = SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=\"dict-1\",\n candidate_rank=1,\n match_type=CandidateMatchType.EXACT,\n confidence_score=1.0,\n proposed_verbose_name=\"Recognized Revenue\",\n proposed_description=\"Trusted dictionary description\",\n proposed_display_format=\"$,.2f\",\n status=CandidateStatus.PROPOSED,\n created_at=now,\n )\n field.candidates = [candidate]\n\n clarification_session = ClarificationSession(\n clarification_session_id=\"clar-1\",\n session_id=\"sess-1\",\n status=ClarificationStatus.PENDING,\n current_question_id=None,\n resolved_count=0,\n remaining_count=1,\n summary_delta=None,\n started_at=now,\n updated_at=now,\n completed_at=None,\n )\n question = ClarificationQuestion(\n question_id=\"q-1\",\n clarification_session_id=\"clar-1\",\n topic_ref=\"dataset.business_purpose\",\n question_text=\"Which business concept does this dataset represent?\",\n why_it_matters=\"This determines how downstream users interpret revenue KPIs.\",\n current_guess=\"Revenue reporting\",\n priority=100,\n state=QuestionState.OPEN,\n created_at=now,\n updated_at=now,\n )\n question.options = [\n ClarificationOption(\n option_id=\"opt-1\",\n question_id=\"q-1\",\n label=\"Revenue reporting\",\n value=\"Revenue reporting\",\n is_recommended=True,\n display_order=1,\n ),\n ClarificationOption(\n option_id=\"opt-2\",\n question_id=\"q-1\",\n label=\"Margin analysis\",\n value=\"Margin analysis\",\n is_recommended=False,\n display_order=2,\n ),\n ]\n question.answer = None\n clarification_session.questions = [question]\n\n session.findings = []\n session.collaborators = []\n session.semantic_sources = [\n SemanticSource(\n source_id=\"dict-1\",\n session_id=\"sess-1\",\n source_type=SemanticSourceType.CONNECTED_DICTIONARY,\n source_ref=\"dict://finance\",\n source_version=\"2026.03\",\n display_name=\"Finance Dictionary\",\n trust_level=TrustLevel.TRUSTED,\n schema_overlap_score=1.0,\n status=SemanticSourceStatus.AVAILABLE,\n created_at=now,\n )\n ]\n session.semantic_fields = [field]\n session.imported_filters = []\n session.template_variables = []\n session.execution_mappings = []\n session.clarification_sessions = [clarification_session]\n session.previews = []\n session.run_contexts = []\n return session\n\n\n# [/DEF:_make_us2_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_us2_session [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\ndef _make_us2_session():\n now = datetime.now(UTC)\n session = _make_session()\n session.readiness_state = ReadinessState.CLARIFICATION_NEEDED\n session.recommended_action = RecommendedAction.START_CLARIFICATION\n session.current_phase = SessionPhase.CLARIFICATION\n field = SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"revenue\",\n field_kind=FieldKind.COLUMN,\n verbose_name=\"Revenue\",\n description=\"AI-generated revenue description\",\n display_format=\"$,.2f\",\n provenance=FieldProvenance.AI_GENERATED,\n source_id=\"source-ai\",\n source_version=None,\n confidence_rank=1,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"agent\",\n user_feedback=None,\n created_at=now,\n updated_at=now,\n )\n candidate = SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=\"dict-1\",\n candidate_rank=1,\n match_type=CandidateMatchType.EXACT,\n confidence_score=1.0,\n proposed_verbose_name=\"Recognized Revenue\",\n proposed_description=\"Trusted dictionary description\",\n proposed_display_format=\"$,.2f\",\n status=CandidateStatus.PROPOSED,\n created_at=now,\n )\n field.candidates = [candidate]\n clarification_session = ClarificationSession(\n clarification_session_id=\"clar-1\",\n session_id=\"sess-1\",\n status=ClarificationStatus.PENDING,\n current_question_id=None,\n resolved_count=0,\n remaining_count=1,\n summary_delta=None,\n started_at=now,\n updated_at=now,\n completed_at=None,\n )\n question = ClarificationQuestion(\n question_id=\"q-1\",\n clarification_session_id=\"clar-1\",\n topic_ref=\"dataset.business_purpose\",\n question_text=\"Which business concept does this dataset represent?\",\n why_it_matters=\"This determines how downstream users interpret revenue KPIs.\",\n current_guess=\"Revenue reporting\",\n priority=100,\n state=QuestionState.OPEN,\n created_at=now,\n updated_at=now,\n )\n question.options = [\n ClarificationOption(\n option_id=\"opt-1\",\n question_id=\"q-1\",\n label=\"Revenue reporting\",\n value=\"Revenue reporting\",\n is_recommended=True,\n display_order=1,\n ),\n ClarificationOption(\n option_id=\"opt-2\",\n question_id=\"q-1\",\n label=\"Margin analysis\",\n value=\"Margin analysis\",\n is_recommended=False,\n display_order=2,\n ),\n ]\n question.answer = None\n clarification_session.questions = [question]\n session.findings = []\n session.collaborators = []\n session.semantic_sources = [\n SemanticSource(\n source_id=\"dict-1\",\n session_id=\"sess-1\",\n source_type=SemanticSourceType.CONNECTED_DICTIONARY,\n source_ref=\"dict://finance\",\n source_version=\"2026.03\",\n display_name=\"Finance Dictionary\",\n trust_level=TrustLevel.TRUSTED,\n schema_overlap_score=1.0,\n status=SemanticSourceStatus.AVAILABLE,\n created_at=now,\n )\n ]\n session.semantic_fields = [field]\n session.imported_filters = []\n session.template_variables = []\n session.execution_mappings = []\n session.clarification_sessions = [clarification_session]\n session.previews = []\n session.run_contexts = []\n return session\n# #endregion _make_us2_session\n" }, { "contract_id": "_make_us3_session", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 260, - "end_line": 327, + "start_line": 235, + "end_line": 296, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -4095,15 +3132,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_us3_session:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_us3_session():\n \"\"\"Fake session factory for US3 flow tests.\n\n `imported_filter` and `template_variable` are bare MagicMocks without spec;\n ORM attribute access is unchecked.\n \"\"\"\n now = datetime.now(timezone.utc)\n session = _make_session()\n session.readiness_state = ReadinessState.MAPPING_REVIEW_NEEDED\n session.recommended_action = RecommendedAction.APPROVE_MAPPING\n session.current_phase = SessionPhase.MAPPING_REVIEW\n\n imported_filter = MagicMock() # @RISK: No spec= guard — enum/field contract changes are undetectable at test time.\n imported_filter.filter_id = \"filter-1\"\n imported_filter.session_id = \"sess-1\"\n imported_filter.filter_name = \"country\"\n imported_filter.display_name = \"Country\"\n imported_filter.raw_value = \"DE\"\n imported_filter.normalized_value = \"DE\"\n imported_filter.source = \"superset_url\"\n imported_filter.confidence_state = \"imported\"\n imported_filter.requires_confirmation = False\n imported_filter.recovery_status = \"recovered\"\n imported_filter.notes = \"Recovered from URL state\"\n\n template_variable = MagicMock() # @RISK: No spec= guard — enum/field contract changes are undetectable at test time.\n template_variable.variable_id = \"var-1\"\n template_variable.session_id = \"sess-1\"\n template_variable.variable_name = \"country\"\n template_variable.expression_source = \"{{ filter_values('country') }}\"\n template_variable.variable_kind = \"native_filter\"\n template_variable.is_required = True\n template_variable.default_value = None\n template_variable.mapping_status = \"unmapped\"\n mapping = ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=\"direct_match\",\n raw_input_value=\"DE\",\n effective_value=\"DE\",\n transformation_note=\"Trimmed imported value\",\n warning_level=\"medium\",\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=now,\n updated_at=now,\n )\n\n session.findings = []\n session.collaborators = []\n session.semantic_sources = []\n session.semantic_fields = []\n session.imported_filters = [imported_filter]\n session.template_variables = [template_variable]\n session.execution_mappings = [mapping]\n session.clarification_sessions = []\n session.previews = []\n session.run_contexts = []\n return session\n\n\n# [/DEF:_make_us3_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_us3_session [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\ndef _make_us3_session():\n \"\"\"Fake session factory for US3 flow tests.\n `imported_filter` and `template_variable` are bare MagicMocks without spec;\n ORM attribute access is unchecked.\n \"\"\"\n now = datetime.now(UTC)\n session = _make_session()\n session.readiness_state = ReadinessState.MAPPING_REVIEW_NEEDED\n session.recommended_action = RecommendedAction.APPROVE_MAPPING\n session.current_phase = SessionPhase.MAPPING_REVIEW\n imported_filter = MagicMock() # @RISK: No spec= guard — enum/field contract changes are undetectable at test time.\n imported_filter.filter_id = \"filter-1\"\n imported_filter.session_id = \"sess-1\"\n imported_filter.filter_name = \"country\"\n imported_filter.display_name = \"Country\"\n imported_filter.raw_value = \"DE\"\n imported_filter.normalized_value = \"DE\"\n imported_filter.source = \"superset_url\"\n imported_filter.confidence_state = \"imported\"\n imported_filter.requires_confirmation = False\n imported_filter.recovery_status = \"recovered\"\n imported_filter.notes = \"Recovered from URL state\"\n template_variable = MagicMock() # @RISK: No spec= guard — enum/field contract changes are undetectable at test time.\n template_variable.variable_id = \"var-1\"\n template_variable.session_id = \"sess-1\"\n template_variable.variable_name = \"country\"\n template_variable.expression_source = \"{{ filter_values('country') }}\"\n template_variable.variable_kind = \"native_filter\"\n template_variable.is_required = True\n template_variable.default_value = None\n template_variable.mapping_status = \"unmapped\"\n mapping = ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=\"direct_match\",\n raw_input_value=\"DE\",\n effective_value=\"DE\",\n transformation_note=\"Trimmed imported value\",\n warning_level=\"medium\",\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=now,\n updated_at=now,\n )\n session.findings = []\n session.collaborators = []\n session.semantic_sources = []\n session.semantic_fields = []\n session.imported_filters = [imported_filter]\n session.template_variables = [template_variable]\n session.execution_mappings = [mapping]\n session.clarification_sessions = []\n session.previews = []\n session.run_contexts = []\n return session\n# #endregion _make_us3_session\n" }, { "contract_id": "_make_preview_ready_session", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 330, - "end_line": 340, + "start_line": 297, + "end_line": 305, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -4126,15 +3163,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_preview_ready_session:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\ndef _make_preview_ready_session():\n session = _make_us3_session()\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n session.current_phase = SessionPhase.PREVIEW\n return session\n\n\n# [/DEF:_make_preview_ready_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_preview_ready_session [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\ndef _make_preview_ready_session():\n session = _make_us3_session()\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n session.current_phase = SessionPhase.PREVIEW\n return session\n# #endregion _make_preview_ready_session\n" }, { "contract_id": "dataset_review_api_dependencies", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 343, - "end_line": 363, + "start_line": 306, + "end_line": 322, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -4157,15 +3194,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:dataset_review_api_dependencies:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n@pytest.fixture(autouse=True)\ndef dataset_review_api_dependencies():\n mock_user = _make_user()\n config_manager = _make_config_manager()\n task_manager = MagicMock()\n\n app.dependency_overrides[get_current_user] = lambda: mock_user\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n\n yield {\n \"user\": mock_user,\n \"config_manager\": config_manager,\n \"task_manager\": task_manager,\n }\n app.dependency_overrides.clear()\n\n\n# [/DEF:dataset_review_api_dependencies:Function]\n" + "anchor_syntax": "region", + "body": "# #region dataset_review_api_dependencies [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n@pytest.fixture(autouse=True)\ndef dataset_review_api_dependencies():\n mock_user = _make_user()\n config_manager = _make_config_manager()\n task_manager = MagicMock()\n app.dependency_overrides[get_current_user] = lambda: mock_user\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n yield {\n \"user\": mock_user,\n \"config_manager\": config_manager,\n \"task_manager\": task_manager,\n }\n app.dependency_overrides.clear()\n# #endregion dataset_review_api_dependencies\n" }, { "contract_id": "test_parse_superset_link_dashboard_partial_recovery", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 366, - "end_line": 400, + "start_line": 323, + "end_line": 353, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4190,15 +3227,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_parse_superset_link_dashboard_partial_recovery:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify dashboard links recover dataset context and preserve explicit partial-recovery markers.\ndef test_parse_superset_link_dashboard_partial_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_detail.return_value = {\n \"id\": 10,\n \"datasets\": [{\"id\": 42}, {\"id\": 77}],\n }\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://superset.local/dashboard/10/?native_filters=%5B%7B%22name%22%3A%22country%22%2C%22value%22%3A%22DE%22%7D%5D\"\n )\n\n assert result.dataset_id == 42\n assert result.dashboard_id == 10\n assert result.dataset_ref == \"public.sales\"\n assert result.partial_recovery is True\n assert \"multiple_dashboard_datasets\" in result.unresolved_references\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n\n\n# [/DEF:test_parse_superset_link_dashboard_partial_recovery:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_parse_superset_link_dashboard_partial_recovery [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify dashboard links recover dataset context and preserve explicit partial-recovery markers.\ndef test_parse_superset_link_dashboard_partial_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_detail.return_value = {\n \"id\": 10,\n \"datasets\": [{\"id\": 42}, {\"id\": 77}],\n }\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://superset.local/dashboard/10/?native_filters=%5B%7B%22name%22%3A%22country%22%2C%22value%22%3A%22DE%22%7D%5D\"\n )\n assert result.dataset_id == 42\n assert result.dashboard_id == 10\n assert result.dataset_ref == \"public.sales\"\n assert result.partial_recovery is True\n assert \"multiple_dashboard_datasets\" in result.unresolved_references\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n# #endregion test_parse_superset_link_dashboard_partial_recovery\n" }, { "contract_id": "test_parse_superset_link_dashboard_slug_recovery", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 403, - "end_line": 437, + "start_line": 354, + "end_line": 384, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4223,15 +3260,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_parse_superset_link_dashboard_slug_recovery:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify dashboard slug links resolve through dashboard detail endpoints and recover dataset context.\ndef test_parse_superset_link_dashboard_slug_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_detail.return_value = {\n \"id\": 15,\n \"datasets\": [{\"id\": 42}],\n }\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"https://ss-dev.bebesh.ru/superset/dashboard/slack/?native_filters_key=8ZLV4M-UXOM\"\n )\n\n assert result.dataset_id == 42\n assert result.dashboard_id == 15\n assert result.dataset_ref == \"public.sales\"\n assert result.partial_recovery is False\n assert result.query_state[\"native_filters_key\"] == \"8ZLV4M-UXOM\"\n fake_client.get_dashboard_detail.assert_called_once_with(\"slack\")\n\n\n# [/DEF:test_parse_superset_link_dashboard_slug_recovery:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_parse_superset_link_dashboard_slug_recovery [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify dashboard slug links resolve through dashboard detail endpoints and recover dataset context.\ndef test_parse_superset_link_dashboard_slug_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_detail.return_value = {\n \"id\": 15,\n \"datasets\": [{\"id\": 42}],\n }\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"https://ss-dev.bebesh.ru/superset/dashboard/slack/?native_filters_key=8ZLV4M-UXOM\"\n )\n assert result.dataset_id == 42\n assert result.dashboard_id == 15\n assert result.dataset_ref == \"public.sales\"\n assert result.partial_recovery is False\n assert result.query_state[\"native_filters_key\"] == \"8ZLV4M-UXOM\"\n fake_client.get_dashboard_detail.assert_called_once_with(\"slack\")\n# #endregion test_parse_superset_link_dashboard_slug_recovery\n" }, { "contract_id": "test_parse_superset_link_dashboard_permalink_partial_recovery", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 440, - "end_line": 532, + "start_line": 385, + "end_line": 469, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4256,15 +3293,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_parse_superset_link_dashboard_permalink_partial_recovery:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify dashboard permalink links no longer fail parsing and preserve permalink filter state for partial recovery.\ndef test_parse_superset_link_dashboard_permalink_partial_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_permalink_state.return_value = {\n \"state\": {\n \"dataMask\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"country\",\n \"filterState\": {\n \"label\": \"Country\",\n \"value\": [\"DE\"],\n },\n \"extraFormData\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n },\n }\n }\n }\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://ss-dev.bebesh.ru/superset/dashboard/p/QabXy6wG30Z/\"\n )\n\n assert result.resource_type == \"dashboard\"\n assert result.dataset_id is None\n assert result.dashboard_id is None\n assert result.dataset_ref == \"dashboard_permalink:QabXy6wG30Z\"\n assert result.partial_recovery is True\n assert (\n \"dashboard_permalink_dataset_binding_unresolved\" in result.unresolved_references\n )\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n assert result.imported_filters[0][\"raw_value\"] == [\"DE\"]\n fake_client.get_dashboard_permalink_state.assert_called_once_with(\"QabXy6wG30Z\")\n\n\n# [DEF:test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify permalink state with nested dashboard id recovers dataset binding and keeps imported filters.\ndef test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_permalink_state.return_value = {\n \"state\": {\n \"form_data\": {\"dashboardId\": 22},\n \"dataMask\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"country\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n }\n },\n }\n }\n fake_client.get_dashboard_detail.return_value = {\"id\": 22, \"datasets\": [{\"id\": 42}]}\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://ss-dev.bebesh.ru/superset/dashboard/p/QabXy6wG30Z/\"\n )\n\n assert result.dashboard_id == 22\n assert result.dataset_id == 42\n assert result.dataset_ref == \"public.sales\"\n assert (\n \"dashboard_permalink_dataset_binding_unresolved\"\n not in result.unresolved_references\n )\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n\n\n# [/DEF:test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state:Function]\n# [/DEF:test_parse_superset_link_dashboard_permalink_partial_recovery:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_parse_superset_link_dashboard_permalink_partial_recovery [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify dashboard permalink links no longer fail parsing and preserve permalink filter state for partial recovery.\ndef test_parse_superset_link_dashboard_permalink_partial_recovery():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_permalink_state.return_value = {\n \"state\": {\n \"dataMask\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"country\",\n \"filterState\": {\n \"label\": \"Country\",\n \"value\": [\"DE\"],\n },\n \"extraFormData\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n },\n }\n }\n }\n }\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://ss-dev.bebesh.ru/superset/dashboard/p/QabXy6wG30Z/\"\n )\n assert result.resource_type == \"dashboard\"\n assert result.dataset_id is None\n assert result.dashboard_id is None\n assert result.dataset_ref == \"dashboard_permalink:QabXy6wG30Z\"\n assert result.partial_recovery is True\n assert (\n \"dashboard_permalink_dataset_binding_unresolved\" in result.unresolved_references\n )\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n assert result.imported_filters[0][\"raw_value\"] == [\"DE\"]\n fake_client.get_dashboard_permalink_state.assert_called_once_with(\"QabXy6wG30Z\")\n# #region test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify permalink state with nested dashboard id recovers dataset binding and keeps imported filters.\ndef test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_permalink_state.return_value = {\n \"state\": {\n \"form_data\": {\"dashboardId\": 22},\n \"dataMask\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"country\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n }\n },\n }\n }\n fake_client.get_dashboard_detail.return_value = {\"id\": 22, \"datasets\": [{\"id\": 42}]}\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://ss-dev.bebesh.ru/superset/dashboard/p/QabXy6wG30Z/\"\n )\n assert result.dashboard_id == 22\n assert result.dataset_id == 42\n assert result.dataset_ref == \"public.sales\"\n assert (\n \"dashboard_permalink_dataset_binding_unresolved\"\n not in result.unresolved_references\n )\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n# #endregion test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state\n# #endregion test_parse_superset_link_dashboard_permalink_partial_recovery\n" }, { "contract_id": "test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 487, - "end_line": 531, + "start_line": 428, + "end_line": 468, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4289,15 +3326,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify permalink state with nested dashboard id recovers dataset binding and keeps imported filters.\ndef test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_permalink_state.return_value = {\n \"state\": {\n \"form_data\": {\"dashboardId\": 22},\n \"dataMask\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"country\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n }\n },\n }\n }\n fake_client.get_dashboard_detail.return_value = {\"id\": 22, \"datasets\": [{\"id\": 42}]}\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://ss-dev.bebesh.ru/superset/dashboard/p/QabXy6wG30Z/\"\n )\n\n assert result.dashboard_id == 22\n assert result.dataset_id == 42\n assert result.dataset_ref == \"public.sales\"\n assert (\n \"dashboard_permalink_dataset_binding_unresolved\"\n not in result.unresolved_references\n )\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n\n\n# [/DEF:test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify permalink state with nested dashboard id recovers dataset binding and keeps imported filters.\ndef test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state():\n env = Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n fake_client = MagicMock()\n fake_client.get_dashboard_permalink_state.return_value = {\n \"state\": {\n \"form_data\": {\"dashboardId\": 22},\n \"dataMask\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"country\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n }\n },\n }\n }\n fake_client.get_dashboard_detail.return_value = {\"id\": 22, \"datasets\": [{\"id\": 42}]}\n fake_client.get_dataset_detail.return_value = {\n \"table_name\": \"sales\",\n \"schema\": \"public\",\n }\n extractor = SupersetContextExtractor(environment=env, client=fake_client)\n result = extractor.parse_superset_link(\n \"http://ss-dev.bebesh.ru/superset/dashboard/p/QabXy6wG30Z/\"\n )\n assert result.dashboard_id == 22\n assert result.dataset_id == 42\n assert result.dataset_ref == \"public.sales\"\n assert (\n \"dashboard_permalink_dataset_binding_unresolved\"\n not in result.unresolved_references\n )\n assert result.imported_filters[0][\"filter_name\"] == \"country\"\n# #endregion test_parse_superset_link_dashboard_permalink_recovers_dataset_from_nested_dashboard_state\n" }, { "contract_id": "test_resolve_from_dictionary_prefers_exact_match", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 535, - "end_line": 577, + "start_line": 470, + "end_line": 508, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4322,15 +3359,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_resolve_from_dictionary_prefers_exact_match:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify trusted dictionary exact matches outrank fuzzy candidates and unresolved fields stay explicit.\ndef test_resolve_from_dictionary_prefers_exact_match():\n resolver = SemanticSourceResolver()\n result = resolver.resolve_from_dictionary(\n {\n \"source_ref\": \"dict://finance\",\n \"rows\": [\n {\n \"field_name\": \"revenue\",\n \"verbose_name\": \"Revenue\",\n \"description\": \"Recognized revenue amount\",\n \"display_format\": \"$,.2f\",\n },\n {\n \"field_name\": \"revnue\",\n \"verbose_name\": \"Revenue typo\",\n \"description\": \"Fuzzy variant\",\n },\n ],\n },\n [\n {\"field_name\": \"revenue\", \"is_locked\": False},\n {\"field_name\": \"margin\", \"is_locked\": False},\n ],\n )\n\n resolved_exact = next(\n item for item in result.resolved_fields if item[\"field_name\"] == \"revenue\"\n )\n unresolved = next(\n item for item in result.resolved_fields if item[\"field_name\"] == \"margin\"\n )\n\n assert resolved_exact[\"applied_candidate\"][\"match_type\"] == \"exact\"\n assert resolved_exact[\"provenance\"] == \"dictionary_exact\"\n assert unresolved[\"status\"] == \"unresolved\"\n assert \"margin\" in result.unresolved_fields\n assert result.partial_recovery is True\n\n\n# [/DEF:test_resolve_from_dictionary_prefers_exact_match:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_resolve_from_dictionary_prefers_exact_match [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify trusted dictionary exact matches outrank fuzzy candidates and unresolved fields stay explicit.\ndef test_resolve_from_dictionary_prefers_exact_match():\n resolver = SemanticSourceResolver()\n result = resolver.resolve_from_dictionary(\n {\n \"source_ref\": \"dict://finance\",\n \"rows\": [\n {\n \"field_name\": \"revenue\",\n \"verbose_name\": \"Revenue\",\n \"description\": \"Recognized revenue amount\",\n \"display_format\": \"$,.2f\",\n },\n {\n \"field_name\": \"revnue\",\n \"verbose_name\": \"Revenue typo\",\n \"description\": \"Fuzzy variant\",\n },\n ],\n },\n [\n {\"field_name\": \"revenue\", \"is_locked\": False},\n {\"field_name\": \"margin\", \"is_locked\": False},\n ],\n )\n resolved_exact = next(\n item for item in result.resolved_fields if item[\"field_name\"] == \"revenue\"\n )\n unresolved = next(\n item for item in result.resolved_fields if item[\"field_name\"] == \"margin\"\n )\n assert resolved_exact[\"applied_candidate\"][\"match_type\"] == \"exact\"\n assert resolved_exact[\"provenance\"] == \"dictionary_exact\"\n assert unresolved[\"status\"] == \"unresolved\"\n assert \"margin\" in result.unresolved_fields\n assert result.partial_recovery is True\n# #endregion test_resolve_from_dictionary_prefers_exact_match\n" }, { "contract_id": "test_orchestrator_start_session_preserves_partial_recovery", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 580, - "end_line": 645, + "start_line": 509, + "end_line": 566, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4355,15 +3392,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_orchestrator_start_session_preserves_partial_recovery:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify session start persists usable recovery-required state when Superset intake is partial.\ndef test_orchestrator_start_session_preserves_partial_recovery(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n created_session = _make_session()\n created_session.readiness_state = ReadinessState.RECOVERY_REQUIRED\n created_session.current_phase = SessionPhase.RECOVERY\n\n repository.create_session.return_value = created_session\n repository.save_profile_and_findings.return_value = created_session\n repository.save_recovery_state.return_value = created_session\n repository.db = MagicMock()\n\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n\n parsed_context = SimpleNamespace(\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n chart_id=None,\n partial_recovery=True,\n unresolved_references=[\"dashboard_dataset_binding_missing\"],\n imported_filters=[],\n )\n\n fake_extractor = MagicMock()\n fake_extractor.parse_superset_link.return_value = parsed_context\n fake_extractor.recover_imported_filters.return_value = []\n fake_extractor.client.get_dataset_detail.return_value = {\n \"id\": 42,\n \"sql\": \"\",\n \"columns\": [],\n \"metrics\": [],\n }\n fake_extractor.discover_template_variables.return_value = []\n\n with patch(\n \"src.services.dataset_review.orchestrator.SupersetContextExtractor\",\n side_effect=[fake_extractor, fake_extractor],\n ):\n result = orchestrator.start_session(\n StartSessionCommand(\n user=dataset_review_api_dependencies[\"user\"],\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n )\n )\n\n assert result.session.readiness_state == ReadinessState.RECOVERY_REQUIRED\n assert result.findings\n assert result.findings[0].severity.value == \"warning\"\n repository.create_session.assert_called_once()\n repository.save_profile_and_findings.assert_called_once()\n\n\n# [/DEF:test_orchestrator_start_session_preserves_partial_recovery:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_orchestrator_start_session_preserves_partial_recovery [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify session start persists usable recovery-required state when Superset intake is partial.\ndef test_orchestrator_start_session_preserves_partial_recovery(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n created_session = _make_session()\n created_session.readiness_state = ReadinessState.RECOVERY_REQUIRED\n created_session.current_phase = SessionPhase.RECOVERY\n repository.create_session.return_value = created_session\n repository.save_profile_and_findings.return_value = created_session\n repository.save_recovery_state.return_value = created_session\n repository.db = MagicMock()\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n parsed_context = SimpleNamespace(\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n chart_id=None,\n partial_recovery=True,\n unresolved_references=[\"dashboard_dataset_binding_missing\"],\n imported_filters=[],\n )\n fake_extractor = MagicMock()\n fake_extractor.parse_superset_link.return_value = parsed_context\n fake_extractor.recover_imported_filters.return_value = []\n fake_extractor.client.get_dataset_detail.return_value = {\n \"id\": 42,\n \"sql\": \"\",\n \"columns\": [],\n \"metrics\": [],\n }\n fake_extractor.discover_template_variables.return_value = []\n with patch(\n \"src.services.dataset_review.orchestrator.SupersetContextExtractor\",\n side_effect=[fake_extractor, fake_extractor],\n ):\n result = orchestrator.start_session(\n StartSessionCommand(\n user=dataset_review_api_dependencies[\"user\"],\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n )\n )\n assert result.session.readiness_state == ReadinessState.RECOVERY_REQUIRED\n assert result.findings\n assert result.findings[0].severity.value == \"warning\"\n repository.create_session.assert_called_once()\n repository.save_profile_and_findings.assert_called_once()\n# #endregion test_orchestrator_start_session_preserves_partial_recovery\n" }, { "contract_id": "test_orchestrator_start_session_bootstraps_recovery_state", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 648, - "end_line": 753, + "start_line": 567, + "end_line": 665, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4388,15 +3425,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_orchestrator_start_session_bootstraps_recovery_state:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify session start persists recovered filters, template variables, and initial execution mappings for review workspace bootstrap.\ndef test_orchestrator_start_session_bootstraps_recovery_state(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n created_session = _make_session()\n created_session.readiness_state = ReadinessState.RECOVERY_REQUIRED\n created_session.current_phase = SessionPhase.RECOVERY\n\n repository.create_session.return_value = created_session\n repository.save_profile_and_findings.return_value = created_session\n repository.save_recovery_state.return_value = created_session\n repository.db = MagicMock()\n\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n\n parsed_context = SimpleNamespace(\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n chart_id=None,\n partial_recovery=True,\n unresolved_references=[\"dashboard_dataset_binding_missing\"],\n imported_filters=[{\"filter_name\": \"country\", \"raw_value\": [\"DE\"]}],\n )\n\n fake_extractor = MagicMock()\n fake_extractor.parse_superset_link.return_value = parsed_context\n fake_extractor.recover_imported_filters.return_value = [\n {\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"raw_value\": [\"DE\"],\n \"normalized_value\": {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n \"source\": \"superset_url\",\n \"confidence_state\": \"imported\",\n \"requires_confirmation\": False,\n \"recovery_status\": \"recovered\",\n \"notes\": \"Recovered from permalink state\",\n }\n ]\n fake_extractor.client.get_dataset_detail.return_value = {\n \"id\": 42,\n \"sql\": \"select * from sales where country in {{ filter_values('country') }}\",\n \"columns\": [],\n \"metrics\": [],\n }\n fake_extractor.discover_template_variables.return_value = [\n {\n \"variable_name\": \"country\",\n \"expression_source\": \"{{ filter_values('country') }}\",\n \"variable_kind\": \"native_filter\",\n \"is_required\": True,\n \"default_value\": None,\n \"mapping_status\": \"unmapped\",\n }\n ]\n\n with patch(\n \"src.services.dataset_review.orchestrator.SupersetContextExtractor\",\n side_effect=[fake_extractor, fake_extractor],\n ):\n result = orchestrator.start_session(\n StartSessionCommand(\n user=dataset_review_api_dependencies[\"user\"],\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n )\n )\n\n assert result.session.readiness_state == ReadinessState.RECOVERY_REQUIRED\n repository.save_recovery_state.assert_called_once()\n saved_filters = repository.save_recovery_state.call_args.args[2]\n saved_variables = repository.save_recovery_state.call_args.args[3]\n saved_mappings = repository.save_recovery_state.call_args.args[4]\n assert len(saved_filters) == 1\n assert saved_filters[0].filter_name == \"country\"\n assert saved_filters[0].normalized_value == {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n }\n assert len(saved_variables) == 1\n assert saved_variables[0].variable_name == \"country\"\n assert len(saved_mappings) == 1\n assert saved_mappings[0].raw_input_value == [\"DE\"]\n\n\n# [/DEF:test_orchestrator_start_session_bootstraps_recovery_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_orchestrator_start_session_bootstraps_recovery_state [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify session start persists recovered filters, template variables, and initial execution mappings for review workspace bootstrap.\ndef test_orchestrator_start_session_bootstraps_recovery_state(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n created_session = _make_session()\n created_session.readiness_state = ReadinessState.RECOVERY_REQUIRED\n created_session.current_phase = SessionPhase.RECOVERY\n repository.create_session.return_value = created_session\n repository.save_profile_and_findings.return_value = created_session\n repository.save_recovery_state.return_value = created_session\n repository.db = MagicMock()\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n parsed_context = SimpleNamespace(\n dataset_ref=\"public.sales\",\n dataset_id=42,\n dashboard_id=10,\n chart_id=None,\n partial_recovery=True,\n unresolved_references=[\"dashboard_dataset_binding_missing\"],\n imported_filters=[{\"filter_name\": \"country\", \"raw_value\": [\"DE\"]}],\n dataset_payload=None,\n )\n fake_extractor = MagicMock()\n fake_extractor.parse_superset_link.return_value = parsed_context\n fake_extractor.recover_imported_filters.return_value = [\n {\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"raw_value\": [\"DE\"],\n \"normalized_value\": {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n \"source\": \"superset_url\",\n \"confidence_state\": \"imported\",\n \"requires_confirmation\": False,\n \"recovery_status\": \"recovered\",\n \"notes\": \"Recovered from permalink state\",\n }\n ]\n fake_extractor.client.get_dataset_detail.return_value = {\n \"id\": 42,\n \"sql\": \"select * from sales where country in {{ filter_values('country') }}\",\n \"columns\": [],\n \"metrics\": [],\n }\n fake_extractor.discover_template_variables.return_value = [\n {\n \"variable_name\": \"country\",\n \"expression_source\": \"{{ filter_values('country') }}\",\n \"variable_kind\": \"native_filter\",\n \"is_required\": True,\n \"default_value\": None,\n \"mapping_status\": \"unmapped\",\n }\n ]\n with patch(\n \"src.services.dataset_review.orchestrator.SupersetContextExtractor\",\n side_effect=[fake_extractor, fake_extractor],\n ):\n result = orchestrator.start_session(\n StartSessionCommand(\n user=dataset_review_api_dependencies[\"user\"],\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n )\n )\n assert result.session.readiness_state == ReadinessState.RECOVERY_REQUIRED\n repository.save_recovery_state.assert_called_once()\n saved_filters = repository.save_recovery_state.call_args.args[2]\n saved_variables = repository.save_recovery_state.call_args.args[3]\n saved_mappings = repository.save_recovery_state.call_args.args[4]\n assert len(saved_filters) == 1\n assert saved_filters[0].filter_name == \"country\"\n assert saved_filters[0].normalized_value == {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n }\n assert len(saved_variables) == 1\n assert saved_variables[0].variable_name == \"country\"\n assert len(saved_mappings) == 1\n assert saved_mappings[0].raw_input_value == [\"DE\"]\n# #endregion test_orchestrator_start_session_bootstraps_recovery_state\n" }, { "contract_id": "test_start_session_endpoint_returns_created_summary", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 756, - "end_line": 786, + "start_line": 666, + "end_line": 691, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4421,15 +3458,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_start_session_endpoint_returns_created_summary:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify POST session lifecycle endpoint returns a persisted ownership-scoped summary.\ndef test_start_session_endpoint_returns_created_summary(\n dataset_review_api_dependencies,\n):\n session = _make_session()\n orchestrator = MagicMock()\n orchestrator.start_session.return_value = SimpleNamespace(\n session=session, findings=[], parsed_context=None\n )\n\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n\n response = client.post(\n \"/api/dataset-orchestration/sessions\",\n json={\n \"source_kind\": \"superset_link\",\n \"source_input\": \"http://superset.local/dashboard/10\",\n \"environment_id\": \"env-1\",\n },\n )\n\n assert response.status_code == 201\n payload = response.json()\n assert payload[\"session_id\"] == \"sess-1\"\n assert payload[\"dataset_ref\"] == \"public.sales\"\n assert payload[\"environment_id\"] == \"env-1\"\n\n\n# [/DEF:test_start_session_endpoint_returns_created_summary:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_start_session_endpoint_returns_created_summary [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify POST session lifecycle endpoint returns a persisted ownership-scoped summary.\ndef test_start_session_endpoint_returns_created_summary(\n dataset_review_api_dependencies,\n):\n session = _make_session()\n orchestrator = MagicMock()\n orchestrator.start_session.return_value = SimpleNamespace(\n session=session, findings=[], parsed_context=None\n )\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n response = client.post(\n \"/api/dataset-orchestration/sessions\",\n json={\n \"source_kind\": \"superset_link\",\n \"source_input\": \"http://superset.local/dashboard/10\",\n \"environment_id\": \"env-1\",\n },\n )\n assert response.status_code == 201\n payload = response.json()\n assert payload[\"session_id\"] == \"sess-1\"\n assert payload[\"dataset_ref\"] == \"public.sales\"\n assert payload[\"environment_id\"] == \"env-1\"\n# #endregion test_start_session_endpoint_returns_created_summary\n" }, { "contract_id": "test_get_session_detail_export_and_lifecycle_endpoints", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 789, - "end_line": 904, + "start_line": 692, + "end_line": 798, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4454,15 +3491,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_session_detail_export_and_lifecycle_endpoints:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Verify lifecycle get/patch/delete plus documentation and validation exports remain ownership-scoped and usable.\ndef test_get_session_detail_export_and_lifecycle_endpoints(\n dataset_review_api_dependencies,\n):\n now = datetime.now(timezone.utc)\n session = MagicMock(spec=DatasetReviewSession)\n session.session_id = \"sess-1\"\n session.user_id = \"user-1\"\n session.environment_id = \"env-1\"\n session.source_kind = \"superset_link\"\n session.source_input = \"http://superset.local/dashboard/10\"\n session.dataset_ref = \"public.sales\"\n session.dataset_id = 42\n session.dashboard_id = 10\n session.readiness_state = ReadinessState.REVIEW_READY\n session.recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n session.status = SessionStatus.ACTIVE\n session.current_phase = SessionPhase.REVIEW\n session.created_at = now\n session.updated_at = now\n session.last_activity_at = now\n session.profile = SimpleNamespace(\n dataset_name=\"sales\",\n business_summary=\"Summary text\",\n confidence_state=ConfidenceState.MOSTLY_CONFIRMED,\n dataset_type=\"unknown\",\n schema_name=None,\n database_name=None,\n business_summary_source=BusinessSummarySource.IMPORTED,\n description=None,\n is_sqllab_view=False,\n completeness_score=None,\n has_blocking_findings=False,\n has_warning_findings=True,\n manual_summary_locked=False,\n created_at=now,\n updated_at=now,\n profile_id=\"profile-1\",\n session_id=\"sess-1\",\n )\n session.findings = [\n SimpleNamespace(\n finding_id=\"f-1\",\n session_id=\"sess-1\",\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"PARTIAL_SUPERSET_RECOVERY\",\n title=\"Partial\",\n message=\"Some filters require review\",\n resolution_state=ResolutionState.OPEN,\n resolution_note=None,\n caused_by_ref=None,\n created_at=now,\n resolved_at=None,\n )\n ]\n session.collaborators = []\n session.semantic_sources = []\n session.semantic_fields = []\n session.imported_filters = []\n session.template_variables = []\n session.execution_mappings = []\n session.clarification_sessions = []\n session.previews = []\n session.run_contexts = []\n\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.list_sessions_for_user.return_value = [session]\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-0\"\n )\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n detail_response = client.get(\"/api/dataset-orchestration/sessions/sess-1\")\n assert detail_response.status_code == 200\n assert detail_response.json()[\"session_id\"] == \"sess-1\"\n\n patch_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1\",\n json={\"status\": \"paused\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n assert patch_response.status_code == 200\n assert patch_response.json()[\"status\"] == \"paused\"\n\n doc_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/exports/documentation?format=json\"\n )\n assert doc_response.status_code == 200\n assert doc_response.json()[\"artifact_type\"] == \"documentation\"\n\n validation_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/exports/validation?format=markdown\"\n )\n assert validation_response.status_code == 200\n assert validation_response.json()[\"artifact_type\"] == \"validation_report\"\n assert \"Validation Report\" in validation_response.json()[\"content\"][\"markdown\"]\n\n delete_response = client.delete(\n \"/api/dataset-orchestration/sessions/sess-1\",\n headers={\"X-Session-Version\": \"1\"},\n )\n assert delete_response.status_code == 204\n\n\n# [/DEF:test_get_session_detail_export_and_lifecycle_endpoints:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_session_detail_export_and_lifecycle_endpoints [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Verify lifecycle get/patch/delete plus documentation and validation exports remain ownership-scoped and usable.\ndef test_get_session_detail_export_and_lifecycle_endpoints(\n dataset_review_api_dependencies,\n):\n now = datetime.now(UTC)\n session = MagicMock(spec=DatasetReviewSession)\n session.session_id = \"sess-1\"\n session.user_id = \"user-1\"\n session.environment_id = \"env-1\"\n session.source_kind = \"superset_link\"\n session.source_input = \"http://superset.local/dashboard/10\"\n session.dataset_ref = \"public.sales\"\n session.dataset_id = 42\n session.dashboard_id = 10\n session.readiness_state = ReadinessState.REVIEW_READY\n session.recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n session.status = SessionStatus.ACTIVE\n session.current_phase = SessionPhase.REVIEW\n session.created_at = now\n session.updated_at = now\n session.last_activity_at = now\n session.profile = SimpleNamespace(\n dataset_name=\"sales\",\n business_summary=\"Summary text\",\n confidence_state=ConfidenceState.MOSTLY_CONFIRMED,\n dataset_type=\"unknown\",\n schema_name=None,\n database_name=None,\n business_summary_source=BusinessSummarySource.IMPORTED,\n description=None,\n is_sqllab_view=False,\n completeness_score=None,\n has_blocking_findings=False,\n has_warning_findings=True,\n manual_summary_locked=False,\n created_at=now,\n updated_at=now,\n profile_id=\"profile-1\",\n session_id=\"sess-1\",\n )\n session.findings = [\n SimpleNamespace(\n finding_id=\"f-1\",\n session_id=\"sess-1\",\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"PARTIAL_SUPERSET_RECOVERY\",\n title=\"Partial\",\n message=\"Some filters require review\",\n resolution_state=ResolutionState.OPEN,\n resolution_note=None,\n caused_by_ref=None,\n created_at=now,\n resolved_at=None,\n )\n ]\n session.collaborators = []\n session.semantic_sources = []\n session.semantic_fields = []\n session.imported_filters = []\n session.template_variables = []\n session.execution_mappings = []\n session.clarification_sessions = []\n session.previews = []\n session.run_contexts = []\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.list_sessions_for_user.return_value = [session]\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-0\"\n )\n app.dependency_overrides[_get_repository] = lambda: repository\n detail_response = client.get(\"/api/dataset-orchestration/sessions/sess-1\")\n assert detail_response.status_code == 200\n assert detail_response.json()[\"session_id\"] == \"sess-1\"\n patch_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1\",\n json={\"status\": \"paused\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n assert patch_response.status_code == 200\n assert patch_response.json()[\"status\"] == \"paused\"\n doc_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/exports/documentation?format=json\"\n )\n assert doc_response.status_code == 200\n assert doc_response.json()[\"artifact_type\"] == \"documentation\"\n validation_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/exports/validation?format=markdown\"\n )\n assert validation_response.status_code == 200\n assert validation_response.json()[\"artifact_type\"] == \"validation_report\"\n assert \"Validation Report\" in validation_response.json()[\"content\"][\"markdown\"]\n delete_response = client.delete(\n \"/api/dataset-orchestration/sessions/sess-1\",\n headers={\"X-Session-Version\": \"1\"},\n )\n assert delete_response.status_code == 204\n# #endregion test_get_session_detail_export_and_lifecycle_endpoints\n" }, { "contract_id": "test_get_clarification_state_returns_empty_payload_when_session_has_no_record", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 907, - "end_line": 927, + "start_line": 799, + "end_line": 814, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4470,15 +3507,15 @@ }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_get_clarification_state_returns_empty_payload_when_session_has_no_record:Function]\n# @PURPOSE: Clarification state endpoint should return a non-blocking empty payload when the session has no clarification aggregate yet.\ndef test_get_clarification_state_returns_empty_payload_when_session_has_no_record(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n response = client.get(\"/api/dataset-orchestration/sessions/sess-1/clarification\")\n\n assert response.status_code == 200\n assert response.json() == {\n \"clarification_session\": None,\n \"current_question\": None,\n }\n\n\n# [/DEF:test_get_clarification_state_returns_empty_payload_when_session_has_no_record:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_clarification_state_returns_empty_payload_when_session_has_no_record [TYPE Function]\n# @BRIEF Clarification state endpoint should return a non-blocking empty payload when the session has no clarification aggregate yet.\ndef test_get_clarification_state_returns_empty_payload_when_session_has_no_record(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n app.dependency_overrides[_get_repository] = lambda: repository\n response = client.get(\"/api/dataset-orchestration/sessions/sess-1/clarification\")\n assert response.status_code == 200\n assert response.json() == {\n \"clarification_session\": None,\n \"current_question\": None,\n }\n# #endregion test_get_clarification_state_returns_empty_payload_when_session_has_no_record\n" }, { "contract_id": "test_us2_clarification_endpoints_persist_answer_and_feedback", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 930, - "end_line": 994, + "start_line": 815, + "end_line": 871, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4503,15 +3540,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_us2_clarification_endpoints_persist_answer_and_feedback:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Clarification endpoints should expose one current question, persist the answer before advancement, and store feedback on the answer audit record.\ndef test_us2_clarification_endpoints_persist_answer_and_feedback(\n dataset_review_api_dependencies,\n):\n session = _make_us2_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n\n def _add_side_effect(obj):\n if obj.__class__.__name__ == \"ClarificationAnswer\":\n session.clarification_sessions[0].questions[0].answer = obj\n\n repository.db.add.side_effect = _add_side_effect\n repository.db.flush.side_effect = lambda: None\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n state_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/clarification\"\n )\n assert state_response.status_code == 200\n state_payload = state_response.json()\n assert (\n state_payload[\"current_question\"][\"why_it_matters\"]\n == \"This determines how downstream users interpret revenue KPIs.\"\n )\n assert state_payload[\"current_question\"][\"current_guess\"] == \"Revenue reporting\"\n assert len(state_payload[\"current_question\"][\"options\"]) == 2\n\n answer_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/clarification/answers\",\n json={\n \"question_id\": \"q-1\",\n \"answer_kind\": \"selected\",\n \"answer_value\": \"Revenue reporting\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert answer_response.status_code == 200\n answer_payload = answer_response.json()\n assert answer_payload[\"session\"][\"readiness_state\"] == \"review_ready\"\n assert answer_payload[\"clarification_state\"][\"current_question\"] is None\n assert answer_payload[\"changed_findings\"][0][\"resolution_state\"] == \"resolved\"\n assert (\n session.clarification_sessions[0].questions[0].answer.answer_value\n == \"Revenue reporting\"\n )\n\n feedback_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/clarification/questions/q-1/feedback\",\n json={\"feedback\": \"up\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n assert feedback_response.status_code == 200\n assert feedback_response.json() == {\"target_id\": \"q-1\", \"feedback\": \"up\"}\n assert session.clarification_sessions[0].questions[0].answer.user_feedback == \"up\"\n\n\n# [/DEF:test_us2_clarification_endpoints_persist_answer_and_feedback:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_us2_clarification_endpoints_persist_answer_and_feedback [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Clarification endpoints should expose one current question, persist the answer before advancement, and store feedback on the answer audit record.\ndef test_us2_clarification_endpoints_persist_answer_and_feedback(\n dataset_review_api_dependencies,\n):\n session = _make_us2_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n def _add_side_effect(obj):\n if obj.__class__.__name__ == \"ClarificationAnswer\":\n session.clarification_sessions[0].questions[0].answer = obj\n repository.db.add.side_effect = _add_side_effect\n repository.db.flush.side_effect = lambda: None\n app.dependency_overrides[_get_repository] = lambda: repository\n state_response = client.get(\n \"/api/dataset-orchestration/sessions/sess-1/clarification\"\n )\n assert state_response.status_code == 200\n state_payload = state_response.json()\n assert (\n state_payload[\"current_question\"][\"why_it_matters\"]\n == \"This determines how downstream users interpret revenue KPIs.\"\n )\n assert state_payload[\"current_question\"][\"current_guess\"] == \"Revenue reporting\"\n assert len(state_payload[\"current_question\"][\"options\"]) == 2\n answer_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/clarification/answers\",\n json={\n \"question_id\": \"q-1\",\n \"answer_kind\": \"selected\",\n \"answer_value\": \"Revenue reporting\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert answer_response.status_code == 200\n answer_payload = answer_response.json()\n assert answer_payload[\"session\"][\"readiness_state\"] == \"review_ready\"\n assert answer_payload[\"clarification_state\"][\"current_question\"] is None\n assert answer_payload[\"changed_findings\"][0][\"resolution_state\"] == \"resolved\"\n assert (\n session.clarification_sessions[0].questions[0].answer.answer_value\n == \"Revenue reporting\"\n )\n feedback_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/clarification/questions/q-1/feedback\",\n json={\"feedback\": \"up\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n assert feedback_response.status_code == 200\n assert feedback_response.json() == {\"target_id\": \"q-1\", \"feedback\": \"up\"}\n assert session.clarification_sessions[0].questions[0].answer.user_feedback == \"up\"\n# #endregion test_us2_clarification_endpoints_persist_answer_and_feedback\n" }, { "contract_id": "test_us2_field_semantic_override_lock_unlock_and_feedback", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 997, - "end_line": 1076, + "start_line": 872, + "end_line": 943, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4536,15 +3573,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_us2_field_semantic_override_lock_unlock_and_feedback:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Semantic field endpoints should apply manual overrides with lock/provenance invariants and persist feedback independently.\ndef test_us2_field_semantic_override_lock_unlock_and_feedback(\n dataset_review_api_dependencies,\n):\n session = _make_us2_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n repository.db.add.side_effect = lambda obj: None\n repository.db.flush.side_effect = lambda: None\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-1\"\n )\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n override_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic\",\n json={\n \"verbose_name\": \"Confirmed Revenue\",\n \"description\": \"Manual business-approved description\",\n \"display_format\": \"$,.0f\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert override_response.status_code == 200\n override_payload = override_response.json()\n assert override_payload[\"provenance\"] == \"manual_override\"\n assert override_payload[\"is_locked\"] is True\n\n unlock_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/unlock\",\n headers={\"X-Session-Version\": \"1\"},\n )\n assert unlock_response.status_code == 200\n assert unlock_response.json()[\"is_locked\"] is False\n\n candidate_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic\",\n json={\"candidate_id\": \"cand-1\", \"lock_field\": True},\n headers={\"X-Session-Version\": \"2\"},\n )\n assert candidate_response.status_code == 200\n candidate_payload = candidate_response.json()\n assert candidate_payload[\"verbose_name\"] == \"Recognized Revenue\"\n assert candidate_payload[\"provenance\"] == \"dictionary_exact\"\n assert candidate_payload[\"is_locked\"] is True\n\n batch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/semantic/approve-batch\",\n json={\n \"items\": [\n {\"field_id\": \"field-1\", \"candidate_id\": \"cand-1\", \"lock_field\": False}\n ]\n },\n headers={\"X-Session-Version\": \"3\"},\n )\n assert batch_response.status_code == 200\n assert batch_response.json()[0][\"field_id\"] == \"field-1\"\n\n feedback_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/feedback\",\n json={\"feedback\": \"down\"},\n headers={\"X-Session-Version\": \"4\"},\n )\n assert feedback_response.status_code == 200\n assert feedback_response.json() == {\"target_id\": \"field-1\", \"feedback\": \"down\"}\n assert session.semantic_fields[0].user_feedback == \"down\"\n\n\n# [/DEF:test_us2_field_semantic_override_lock_unlock_and_feedback:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_us2_field_semantic_override_lock_unlock_and_feedback [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Semantic field endpoints should apply manual overrides with lock/provenance invariants and persist feedback independently.\ndef test_us2_field_semantic_override_lock_unlock_and_feedback(\n dataset_review_api_dependencies,\n):\n session = _make_us2_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n repository.db.add.side_effect = lambda obj: None\n repository.db.flush.side_effect = lambda: None\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-1\"\n )\n app.dependency_overrides[_get_repository] = lambda: repository\n override_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic\",\n json={\n \"verbose_name\": \"Confirmed Revenue\",\n \"description\": \"Manual business-approved description\",\n \"display_format\": \"$,.0f\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert override_response.status_code == 200\n override_payload = override_response.json()\n assert override_payload[\"provenance\"] == \"manual_override\"\n assert override_payload[\"is_locked\"] is True\n unlock_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/unlock\",\n headers={\"X-Session-Version\": \"1\"},\n )\n assert unlock_response.status_code == 200\n assert unlock_response.json()[\"is_locked\"] is False\n candidate_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/semantic\",\n json={\"candidate_id\": \"cand-1\", \"lock_field\": True},\n headers={\"X-Session-Version\": \"2\"},\n )\n assert candidate_response.status_code == 200\n candidate_payload = candidate_response.json()\n assert candidate_payload[\"verbose_name\"] == \"Recognized Revenue\"\n assert candidate_payload[\"provenance\"] == \"dictionary_exact\"\n assert candidate_payload[\"is_locked\"] is True\n batch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/semantic/approve-batch\",\n json={\n \"items\": [\n {\"field_id\": \"field-1\", \"candidate_id\": \"cand-1\", \"lock_field\": False}\n ]\n },\n headers={\"X-Session-Version\": \"3\"},\n )\n assert batch_response.status_code == 200\n assert batch_response.json()[0][\"field_id\"] == \"field-1\"\n feedback_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/fields/field-1/feedback\",\n json={\"feedback\": \"down\"},\n headers={\"X-Session-Version\": \"4\"},\n )\n assert feedback_response.status_code == 200\n assert feedback_response.json() == {\"target_id\": \"field-1\", \"feedback\": \"down\"}\n assert session.semantic_fields[0].user_feedback == \"down\"\n# #endregion test_us2_field_semantic_override_lock_unlock_and_feedback\n" }, { "contract_id": "test_us3_mapping_patch_approval_preview_and_launch_endpoints", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1079, - "end_line": 1284, + "start_line": 944, + "end_line": 1133, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4569,15 +3606,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_us3_mapping_patch_approval_preview_and_launch_endpoints:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: US3 execution endpoints should persist manual overrides, preserve explicit approval semantics, return contract-shaped preview truth, and expose audited launch handoff.\ndef test_us3_mapping_patch_approval_preview_and_launch_endpoints(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n latest_preview = CompiledPreview(\n preview_id=\"preview-old\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'FR'\",\n preview_fingerprint=\"fingerprint-old\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(timezone.utc),\n created_at=datetime.now(timezone.utc),\n )\n session.previews = [latest_preview]\n\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-2\"\n )\n\n preview = SimpleNamespace(\n preview_id=\"preview-1\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'DE'\",\n preview_fingerprint=\"fingerprint-1\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(timezone.utc),\n created_at=datetime.now(timezone.utc),\n )\n run_context = SimpleNamespace(\n run_context_id=\"run-1\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-1\",\n sql_lab_session_ref=\"sql-lab-77\",\n effective_filters=[{\"mapping_id\": \"map-1\", \"effective_value\": \"EU\"}],\n template_params={\"country\": \"EU\"},\n approved_mapping_ids=[\"map-1\"],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(timezone.utc),\n )\n orchestrator = MagicMock()\n orchestrator.prepare_launch_preview.return_value = PreparePreviewResult(\n session=session,\n preview=preview,\n blocked_reasons=[],\n )\n orchestrator.launch_dataset.return_value = LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n\n def _assert_expected_preview_version(command):\n assert command.expected_version == 3\n return PreparePreviewResult(\n session=session, preview=preview, blocked_reasons=[]\n )\n\n def _assert_expected_launch_version(command):\n assert command.expected_version == 5\n return LaunchDatasetResult(\n session=session, run_context=run_context, blocked_reasons=[]\n )\n\n orchestrator.prepare_launch_preview.side_effect = _assert_expected_preview_version\n orchestrator.launch_dataset.side_effect = _assert_expected_launch_version\n\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n\n patch_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1\",\n json={\n \"effective_value\": \"EU\",\n \"mapping_method\": \"manual_override\",\n \"transformation_note\": \"Manual override for SQL Lab launch\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert patch_response.status_code == 200\n patch_payload = patch_response.json()\n assert patch_payload[\"mapping_id\"] == \"map-1\"\n assert patch_payload[\"mapping_method\"] == \"manual_override\"\n assert patch_payload[\"effective_value\"] == \"EU\"\n assert patch_payload[\"approval_state\"] == \"approved\"\n assert patch_payload[\"approved_by_user_id\"] == \"user-1\"\n assert session.execution_mappings[0].mapping_method == MappingMethod.MANUAL_OVERRIDE\n assert (\n session.execution_mappings[0].transformation_note\n == \"Manual override for SQL Lab launch\"\n )\n assert session.execution_mappings[0].effective_value == \"EU\"\n assert session.recommended_action == RecommendedAction.GENERATE_SQL_PREVIEW\n assert latest_preview.preview_status == PreviewStatus.STALE\n\n approve_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1/approve\",\n json={\"approval_note\": \"Approved after reviewing transformation\"},\n headers={\"X-Session-Version\": \"1\"},\n )\n assert approve_response.status_code == 200\n approve_payload = approve_response.json()\n assert approve_payload[\"mapping_id\"] == \"map-1\"\n assert approve_payload[\"approval_state\"] == \"approved\"\n assert approve_payload[\"approved_by_user_id\"] == \"user-1\"\n assert (\n session.execution_mappings[0].transformation_note\n == \"Approved after reviewing transformation\"\n )\n\n batch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/approve-batch\",\n json={\"mapping_ids\": [\"map-1\"]},\n headers={\"X-Session-Version\": \"2\"},\n )\n assert batch_response.status_code == 200\n assert batch_response.json()[0][\"mapping_id\"] == \"map-1\"\n\n list_response = client.get(\"/api/dataset-orchestration/sessions/sess-1/mappings\")\n assert list_response.status_code == 200\n assert list_response.json()[\"items\"][0][\"mapping_id\"] == \"map-1\"\n\n preview_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"3\"},\n )\n assert preview_response.status_code == 200\n preview_payload = preview_response.json()\n assert preview_payload[\"preview_id\"] == \"preview-1\"\n assert preview_payload[\"preview_status\"] == \"ready\"\n assert preview_payload[\"compiled_by\"] == \"superset\"\n assert \"SELECT * FROM sales\" in preview_payload[\"compiled_sql\"]\n\n def _assert_expected_pending_preview_version(command):\n assert command.expected_version == 4\n return PreparePreviewResult(\n session=session,\n preview=SimpleNamespace(\n preview_id=\"preview-pending\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.PENDING,\n compiled_sql=None,\n preview_fingerprint=\"fingerprint-pending\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=None,\n created_at=datetime.now(timezone.utc),\n ),\n blocked_reasons=[],\n )\n\n orchestrator.prepare_launch_preview.side_effect = (\n _assert_expected_pending_preview_version\n )\n preview_enqueue_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"4\"},\n )\n assert preview_enqueue_response.status_code == 202\n assert preview_enqueue_response.json() == {\n \"session_id\": \"sess-1\",\n \"session_version\": 3,\n \"preview_status\": \"pending\",\n \"task_id\": None,\n }\n\n launch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/launch\",\n headers={\"X-Session-Version\": \"5\"},\n )\n assert launch_response.status_code == 201\n launch_payload = launch_response.json()\n assert launch_payload[\"run_context\"][\"run_context_id\"] == \"run-1\"\n assert launch_payload[\"run_context\"][\"sql_lab_session_ref\"] == \"sql-lab-77\"\n assert launch_payload[\"run_context\"][\"launch_status\"] == \"started\"\n assert (\n launch_payload[\"redirect_url\"]\n == \"http://superset.local/superset/sqllab?queryId=sql-lab-77\"\n )\n\n\n# [/DEF:test_us3_mapping_patch_approval_preview_and_launch_endpoints:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_us3_mapping_patch_approval_preview_and_launch_endpoints [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF US3 execution endpoints should persist manual overrides, preserve explicit approval semantics, return contract-shaped preview truth, and expose audited launch handoff.\ndef test_us3_mapping_patch_approval_preview_and_launch_endpoints(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n latest_preview = CompiledPreview(\n preview_id=\"preview-old\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'FR'\",\n preview_fingerprint=\"fingerprint-old\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(UTC),\n created_at=datetime.now(UTC),\n )\n session.previews = [latest_preview]\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = lambda current, expected: current\n repository.bump_session_version.side_effect = lambda current: setattr(\n current, \"version\", int(getattr(current, \"version\", 0) or 0) + 1\n ) or getattr(current, \"version\", 0)\n repository.db = MagicMock()\n repository.db.commit.side_effect = lambda: None\n repository.db.refresh.side_effect = lambda obj: None\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n repository.event_logger.log_for_session.return_value = SimpleNamespace(\n session_event_id=\"evt-2\"\n )\n preview = SimpleNamespace(\n preview_id=\"preview-1\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'DE'\",\n preview_fingerprint=\"fingerprint-1\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(UTC),\n created_at=datetime.now(UTC),\n )\n run_context = SimpleNamespace(\n run_context_id=\"run-1\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-1\",\n sql_lab_session_ref=\"sql-lab-77\",\n effective_filters=[{\"mapping_id\": \"map-1\", \"effective_value\": \"EU\"}],\n template_params={\"country\": \"EU\"},\n approved_mapping_ids=[\"map-1\"],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(UTC),\n )\n orchestrator = MagicMock()\n orchestrator.prepare_launch_preview.return_value = PreparePreviewResult(\n session=session,\n preview=preview,\n blocked_reasons=[],\n )\n orchestrator.launch_dataset.return_value = LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n def _assert_expected_preview_version(command):\n assert command.expected_version == 3\n return PreparePreviewResult(\n session=session, preview=preview, blocked_reasons=[]\n )\n def _assert_expected_launch_version(command):\n assert command.expected_version == 5\n return LaunchDatasetResult(\n session=session, run_context=run_context, blocked_reasons=[]\n )\n orchestrator.prepare_launch_preview.side_effect = _assert_expected_preview_version\n orchestrator.launch_dataset.side_effect = _assert_expected_launch_version\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n patch_response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1\",\n json={\n \"effective_value\": \"EU\",\n \"mapping_method\": \"manual_override\",\n \"transformation_note\": \"Manual override for SQL Lab launch\",\n },\n headers={\"X-Session-Version\": \"0\"},\n )\n assert patch_response.status_code == 200\n patch_payload = patch_response.json()\n assert patch_payload[\"mapping_id\"] == \"map-1\"\n assert patch_payload[\"mapping_method\"] == \"manual_override\"\n assert patch_payload[\"effective_value\"] == \"EU\"\n assert patch_payload[\"approval_state\"] == \"approved\"\n assert patch_payload[\"approved_by_user_id\"] == \"user-1\"\n assert session.execution_mappings[0].mapping_method == MappingMethod.MANUAL_OVERRIDE\n assert (\n session.execution_mappings[0].transformation_note\n == \"Manual override for SQL Lab launch\"\n )\n assert session.execution_mappings[0].effective_value == \"EU\"\n assert session.recommended_action == RecommendedAction.GENERATE_SQL_PREVIEW\n assert latest_preview.preview_status == PreviewStatus.STALE\n approve_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1/approve\",\n json={\"approval_note\": \"Approved after reviewing transformation\"},\n headers={\"X-Session-Version\": \"1\"},\n )\n assert approve_response.status_code == 200\n approve_payload = approve_response.json()\n assert approve_payload[\"mapping_id\"] == \"map-1\"\n assert approve_payload[\"approval_state\"] == \"approved\"\n assert approve_payload[\"approved_by_user_id\"] == \"user-1\"\n assert (\n session.execution_mappings[0].transformation_note\n == \"Approved after reviewing transformation\"\n )\n batch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/approve-batch\",\n json={\"mapping_ids\": [\"map-1\"]},\n headers={\"X-Session-Version\": \"2\"},\n )\n assert batch_response.status_code == 200\n assert batch_response.json()[0][\"mapping_id\"] == \"map-1\"\n list_response = client.get(\"/api/dataset-orchestration/sessions/sess-1/mappings\")\n assert list_response.status_code == 200\n assert list_response.json()[\"items\"][0][\"mapping_id\"] == \"map-1\"\n preview_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"3\"},\n )\n assert preview_response.status_code == 200\n preview_payload = preview_response.json()\n assert preview_payload[\"preview_id\"] == \"preview-1\"\n assert preview_payload[\"preview_status\"] == \"ready\"\n assert preview_payload[\"compiled_by\"] == \"superset\"\n assert \"SELECT * FROM sales\" in preview_payload[\"compiled_sql\"]\n def _assert_expected_pending_preview_version(command):\n assert command.expected_version == 4\n return PreparePreviewResult(\n session=session,\n preview=SimpleNamespace(\n preview_id=\"preview-pending\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.PENDING,\n compiled_sql=None,\n preview_fingerprint=\"fingerprint-pending\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=None,\n created_at=datetime.now(UTC),\n ),\n blocked_reasons=[],\n )\n orchestrator.prepare_launch_preview.side_effect = (\n _assert_expected_pending_preview_version\n )\n preview_enqueue_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"4\"},\n )\n assert preview_enqueue_response.status_code == 202\n assert preview_enqueue_response.json() == {\n \"session_id\": \"sess-1\",\n \"session_version\": 3,\n \"preview_status\": \"pending\",\n \"task_id\": None,\n }\n launch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/launch\",\n headers={\"X-Session-Version\": \"5\"},\n )\n assert launch_response.status_code == 201\n launch_payload = launch_response.json()\n assert launch_payload[\"run_context\"][\"run_context_id\"] == \"run-1\"\n assert launch_payload[\"run_context\"][\"sql_lab_session_ref\"] == \"sql-lab-77\"\n assert launch_payload[\"run_context\"][\"launch_status\"] == \"started\"\n assert (\n launch_payload[\"redirect_url\"]\n == \"http://superset.local/superset/sqllab?queryId=sql-lab-77\"\n )\n# #endregion test_us3_mapping_patch_approval_preview_and_launch_endpoints\n" }, { "contract_id": "test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1287, - "end_line": 1383, + "start_line": 1134, + "end_line": 1216, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4602,15 +3639,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Preview response should expose the refreshed session version so the normal preview-then-launch UI flow can satisfy optimistic locking without a forced full reload.\ndef test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n session.version = 4\n\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n\n def _require_session_version(current, expected):\n if int(getattr(current, \"version\", 0) or 0) != expected:\n raise DatasetReviewSessionVersionConflictError(\n current.session_id,\n expected,\n int(getattr(current, \"version\", 0) or 0),\n )\n return current\n\n repository.require_session_version.side_effect = _require_session_version\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n preview = SimpleNamespace(\n preview_id=\"preview-2\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'FR'\",\n preview_fingerprint=\"fingerprint-2\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(timezone.utc),\n created_at=datetime.now(timezone.utc),\n )\n run_context = SimpleNamespace(\n run_context_id=\"run-2\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-2\",\n sql_lab_session_ref=\"sql-lab-88\",\n effective_filters=[{\"mapping_id\": \"map-1\", \"effective_value\": \"FR\"}],\n template_params={\"country\": \"FR\"},\n approved_mapping_ids=[],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(timezone.utc),\n )\n orchestrator = MagicMock()\n\n def _prepare_preview(command):\n assert command.expected_version == 4\n session.version = 5\n return PreparePreviewResult(\n session=session,\n preview=preview,\n blocked_reasons=[],\n )\n\n def _launch_dataset(command):\n assert command.expected_version == 5\n return LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n\n orchestrator.prepare_launch_preview.side_effect = _prepare_preview\n orchestrator.launch_dataset.side_effect = _launch_dataset\n\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n\n preview_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"4\"},\n )\n\n assert preview_response.status_code == 200\n preview_payload = preview_response.json()\n assert preview_payload[\"session_version\"] == 5\n\n launch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/launch\",\n headers={\"X-Session-Version\": str(preview_payload[\"session_version\"])},\n )\n\n assert launch_response.status_code == 201\n assert launch_response.json()[\"run_context\"][\"run_context_id\"] == \"run-2\"\n\n\n# [/DEF:test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Preview response should expose the refreshed session version so the normal preview-then-launch UI flow can satisfy optimistic locking without a forced full reload.\ndef test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n session.version = 4\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n def _require_session_version(current, expected):\n if int(getattr(current, \"version\", 0) or 0) != expected:\n raise DatasetReviewSessionVersionConflictError(\n current.session_id,\n expected,\n int(getattr(current, \"version\", 0) or 0),\n )\n return current\n repository.require_session_version.side_effect = _require_session_version\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n preview = SimpleNamespace(\n preview_id=\"preview-2\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.READY,\n compiled_sql=\"SELECT * FROM sales WHERE country = 'FR'\",\n preview_fingerprint=\"fingerprint-2\",\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(UTC),\n created_at=datetime.now(UTC),\n )\n run_context = SimpleNamespace(\n run_context_id=\"run-2\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-2\",\n sql_lab_session_ref=\"sql-lab-88\",\n effective_filters=[{\"mapping_id\": \"map-1\", \"effective_value\": \"FR\"}],\n template_params={\"country\": \"FR\"},\n approved_mapping_ids=[],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(UTC),\n )\n orchestrator = MagicMock()\n def _prepare_preview(command):\n assert command.expected_version == 4\n session.version = 5\n return PreparePreviewResult(\n session=session,\n preview=preview,\n blocked_reasons=[],\n )\n def _launch_dataset(command):\n assert command.expected_version == 5\n return LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n orchestrator.prepare_launch_preview.side_effect = _prepare_preview\n orchestrator.launch_dataset.side_effect = _launch_dataset\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n preview_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"4\"},\n )\n assert preview_response.status_code == 200\n preview_payload = preview_response.json()\n assert preview_payload[\"session_version\"] == 5\n launch_response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/launch\",\n headers={\"X-Session-Version\": str(preview_payload[\"session_version\"])},\n )\n assert launch_response.status_code == 201\n assert launch_response.json()[\"run_context\"][\"run_context_id\"] == \"run-2\"\n# #endregion test_us3_preview_response_propagates_refreshed_session_version_for_launch_follow_up\n" }, { "contract_id": "test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1386, - "end_line": 1437, + "start_line": 1217, + "end_line": 1262, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4635,15 +3672,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Preview endpoint should preserve API contract and surface generic upstream preview failures without fabricating dashboard-not-found semantics for non-dashboard 404s.\ndef test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n failed_preview = SimpleNamespace(\n preview_id=\"preview-failed\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=\"fingerprint-failed\",\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=\"RuntimeError: [API_FAILURE] API resource not found at endpoint '/chart/data' | Context: {'status_code': 404, 'endpoint': '/chart/data', 'subtype': 'not_found'}\",\n compiled_at=None,\n created_at=datetime.now(timezone.utc),\n )\n orchestrator = MagicMock()\n orchestrator.prepare_launch_preview.return_value = PreparePreviewResult(\n session=session,\n preview=failed_preview,\n blocked_reasons=[],\n )\n\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n\n response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"0\"},\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"preview_id\"] == \"preview-failed\"\n assert payload[\"preview_status\"] == \"failed\"\n assert payload[\"compiled_sql\"] is None\n assert payload[\"compiled_by\"] == \"superset\"\n assert payload[\"error_code\"] == \"superset_preview_failed\"\n assert \"/chart/data\" in payload[\"error_details\"]\n assert \"API resource not found\" in payload[\"error_details\"]\n assert \"Dashboard not found\" not in payload[\"error_details\"]\n\n\n# [/DEF:test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Preview endpoint should preserve API contract and surface generic upstream preview failures without fabricating dashboard-not-found semantics for non-dashboard 404s.\ndef test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n failed_preview = SimpleNamespace(\n preview_id=\"preview-failed\",\n session_id=\"sess-1\",\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=\"fingerprint-failed\",\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=\"RuntimeError: [API_FAILURE] API resource not found at endpoint '/chart/data' | Context: {'status_code': 404, 'endpoint': '/chart/data', 'subtype': 'not_found'}\",\n compiled_at=None,\n created_at=datetime.now(UTC),\n )\n orchestrator = MagicMock()\n orchestrator.prepare_launch_preview.return_value = PreparePreviewResult(\n session=session,\n preview=failed_preview,\n blocked_reasons=[],\n )\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n response = client.post(\n \"/api/dataset-orchestration/sessions/sess-1/preview\",\n headers={\"X-Session-Version\": \"0\"},\n )\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"preview_id\"] == \"preview-failed\"\n assert payload[\"preview_status\"] == \"failed\"\n assert payload[\"compiled_sql\"] is None\n assert payload[\"compiled_by\"] == \"superset\"\n assert payload[\"error_code\"] == \"superset_preview_failed\"\n assert \"/chart/data\" in payload[\"error_details\"]\n assert \"API resource not found\" in payload[\"error_details\"]\n assert \"Dashboard not found\" not in payload[\"error_details\"]\n# #endregion test_us3_preview_endpoint_returns_failed_preview_without_false_dashboard_not_found_contract_drift\n" }, { "contract_id": "test_mutation_endpoints_surface_session_version_conflict_payload", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1440, - "end_line": 1477, + "start_line": 1263, + "end_line": 1295, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4668,15 +3705,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_mutation_endpoints_surface_session_version_conflict_payload:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Dataset review mutation endpoints should return deterministic 409 conflict semantics when optimistic-lock versions are stale.\ndef test_mutation_endpoints_surface_session_version_conflict_payload(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = (\n DatasetReviewSessionVersionConflictError(\n session_id=\"sess-1\",\n expected_version=2,\n actual_version=5,\n )\n )\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1\",\n json={\n \"effective_value\": \"EU\",\n \"mapping_method\": \"manual_override\",\n },\n headers={\"X-Session-Version\": \"2\"},\n )\n\n assert response.status_code == 409\n payload = response.json()[\"detail\"]\n assert payload[\"error_code\"] == \"session_version_conflict\"\n assert payload[\"expected_version\"] == 2\n assert payload[\"actual_version\"] == 5\n\n\n# [/DEF:test_mutation_endpoints_surface_session_version_conflict_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_mutation_endpoints_surface_session_version_conflict_payload [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Dataset review mutation endpoints should return deterministic 409 conflict semantics when optimistic-lock versions are stale.\ndef test_mutation_endpoints_surface_session_version_conflict_payload(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.side_effect = (\n DatasetReviewSessionVersionConflictError(\n session_id=\"sess-1\",\n expected_version=2,\n actual_version=5,\n )\n )\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n app.dependency_overrides[_get_repository] = lambda: repository\n response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1/mappings/map-1\",\n json={\n \"effective_value\": \"EU\",\n \"mapping_method\": \"manual_override\",\n },\n headers={\"X-Session-Version\": \"2\"},\n )\n assert response.status_code == 409\n payload = response.json()[\"detail\"]\n assert payload[\"error_code\"] == \"session_version_conflict\"\n assert payload[\"expected_version\"] == 2\n assert payload[\"actual_version\"] == 5\n# #endregion test_mutation_endpoints_surface_session_version_conflict_payload\n" }, { "contract_id": "test_update_session_surfaces_commit_time_session_version_conflict_payload", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1480, - "end_line": 1515, + "start_line": 1296, + "end_line": 1326, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4701,15 +3738,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_update_session_surfaces_commit_time_session_version_conflict_payload:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Session lifecycle mutation should return deterministic 409 conflict semantics when commit-time optimistic locking rejects a stale write.\ndef test_update_session_surfaces_commit_time_session_version_conflict_payload(\n dataset_review_api_dependencies,\n):\n session = _make_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.return_value = session\n repository.commit_session_mutation.side_effect = (\n DatasetReviewSessionVersionConflictError(\n session_id=\"sess-1\",\n expected_version=0,\n actual_version=1,\n )\n )\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n app.dependency_overrides[_get_repository] = lambda: repository\n\n response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1\",\n json={\"status\": \"paused\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n\n assert response.status_code == 409\n payload = response.json()[\"detail\"]\n assert payload[\"error_code\"] == \"session_version_conflict\"\n assert payload[\"expected_version\"] == 0\n assert payload[\"actual_version\"] == 1\n\n\n# [/DEF:test_update_session_surfaces_commit_time_session_version_conflict_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_update_session_surfaces_commit_time_session_version_conflict_payload [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Session lifecycle mutation should return deterministic 409 conflict semantics when commit-time optimistic locking rejects a stale write.\ndef test_update_session_surfaces_commit_time_session_version_conflict_payload(\n dataset_review_api_dependencies,\n):\n session = _make_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.require_session_version.return_value = session\n repository.commit_session_mutation.side_effect = (\n DatasetReviewSessionVersionConflictError(\n session_id=\"sess-1\",\n expected_version=0,\n actual_version=1,\n )\n )\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n app.dependency_overrides[_get_repository] = lambda: repository\n response = client.patch(\n \"/api/dataset-orchestration/sessions/sess-1\",\n json={\"status\": \"paused\"},\n headers={\"X-Session-Version\": \"0\"},\n )\n assert response.status_code == 409\n payload = response.json()[\"detail\"]\n assert payload[\"error_code\"] == \"session_version_conflict\"\n assert payload[\"expected_version\"] == 0\n assert payload[\"actual_version\"] == 1\n# #endregion test_update_session_surfaces_commit_time_session_version_conflict_payload\n" }, { "contract_id": "test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1518, - "end_line": 1588, + "start_line": 1327, + "end_line": 1389, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4734,15 +3771,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Recovered imported filters with values should flow into preview filter context even when no template variable mapping exists.\ndef test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n\n recovered_filter = MagicMock()\n recovered_filter.filter_id = \"filter-country\"\n recovered_filter.filter_name = \"country\"\n recovered_filter.display_name = \"Country\"\n recovered_filter.raw_value = [\"DE\", \"FR\"]\n recovered_filter.normalized_value = [\"DE\", \"FR\"]\n recovered_filter.requires_confirmation = False\n recovered_filter.recovery_status = \"recovered\"\n\n session.imported_filters = [recovered_filter]\n session.template_variables = []\n session.execution_mappings = []\n session.semantic_fields = []\n\n snapshot = orchestrator._build_execution_snapshot(session)\n\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"preview_blockers\"] == []\n recovered_filter.normalized_value = {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n }\n\n snapshot = orchestrator._build_execution_snapshot(session)\n\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"preview_blockers\"] == []\n assert snapshot[\"effective_filters\"] == [\n {\n \"filter_id\": \"filter-country\",\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\", \"FR\"],\n \"raw_input_value\": [\"DE\", \"FR\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n }\n ]\n\n\n# [/DEF:test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Recovered imported filters with values should flow into preview filter context even when no template variable mapping exists.\ndef test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n recovered_filter = MagicMock()\n recovered_filter.filter_id = \"filter-country\"\n recovered_filter.filter_name = \"country\"\n recovered_filter.display_name = \"Country\"\n recovered_filter.raw_value = [\"DE\", \"FR\"]\n recovered_filter.normalized_value = [\"DE\", \"FR\"]\n recovered_filter.requires_confirmation = False\n recovered_filter.recovery_status = \"recovered\"\n session.imported_filters = [recovered_filter]\n session.template_variables = []\n session.execution_mappings = []\n session.semantic_fields = []\n snapshot = build_execution_snapshot(session)\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"preview_blockers\"] == []\n recovered_filter.normalized_value = {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n }\n snapshot = build_execution_snapshot(session)\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"preview_blockers\"] == []\n assert snapshot[\"effective_filters\"] == [\n {\n \"filter_id\": \"filter-country\",\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\", \"FR\"],\n \"raw_input_value\": [\"DE\", \"FR\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n }\n ]\n# #endregion test_execution_snapshot_includes_recovered_imported_filters_without_template_mapping\n" }, { "contract_id": "test_execution_snapshot_preserves_mapped_template_variables_and_filter_context", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1591, - "end_line": 1627, + "start_line": 1390, + "end_line": 1422, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4767,15 +3804,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_execution_snapshot_preserves_mapped_template_variables_and_filter_context:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Mapped template variables should still populate template params while contributing their effective filter context.\ndef test_execution_snapshot_preserves_mapped_template_variables_and_filter_context(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n\n snapshot = orchestrator._build_execution_snapshot(session)\n\n assert snapshot[\"template_params\"] == {\"country\": \"DE\"}\n assert snapshot[\"preview_blockers\"] == []\n assert snapshot[\"effective_filters\"] == [\n {\n \"mapping_id\": \"map-1\",\n \"filter_id\": \"filter-1\",\n \"filter_name\": \"country\",\n \"variable_id\": \"var-1\",\n \"variable_name\": \"country\",\n \"effective_value\": \"DE\",\n \"raw_input_value\": \"DE\",\n }\n ]\n assert snapshot[\"open_warning_refs\"] == [\"map-1\"]\n\n\n# [/DEF:test_execution_snapshot_preserves_mapped_template_variables_and_filter_context:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_execution_snapshot_preserves_mapped_template_variables_and_filter_context [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Mapped template variables should still populate template params while contributing their effective filter context.\ndef test_execution_snapshot_preserves_mapped_template_variables_and_filter_context(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n snapshot = build_execution_snapshot(session)\n assert snapshot[\"template_params\"] == {\"country\": \"DE\"}\n assert snapshot[\"preview_blockers\"] == []\n assert snapshot[\"effective_filters\"] == [\n {\n \"mapping_id\": \"map-1\",\n \"filter_id\": \"filter-1\",\n \"filter_name\": \"country\",\n \"variable_id\": \"var-1\",\n \"variable_name\": \"country\",\n \"effective_value\": \"DE\",\n \"raw_input_value\": \"DE\",\n }\n ]\n assert snapshot[\"open_warning_refs\"] == [\"map-1\"]\n# #endregion test_execution_snapshot_preserves_mapped_template_variables_and_filter_context\n" }, { "contract_id": "test_execution_snapshot_skips_partial_imported_filters_without_values", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1630, - "end_line": 1669, + "start_line": 1423, + "end_line": 1456, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4800,15 +3837,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_execution_snapshot_skips_partial_imported_filters_without_values:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Partial imported filters without raw or normalized values must not emit bogus active preview filters.\ndef test_execution_snapshot_skips_partial_imported_filters_without_values(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n\n unresolved_filter = MagicMock()\n unresolved_filter.filter_id = \"filter-region\"\n unresolved_filter.filter_name = \"region\"\n unresolved_filter.display_name = \"Region\"\n unresolved_filter.raw_value = None\n unresolved_filter.normalized_value = None\n unresolved_filter.requires_confirmation = True\n unresolved_filter.recovery_status = \"partial\"\n\n session.imported_filters = [unresolved_filter]\n session.template_variables = []\n session.execution_mappings = []\n session.semantic_fields = []\n\n snapshot = orchestrator._build_execution_snapshot(session)\n\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"effective_filters\"] == []\n assert snapshot[\"preview_blockers\"] == []\n\n\n# [/DEF:test_execution_snapshot_skips_partial_imported_filters_without_values:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_execution_snapshot_skips_partial_imported_filters_without_values [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Partial imported filters without raw or normalized values must not emit bogus active preview filters.\ndef test_execution_snapshot_skips_partial_imported_filters_without_values(\n dataset_review_api_dependencies,\n):\n repository = MagicMock()\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=dataset_review_api_dependencies[\"config_manager\"],\n # WARNING: task_manager=None — all async task dispatch paths are bypassed.\n # Task dispatch failures are invisible to this test.\n task_manager=None,\n )\n session = _make_preview_ready_session()\n unresolved_filter = MagicMock()\n unresolved_filter.filter_id = \"filter-region\"\n unresolved_filter.filter_name = \"region\"\n unresolved_filter.display_name = \"Region\"\n unresolved_filter.raw_value = None\n unresolved_filter.normalized_value = None\n unresolved_filter.requires_confirmation = True\n unresolved_filter.recovery_status = \"partial\"\n session.imported_filters = [unresolved_filter]\n session.template_variables = []\n session.execution_mappings = []\n session.semantic_fields = []\n snapshot = build_execution_snapshot(session)\n assert snapshot[\"template_params\"] == {}\n assert snapshot[\"effective_filters\"] == []\n assert snapshot[\"preview_blockers\"] == []\n# #endregion test_execution_snapshot_skips_partial_imported_filters_without_values\n" }, { "contract_id": "test_us3_launch_endpoint_requires_launch_permission", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1672, - "end_line": 1725, + "start_line": 1457, + "end_line": 1504, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4833,15 +3870,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_us3_launch_endpoint_requires_launch_permission:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Launch endpoint should enforce the contract RBAC permission instead of the generic session-manage permission.\ndef test_us3_launch_endpoint_requires_launch_permission(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n\n run_context = SimpleNamespace(\n run_context_id=\"run-1\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-1\",\n sql_lab_session_ref=\"sql-lab-77\",\n effective_filters=[],\n template_params={},\n approved_mapping_ids=[\"map-1\"],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(timezone.utc),\n )\n orchestrator = MagicMock()\n orchestrator.launch_dataset.return_value = LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n dataset_review_api_dependencies[\"user\"].roles = [\n SimpleNamespace(\n name=\"SessionManager\",\n permissions=[SimpleNamespace(resource=\"dataset:session\", action=\"MANAGE\")],\n )\n ]\n\n response = client.post(\"/api/dataset-orchestration/sessions/sess-1/launch\")\n\n assert response.status_code == 403\n assert (\n response.json()[\"detail\"]\n == \"Permission denied for dataset:execution:launch:EXECUTE\"\n )\n\n\n# [/DEF:test_us3_launch_endpoint_requires_launch_permission:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_us3_launch_endpoint_requires_launch_permission [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Launch endpoint should enforce the contract RBAC permission instead of the generic session-manage permission.\ndef test_us3_launch_endpoint_requires_launch_permission(\n dataset_review_api_dependencies,\n):\n session = _make_us3_session()\n repository = MagicMock()\n repository.load_session_detail.return_value = session\n repository.db = MagicMock()\n repository.event_logger = MagicMock(spec=SessionEventLogger)\n run_context = SimpleNamespace(\n run_context_id=\"run-1\",\n session_id=\"sess-1\",\n dataset_ref=\"public.sales\",\n environment_id=\"env-1\",\n preview_id=\"preview-1\",\n sql_lab_session_ref=\"sql-lab-77\",\n effective_filters=[],\n template_params={},\n approved_mapping_ids=[\"map-1\"],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=LaunchStatus.STARTED,\n launch_error=None,\n created_at=datetime.now(UTC),\n )\n orchestrator = MagicMock()\n orchestrator.launch_dataset.return_value = LaunchDatasetResult(\n session=session,\n run_context=run_context,\n blocked_reasons=[],\n )\n app.dependency_overrides[_get_repository] = lambda: repository\n app.dependency_overrides[_get_orchestrator] = lambda: orchestrator\n dataset_review_api_dependencies[\"user\"].roles = [\n SimpleNamespace(\n name=\"SessionManager\",\n permissions=[SimpleNamespace(resource=\"dataset:session\", action=\"MANAGE\")],\n )\n ]\n response = client.post(\"/api/dataset-orchestration/sessions/sess-1/launch\")\n assert response.status_code == 403\n assert (\n response.json()[\"detail\"]\n == \"Permission denied for dataset:execution:launch:EXECUTE\"\n )\n# #endregion test_us3_launch_endpoint_requires_launch_permission\n" }, { "contract_id": "test_semantic_source_version_propagation_preserves_locked_fields", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_dataset_review_api.py", - "start_line": 1728, - "end_line": 1764, + "start_line": 1505, + "end_line": 1536, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4866,59 +3903,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_semantic_source_version_propagation_preserves_locked_fields:Function]\n# @RELATION: BINDS_TO -> DatasetReviewApiTests\n# @PURPOSE: Updated semantic source versions should mark unlocked fields reviewable while preserving locked manual values.\ndef test_semantic_source_version_propagation_preserves_locked_fields():\n resolver = SemanticSourceResolver()\n source = SimpleNamespace(source_id=\"src-1\", source_version=\"2026.04\")\n\n unlocked_field = SimpleNamespace(\n source_id=\"src-1\",\n source_version=\"2026.03\",\n is_locked=False,\n provenance=FieldProvenance.DICTIONARY_EXACT,\n needs_review=False,\n has_conflict=False,\n )\n locked_field = SimpleNamespace(\n source_id=\"src-1\",\n source_version=\"2026.03\",\n is_locked=True,\n provenance=FieldProvenance.MANUAL_OVERRIDE,\n needs_review=False,\n has_conflict=False,\n )\n\n result = resolver.propagate_source_version_update(\n source, [unlocked_field, locked_field]\n )\n\n assert result[\"propagated\"] == 1\n assert result[\"preserved_locked\"] == 1\n assert unlocked_field.source_version == \"2026.04\"\n assert unlocked_field.needs_review is True\n assert locked_field.source_version == \"2026.03\"\n assert locked_field.needs_review is False\n\n\n# [/DEF:test_semantic_source_version_propagation_preserves_locked_fields:Function]\n" - }, - { - "contract_id": "DatasetsApiTests", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 1, - "end_line": 326, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Endpoint contracts remain stable for success and validation failure paths.", - "LAYER": "API", - "PURPOSE": "Unit tests for datasets API endpoints.", - "SEMANTICS": [ - "datasets", - "api", - "tests", - "pagination", - "mapping", - "docs" - ] - }, - "relations": [ - { - "source_id": "DatasetsApiTests", - "relation_type": "DEPENDS_ON", - "target_id": "DatasetsApi", - "target_ref": "[DatasetsApi]" - } - ], - "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" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetsApiTests:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: datasets, api, tests, pagination, mapping, docs\n# @PURPOSE: Unit tests for datasets API endpoints.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DatasetsApi]\n# @INVARIANT: Endpoint contracts remain stable for success and validation failure paths.\n\nimport pytest\nfrom unittest.mock import MagicMock, patch, AsyncMock\nfrom fastapi.testclient import TestClient\nfrom src.app import app\nfrom src.api.routes.datasets import DatasetsResponse, DatasetDetailResponse\nfrom src.dependencies import (\n get_current_user,\n has_permission,\n get_config_manager,\n get_task_manager,\n get_resource_service,\n get_mapping_service,\n)\n\n# Global mock user for get_current_user dependency overrides\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture(autouse=True)\ndef mock_deps():\n \"\"\"Bare MagicMock — no spec guards. All service method calls succeed silently.\n\n Authorization, data integrity, and error paths are invisible to this fixture.\n \"\"\"\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n config_manager = MagicMock()\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n task_manager = MagicMock()\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n resource_service = MagicMock()\n mapping_service = MagicMock()\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n app.dependency_overrides[get_mapping_service] = lambda: mapping_service\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:migration\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:backup\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"tasks\", \"READ\")] = lambda: mock_user\n\n yield {\n \"config\": config_manager,\n \"task\": task_manager,\n \"resource\": resource_service,\n \"mapping\": mapping_service,\n }\n app.dependency_overrides.clear()\n\n\nclient = TestClient(app)\n\n\n# [DEF:test_get_datasets_success:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate successful datasets listing contract for an existing environment.\n# @TEST: GET /api/datasets returns 200 and valid schema\n# @PRE: env_id exists\n# @POST: Response matches DatasetsResponse schema\ndef test_get_datasets_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # Mock resource service response\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"sales_data\",\n \"schema\": \"public\",\n \"database\": \"sales_db\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": {\"task_id\": \"task-1\", \"status\": \"SUCCESS\"},\n }\n ]\n )\n\n response = client.get(\"/api/datasets?env_id=prod\")\n\n assert response.status_code == 200\n data = response.json()\n assert \"datasets\" in data\n assert len(data[\"datasets\"]) >= 0\n # Validate against Pydantic model\n DatasetsResponse(**data)\n\n\n# [/DEF:test_get_datasets_success:Function]\n\n\n# [DEF:test_get_datasets_env_not_found:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate datasets listing returns 404 when the requested environment does not exist.\n# @TEST: GET /api/datasets returns 404 if env_id missing\n# @PRE: env_id does not exist\n# @POST: Returns 404 error\ndef test_get_datasets_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n\n response = client.get(\"/api/datasets?env_id=nonexistent\")\n\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_env_not_found:Function]\n\n\n# [DEF:test_get_datasets_invalid_pagination:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate datasets listing rejects invalid pagination parameters with 400 responses.\n# @TEST: GET /api/datasets returns 400 for invalid page/page_size\n# @PRE: page < 1 or page_size > 100\n# @POST: Returns 400 error\ndef test_get_datasets_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # Invalid page\n response = client.get(\"/api/datasets?env_id=prod&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n\n # Invalid page_size (too small)\n response = client.get(\"/api/datasets?env_id=prod&page_size=0\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n # @TEST_EDGE: page_size > 100 exceeds max\n response = client.get(\"/api/datasets?env_id=prod&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_invalid_pagination:Function]\n\n\n# [DEF:test_map_columns_success:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate map-columns request creates an async mapping task and returns its identifier.\n# @TEST: POST /api/datasets/map-columns creates mapping task\n# @PRE: Valid env_id, dataset_ids, source_type\n# @POST: Returns task_id\ndef test_map_columns_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # Mock task manager\n mock_task = MagicMock()\n mock_task.id = \"task-123\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1, 2, 3], \"source_type\": \"postgresql\"},\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_map_columns_success:Function]\n\n\n# [DEF:test_map_columns_invalid_source_type:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate map-columns rejects unsupported source types with a 400 contract response.\n# @TEST: POST /api/datasets/map-columns returns 400 for invalid source_type\n# @PRE: source_type is not 'postgresql' or 'xlsx'\n# @POST: Returns 400 error\ndef test_map_columns_invalid_source_type(mock_deps):\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1], \"source_type\": \"invalid\"},\n )\n\n assert response.status_code == 400\n assert \"Source type must be 'postgresql' or 'xlsx'\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_map_columns_invalid_source_type:Function]\n\n\n# [DEF:test_generate_docs_success:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @TEST: POST /api/datasets/generate-docs creates doc generation task\n# @PRE: Valid env_id, dataset_ids, llm_provider\n# @PURPOSE: Validate generate-docs request creates an async documentation task and returns its identifier.\n# @POST: Returns task_id\ndef test_generate_docs_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # Mock task manager\n mock_task = MagicMock()\n mock_task.id = \"task-456\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1], \"llm_provider\": \"openai\"},\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_generate_docs_success:Function]\n\n\n# [DEF:test_map_columns_empty_ids:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate map-columns rejects empty dataset identifier lists.\n# @TEST: POST /api/datasets/map-columns returns 400 for empty dataset_ids\n# @PRE: dataset_ids is empty\n# @POST: Returns 400 error\ndef test_map_columns_empty_ids(mock_deps):\n \"\"\"@PRE: dataset_ids must be non-empty.\"\"\"\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [], \"source_type\": \"postgresql\"},\n )\n assert response.status_code == 400\n assert \"At least one dataset ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_map_columns_empty_ids:Function]\n\n\n# [DEF:test_generate_docs_empty_ids:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate generate-docs rejects empty dataset identifier lists.\n# @TEST: POST /api/datasets/generate-docs returns 400 for empty dataset_ids\n# @PRE: dataset_ids is empty\n# @POST: Returns 400 error\ndef test_generate_docs_empty_ids(mock_deps):\n \"\"\"@PRE: dataset_ids must be non-empty.\"\"\"\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [], \"llm_provider\": \"openai\"},\n )\n assert response.status_code == 400\n assert \"At least one dataset ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_generate_docs_empty_ids:Function]\n\n\n# [DEF:test_generate_docs_env_not_found:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @TEST: POST /api/datasets/generate-docs returns 404 for missing env\n# @PRE: env_id does not exist\n# @PURPOSE: Validate generate-docs returns 404 when the requested environment cannot be resolved.\n# @POST: Returns 404 error\ndef test_generate_docs_env_not_found(mock_deps):\n \"\"\"@PRE: env_id must be a valid environment.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"ghost\", \"dataset_ids\": [1], \"llm_provider\": \"openai\"},\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_generate_docs_env_not_found:Function]\n\n\n# [DEF:test_get_datasets_superset_failure:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate datasets listing surfaces a 503 contract when Superset access fails.\n# @TEST_EDGE: external_superset_failure -> {status: 503}\n# @POST: Returns 503 with stable error detail when upstream dataset fetch fails.\ndef test_get_datasets_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n\n response = client.get(\"/api/datasets?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch datasets\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_superset_failure:Function]\n\n\n# [/DEF:DatasetsApiTests:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_semantic_source_version_propagation_preserves_locked_fields [TYPE Function]\n# @RELATION BINDS_TO -> DatasetReviewApiTests\n# @BRIEF Updated semantic source versions should mark unlocked fields reviewable while preserving locked manual values.\ndef test_semantic_source_version_propagation_preserves_locked_fields():\n resolver = SemanticSourceResolver()\n source = SimpleNamespace(source_id=\"src-1\", source_version=\"2026.04\")\n unlocked_field = SimpleNamespace(\n source_id=\"src-1\",\n source_version=\"2026.03\",\n is_locked=False,\n provenance=FieldProvenance.DICTIONARY_EXACT,\n needs_review=False,\n has_conflict=False,\n )\n locked_field = SimpleNamespace(\n source_id=\"src-1\",\n source_version=\"2026.03\",\n is_locked=True,\n provenance=FieldProvenance.MANUAL_OVERRIDE,\n needs_review=False,\n has_conflict=False,\n )\n result = resolver.propagate_source_version_update(\n source, [unlocked_field, locked_field]\n )\n assert result[\"propagated\"] == 1\n assert result[\"preserved_locked\"] == 1\n assert unlocked_field.source_version == \"2026.04\"\n assert unlocked_field.needs_review is True\n assert locked_field.source_version == \"2026.03\"\n assert locked_field.needs_review is False\n# #endregion test_semantic_source_version_propagation_preserves_locked_fields\n" }, { "contract_id": "test_get_datasets_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 75, - "end_line": 111, + "start_line": 64, + "end_line": 95, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -4970,15 +3963,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_datasets_success:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate successful datasets listing contract for an existing environment.\n# @TEST: GET /api/datasets returns 200 and valid schema\n# @PRE: env_id exists\n# @POST: Response matches DatasetsResponse schema\ndef test_get_datasets_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # Mock resource service response\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"sales_data\",\n \"schema\": \"public\",\n \"database\": \"sales_db\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": {\"task_id\": \"task-1\", \"status\": \"SUCCESS\"},\n }\n ]\n )\n\n response = client.get(\"/api/datasets?env_id=prod\")\n\n assert response.status_code == 200\n data = response.json()\n assert \"datasets\" in data\n assert len(data[\"datasets\"]) >= 0\n # Validate against Pydantic model\n DatasetsResponse(**data)\n\n\n# [/DEF:test_get_datasets_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_datasets_success [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @BRIEF Validate successful datasets listing contract for an existing environment.\n# @TEST: GET /api/datasets returns 200 and valid schema\n# @PRE: env_id exists\n# @POST: Response matches DatasetsResponse schema\ndef test_get_datasets_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n # Mock resource service response\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"sales_data\",\n \"schema\": \"public\",\n \"database\": \"sales_db\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": {\"task_id\": \"task-1\", \"status\": \"SUCCESS\"},\n }\n ]\n )\n response = client.get(\"/api/datasets?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert \"datasets\" in data\n assert len(data[\"datasets\"]) >= 0\n # Validate against Pydantic model\n DatasetsResponse(**data)\n# #endregion test_get_datasets_success\n" }, { "contract_id": "test_get_datasets_env_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 114, - "end_line": 129, + "start_line": 96, + "end_line": 107, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5030,15 +4023,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_datasets_env_not_found:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate datasets listing returns 404 when the requested environment does not exist.\n# @TEST: GET /api/datasets returns 404 if env_id missing\n# @PRE: env_id does not exist\n# @POST: Returns 404 error\ndef test_get_datasets_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n\n response = client.get(\"/api/datasets?env_id=nonexistent\")\n\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_env_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_datasets_env_not_found [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @BRIEF Validate datasets listing returns 404 when the requested environment does not exist.\n# @TEST: GET /api/datasets returns 404 if env_id missing\n# @PRE: env_id does not exist\n# @POST: Returns 404 error\ndef test_get_datasets_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/datasets?env_id=nonexistent\")\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n# #endregion test_get_datasets_env_not_found\n" }, { "contract_id": "test_get_datasets_invalid_pagination", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 132, - "end_line": 159, + "start_line": 108, + "end_line": 130, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5090,15 +4083,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_datasets_invalid_pagination:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate datasets listing rejects invalid pagination parameters with 400 responses.\n# @TEST: GET /api/datasets returns 400 for invalid page/page_size\n# @PRE: page < 1 or page_size > 100\n# @POST: Returns 400 error\ndef test_get_datasets_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # Invalid page\n response = client.get(\"/api/datasets?env_id=prod&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n\n # Invalid page_size (too small)\n response = client.get(\"/api/datasets?env_id=prod&page_size=0\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n # @TEST_EDGE: page_size > 100 exceeds max\n response = client.get(\"/api/datasets?env_id=prod&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_invalid_pagination:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_datasets_invalid_pagination [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @BRIEF Validate datasets listing rejects invalid pagination parameters with 400 responses.\n# @TEST: GET /api/datasets returns 400 for invalid page/page_size\n# @PRE: page < 1 or page_size > 100\n# @POST: Returns 400 error\ndef test_get_datasets_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n # Invalid page\n response = client.get(\"/api/datasets?env_id=prod&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n # Invalid page_size (too small)\n response = client.get(\"/api/datasets?env_id=prod&page_size=0\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n # @TEST_EDGE: page_size > 100 exceeds max\n response = client.get(\"/api/datasets?env_id=prod&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n# #endregion test_get_datasets_invalid_pagination\n" }, { "contract_id": "test_map_columns_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 162, - "end_line": 191, + "start_line": 131, + "end_line": 155, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5150,15 +4143,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_map_columns_success:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate map-columns request creates an async mapping task and returns its identifier.\n# @TEST: POST /api/datasets/map-columns creates mapping task\n# @PRE: Valid env_id, dataset_ids, source_type\n# @POST: Returns task_id\ndef test_map_columns_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # Mock task manager\n mock_task = MagicMock()\n mock_task.id = \"task-123\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1, 2, 3], \"source_type\": \"postgresql\"},\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_map_columns_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_map_columns_success [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @BRIEF Validate map-columns request creates an async mapping task and returns its identifier.\n# @TEST: POST /api/datasets/map-columns creates mapping task\n# @PRE: Valid env_id, dataset_ids, source_type\n# @POST: Returns task_id\ndef test_map_columns_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n # Mock task manager\n mock_task = MagicMock()\n mock_task.id = \"task-123\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1, 2, 3], \"source_type\": \"postgresql\"},\n )\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n# #endregion test_map_columns_success\n" }, { "contract_id": "test_map_columns_invalid_source_type", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 194, - "end_line": 210, + "start_line": 156, + "end_line": 169, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5210,15 +4203,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_map_columns_invalid_source_type:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate map-columns rejects unsupported source types with a 400 contract response.\n# @TEST: POST /api/datasets/map-columns returns 400 for invalid source_type\n# @PRE: source_type is not 'postgresql' or 'xlsx'\n# @POST: Returns 400 error\ndef test_map_columns_invalid_source_type(mock_deps):\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1], \"source_type\": \"invalid\"},\n )\n\n assert response.status_code == 400\n assert \"Source type must be 'postgresql' or 'xlsx'\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_map_columns_invalid_source_type:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_map_columns_invalid_source_type [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @BRIEF Validate map-columns rejects unsupported source types with a 400 contract response.\n# @TEST: POST /api/datasets/map-columns returns 400 for invalid source_type\n# @PRE: source_type is not 'postgresql' or 'xlsx'\n# @POST: Returns 400 error\ndef test_map_columns_invalid_source_type(mock_deps):\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1], \"source_type\": \"invalid\"},\n )\n assert response.status_code == 400\n assert \"Source type must be 'postgresql' or 'xlsx'\" in response.json()[\"detail\"]\n# #endregion test_map_columns_invalid_source_type\n" }, { "contract_id": "test_generate_docs_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 213, - "end_line": 242, + "start_line": 170, + "end_line": 194, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5270,15 +4263,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_generate_docs_success:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @TEST: POST /api/datasets/generate-docs creates doc generation task\n# @PRE: Valid env_id, dataset_ids, llm_provider\n# @PURPOSE: Validate generate-docs request creates an async documentation task and returns its identifier.\n# @POST: Returns task_id\ndef test_generate_docs_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # Mock task manager\n mock_task = MagicMock()\n mock_task.id = \"task-456\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1], \"llm_provider\": \"openai\"},\n )\n\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n\n\n# [/DEF:test_generate_docs_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_generate_docs_success [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @TEST: POST /api/datasets/generate-docs creates doc generation task\n# @PRE: Valid env_id, dataset_ids, llm_provider\n# @BRIEF Validate generate-docs request creates an async documentation task and returns its identifier.\n# @POST: Returns task_id\ndef test_generate_docs_success(mock_deps):\n # Mock environment\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n # Mock task manager\n mock_task = MagicMock()\n mock_task.id = \"task-456\"\n mock_deps[\"task\"].create_task = AsyncMock(return_value=mock_task)\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [1], \"llm_provider\": \"openai\"},\n )\n assert response.status_code == 200\n data = response.json()\n assert \"task_id\" in data\n # @POST/@SIDE_EFFECT: create_task was called\n mock_deps[\"task\"].create_task.assert_called_once()\n# #endregion test_generate_docs_success\n" }, { "contract_id": "test_map_columns_empty_ids", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 245, - "end_line": 261, + "start_line": 195, + "end_line": 209, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5330,15 +4323,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_map_columns_empty_ids:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate map-columns rejects empty dataset identifier lists.\n# @TEST: POST /api/datasets/map-columns returns 400 for empty dataset_ids\n# @PRE: dataset_ids is empty\n# @POST: Returns 400 error\ndef test_map_columns_empty_ids(mock_deps):\n \"\"\"@PRE: dataset_ids must be non-empty.\"\"\"\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [], \"source_type\": \"postgresql\"},\n )\n assert response.status_code == 400\n assert \"At least one dataset ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_map_columns_empty_ids:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_map_columns_empty_ids [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @BRIEF Validate map-columns rejects empty dataset identifier lists.\n# @TEST: POST /api/datasets/map-columns returns 400 for empty dataset_ids\n# @PRE: dataset_ids is empty\n# @POST: Returns 400 error\ndef test_map_columns_empty_ids(mock_deps):\n \"\"\"@PRE: dataset_ids must be non-empty.\"\"\"\n response = client.post(\n \"/api/datasets/map-columns\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [], \"source_type\": \"postgresql\"},\n )\n assert response.status_code == 400\n assert \"At least one dataset ID must be provided\" in response.json()[\"detail\"]\n# #endregion test_map_columns_empty_ids\n" }, { "contract_id": "test_generate_docs_empty_ids", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 264, - "end_line": 280, + "start_line": 210, + "end_line": 224, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5390,15 +4383,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_generate_docs_empty_ids:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate generate-docs rejects empty dataset identifier lists.\n# @TEST: POST /api/datasets/generate-docs returns 400 for empty dataset_ids\n# @PRE: dataset_ids is empty\n# @POST: Returns 400 error\ndef test_generate_docs_empty_ids(mock_deps):\n \"\"\"@PRE: dataset_ids must be non-empty.\"\"\"\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [], \"llm_provider\": \"openai\"},\n )\n assert response.status_code == 400\n assert \"At least one dataset ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_generate_docs_empty_ids:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_generate_docs_empty_ids [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @BRIEF Validate generate-docs rejects empty dataset identifier lists.\n# @TEST: POST /api/datasets/generate-docs returns 400 for empty dataset_ids\n# @PRE: dataset_ids is empty\n# @POST: Returns 400 error\ndef test_generate_docs_empty_ids(mock_deps):\n \"\"\"@PRE: dataset_ids must be non-empty.\"\"\"\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"prod\", \"dataset_ids\": [], \"llm_provider\": \"openai\"},\n )\n assert response.status_code == 400\n assert \"At least one dataset ID must be provided\" in response.json()[\"detail\"]\n# #endregion test_generate_docs_empty_ids\n" }, { "contract_id": "test_generate_docs_env_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 283, - "end_line": 300, + "start_line": 225, + "end_line": 240, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5450,15 +4443,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_generate_docs_env_not_found:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @TEST: POST /api/datasets/generate-docs returns 404 for missing env\n# @PRE: env_id does not exist\n# @PURPOSE: Validate generate-docs returns 404 when the requested environment cannot be resolved.\n# @POST: Returns 404 error\ndef test_generate_docs_env_not_found(mock_deps):\n \"\"\"@PRE: env_id must be a valid environment.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"ghost\", \"dataset_ids\": [1], \"llm_provider\": \"openai\"},\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_generate_docs_env_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_generate_docs_env_not_found [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @TEST: POST /api/datasets/generate-docs returns 404 for missing env\n# @PRE: env_id does not exist\n# @BRIEF Validate generate-docs returns 404 when the requested environment cannot be resolved.\n# @POST: Returns 404 error\ndef test_generate_docs_env_not_found(mock_deps):\n \"\"\"@PRE: env_id must be a valid environment.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/datasets/generate-docs\",\n json={\"env_id\": \"ghost\", \"dataset_ids\": [1], \"llm_provider\": \"openai\"},\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n# #endregion test_generate_docs_env_not_found\n" }, { "contract_id": "test_get_datasets_superset_failure", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_datasets.py", - "start_line": 303, - "end_line": 323, + "start_line": 241, + "end_line": 258, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5494,59 +4487,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_datasets_superset_failure:Function]\n# @RELATION: BINDS_TO -> [DatasetsApiTests:Module]\n# @PURPOSE: Validate datasets listing surfaces a 503 contract when Superset access fails.\n# @TEST_EDGE: external_superset_failure -> {status: 503}\n# @POST: Returns 503 with stable error detail when upstream dataset fetch fails.\ndef test_get_datasets_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n\n response = client.get(\"/api/datasets?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch datasets\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_superset_failure:Function]\n" - }, - { - "contract_id": "DbMock", - "contract_type": "Class", - "file_path": "backend/src/api/routes/__tests__/test_git_api.py", - "start_line": 14, - "end_line": 87, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "INVARIANT": "Supports only the SQLAlchemy-like operations exercised by this test module.", - "PURPOSE": "In-memory session double for git route tests with minimal query/filter persistence semantics." - }, - "relations": [ - { - "source_id": "DbMock", - "relation_type": "BINDS_TO", - "target_id": "TestGitApi", - "target_ref": "[TestGitApi]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:DbMock:Class]\n# @RELATION: BINDS_TO -> [TestGitApi]\n# @COMPLEXITY: 2\n# @PURPOSE: In-memory session double for git route tests with minimal query/filter persistence semantics.\n# @INVARIANT: Supports only the SQLAlchemy-like operations exercised by this test module.\nclass DbMock:\n def __init__(self, data=None):\n self._data = data or []\n self._deleted = []\n self._added = []\n self._filtered = None\n\n def query(self, model):\n self._model = model\n self._filtered = None\n return self\n\n def filter(self, condition):\n # Honor simple SQLAlchemy equality expressions used by these route tests.\n candidates = [\n item\n for item in self._data\n if not hasattr(self, \"_model\") or isinstance(item, self._model)\n ]\n try:\n left_key = getattr(getattr(condition, \"left\", None), \"key\", None)\n right_value = getattr(getattr(condition, \"right\", None), \"value\", None)\n if left_key is not None and right_value is not None:\n self._filtered = [\n item\n for item in candidates\n if getattr(item, left_key, None) == right_value\n ]\n else:\n self._filtered = candidates\n except Exception:\n self._filtered = candidates\n return self\n\n def first(self):\n if self._filtered is not None:\n return self._filtered[0] if self._filtered else None\n for item in self._data:\n if hasattr(self, \"_model\") and isinstance(item, self._model):\n return item\n return None\n\n def all(self):\n if self._filtered is not None:\n return list(self._filtered)\n return self._data\n\n def add(self, item):\n self._added.append(item)\n if not hasattr(item, \"id\") or not item.id:\n item.id = \"mocked-id\"\n self._data.append(item)\n\n def delete(self, item):\n self._deleted.append(item)\n if item in self._data:\n self._data.remove(item)\n\n def commit(self):\n pass\n\n def refresh(self, item):\n if not hasattr(item, \"status\"):\n item.status = GitStatus.CONNECTED\n if not hasattr(item, \"last_validated\"):\n item.last_validated = \"2026-03-08T00:00:00Z\"\n\n\n# [/DEF:DbMock:Class]\n" + "anchor_syntax": "region", + "body": "# #region test_get_datasets_superset_failure [TYPE Function]\n# @RELATION BINDS_TO -> [DatasetsApiTests:Module]\n# @BRIEF Validate datasets listing surfaces a 503 contract when Superset access fails.\n# @TEST_EDGE: external_superset_failure -> {status: 503}\n# @POST: Returns 503 with stable error detail when upstream dataset fetch fails.\ndef test_get_datasets_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n response = client.get(\"/api/datasets?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch datasets\" in response.json()[\"detail\"]\n# #endregion test_get_datasets_superset_failure\n" }, { "contract_id": "test_get_git_configs_masks_pat", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_api.py", - "start_line": 90, - "end_line": 119, + "start_line": 77, + "end_line": 102, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5571,15 +4520,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_git_configs_masks_pat:Function]\n# @RELATION: BINDS_TO -> [TestGitApi]\n# @PURPOSE: Validate listing git configs masks stored PAT values in API-facing responses.\ndef test_get_git_configs_masks_pat():\n \"\"\"\n @PRE: Database session `db` is available.\n @POST: Returns a list of all GitServerConfig objects from the database with PAT masked.\n \"\"\"\n db = DbMock(\n [\n GitServerConfig(\n id=\"config-1\",\n name=\"Test Server\",\n provider=GitProvider.GITHUB,\n url=\"https://github.com\",\n pat=\"secret-token\",\n status=GitStatus.CONNECTED,\n last_validated=\"2026-03-08T00:00:00Z\",\n )\n ]\n )\n\n result = asyncio.run(git_routes.get_git_configs(db=db))\n\n assert len(result) == 1\n assert result[0].pat == \"********\"\n assert result[0].name == \"Test Server\"\n\n\n# [/DEF:test_get_git_configs_masks_pat:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_git_configs_masks_pat [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate listing git configs masks stored PAT values in API-facing responses.\ndef test_get_git_configs_masks_pat():\n \"\"\"\n @PRE: Database session `db` is available.\n @POST: Returns a list of all GitServerConfig objects from the database with PAT masked.\n \"\"\"\n db = DbMock(\n [\n GitServerConfig(\n id=\"config-1\",\n name=\"Test Server\",\n provider=GitProvider.GITHUB,\n url=\"https://github.com\",\n pat=\"secret-token\",\n status=GitStatus.CONNECTED,\n last_validated=\"2026-03-08T00:00:00Z\",\n )\n ]\n )\n result = asyncio.run(git_routes.get_git_configs(db=db))\n assert len(result) == 1\n assert result[0].pat == \"********\"\n assert result[0].name == \"Test Server\"\n# #endregion test_get_git_configs_masks_pat\n" }, { "contract_id": "test_create_git_config_persists_config", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_api.py", - "start_line": 122, - "end_line": 152, + "start_line": 103, + "end_line": 128, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5604,15 +4553,62 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_create_git_config_persists_config:Function]\n# @RELATION: BINDS_TO -> [TestGitApi]\n# @PURPOSE: Validate creating git config persists supplied server attributes in backing session.\ndef test_create_git_config_persists_config():\n \"\"\"\n @PRE: `config` contains valid GitServerConfigCreate data.\n @POST: A new GitServerConfig record is created in the database.\n \"\"\"\n from src.api.routes.git_schemas import GitServerConfigCreate\n\n db = DbMock()\n config = GitServerConfigCreate(\n name=\"New Server\",\n provider=GitProvider.GITLAB,\n url=\"https://gitlab.com\",\n pat=\"new-token\",\n default_branch=\"master\",\n )\n\n result = asyncio.run(git_routes.create_git_config(config=config, db=db))\n\n assert len(db._added) == 1\n assert db._added[0].name == \"New Server\"\n assert db._added[0].pat == \"new-token\"\n assert result.name == \"New Server\"\n assert (\n result.pat == \"new-token\"\n ) # Note: route returns unmasked until serialized by FastAPI usually, but in tests schema might catch it or not.\n\n\n# [/DEF:test_create_git_config_persists_config:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_create_git_config_persists_config [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate creating git config persists supplied server attributes in backing session.\ndef test_create_git_config_persists_config():\n \"\"\"\n @PRE: `config` contains valid GitServerConfigCreate data.\n @POST: A new GitServerConfig record is created in the database.\n \"\"\"\n from src.api.routes.git_schemas import GitServerConfigCreate\n db = DbMock()\n config = GitServerConfigCreate(\n name=\"New Server\",\n provider=GitProvider.GITLAB,\n url=\"https://gitlab.com\",\n pat=\"new-token\",\n default_branch=\"master\",\n )\n result = asyncio.run(git_routes.create_git_config(config=config, db=db))\n assert len(db._added) == 1\n assert db._added[0].name == \"New Server\"\n assert db._added[0].pat == \"new-token\"\n assert result.name == \"New Server\"\n assert (\n result.pat == \"new-token\"\n ) # Note: route returns unmasked until serialized by FastAPI usually, but in tests schema might catch it or not.\n# #endregion test_create_git_config_persists_config\n" + }, + { + "contract_id": "test_update_git_config_modifies_record", + "contract_type": "Function", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 132, + "end_line": 175, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Validate updating git config modifies mutable fields while preserving masked PAT semantics." + }, + "relations": [ + { + "source_id": "test_update_git_config_modifies_record", + "relation_type": "BINDS_TO", + "target_id": "TestGitApi", + "target_ref": "[TestGitApi]" + } + ], + "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", + "body": "# #region test_update_git_config_modifies_record [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate updating git config modifies mutable fields while preserving masked PAT semantics.\ndef test_update_git_config_modifies_record():\n \"\"\"\n @PRE: `config_id` corresponds to an existing configuration.\n @POST: The configuration record is updated in the database, preserving PAT if masked is sent.\n \"\"\"\n existing_config = GitServerConfig(\n id=\"config-1\",\n name=\"Old Server\",\n provider=GitProvider.GITHUB,\n url=\"https://github.com\",\n pat=\"old-token\",\n status=GitStatus.CONNECTED,\n last_validated=\"2026-03-08T00:00:00Z\",\n )\n # The monkeypatched query will return existing_config as it's the only one in the list\n # #region SingleConfigDbMock [TYPE Class]\n class SingleConfigDbMock:\n def query(self, *args):\n return self\n def filter(self, *args):\n return self\n def first(self):\n return existing_config\n def commit(self):\n pass\n def refresh(self, config):\n pass\n # #endregion SingleConfigDbMock\n db = SingleConfigDbMock()\n update_data = GitServerConfigUpdate(name=\"Updated Server\", pat=\"********\")\n result = asyncio.run(\n git_routes.update_git_config(\n config_id=\"config-1\", config_update=update_data, db=db\n )\n )\n assert existing_config.name == \"Updated Server\"\n assert (\n existing_config.pat == \"old-token\"\n ) # Ensure PAT is not overwritten with asterisks\n assert result.pat == \"********\"\n# #endregion test_update_git_config_modifies_record\n" + }, + { + "contract_id": "SingleConfigDbMock", + "contract_type": "Class", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 150, + "end_line": 162, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": " # #region SingleConfigDbMock [TYPE Class]\n class SingleConfigDbMock:\n def query(self, *args):\n return self\n def filter(self, *args):\n return self\n def first(self):\n return existing_config\n def commit(self):\n pass\n def refresh(self, config):\n pass\n # #endregion SingleConfigDbMock\n" }, { "contract_id": "test_update_git_config_raises_404_if_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_api.py", - "start_line": 212, - "end_line": 234, + "start_line": 176, + "end_line": 194, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5637,15 +4633,161 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_update_git_config_raises_404_if_not_found:Function]\n# @RELATION: BINDS_TO -> [TestGitApi]\n# @PURPOSE: Validate updating non-existent git config raises HTTP 404 contract response.\ndef test_update_git_config_raises_404_if_not_found():\n \"\"\"\n @PRE: `config_id` corresponds to a missing configuration.\n @THROW: HTTPException 404\n \"\"\"\n db = DbMock([]) # Empty db\n update_data = GitServerConfigUpdate(name=\"Updated Server\", pat=\"new-token\")\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(\n git_routes.update_git_config(\n config_id=\"config-1\", config_update=update_data, db=db\n )\n )\n\n assert exc_info.value.status_code == 404\n assert exc_info.value.detail == \"Configuration not found\"\n\n\n# [/DEF:test_update_git_config_raises_404_if_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_update_git_config_raises_404_if_not_found [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate updating non-existent git config raises HTTP 404 contract response.\ndef test_update_git_config_raises_404_if_not_found():\n \"\"\"\n @PRE: `config_id` corresponds to a missing configuration.\n @THROW: HTTPException 404\n \"\"\"\n db = DbMock([]) # Empty db\n update_data = GitServerConfigUpdate(name=\"Updated Server\", pat=\"new-token\")\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(\n git_routes.update_git_config(\n config_id=\"config-1\", config_update=update_data, db=db\n )\n )\n assert exc_info.value.status_code == 404\n assert exc_info.value.detail == \"Configuration not found\"\n# #endregion test_update_git_config_raises_404_if_not_found\n" + }, + { + "contract_id": "test_delete_git_config_removes_record", + "contract_type": "Function", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 195, + "end_line": 221, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Validate deleting existing git config removes record and returns success payload." + }, + "relations": [ + { + "source_id": "test_delete_git_config_removes_record", + "relation_type": "BINDS_TO", + "target_id": "TestGitApi", + "target_ref": "[TestGitApi]" + } + ], + "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", + "body": "# #region test_delete_git_config_removes_record [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate deleting existing git config removes record and returns success payload.\ndef test_delete_git_config_removes_record():\n \"\"\"\n @PRE: `config_id` corresponds to an existing configuration.\n @POST: The configuration record is removed from the database.\n \"\"\"\n existing_config = GitServerConfig(id=\"config-1\")\n # #region SingleConfigDbMock [TYPE Class]\n class SingleConfigDbMock:\n def query(self, *args):\n return self\n def filter(self, *args):\n return self\n def first(self):\n return existing_config\n def delete(self, config):\n self.deleted = config\n def commit(self):\n pass\n # #endregion SingleConfigDbMock\n db = SingleConfigDbMock()\n result = asyncio.run(git_routes.delete_git_config(config_id=\"config-1\", db=db))\n assert db.deleted == existing_config\n assert result[\"status\"] == \"success\"\n# #endregion test_delete_git_config_removes_record\n" + }, + { + "contract_id": "test_test_git_config_validates_connection_successfully", + "contract_type": "Function", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 222, + "end_line": 246, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Validate test-connection endpoint returns success when provider connectivity check passes." + }, + "relations": [ + { + "source_id": "test_test_git_config_validates_connection_successfully", + "relation_type": "BINDS_TO", + "target_id": "TestGitApi", + "target_ref": "[TestGitApi]" + } + ], + "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", + "body": "# #region test_test_git_config_validates_connection_successfully [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate test-connection endpoint returns success when provider connectivity check passes.\ndef test_test_git_config_validates_connection_successfully(monkeypatch):\n \"\"\"\n @PRE: `config` contains provider, url, and pat.\n @POST: Returns success if the connection is validated via GitService.\n \"\"\"\n # #region MockGitService [TYPE Class]\n class MockGitService:\n async def test_connection(self, provider, url, pat):\n return True\n # #endregion MockGitService\n monkeypatch.setattr(git_routes, \"git_service\", MockGitService())\n from src.api.routes.git_schemas import GitServerConfigCreate\n config = GitServerConfigCreate(\n name=\"Test Server\",\n provider=GitProvider.GITHUB,\n url=\"https://github.com\",\n pat=\"test-pat\",\n )\n db = DbMock([])\n result = asyncio.run(git_routes.test_git_config(config=config, db=db))\n assert result[\"status\"] == \"success\"\n# #endregion test_test_git_config_validates_connection_successfully\n" + }, + { + "contract_id": "MockGitService", + "contract_type": "Class", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 230, + "end_line": 234, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": " # #region MockGitService [TYPE Class]\n class MockGitService:\n async def test_connection(self, provider, url, pat):\n return True\n # #endregion MockGitService\n" + }, + { + "contract_id": "test_test_git_config_fails_validation", + "contract_type": "Function", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 247, + "end_line": 273, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Validate test-connection endpoint raises HTTP 400 when provider connectivity check fails." + }, + "relations": [ + { + "source_id": "test_test_git_config_fails_validation", + "relation_type": "BINDS_TO", + "target_id": "TestGitApi", + "target_ref": "[TestGitApi]" + } + ], + "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", + "body": "# #region test_test_git_config_fails_validation [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate test-connection endpoint raises HTTP 400 when provider connectivity check fails.\ndef test_test_git_config_fails_validation(monkeypatch):\n \"\"\"\n @PRE: `config` contains provider, url, and pat BUT connection fails.\n @THROW: HTTPException 400\n \"\"\"\n # #region MockGitService [TYPE Class]\n class MockGitService:\n async def test_connection(self, provider, url, pat):\n return False\n # #endregion MockGitService\n monkeypatch.setattr(git_routes, \"git_service\", MockGitService())\n from src.api.routes.git_schemas import GitServerConfigCreate\n config = GitServerConfigCreate(\n name=\"Test Server\",\n provider=GitProvider.GITHUB,\n url=\"https://github.com\",\n pat=\"bad-pat\",\n )\n db = DbMock([])\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.test_git_config(config=config, db=db))\n assert exc_info.value.status_code == 400\n assert exc_info.value.detail == \"Connection failed\"\n# #endregion test_test_git_config_fails_validation\n" + }, + { + "contract_id": "test_list_gitea_repositories_returns_payload", + "contract_type": "Function", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 274, + "end_line": 304, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Validate gitea repositories endpoint returns normalized list for GITEA provider configs." + }, + "relations": [ + { + "source_id": "test_list_gitea_repositories_returns_payload", + "relation_type": "BINDS_TO", + "target_id": "TestGitApi", + "target_ref": "[TestGitApi]" + } + ], + "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", + "body": "# #region test_list_gitea_repositories_returns_payload [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate gitea repositories endpoint returns normalized list for GITEA provider configs.\ndef test_list_gitea_repositories_returns_payload(monkeypatch):\n \"\"\"\n @PRE: config_id exists and provider is GITEA.\n @POST: Returns repositories visible to PAT user.\n \"\"\"\n # #region MockGitService [TYPE Class]\n class MockGitService:\n async def list_gitea_repositories(self, url, pat):\n return [\n {\"name\": \"test-repo\", \"full_name\": \"owner/test-repo\", \"private\": True}\n ]\n # #endregion MockGitService\n monkeypatch.setattr(git_routes, \"git_service\", MockGitService())\n existing_config = GitServerConfig(\n id=\"config-1\",\n name=\"Gitea Server\",\n provider=GitProvider.GITEA,\n url=\"https://gitea.local\",\n pat=\"gitea-token\",\n )\n db = DbMock([existing_config])\n result = asyncio.run(\n git_routes.list_gitea_repositories(config_id=\"config-1\", db=db)\n )\n assert len(result) == 1\n assert result[0].name == \"test-repo\"\n assert result[0].private is True\n# #endregion test_list_gitea_repositories_returns_payload\n" }, { "contract_id": "test_list_gitea_repositories_rejects_non_gitea", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_api.py", - "start_line": 381, - "end_line": 405, + "start_line": 305, + "end_line": 325, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5670,47 +4812,81 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_list_gitea_repositories_rejects_non_gitea:Function]\n# @RELATION: BINDS_TO -> [TestGitApi]\n# @PURPOSE: Validate gitea repositories endpoint rejects non-GITEA providers with HTTP 400.\ndef test_list_gitea_repositories_rejects_non_gitea(monkeypatch):\n \"\"\"\n @PRE: config_id exists and provider is NOT GITEA.\n @THROW: HTTPException 400\n \"\"\"\n existing_config = GitServerConfig(\n id=\"config-1\",\n name=\"GitHub Server\",\n provider=GitProvider.GITHUB,\n url=\"https://github.com\",\n pat=\"token\",\n )\n db = DbMock([existing_config])\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.list_gitea_repositories(config_id=\"config-1\", db=db))\n\n assert exc_info.value.status_code == 400\n assert \"GITEA provider only\" in exc_info.value.detail\n\n\n# [/DEF:test_list_gitea_repositories_rejects_non_gitea:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_list_gitea_repositories_rejects_non_gitea [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate gitea repositories endpoint rejects non-GITEA providers with HTTP 400.\ndef test_list_gitea_repositories_rejects_non_gitea(monkeypatch):\n \"\"\"\n @PRE: config_id exists and provider is NOT GITEA.\n @THROW: HTTPException 400\n \"\"\"\n existing_config = GitServerConfig(\n id=\"config-1\",\n name=\"GitHub Server\",\n provider=GitProvider.GITHUB,\n url=\"https://github.com\",\n pat=\"token\",\n )\n db = DbMock([existing_config])\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.list_gitea_repositories(config_id=\"config-1\", db=db))\n assert exc_info.value.status_code == 400\n assert \"GITEA provider only\" in exc_info.value.detail\n# #endregion test_list_gitea_repositories_rejects_non_gitea\n" }, { - "contract_id": "TestGitStatusRoute", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 1, - "end_line": 454, - "tier": "TIER_2", - "complexity": 3, + "contract_id": "test_create_remote_repository_creates_provider_repo", + "contract_type": "Function", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 326, + "end_line": 365, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": "3", - "LAYER": "Domain (Tests)", - "PURPOSE": "Validate status endpoint behavior for missing and error repository states.", - "SEMANTICS": [ - "tests", - "git", - "api", - "status", - "no_repo" - ] + "PURPOSE": "Validate remote repository creation endpoint maps provider response into normalized payload." }, "relations": [ { - "source_id": "TestGitStatusRoute", - "relation_type": "VERIFIES", - "target_id": "GitApi", - "target_ref": "[GitApi]" + "source_id": "test_create_remote_repository_creates_provider_repo", + "relation_type": "BINDS_TO", + "target_id": "TestGitApi", + "target_ref": "[TestGitApi]" } ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TestGitStatusRoute:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, git, api, status, no_repo\n# @PURPOSE: Validate status endpoint behavior for missing and error repository states.\n# @LAYER: Domain (Tests)\n# @RELATION: VERIFIES -> [GitApi]\n\nfrom fastapi import HTTPException\nimport pytest\nimport asyncio\nfrom unittest.mock import MagicMock\n\nfrom src.api.routes import git as git_routes\n\n\n# [DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure missing local repository is represented as NO_REPO payload instead of an API error.\n# @PRE: GitService.get_status raises HTTPException(404).\n# @POST: Route returns a deterministic NO_REPO status payload.\ndef test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch):\n class MissingRepoGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/missing-repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise AssertionError(\"get_status must not be called when repository path is missing\")\n\n monkeypatch.setattr(git_routes, \"git_service\", MissingRepoGitService())\n\n response = asyncio.run(git_routes.get_repository_status(34))\n\n assert response[\"sync_status\"] == \"NO_REPO\"\n assert response[\"sync_state\"] == \"NO_REPO\"\n assert response[\"has_repo\"] is False\n assert response[\"current_branch\"] is None\n# [/DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function]\n\n\n# [DEF:test_get_repository_status_propagates_non_404_http_exception:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure HTTP exceptions other than 404 are not masked.\n# @PRE: GitService.get_status raises HTTPException with non-404 status.\n# @POST: Raised exception preserves original status and detail.\ndef test_get_repository_status_propagates_non_404_http_exception(monkeypatch):\n class ConflictGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/existing-repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise HTTPException(status_code=409, detail=\"Conflict\")\n\n monkeypatch.setattr(git_routes, \"git_service\", ConflictGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_repository_status(34))\n\n assert exc_info.value.status_code == 409\n assert exc_info.value.detail == \"Conflict\"\n# [/DEF:test_get_repository_status_propagates_non_404_http_exception:Function]\n\n\n# [DEF:test_get_repository_diff_propagates_http_exception:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure diff endpoint preserves domain HTTP errors from GitService.\n# @PRE: GitService.get_diff raises HTTPException.\n# @POST: Endpoint raises same HTTPException values.\ndef test_get_repository_diff_propagates_http_exception(monkeypatch):\n class DiffGitService:\n def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:\n raise HTTPException(status_code=404, detail=\"Repository missing\")\n\n monkeypatch.setattr(git_routes, \"git_service\", DiffGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_repository_diff(12))\n\n assert exc_info.value.status_code == 404\n assert exc_info.value.detail == \"Repository missing\"\n# [/DEF:test_get_repository_diff_propagates_http_exception:Function]\n\n\n# [DEF:test_get_history_wraps_unexpected_error_as_500:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors.\n# @PRE: GitService.get_commit_history raises ValueError.\n# @POST: Endpoint returns HTTPException with status 500 and route context.\ndef test_get_history_wraps_unexpected_error_as_500(monkeypatch):\n class HistoryGitService:\n def get_commit_history(self, dashboard_id: int, limit: int = 50):\n raise ValueError(\"broken parser\")\n\n monkeypatch.setattr(git_routes, \"git_service\", HistoryGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_history(12))\n\n assert exc_info.value.status_code == 500\n assert exc_info.value.detail == \"get_history failed: broken parser\"\n# [/DEF:test_get_history_wraps_unexpected_error_as_500:Function]\n\n\n# [DEF:test_commit_changes_wraps_unexpected_error_as_500:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure commit endpoint does not leak unexpected errors as 400.\n# @PRE: GitService.commit_changes raises RuntimeError.\n# @POST: Endpoint raises HTTPException(500) with route context.\ndef test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):\n class CommitGitService:\n def commit_changes(self, dashboard_id: int, message: str, files):\n raise RuntimeError(\"index lock\")\n\n class CommitPayload:\n message = \"test\"\n files = [\"dashboards/a.yaml\"]\n\n monkeypatch.setattr(git_routes, \"git_service\", CommitGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.commit_changes(12, CommitPayload()))\n\n assert exc_info.value.status_code == 500\n assert exc_info.value.detail == \"commit_changes failed: index lock\"\n# [/DEF:test_commit_changes_wraps_unexpected_error_as_500:Function]\n\n\n# [DEF:test_get_repository_status_batch_returns_mixed_statuses:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint returns per-dashboard statuses in one response.\n# @PRE: Some repositories are missing and some are initialized.\n# @POST: Returned map includes resolved status for each requested dashboard ID.\ndef test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):\n class BatchGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n if dashboard_id == 2:\n return {\"sync_state\": \"SYNCED\", \"sync_status\": \"OK\"}\n raise HTTPException(status_code=404, detail=\"not found\")\n\n monkeypatch.setattr(git_routes, \"git_service\", BatchGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda path: path.endswith(\"/repo-2\"))\n\n class BatchRequest:\n dashboard_ids = [1, 2]\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert response.statuses[\"1\"][\"sync_status\"] == \"NO_REPO\"\n assert response.statuses[\"2\"][\"sync_state\"] == \"SYNCED\"\n# [/DEF:test_get_repository_status_batch_returns_mixed_statuses:Function]\n\n\n# [DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint marks failed items as ERROR without failing entire request.\n# @PRE: GitService raises non-HTTP exception for one dashboard.\n# @POST: Failed dashboard status is marked as ERROR.\ndef test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch):\n class BatchErrorGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise RuntimeError(\"boom\")\n\n monkeypatch.setattr(git_routes, \"git_service\", BatchErrorGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n class BatchRequest:\n dashboard_ids = [9]\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert response.statuses[\"9\"][\"sync_status\"] == \"ERROR\"\n assert response.statuses[\"9\"][\"sync_state\"] == \"ERROR\"\n# [/DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function]\n\n\n# [DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint protects server from oversized payloads.\n# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.\n# @POST: Result contains unique IDs up to configured cap.\ndef test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch):\n class SafeBatchGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n return {\"sync_state\": \"SYNCED\", \"sync_status\": \"OK\"}\n\n monkeypatch.setattr(git_routes, \"git_service\", SafeBatchGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n class BatchRequest:\n dashboard_ids = [1, 1] + list(range(2, 90))\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert len(response.statuses) == git_routes.MAX_REPOSITORY_STATUS_BATCH\n assert \"1\" in response.statuses\n# [/DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function]\n\n\n# [DEF:test_commit_changes_applies_profile_identity_before_commit:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure commit route configures repository identity from profile preferences before commit call.\n# @PRE: Profile preference contains git_username/git_email for current user.\n# @POST: git_service.configure_identity receives resolved identity and commit proceeds.\ndef test_commit_changes_applies_profile_identity_before_commit(monkeypatch):\n class IdentityGitService:\n def __init__(self):\n self.configured_identity = None\n self.commit_payload = None\n\n def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):\n self.configured_identity = (dashboard_id, git_username, git_email)\n\n def commit_changes(self, dashboard_id: int, message: str, files):\n self.commit_payload = (dashboard_id, message, files)\n\n class PreferenceRow:\n git_username = \"user_1\"\n git_email = \"user1@mail.ru\"\n\n class PreferenceQuery:\n def filter(self, *_args, **_kwargs):\n return self\n\n def first(self):\n return PreferenceRow()\n\n class DbStub:\n def query(self, _model):\n return PreferenceQuery()\n\n class UserStub:\n id = \"u-1\"\n\n class CommitPayload:\n message = \"test\"\n files = [\"dashboards/a.yaml\"]\n\n identity_service = IdentityGitService()\n monkeypatch.setattr(git_routes, \"git_service\", identity_service)\n monkeypatch.setattr(\n git_routes,\n \"_resolve_dashboard_id_from_ref\",\n lambda *_args, **_kwargs: 12,\n )\n\n asyncio.run(\n git_routes.commit_changes(\n \"dashboard-12\",\n CommitPayload(),\n config_manager=MagicMock(),\n db=DbStub(),\n current_user=UserStub(),\n )\n )\n\n assert identity_service.configured_identity == (12, \"user_1\", \"user1@mail.ru\")\n assert identity_service.commit_payload == (12, \"test\", [\"dashboards/a.yaml\"])\n# [/DEF:test_commit_changes_applies_profile_identity_before_commit:Function]\n\n\n# [DEF:test_pull_changes_applies_profile_identity_before_pull:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure pull route configures repository identity from profile preferences before pull call.\n# @PRE: Profile preference contains git_username/git_email for current user.\n# @POST: git_service.configure_identity receives resolved identity and pull proceeds.\ndef test_pull_changes_applies_profile_identity_before_pull(monkeypatch):\n class IdentityGitService:\n def __init__(self):\n self.configured_identity = None\n self.pulled_dashboard_id = None\n\n def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):\n self.configured_identity = (dashboard_id, git_username, git_email)\n\n def pull_changes(self, dashboard_id: int):\n self.pulled_dashboard_id = dashboard_id\n\n class PreferenceRow:\n git_username = \"user_1\"\n git_email = \"user1@mail.ru\"\n\n class PreferenceQuery:\n def filter(self, *_args, **_kwargs):\n return self\n\n def first(self):\n return PreferenceRow()\n\n class DbStub:\n def query(self, _model):\n return PreferenceQuery()\n\n class UserStub:\n id = \"u-1\"\n\n identity_service = IdentityGitService()\n monkeypatch.setattr(git_routes, \"git_service\", identity_service)\n monkeypatch.setattr(\n git_routes,\n \"_resolve_dashboard_id_from_ref\",\n lambda *_args, **_kwargs: 12,\n )\n\n asyncio.run(\n git_routes.pull_changes(\n \"dashboard-12\",\n config_manager=MagicMock(),\n db=DbStub(),\n current_user=UserStub(),\n )\n )\n\n assert identity_service.configured_identity == (12, \"user_1\", \"user1@mail.ru\")\n assert identity_service.pulled_dashboard_id == 12\n# [/DEF:test_pull_changes_applies_profile_identity_before_pull:Function]\n\n\n# [DEF:test_get_merge_status_returns_service_payload:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure merge status route returns service payload as-is.\n# @PRE: git_service.get_merge_status returns unfinished merge payload.\n# @POST: Route response contains has_unfinished_merge=True.\ndef test_get_merge_status_returns_service_payload(monkeypatch):\n class MergeStatusGitService:\n def get_merge_status(self, dashboard_id: int) -> dict:\n return {\n \"has_unfinished_merge\": True,\n \"repository_path\": \"/tmp/repo-12\",\n \"git_dir\": \"/tmp/repo-12/.git\",\n \"current_branch\": \"dev\",\n \"merge_head\": \"abc\",\n \"merge_message_preview\": \"merge msg\",\n \"conflicts_count\": 2,\n }\n\n monkeypatch.setattr(git_routes, \"git_service\", MergeStatusGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.get_merge_status(\n \"dashboard-12\",\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"has_unfinished_merge\"] is True\n assert response[\"conflicts_count\"] == 2\n# [/DEF:test_get_merge_status_returns_service_payload:Function]\n\n\n# [DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure merge resolve route forwards parsed resolutions to service.\n# @PRE: resolve_data has one file strategy.\n# @POST: Service receives normalized list and route returns resolved files.\ndef test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch):\n captured = {}\n\n class MergeResolveGitService:\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions):\n captured[\"dashboard_id\"] = dashboard_id\n captured[\"resolutions\"] = resolutions\n return [\"dashboards/a.yaml\"]\n\n class ResolveData:\n class _Resolution:\n def dict(self):\n return {\"file_path\": \"dashboards/a.yaml\", \"resolution\": \"mine\", \"content\": None}\n\n resolutions = [_Resolution()]\n\n monkeypatch.setattr(git_routes, \"git_service\", MergeResolveGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.resolve_merge_conflicts(\n \"dashboard-12\",\n ResolveData(),\n config_manager=MagicMock(),\n )\n )\n\n assert captured[\"dashboard_id\"] == 12\n assert captured[\"resolutions\"][0][\"resolution\"] == \"mine\"\n assert response[\"resolved_files\"] == [\"dashboards/a.yaml\"]\n# [/DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function]\n\n\n# [DEF:test_abort_merge_calls_service_and_returns_result:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure abort route delegates to service.\n# @PRE: Service abort_merge returns aborted status.\n# @POST: Route returns aborted status.\ndef test_abort_merge_calls_service_and_returns_result(monkeypatch):\n class AbortGitService:\n def abort_merge(self, dashboard_id: int):\n assert dashboard_id == 12\n return {\"status\": \"aborted\"}\n\n monkeypatch.setattr(git_routes, \"git_service\", AbortGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.abort_merge(\n \"dashboard-12\",\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"status\"] == \"aborted\"\n# [/DEF:test_abort_merge_calls_service_and_returns_result:Function]\n\n\n# [DEF:test_continue_merge_passes_message_and_returns_commit:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure continue route passes commit message to service.\n# @PRE: continue_data.message is provided.\n# @POST: Route returns committed status and hash.\ndef test_continue_merge_passes_message_and_returns_commit(monkeypatch):\n class ContinueGitService:\n def continue_merge(self, dashboard_id: int, message: str):\n assert dashboard_id == 12\n assert message == \"Resolve all conflicts\"\n return {\"status\": \"committed\", \"commit_hash\": \"abc123\"}\n\n class ContinueData:\n message = \"Resolve all conflicts\"\n\n monkeypatch.setattr(git_routes, \"git_service\", ContinueGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.continue_merge(\n \"dashboard-12\",\n ContinueData(),\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"status\"] == \"committed\"\n assert response[\"commit_hash\"] == \"abc123\"\n# [/DEF:test_continue_merge_passes_message_and_returns_commit:Function]\n\n\n# [/DEF:TestGitStatusRoute:Module]\n" + "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", + "body": "# #region test_create_remote_repository_creates_provider_repo [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate remote repository creation endpoint maps provider response into normalized payload.\ndef test_create_remote_repository_creates_provider_repo(monkeypatch):\n \"\"\"\n @PRE: config_id exists and PAT has creation permissions.\n @POST: Returns normalized remote repository payload.\n \"\"\"\n # #region MockGitService [TYPE Class]\n class MockGitService:\n async def create_gitlab_repository(\n self, server_url, pat, name, private, description, auto_init, default_branch\n ):\n return {\n \"name\": name,\n \"full_name\": f\"user/{name}\",\n \"private\": private,\n \"clone_url\": f\"{server_url}/user/{name}.git\",\n }\n # #endregion MockGitService\n monkeypatch.setattr(git_routes, \"git_service\", MockGitService())\n from src.api.routes.git_schemas import RemoteRepoCreateRequest\n existing_config = GitServerConfig(\n id=\"config-1\",\n name=\"GitLab Server\",\n provider=GitProvider.GITLAB,\n url=\"https://gitlab.com\",\n pat=\"token\",\n )\n db = DbMock([existing_config])\n request = RemoteRepoCreateRequest(name=\"new-repo\", private=True, description=\"desc\")\n result = asyncio.run(\n git_routes.create_remote_repository(\n config_id=\"config-1\", request=request, db=db\n )\n )\n assert result.provider == GitProvider.GITLAB\n assert result.name == \"new-repo\"\n assert result.full_name == \"user/new-repo\"\n# #endregion test_create_remote_repository_creates_provider_repo\n" + }, + { + "contract_id": "test_init_repository_initializes_and_saves_binding", + "contract_type": "Function", + "file_path": "backend/src/api/routes/__tests__/test_git_api.py", + "start_line": 366, + "end_line": 414, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Validate repository initialization endpoint creates local repo and persists dashboard binding." + }, + "relations": [ + { + "source_id": "test_init_repository_initializes_and_saves_binding", + "relation_type": "BINDS_TO", + "target_id": "TestGitApi", + "target_ref": "[TestGitApi]" + } + ], + "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", + "body": "# #region test_init_repository_initializes_and_saves_binding [TYPE Function]\n# @RELATION BINDS_TO -> [TestGitApi]\n# @BRIEF Validate repository initialization endpoint creates local repo and persists dashboard binding.\ndef test_init_repository_initializes_and_saves_binding(monkeypatch):\n \"\"\"\n @PRE: `dashboard_ref` exists and `init_data` contains valid config_id and remote_url.\n @POST: Repository is initialized on disk and a GitRepository record is saved in DB.\n \"\"\"\n from src.api.routes.git_schemas import RepoInitRequest\n # #region MockGitService [TYPE Class]\n class MockGitService:\n def init_repo(self, dashboard_id, remote_url, pat, repo_key, default_branch):\n self.init_called = True\n def _get_repo_path(self, dashboard_id, repo_key):\n return f\"/tmp/repos/{repo_key}\"\n # #endregion MockGitService\n git_service_mock = MockGitService()\n monkeypatch.setattr(git_routes, \"git_service\", git_service_mock)\n monkeypatch.setattr(\n git_routes, \"_resolve_dashboard_id_from_ref\", lambda *args, **kwargs: 123\n )\n monkeypatch.setattr(\n git_routes,\n \"_resolve_repo_key_from_ref\",\n lambda *args, **kwargs: \"dashboard-123\",\n )\n existing_config = GitServerConfig(\n id=\"config-1\",\n name=\"GitLab Server\",\n provider=GitProvider.GITLAB,\n url=\"https://gitlab.com\",\n pat=\"token\",\n default_branch=\"main\",\n )\n db = DbMock([existing_config])\n init_data = RepoInitRequest(\n config_id=\"config-1\", remote_url=\"https://git.local/repo.git\"\n )\n result = asyncio.run(\n git_routes.init_repository(\n dashboard_ref=\"123\", init_data=init_data, config_manager=MagicMock(), db=db\n )\n )\n assert result[\"status\"] == \"success\"\n assert git_service_mock.init_called is True\n assert len(db._added) == 1\n assert isinstance(db._added[0], GitRepository)\n assert db._added[0].dashboard_id == 123\n# #endregion test_init_repository_initializes_and_saves_binding\n" }, { "contract_id": "test_get_repository_status_returns_no_repo_payload_for_missing_repo", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 16, - "end_line": 37, + "start_line": 14, + "end_line": 31, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5755,15 +4931,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure missing local repository is represented as NO_REPO payload instead of an API error.\n# @PRE: GitService.get_status raises HTTPException(404).\n# @POST: Route returns a deterministic NO_REPO status payload.\ndef test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch):\n class MissingRepoGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/missing-repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise AssertionError(\"get_status must not be called when repository path is missing\")\n\n monkeypatch.setattr(git_routes, \"git_service\", MissingRepoGitService())\n\n response = asyncio.run(git_routes.get_repository_status(34))\n\n assert response[\"sync_status\"] == \"NO_REPO\"\n assert response[\"sync_state\"] == \"NO_REPO\"\n assert response[\"has_repo\"] is False\n assert response[\"current_branch\"] is None\n# [/DEF:test_get_repository_status_returns_no_repo_payload_for_missing_repo:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_repository_status_returns_no_repo_payload_for_missing_repo [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure missing local repository is represented as NO_REPO payload instead of an API error.\n# @PRE: GitService.get_status raises HTTPException(404).\n# @POST: Route returns a deterministic NO_REPO status payload.\ndef test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch):\n class MissingRepoGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/missing-repo-{dashboard_id}\"\n def get_status(self, dashboard_id: int) -> dict:\n raise AssertionError(\"get_status must not be called when repository path is missing\")\n monkeypatch.setattr(git_routes, \"git_service\", MissingRepoGitService())\n response = asyncio.run(git_routes.get_repository_status(34))\n assert response[\"sync_status\"] == \"NO_REPO\"\n assert response[\"sync_state\"] == \"NO_REPO\"\n assert response[\"has_repo\"] is False\n assert response[\"current_branch\"] is None\n# #endregion test_get_repository_status_returns_no_repo_payload_for_missing_repo\n" }, { "contract_id": "test_get_repository_status_propagates_non_404_http_exception", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 40, - "end_line": 61, + "start_line": 32, + "end_line": 49, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5808,15 +4984,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_repository_status_propagates_non_404_http_exception:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure HTTP exceptions other than 404 are not masked.\n# @PRE: GitService.get_status raises HTTPException with non-404 status.\n# @POST: Raised exception preserves original status and detail.\ndef test_get_repository_status_propagates_non_404_http_exception(monkeypatch):\n class ConflictGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/existing-repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise HTTPException(status_code=409, detail=\"Conflict\")\n\n monkeypatch.setattr(git_routes, \"git_service\", ConflictGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_repository_status(34))\n\n assert exc_info.value.status_code == 409\n assert exc_info.value.detail == \"Conflict\"\n# [/DEF:test_get_repository_status_propagates_non_404_http_exception:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_repository_status_propagates_non_404_http_exception [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure HTTP exceptions other than 404 are not masked.\n# @PRE: GitService.get_status raises HTTPException with non-404 status.\n# @POST: Raised exception preserves original status and detail.\ndef test_get_repository_status_propagates_non_404_http_exception(monkeypatch):\n class ConflictGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/existing-repo-{dashboard_id}\"\n def get_status(self, dashboard_id: int) -> dict:\n raise HTTPException(status_code=409, detail=\"Conflict\")\n monkeypatch.setattr(git_routes, \"git_service\", ConflictGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_repository_status(34))\n assert exc_info.value.status_code == 409\n assert exc_info.value.detail == \"Conflict\"\n# #endregion test_get_repository_status_propagates_non_404_http_exception\n" }, { "contract_id": "test_get_repository_diff_propagates_http_exception", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 64, - "end_line": 81, + "start_line": 50, + "end_line": 64, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5861,15 +5037,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_repository_diff_propagates_http_exception:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure diff endpoint preserves domain HTTP errors from GitService.\n# @PRE: GitService.get_diff raises HTTPException.\n# @POST: Endpoint raises same HTTPException values.\ndef test_get_repository_diff_propagates_http_exception(monkeypatch):\n class DiffGitService:\n def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:\n raise HTTPException(status_code=404, detail=\"Repository missing\")\n\n monkeypatch.setattr(git_routes, \"git_service\", DiffGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_repository_diff(12))\n\n assert exc_info.value.status_code == 404\n assert exc_info.value.detail == \"Repository missing\"\n# [/DEF:test_get_repository_diff_propagates_http_exception:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_repository_diff_propagates_http_exception [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure diff endpoint preserves domain HTTP errors from GitService.\n# @PRE: GitService.get_diff raises HTTPException.\n# @POST: Endpoint raises same HTTPException values.\ndef test_get_repository_diff_propagates_http_exception(monkeypatch):\n class DiffGitService:\n def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:\n raise HTTPException(status_code=404, detail=\"Repository missing\")\n monkeypatch.setattr(git_routes, \"git_service\", DiffGitService())\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_repository_diff(12))\n assert exc_info.value.status_code == 404\n assert exc_info.value.detail == \"Repository missing\"\n# #endregion test_get_repository_diff_propagates_http_exception\n" }, { "contract_id": "test_get_history_wraps_unexpected_error_as_500", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 84, - "end_line": 101, + "start_line": 65, + "end_line": 79, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5914,15 +5090,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_history_wraps_unexpected_error_as_500:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors.\n# @PRE: GitService.get_commit_history raises ValueError.\n# @POST: Endpoint returns HTTPException with status 500 and route context.\ndef test_get_history_wraps_unexpected_error_as_500(monkeypatch):\n class HistoryGitService:\n def get_commit_history(self, dashboard_id: int, limit: int = 50):\n raise ValueError(\"broken parser\")\n\n monkeypatch.setattr(git_routes, \"git_service\", HistoryGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_history(12))\n\n assert exc_info.value.status_code == 500\n assert exc_info.value.detail == \"get_history failed: broken parser\"\n# [/DEF:test_get_history_wraps_unexpected_error_as_500:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_history_wraps_unexpected_error_as_500 [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors.\n# @PRE: GitService.get_commit_history raises ValueError.\n# @POST: Endpoint returns HTTPException with status 500 and route context.\ndef test_get_history_wraps_unexpected_error_as_500(monkeypatch):\n class HistoryGitService:\n def get_commit_history(self, dashboard_id: int, limit: int = 50):\n raise ValueError(\"broken parser\")\n monkeypatch.setattr(git_routes, \"git_service\", HistoryGitService())\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.get_history(12))\n assert exc_info.value.status_code == 500\n assert exc_info.value.detail == \"get_history failed: broken parser\"\n# #endregion test_get_history_wraps_unexpected_error_as_500\n" }, { "contract_id": "test_commit_changes_wraps_unexpected_error_as_500", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 104, - "end_line": 125, + "start_line": 80, + "end_line": 97, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5967,15 +5143,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_commit_changes_wraps_unexpected_error_as_500:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure commit endpoint does not leak unexpected errors as 400.\n# @PRE: GitService.commit_changes raises RuntimeError.\n# @POST: Endpoint raises HTTPException(500) with route context.\ndef test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):\n class CommitGitService:\n def commit_changes(self, dashboard_id: int, message: str, files):\n raise RuntimeError(\"index lock\")\n\n class CommitPayload:\n message = \"test\"\n files = [\"dashboards/a.yaml\"]\n\n monkeypatch.setattr(git_routes, \"git_service\", CommitGitService())\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.commit_changes(12, CommitPayload()))\n\n assert exc_info.value.status_code == 500\n assert exc_info.value.detail == \"commit_changes failed: index lock\"\n# [/DEF:test_commit_changes_wraps_unexpected_error_as_500:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_commit_changes_wraps_unexpected_error_as_500 [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure commit endpoint does not leak unexpected errors as 400.\n# @PRE: GitService.commit_changes raises RuntimeError.\n# @POST: Endpoint raises HTTPException(500) with route context.\ndef test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):\n class CommitGitService:\n def commit_changes(self, dashboard_id: int, message: str, files):\n raise RuntimeError(\"index lock\")\n class CommitPayload:\n message = \"test\"\n files = [\"dashboards/a.yaml\"]\n monkeypatch.setattr(git_routes, \"git_service\", CommitGitService())\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(git_routes.commit_changes(12, CommitPayload()))\n assert exc_info.value.status_code == 500\n assert exc_info.value.detail == \"commit_changes failed: index lock\"\n# #endregion test_commit_changes_wraps_unexpected_error_as_500\n" }, { "contract_id": "test_get_repository_status_batch_returns_mixed_statuses", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 128, - "end_line": 153, + "start_line": 98, + "end_line": 118, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6020,15 +5196,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_repository_status_batch_returns_mixed_statuses:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint returns per-dashboard statuses in one response.\n# @PRE: Some repositories are missing and some are initialized.\n# @POST: Returned map includes resolved status for each requested dashboard ID.\ndef test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):\n class BatchGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n if dashboard_id == 2:\n return {\"sync_state\": \"SYNCED\", \"sync_status\": \"OK\"}\n raise HTTPException(status_code=404, detail=\"not found\")\n\n monkeypatch.setattr(git_routes, \"git_service\", BatchGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda path: path.endswith(\"/repo-2\"))\n\n class BatchRequest:\n dashboard_ids = [1, 2]\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert response.statuses[\"1\"][\"sync_status\"] == \"NO_REPO\"\n assert response.statuses[\"2\"][\"sync_state\"] == \"SYNCED\"\n# [/DEF:test_get_repository_status_batch_returns_mixed_statuses:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_repository_status_batch_returns_mixed_statuses [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure batch endpoint returns per-dashboard statuses in one response.\n# @PRE: Some repositories are missing and some are initialized.\n# @POST: Returned map includes resolved status for each requested dashboard ID.\ndef test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):\n class BatchGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n def get_status(self, dashboard_id: int) -> dict:\n if dashboard_id == 2:\n return {\"sync_state\": \"SYNCED\", \"sync_status\": \"OK\"}\n raise HTTPException(status_code=404, detail=\"not found\")\n monkeypatch.setattr(git_routes, \"git_service\", BatchGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda path: path.endswith(\"/repo-2\"))\n class BatchRequest:\n dashboard_ids = [1, 2]\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n assert response.statuses[\"1\"][\"sync_status\"] == \"NO_REPO\"\n assert response.statuses[\"2\"][\"sync_state\"] == \"SYNCED\"\n# #endregion test_get_repository_status_batch_returns_mixed_statuses\n" }, { "contract_id": "test_get_repository_status_batch_marks_item_as_error_on_service_failure", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 156, - "end_line": 179, + "start_line": 119, + "end_line": 137, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6073,15 +5249,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint marks failed items as ERROR without failing entire request.\n# @PRE: GitService raises non-HTTP exception for one dashboard.\n# @POST: Failed dashboard status is marked as ERROR.\ndef test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch):\n class BatchErrorGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n raise RuntimeError(\"boom\")\n\n monkeypatch.setattr(git_routes, \"git_service\", BatchErrorGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n class BatchRequest:\n dashboard_ids = [9]\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert response.statuses[\"9\"][\"sync_status\"] == \"ERROR\"\n assert response.statuses[\"9\"][\"sync_state\"] == \"ERROR\"\n# [/DEF:test_get_repository_status_batch_marks_item_as_error_on_service_failure:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_repository_status_batch_marks_item_as_error_on_service_failure [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure batch endpoint marks failed items as ERROR without failing entire request.\n# @PRE: GitService raises non-HTTP exception for one dashboard.\n# @POST: Failed dashboard status is marked as ERROR.\ndef test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch):\n class BatchErrorGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n def get_status(self, dashboard_id: int) -> dict:\n raise RuntimeError(\"boom\")\n monkeypatch.setattr(git_routes, \"git_service\", BatchErrorGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n class BatchRequest:\n dashboard_ids = [9]\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n assert response.statuses[\"9\"][\"sync_status\"] == \"ERROR\"\n assert response.statuses[\"9\"][\"sync_state\"] == \"ERROR\"\n# #endregion test_get_repository_status_batch_marks_item_as_error_on_service_failure\n" }, { "contract_id": "test_get_repository_status_batch_deduplicates_and_truncates_ids", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 182, - "end_line": 205, + "start_line": 138, + "end_line": 156, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6126,15 +5302,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure batch endpoint protects server from oversized payloads.\n# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.\n# @POST: Result contains unique IDs up to configured cap.\ndef test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch):\n class SafeBatchGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n\n def get_status(self, dashboard_id: int) -> dict:\n return {\"sync_state\": \"SYNCED\", \"sync_status\": \"OK\"}\n\n monkeypatch.setattr(git_routes, \"git_service\", SafeBatchGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n\n class BatchRequest:\n dashboard_ids = [1, 1] + list(range(2, 90))\n\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n\n assert len(response.statuses) == git_routes.MAX_REPOSITORY_STATUS_BATCH\n assert \"1\" in response.statuses\n# [/DEF:test_get_repository_status_batch_deduplicates_and_truncates_ids:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_repository_status_batch_deduplicates_and_truncates_ids [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure batch endpoint protects server from oversized payloads.\n# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.\n# @POST: Result contains unique IDs up to configured cap.\ndef test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch):\n class SafeBatchGitService:\n def _get_repo_path(self, dashboard_id: int) -> str:\n return f\"/tmp/repo-{dashboard_id}\"\n def get_status(self, dashboard_id: int) -> dict:\n return {\"sync_state\": \"SYNCED\", \"sync_status\": \"OK\"}\n monkeypatch.setattr(git_routes, \"git_service\", SafeBatchGitService())\n monkeypatch.setattr(git_routes.os.path, \"exists\", lambda _path: True)\n class BatchRequest:\n dashboard_ids = [1, 1] + list(range(2, 90))\n response = asyncio.run(git_routes.get_repository_status_batch(BatchRequest()))\n assert len(response.statuses) == git_routes.MAX_REPOSITORY_STATUS_BATCH\n assert \"1\" in response.statuses\n# #endregion test_get_repository_status_batch_deduplicates_and_truncates_ids\n" }, { "contract_id": "test_commit_changes_applies_profile_identity_before_commit", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 208, - "end_line": 267, + "start_line": 157, + "end_line": 205, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6179,15 +5355,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_commit_changes_applies_profile_identity_before_commit:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure commit route configures repository identity from profile preferences before commit call.\n# @PRE: Profile preference contains git_username/git_email for current user.\n# @POST: git_service.configure_identity receives resolved identity and commit proceeds.\ndef test_commit_changes_applies_profile_identity_before_commit(monkeypatch):\n class IdentityGitService:\n def __init__(self):\n self.configured_identity = None\n self.commit_payload = None\n\n def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):\n self.configured_identity = (dashboard_id, git_username, git_email)\n\n def commit_changes(self, dashboard_id: int, message: str, files):\n self.commit_payload = (dashboard_id, message, files)\n\n class PreferenceRow:\n git_username = \"user_1\"\n git_email = \"user1@mail.ru\"\n\n class PreferenceQuery:\n def filter(self, *_args, **_kwargs):\n return self\n\n def first(self):\n return PreferenceRow()\n\n class DbStub:\n def query(self, _model):\n return PreferenceQuery()\n\n class UserStub:\n id = \"u-1\"\n\n class CommitPayload:\n message = \"test\"\n files = [\"dashboards/a.yaml\"]\n\n identity_service = IdentityGitService()\n monkeypatch.setattr(git_routes, \"git_service\", identity_service)\n monkeypatch.setattr(\n git_routes,\n \"_resolve_dashboard_id_from_ref\",\n lambda *_args, **_kwargs: 12,\n )\n\n asyncio.run(\n git_routes.commit_changes(\n \"dashboard-12\",\n CommitPayload(),\n config_manager=MagicMock(),\n db=DbStub(),\n current_user=UserStub(),\n )\n )\n\n assert identity_service.configured_identity == (12, \"user_1\", \"user1@mail.ru\")\n assert identity_service.commit_payload == (12, \"test\", [\"dashboards/a.yaml\"])\n# [/DEF:test_commit_changes_applies_profile_identity_before_commit:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_commit_changes_applies_profile_identity_before_commit [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure commit route configures repository identity from profile preferences before commit call.\n# @PRE: Profile preference contains git_username/git_email for current user.\n# @POST: git_service.configure_identity receives resolved identity and commit proceeds.\ndef test_commit_changes_applies_profile_identity_before_commit(monkeypatch):\n class IdentityGitService:\n def __init__(self):\n self.configured_identity = None\n self.commit_payload = None\n def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):\n self.configured_identity = (dashboard_id, git_username, git_email)\n def commit_changes(self, dashboard_id: int, message: str, files):\n self.commit_payload = (dashboard_id, message, files)\n class PreferenceRow:\n git_username = \"user_1\"\n git_email = \"user1@mail.ru\"\n class PreferenceQuery:\n def filter(self, *_args, **_kwargs):\n return self\n def first(self):\n return PreferenceRow()\n class DbStub:\n def query(self, _model):\n return PreferenceQuery()\n class UserStub:\n id = \"u-1\"\n class CommitPayload:\n message = \"test\"\n files = [\"dashboards/a.yaml\"]\n identity_service = IdentityGitService()\n monkeypatch.setattr(git_routes, \"git_service\", identity_service)\n monkeypatch.setattr(\n git_routes,\n \"_resolve_dashboard_id_from_ref\",\n lambda *_args, **_kwargs: 12,\n )\n asyncio.run(\n git_routes.commit_changes(\n \"dashboard-12\",\n CommitPayload(),\n config_manager=MagicMock(),\n db=DbStub(),\n current_user=UserStub(),\n )\n )\n assert identity_service.configured_identity == (12, \"user_1\", \"user1@mail.ru\")\n assert identity_service.commit_payload == (12, \"test\", [\"dashboards/a.yaml\"])\n# #endregion test_commit_changes_applies_profile_identity_before_commit\n" }, { "contract_id": "test_pull_changes_applies_profile_identity_before_pull", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 270, - "end_line": 324, + "start_line": 206, + "end_line": 250, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6232,15 +5408,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_pull_changes_applies_profile_identity_before_pull:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure pull route configures repository identity from profile preferences before pull call.\n# @PRE: Profile preference contains git_username/git_email for current user.\n# @POST: git_service.configure_identity receives resolved identity and pull proceeds.\ndef test_pull_changes_applies_profile_identity_before_pull(monkeypatch):\n class IdentityGitService:\n def __init__(self):\n self.configured_identity = None\n self.pulled_dashboard_id = None\n\n def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):\n self.configured_identity = (dashboard_id, git_username, git_email)\n\n def pull_changes(self, dashboard_id: int):\n self.pulled_dashboard_id = dashboard_id\n\n class PreferenceRow:\n git_username = \"user_1\"\n git_email = \"user1@mail.ru\"\n\n class PreferenceQuery:\n def filter(self, *_args, **_kwargs):\n return self\n\n def first(self):\n return PreferenceRow()\n\n class DbStub:\n def query(self, _model):\n return PreferenceQuery()\n\n class UserStub:\n id = \"u-1\"\n\n identity_service = IdentityGitService()\n monkeypatch.setattr(git_routes, \"git_service\", identity_service)\n monkeypatch.setattr(\n git_routes,\n \"_resolve_dashboard_id_from_ref\",\n lambda *_args, **_kwargs: 12,\n )\n\n asyncio.run(\n git_routes.pull_changes(\n \"dashboard-12\",\n config_manager=MagicMock(),\n db=DbStub(),\n current_user=UserStub(),\n )\n )\n\n assert identity_service.configured_identity == (12, \"user_1\", \"user1@mail.ru\")\n assert identity_service.pulled_dashboard_id == 12\n# [/DEF:test_pull_changes_applies_profile_identity_before_pull:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_pull_changes_applies_profile_identity_before_pull [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure pull route configures repository identity from profile preferences before pull call.\n# @PRE: Profile preference contains git_username/git_email for current user.\n# @POST: git_service.configure_identity receives resolved identity and pull proceeds.\ndef test_pull_changes_applies_profile_identity_before_pull(monkeypatch):\n class IdentityGitService:\n def __init__(self):\n self.configured_identity = None\n self.pulled_dashboard_id = None\n def configure_identity(self, dashboard_id: int, git_username: str, git_email: str):\n self.configured_identity = (dashboard_id, git_username, git_email)\n def pull_changes(self, dashboard_id: int):\n self.pulled_dashboard_id = dashboard_id\n class PreferenceRow:\n git_username = \"user_1\"\n git_email = \"user1@mail.ru\"\n class PreferenceQuery:\n def filter(self, *_args, **_kwargs):\n return self\n def first(self):\n return PreferenceRow()\n class DbStub:\n def query(self, _model):\n return PreferenceQuery()\n class UserStub:\n id = \"u-1\"\n identity_service = IdentityGitService()\n monkeypatch.setattr(git_routes, \"git_service\", identity_service)\n monkeypatch.setattr(\n git_routes,\n \"_resolve_dashboard_id_from_ref\",\n lambda *_args, **_kwargs: 12,\n )\n asyncio.run(\n git_routes.pull_changes(\n \"dashboard-12\",\n config_manager=MagicMock(),\n db=DbStub(),\n current_user=UserStub(),\n )\n )\n assert identity_service.configured_identity == (12, \"user_1\", \"user1@mail.ru\")\n assert identity_service.pulled_dashboard_id == 12\n# #endregion test_pull_changes_applies_profile_identity_before_pull\n" }, { "contract_id": "test_get_merge_status_returns_service_payload", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 327, - "end_line": 357, + "start_line": 251, + "end_line": 278, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6285,15 +5461,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_merge_status_returns_service_payload:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure merge status route returns service payload as-is.\n# @PRE: git_service.get_merge_status returns unfinished merge payload.\n# @POST: Route response contains has_unfinished_merge=True.\ndef test_get_merge_status_returns_service_payload(monkeypatch):\n class MergeStatusGitService:\n def get_merge_status(self, dashboard_id: int) -> dict:\n return {\n \"has_unfinished_merge\": True,\n \"repository_path\": \"/tmp/repo-12\",\n \"git_dir\": \"/tmp/repo-12/.git\",\n \"current_branch\": \"dev\",\n \"merge_head\": \"abc\",\n \"merge_message_preview\": \"merge msg\",\n \"conflicts_count\": 2,\n }\n\n monkeypatch.setattr(git_routes, \"git_service\", MergeStatusGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.get_merge_status(\n \"dashboard-12\",\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"has_unfinished_merge\"] is True\n assert response[\"conflicts_count\"] == 2\n# [/DEF:test_get_merge_status_returns_service_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_merge_status_returns_service_payload [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure merge status route returns service payload as-is.\n# @PRE: git_service.get_merge_status returns unfinished merge payload.\n# @POST: Route response contains has_unfinished_merge=True.\ndef test_get_merge_status_returns_service_payload(monkeypatch):\n class MergeStatusGitService:\n def get_merge_status(self, dashboard_id: int) -> dict:\n return {\n \"has_unfinished_merge\": True,\n \"repository_path\": \"/tmp/repo-12\",\n \"git_dir\": \"/tmp/repo-12/.git\",\n \"current_branch\": \"dev\",\n \"merge_head\": \"abc\",\n \"merge_message_preview\": \"merge msg\",\n \"conflicts_count\": 2,\n }\n monkeypatch.setattr(git_routes, \"git_service\", MergeStatusGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n response = asyncio.run(\n git_routes.get_merge_status(\n \"dashboard-12\",\n config_manager=MagicMock(),\n )\n )\n assert response[\"has_unfinished_merge\"] is True\n assert response[\"conflicts_count\"] == 2\n# #endregion test_get_merge_status_returns_service_payload\n" }, { "contract_id": "test_resolve_merge_conflicts_passes_resolution_items_to_service", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 360, - "end_line": 395, + "start_line": 279, + "end_line": 308, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6338,15 +5514,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure merge resolve route forwards parsed resolutions to service.\n# @PRE: resolve_data has one file strategy.\n# @POST: Service receives normalized list and route returns resolved files.\ndef test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch):\n captured = {}\n\n class MergeResolveGitService:\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions):\n captured[\"dashboard_id\"] = dashboard_id\n captured[\"resolutions\"] = resolutions\n return [\"dashboards/a.yaml\"]\n\n class ResolveData:\n class _Resolution:\n def dict(self):\n return {\"file_path\": \"dashboards/a.yaml\", \"resolution\": \"mine\", \"content\": None}\n\n resolutions = [_Resolution()]\n\n monkeypatch.setattr(git_routes, \"git_service\", MergeResolveGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.resolve_merge_conflicts(\n \"dashboard-12\",\n ResolveData(),\n config_manager=MagicMock(),\n )\n )\n\n assert captured[\"dashboard_id\"] == 12\n assert captured[\"resolutions\"][0][\"resolution\"] == \"mine\"\n assert response[\"resolved_files\"] == [\"dashboards/a.yaml\"]\n# [/DEF:test_resolve_merge_conflicts_passes_resolution_items_to_service:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_resolve_merge_conflicts_passes_resolution_items_to_service [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure merge resolve route forwards parsed resolutions to service.\n# @PRE: resolve_data has one file strategy.\n# @POST: Service receives normalized list and route returns resolved files.\ndef test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch):\n captured = {}\n class MergeResolveGitService:\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions):\n captured[\"dashboard_id\"] = dashboard_id\n captured[\"resolutions\"] = resolutions\n return [\"dashboards/a.yaml\"]\n class ResolveData:\n class _Resolution:\n def dict(self):\n return {\"file_path\": \"dashboards/a.yaml\", \"resolution\": \"mine\", \"content\": None}\n resolutions = [_Resolution()]\n monkeypatch.setattr(git_routes, \"git_service\", MergeResolveGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n response = asyncio.run(\n git_routes.resolve_merge_conflicts(\n \"dashboard-12\",\n ResolveData(),\n config_manager=MagicMock(),\n )\n )\n assert captured[\"dashboard_id\"] == 12\n assert captured[\"resolutions\"][0][\"resolution\"] == \"mine\"\n assert response[\"resolved_files\"] == [\"dashboards/a.yaml\"]\n# #endregion test_resolve_merge_conflicts_passes_resolution_items_to_service\n" }, { "contract_id": "test_abort_merge_calls_service_and_returns_result", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 398, - "end_line": 420, + "start_line": 309, + "end_line": 328, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6391,15 +5567,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_abort_merge_calls_service_and_returns_result:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure abort route delegates to service.\n# @PRE: Service abort_merge returns aborted status.\n# @POST: Route returns aborted status.\ndef test_abort_merge_calls_service_and_returns_result(monkeypatch):\n class AbortGitService:\n def abort_merge(self, dashboard_id: int):\n assert dashboard_id == 12\n return {\"status\": \"aborted\"}\n\n monkeypatch.setattr(git_routes, \"git_service\", AbortGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.abort_merge(\n \"dashboard-12\",\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"status\"] == \"aborted\"\n# [/DEF:test_abort_merge_calls_service_and_returns_result:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_abort_merge_calls_service_and_returns_result [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure abort route delegates to service.\n# @PRE: Service abort_merge returns aborted status.\n# @POST: Route returns aborted status.\ndef test_abort_merge_calls_service_and_returns_result(monkeypatch):\n class AbortGitService:\n def abort_merge(self, dashboard_id: int):\n assert dashboard_id == 12\n return {\"status\": \"aborted\"}\n monkeypatch.setattr(git_routes, \"git_service\", AbortGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n response = asyncio.run(\n git_routes.abort_merge(\n \"dashboard-12\",\n config_manager=MagicMock(),\n )\n )\n assert response[\"status\"] == \"aborted\"\n# #endregion test_abort_merge_calls_service_and_returns_result\n" }, { "contract_id": "test_continue_merge_passes_message_and_returns_commit", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_git_status_route.py", - "start_line": 423, - "end_line": 451, + "start_line": 329, + "end_line": 353, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6444,40 +5620,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_continue_merge_passes_message_and_returns_commit:Function]\n# @RELATION: BINDS_TO -> TestGitStatusRoute\n# @PURPOSE: Ensure continue route passes commit message to service.\n# @PRE: continue_data.message is provided.\n# @POST: Route returns committed status and hash.\ndef test_continue_merge_passes_message_and_returns_commit(monkeypatch):\n class ContinueGitService:\n def continue_merge(self, dashboard_id: int, message: str):\n assert dashboard_id == 12\n assert message == \"Resolve all conflicts\"\n return {\"status\": \"committed\", \"commit_hash\": \"abc123\"}\n\n class ContinueData:\n message = \"Resolve all conflicts\"\n\n monkeypatch.setattr(git_routes, \"git_service\", ContinueGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n\n response = asyncio.run(\n git_routes.continue_merge(\n \"dashboard-12\",\n ContinueData(),\n config_manager=MagicMock(),\n )\n )\n\n assert response[\"status\"] == \"committed\"\n assert response[\"commit_hash\"] == \"abc123\"\n# [/DEF:test_continue_merge_passes_message_and_returns_commit:Function]\n" - }, - { - "contract_id": "TestMigrationRoutes", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_migration_routes.py", - "start_line": 1, - "end_line": 656, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "API", - "PURPOSE": "Unit tests for migration API route handlers." - }, - "relations": [ - { - "source_id": "TestMigrationRoutes", - "relation_type": "VERIFIES", - "target_id": "backend.src.api.routes.migration", - "target_ref": "backend.src.api.routes.migration" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TestMigrationRoutes:Module]\n#\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for migration API route handlers.\n# @LAYER: API\n# @RELATION: VERIFIES -> backend.src.api.routes.migration\n#\nimport pytest\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, AsyncMock, patch\nfrom datetime import datetime, timezone\n\n# Add backend directory to sys.path\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nimport os\n\n# Force SQLite in-memory for all database connections BEFORE importing any application code\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"TASKS_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"AUTH_DATABASE_URL\"] = \"sqlite:///:memory:\"\n# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.\nos.environ[\"ENVIRONMENT\"] = \"testing\"\n\n\nfrom fastapi import HTTPException\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.models.mapping import Base, ResourceMapping, ResourceType\n\n# Patch the get_db dependency if `src.api.routes.migration` imports it\nfrom unittest.mock import patch\n\npatch(\"src.core.database.get_db\").start()\n\n# --- Fixtures ---\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"In-memory SQLite session for testing.\"\"\"\n from sqlalchemy.pool import StaticPool\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n connect_args={\"check_same_thread\": False},\n poolclass=StaticPool,\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n\n\n# [DEF:_make_config_manager:Function]\n# @RELATION: BINDS_TO -> TestMigrationRoutes\ndef _make_config_manager(cron=\"0 2 * * *\"):\n \"\"\"Creates a mock config manager with a realistic AppConfig-like object.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = cron\n config = MagicMock()\n config.settings = settings\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.save_config = MagicMock()\n return cm\n\n\n# --- get_migration_settings tests ---\n\n# [/DEF:_make_config_manager:Function]\n\n\n@pytest.mark.asyncio\nasync def test_get_migration_settings_returns_default_cron():\n \"\"\"Verify the settings endpoint returns the stored cron string.\"\"\"\n from src.api.routes.migration import get_migration_settings\n\n cm = _make_config_manager(cron=\"0 3 * * *\")\n\n # Call the handler directly, bypassing Depends\n result = await get_migration_settings(config_manager=cm, _=None)\n\n assert result == {\"cron\": \"0 3 * * *\"}\n cm.get_config.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_get_migration_settings_returns_fallback_when_no_cron():\n \"\"\"When migration_sync_cron uses the default, should return '0 2 * * *'.\"\"\"\n from src.api.routes.migration import get_migration_settings\n\n # Use the default cron value (simulating a fresh config)\n cm = _make_config_manager()\n\n result = await get_migration_settings(config_manager=cm, _=None)\n\n assert result == {\"cron\": \"0 2 * * *\"}\n\n\n# --- update_migration_settings tests ---\n\n\n@pytest.mark.asyncio\nasync def test_update_migration_settings_saves_cron():\n \"\"\"Verify that a valid cron update saves to config.\"\"\"\n from src.api.routes.migration import update_migration_settings\n\n cm = _make_config_manager()\n\n result = await update_migration_settings(\n payload={\"cron\": \"0 4 * * *\"}, config_manager=cm, _=None\n )\n\n assert result[\"cron\"] == \"0 4 * * *\"\n assert result[\"status\"] == \"updated\"\n cm.save_config.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_update_migration_settings_rejects_missing_cron():\n \"\"\"Verify 400 error when 'cron' key is missing from payload.\"\"\"\n from src.api.routes.migration import update_migration_settings\n\n cm = _make_config_manager()\n\n with pytest.raises(HTTPException) as exc_info:\n await update_migration_settings(\n payload={\"interval\": \"daily\"}, config_manager=cm, _=None\n )\n\n assert exc_info.value.status_code == 400\n assert \"cron\" in exc_info.value.detail.lower()\n\n\n# --- get_resource_mappings tests ---\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_returns_formatted_list(db_session):\n \"\"\"Verify mappings are returned as formatted dicts with correct keys.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n # Populate test data\n m1 = ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"uuid-1\",\n remote_integer_id=\"42\",\n resource_name=\"Sales Chart\",\n last_synced_at=datetime(2026, 1, 15, 12, 0, 0, tzinfo=timezone.utc),\n )\n db_session.add(m1)\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n\n assert result[\"total\"] == 1\n assert len(result[\"items\"]) == 1\n assert result[\"items\"][0][\"environment_id\"] == \"prod\"\n assert result[\"items\"][0][\"resource_type\"] == \"chart\"\n assert result[\"items\"][0][\"uuid\"] == \"uuid-1\"\n assert result[\"items\"][0][\"remote_id\"] == \"42\"\n assert result[\"items\"][0][\"resource_name\"] == \"Sales Chart\"\n assert result[\"items\"][0][\"last_synced_at\"] is not None\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_respects_pagination(db_session):\n \"\"\"Verify skip and limit parameters work correctly.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n for i in range(5):\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.DATASET,\n uuid=f\"uuid-{i}\",\n remote_integer_id=str(i),\n )\n )\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=2,\n limit=2,\n search=None,\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n\n assert result[\"total\"] == 5\n assert len(result[\"items\"]) == 2\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_search_by_name(db_session):\n \"\"\"Verify search filters by resource_name.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"Sales Chart\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"Revenue Dashboard\",\n )\n )\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=\"sales\",\n env_id=None,\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"resource_name\"] == \"Sales Chart\"\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_filter_by_env(db_session):\n \"\"\"Verify env_id filter returns only matching environment.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n db_session.add(\n ResourceMapping(\n environment_id=\"ss1\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"Chart A\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"ss2\",\n resource_type=ResourceType.CHART,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"Chart B\",\n )\n )\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=\"ss2\",\n resource_type=None,\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"environment_id\"] == \"ss2\"\n\n\n@pytest.mark.asyncio\nasync def test_get_resource_mappings_filter_by_type(db_session):\n \"\"\"Verify resource_type filter returns only matching type.\"\"\"\n from src.api.routes.migration import get_resource_mappings\n\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.CHART,\n uuid=\"u1\",\n remote_integer_id=\"1\",\n resource_name=\"My Chart\",\n )\n )\n db_session.add(\n ResourceMapping(\n environment_id=\"prod\",\n resource_type=ResourceType.DATASET,\n uuid=\"u2\",\n remote_integer_id=\"2\",\n resource_name=\"My Dataset\",\n )\n )\n db_session.commit()\n\n result = await get_resource_mappings(\n skip=0,\n limit=50,\n search=None,\n env_id=None,\n resource_type=\"dataset\",\n db=db_session,\n _=None,\n )\n assert result[\"total\"] == 1\n assert result[\"items\"][0][\"resource_type\"] == \"dataset\"\n\n\n# --- trigger_sync_now tests ---\n\n\n@pytest.fixture\n# [DEF:_mock_env:Function]\n# @RELATION: BINDS_TO -> TestMigrationRoutes\ndef _mock_env():\n \"\"\"Creates a mock config environment object.\"\"\"\n env = MagicMock()\n env.id = \"test-env-1\"\n env.name = \"Test Env\"\n env.url = \"http://superset.test\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n return env\n\n\n# [/DEF:_mock_env:Function]\n\n\n# [DEF:_make_sync_config_manager:Function]\n# @RELATION: BINDS_TO -> TestMigrationRoutes\ndef _make_sync_config_manager(environments):\n \"\"\"Creates a mock config manager with environments list.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = \"0 2 * * *\"\n config = MagicMock()\n config.settings = settings\n config.environments = environments\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.get_environments.return_value = environments\n return cm\n\n\n# [/DEF:_make_sync_config_manager:Function]\n\n\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_creates_env_row_and_syncs(db_session, _mock_env):\n \"\"\"Verify that trigger_sync_now creates an Environment row in DB before syncing,\n preventing FK constraint violations on resource_mappings inserts.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n from src.models.mapping import Environment as EnvironmentModel\n\n cm = _make_sync_config_manager([_mock_env])\n\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.IdMappingService\") as MockService,\n ):\n mock_client_instance = MagicMock()\n MockClient.return_value = mock_client_instance\n mock_service_instance = MagicMock()\n MockService.return_value = mock_service_instance\n\n result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n\n # Environment row must exist in DB\n env_row = db_session.query(EnvironmentModel).filter_by(id=\"test-env-1\").first()\n assert env_row is not None\n assert env_row.name == \"Test Env\"\n assert env_row.url == \"http://superset.test\"\n\n # Sync must have been called\n mock_service_instance.sync_environment.assert_called_once_with(\n \"test-env-1\", mock_client_instance\n )\n assert result[\"synced_count\"] == 1\n assert result[\"failed_count\"] == 0\n\n\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_rejects_empty_environments(db_session):\n \"\"\"Verify 400 error when no environments are configured.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n\n cm = _make_sync_config_manager([])\n\n with pytest.raises(HTTPException) as exc_info:\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n\n assert exc_info.value.status_code == 400\n assert \"No environments\" in exc_info.value.detail\n\n\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_handles_partial_failure(db_session, _mock_env):\n \"\"\"Verify that if sync_environment raises for one env, it's captured in failed list.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n\n env2 = MagicMock()\n env2.id = \"test-env-2\"\n env2.name = \"Failing Env\"\n env2.url = \"http://fail.test\"\n env2.username = \"admin\"\n env2.password = \"admin\"\n env2.verify_ssl = False\n env2.timeout = 30\n\n cm = _make_sync_config_manager([_mock_env, env2])\n\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.IdMappingService\") as MockService,\n ):\n mock_service_instance = MagicMock()\n mock_service_instance.sync_environment.side_effect = [\n None,\n RuntimeError(\"Connection refused\"),\n ]\n MockService.return_value = mock_service_instance\n MockClient.return_value = MagicMock()\n\n result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n\n assert result[\"synced_count\"] == 1\n assert result[\"failed_count\"] == 1\n assert result[\"details\"][\"failed\"][0][\"env_id\"] == \"test-env-2\"\n\n\n@pytest.mark.asyncio\nasync def test_trigger_sync_now_idempotent_env_upsert(db_session, _mock_env):\n \"\"\"Verify that calling sync twice doesn't duplicate the Environment row.\"\"\"\n from src.api.routes.migration import trigger_sync_now\n from src.models.mapping import Environment as EnvironmentModel\n\n cm = _make_sync_config_manager([_mock_env])\n\n with (\n patch(\"src.api.routes.migration.SupersetClient\"),\n patch(\"src.api.routes.migration.IdMappingService\"),\n ):\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n await trigger_sync_now(config_manager=cm, db=db_session, _=None)\n\n env_count = db_session.query(EnvironmentModel).filter_by(id=\"test-env-1\").count()\n assert env_count == 1\n\n\n# --- get_dashboards tests ---\n\n\n@pytest.mark.asyncio\nasync def test_get_dashboards_success(_mock_env):\n from src.api.routes.migration import get_dashboards\n\n cm = _make_sync_config_manager([_mock_env])\n\n with patch(\"src.api.routes.migration.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n mock_client.get_dashboards_summary.return_value = [{\"id\": 1, \"title\": \"Test\"}]\n MockClient.return_value = mock_client\n\n result = await get_dashboards(env_id=\"test-env-1\", config_manager=cm, _=None)\n assert len(result) == 1\n assert result[0][\"id\"] == 1\n\n\n@pytest.mark.asyncio\nasync def test_get_dashboards_invalid_env_raises_404(_mock_env):\n from src.api.routes.migration import get_dashboards\n\n cm = _make_sync_config_manager([_mock_env])\n\n with pytest.raises(HTTPException) as exc:\n await get_dashboards(env_id=\"wrong-env\", config_manager=cm, _=None)\n assert exc.value.status_code == 404\n\n\n# --- execute_migration tests ---\n\n\n@pytest.mark.asyncio\nasync def test_execute_migration_success(_mock_env):\n from src.api.routes.migration import execute_migration\n from src.models.dashboard import DashboardSelection\n\n cm = _make_sync_config_manager([_mock_env, _mock_env]) # Need both source/target\n tm = MagicMock()\n tm.create_task = AsyncMock(return_value=MagicMock(id=\"task-123\"))\n\n selection = DashboardSelection(\n source_env_id=\"test-env-1\", target_env_id=\"test-env-1\", selected_ids=[1, 2]\n )\n\n result = await execute_migration(\n selection=selection, config_manager=cm, task_manager=tm, _=None\n )\n assert result[\"task_id\"] == \"task-123\"\n tm.create_task.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_execute_migration_invalid_env_raises_400(_mock_env):\n from src.api.routes.migration import execute_migration\n from src.models.dashboard import DashboardSelection\n\n cm = _make_sync_config_manager([_mock_env])\n selection = DashboardSelection(\n source_env_id=\"test-env-1\", target_env_id=\"non-existent\", selected_ids=[1]\n )\n\n with pytest.raises(HTTPException) as exc:\n await execute_migration(\n selection=selection, config_manager=cm, task_manager=MagicMock(), _=None\n )\n assert exc.value.status_code == 400\n\n\n@pytest.mark.asyncio\nasync def test_dry_run_migration_returns_diff_and_risk(db_session):\n # @TEST_EDGE: missing_target_datasource -> validates high risk item generation\n # @TEST_EDGE: breaking_reference -> validates high risk on missing dataset link\n from src.api.routes.migration import dry_run_migration\n from src.models.dashboard import DashboardSelection\n\n env_source = MagicMock()\n env_source.id = \"src\"\n env_source.name = \"Source\"\n env_source.url = \"http://source\"\n env_source.username = \"admin\"\n env_source.password = \"admin\"\n env_source.verify_ssl = False\n env_source.timeout = 30\n\n env_target = MagicMock()\n env_target.id = \"tgt\"\n env_target.name = \"Target\"\n env_target.url = \"http://target\"\n env_target.username = \"admin\"\n env_target.password = \"admin\"\n env_target.verify_ssl = False\n env_target.timeout = 30\n\n cm = _make_sync_config_manager([env_source, env_target])\n selection = DashboardSelection(\n selected_ids=[42],\n source_env_id=\"src\",\n target_env_id=\"tgt\",\n replace_db_config=False,\n fix_cross_filters=True,\n )\n\n with (\n patch(\"src.api.routes.migration.SupersetClient\") as MockClient,\n patch(\"src.api.routes.migration.MigrationDryRunService\") as MockService,\n ):\n source_client = MagicMock()\n target_client = MagicMock()\n MockClient.side_effect = [source_client, target_client]\n\n service_instance = MagicMock()\n service_payload = {\n \"generated_at\": \"2026-02-27T00:00:00+00:00\",\n \"selection\": selection.model_dump(),\n \"selected_dashboard_titles\": [\"Sales\"],\n \"diff\": {\n \"dashboards\": {\n \"create\": [],\n \"update\": [{\"uuid\": \"dash-1\"}],\n \"delete\": [],\n },\n \"charts\": {\"create\": [{\"uuid\": \"chart-1\"}], \"update\": [], \"delete\": []},\n \"datasets\": {\n \"create\": [{\"uuid\": \"dataset-1\"}],\n \"update\": [],\n \"delete\": [],\n },\n },\n \"summary\": {\n \"dashboards\": {\"create\": 0, \"update\": 1, \"delete\": 0},\n \"charts\": {\"create\": 1, \"update\": 0, \"delete\": 0},\n \"datasets\": {\"create\": 1, \"update\": 0, \"delete\": 0},\n \"selected_dashboards\": 1,\n },\n \"risk\": {\n \"score\": 75,\n \"level\": \"high\",\n \"items\": [\n {\"code\": \"missing_datasource\"},\n {\"code\": \"breaking_reference\"},\n ],\n },\n }\n service_instance.run.return_value = service_payload\n MockService.return_value = service_instance\n\n result = await dry_run_migration(\n selection=selection, config_manager=cm, db=db_session, _=None\n )\n\n assert result[\"summary\"][\"dashboards\"][\"update\"] == 1\n assert result[\"summary\"][\"charts\"][\"create\"] == 1\n assert result[\"summary\"][\"datasets\"][\"create\"] == 1\n assert result[\"risk\"][\"score\"] > 0\n assert any(item[\"code\"] == \"missing_datasource\" for item in result[\"risk\"][\"items\"])\n assert any(item[\"code\"] == \"breaking_reference\" for item in result[\"risk\"][\"items\"])\n\n\n@pytest.mark.asyncio\nasync def test_dry_run_migration_rejects_same_environment(db_session):\n from src.api.routes.migration import dry_run_migration\n from src.models.dashboard import DashboardSelection\n\n env = MagicMock()\n env.id = \"same\"\n env.name = \"Same\"\n env.url = \"http://same\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n\n cm = _make_sync_config_manager([env])\n selection = DashboardSelection(\n selected_ids=[1], source_env_id=\"same\", target_env_id=\"same\"\n )\n\n with pytest.raises(HTTPException) as exc:\n await dry_run_migration(\n selection=selection, config_manager=cm, db=db_session, _=None\n )\n assert exc.value.status_code == 400\n\n\n# [/DEF:TestMigrationRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_continue_merge_passes_message_and_returns_commit [TYPE Function]\n# @RELATION BINDS_TO -> TestGitStatusRoute\n# @BRIEF Ensure continue route passes commit message to service.\n# @PRE: continue_data.message is provided.\n# @POST: Route returns committed status and hash.\ndef test_continue_merge_passes_message_and_returns_commit(monkeypatch):\n class ContinueGitService:\n def continue_merge(self, dashboard_id: int, message: str):\n assert dashboard_id == 12\n assert message == \"Resolve all conflicts\"\n return {\"status\": \"committed\", \"commit_hash\": \"abc123\"}\n class ContinueData:\n message = \"Resolve all conflicts\"\n monkeypatch.setattr(git_routes, \"git_service\", ContinueGitService())\n monkeypatch.setattr(git_routes, \"_resolve_dashboard_id_from_ref\", lambda *_args, **_kwargs: 12)\n response = asyncio.run(\n git_routes.continue_merge(\n \"dashboard-12\",\n ContinueData(),\n config_manager=MagicMock(),\n )\n )\n assert response[\"status\"] == \"committed\"\n assert response[\"commit_hash\"] == \"abc123\"\n# #endregion test_continue_merge_passes_message_and_returns_commit\n" }, { "contract_id": "_mock_env", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_migration_routes.py", - "start_line": 332, - "end_line": 347, + "start_line": 268, + "end_line": 281, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -6500,15 +5651,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_mock_env:Function]\n# @RELATION: BINDS_TO -> TestMigrationRoutes\ndef _mock_env():\n \"\"\"Creates a mock config environment object.\"\"\"\n env = MagicMock()\n env.id = \"test-env-1\"\n env.name = \"Test Env\"\n env.url = \"http://superset.test\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n return env\n\n\n# [/DEF:_mock_env:Function]\n" + "anchor_syntax": "region", + "body": "# #region _mock_env [TYPE Function]\n# @RELATION BINDS_TO -> TestMigrationRoutes\ndef _mock_env():\n \"\"\"Creates a mock config environment object.\"\"\"\n env = MagicMock()\n env.id = \"test-env-1\"\n env.name = \"Test Env\"\n env.url = \"http://superset.test\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = False\n env.timeout = 30\n return env\n# #endregion _mock_env\n" }, { "contract_id": "_make_sync_config_manager", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_migration_routes.py", - "start_line": 350, - "end_line": 365, + "start_line": 282, + "end_line": 295, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -6531,67 +5682,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_sync_config_manager:Function]\n# @RELATION: BINDS_TO -> TestMigrationRoutes\ndef _make_sync_config_manager(environments):\n \"\"\"Creates a mock config manager with environments list.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = \"0 2 * * *\"\n config = MagicMock()\n config.settings = settings\n config.environments = environments\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.get_environments.return_value = environments\n return cm\n\n\n# [/DEF:_make_sync_config_manager:Function]\n" - }, - { - "contract_id": "TestProfileApi", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 1, - "end_line": 307, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "API", - "PURPOSE": "Verifies profile API route contracts for preference read/update and Superset account lookup.", - "SEMANTICS": [ - "tests", - "profile", - "api", - "preferences", - "lookup", - "contract" - ] - }, - "relations": [ - { - "source_id": "TestProfileApi", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestProfileApi:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, profile, api, preferences, lookup, contract\n# @PURPOSE: Verifies profile API route contracts for preference read/update and Superset account lookup.\n# @LAYER: API\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime, timezone\nfrom unittest.mock import MagicMock, patch\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.database import get_db\nfrom src.dependencies import get_config_manager, get_current_user\nfrom src.schemas.profile import (\n ProfilePermissionState,\n ProfilePreference,\n ProfilePreferenceResponse,\n ProfileSecuritySummary,\n SupersetAccountCandidate,\n SupersetAccountLookupResponse,\n)\nfrom src.services.profile_service import (\n EnvironmentNotFoundError,\n ProfileAuthorizationError,\n ProfileValidationError,\n)\n# [/SECTION]\n\n\nclient = TestClient(app)\n\n\n# [DEF:mock_profile_route_dependencies:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Provides deterministic dependency overrides for profile route tests.\n# @PRE: App instance is initialized.\n# @POST: Dependencies are overridden for current test and restored afterward.\ndef mock_profile_route_dependencies():\n mock_user = MagicMock()\n mock_user.id = \"u-1\"\n mock_user.username = \"test-user\"\n\n mock_db = MagicMock()\n mock_config_manager = MagicMock()\n\n app.dependency_overrides[get_current_user] = lambda: mock_user\n app.dependency_overrides[get_db] = lambda: mock_db\n app.dependency_overrides[get_config_manager] = lambda: mock_config_manager\n\n return mock_user, mock_db, mock_config_manager\n# [/DEF:mock_profile_route_dependencies:Function]\n\n\n# [DEF:profile_route_deps_fixture:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Pytest fixture wrapper for profile route dependency overrides.\n# @PRE: None.\n# @POST: Yields overridden dependencies and clears overrides after test.\nimport pytest\n\n\n@pytest.fixture(autouse=True)\ndef profile_route_deps_fixture():\n yielded = mock_profile_route_dependencies()\n yield yielded\n app.dependency_overrides.clear()\n# [/DEF:profile_route_deps_fixture:Function]\n\n\n# [DEF:_build_preference_response:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Builds stable profile preference response payload for route tests.\n# @PRE: user_id is provided.\n# @POST: Returns ProfilePreferenceResponse object with deterministic timestamps.\ndef _build_preference_response(user_id: str = \"u-1\") -> ProfilePreferenceResponse:\n now = datetime.now(timezone.utc)\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference loaded\",\n preference=ProfilePreference(\n user_id=user_id,\n superset_username=\"John_Doe\",\n superset_username_normalized=\"john_doe\",\n show_only_my_dashboards=True,\n show_only_slug_dashboards=True,\n git_username=\"ivan.ivanov\",\n git_email=\"ivan@company.local\",\n has_git_personal_access_token=True,\n git_personal_access_token_masked=\"iv***al\",\n start_page=\"reports\",\n auto_open_task_drawer=False,\n dashboards_table_density=\"compact\",\n created_at=now,\n updated_at=now,\n ),\n security=ProfileSecuritySummary(\n read_only=True,\n auth_source=\"adfs\",\n current_role=\"Data Engineer\",\n role_source=\"adfs\",\n roles=[\"Data Engineer\"],\n permissions=[\n ProfilePermissionState(key=\"migration:run\", allowed=True),\n ProfilePermissionState(key=\"admin:users\", allowed=False),\n ],\n ),\n )\n# [/DEF:_build_preference_response:Function]\n\n\n# [DEF:test_get_profile_preferences_returns_self_payload:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies GET /api/profile/preferences returns stable self-scoped payload.\n# @PRE: Authenticated user context is available.\n# @POST: Response status is 200 and payload contains current user preference.\ndef test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture):\n mock_user, _, _ = profile_route_deps_fixture\n service = MagicMock()\n service.get_my_preference.return_value = _build_preference_response(user_id=mock_user.id)\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/preferences\")\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"preference\"][\"user_id\"] == mock_user.id\n assert payload[\"preference\"][\"superset_username_normalized\"] == \"john_doe\"\n assert payload[\"preference\"][\"git_username\"] == \"ivan.ivanov\"\n assert payload[\"preference\"][\"git_email\"] == \"ivan@company.local\"\n assert payload[\"preference\"][\"show_only_slug_dashboards\"] is True\n assert payload[\"preference\"][\"has_git_personal_access_token\"] is True\n assert payload[\"preference\"][\"git_personal_access_token_masked\"] == \"iv***al\"\n assert payload[\"preference\"][\"start_page\"] == \"reports\"\n assert payload[\"preference\"][\"auto_open_task_drawer\"] is False\n assert payload[\"preference\"][\"dashboards_table_density\"] == \"compact\"\n assert payload[\"security\"][\"read_only\"] is True\n assert payload[\"security\"][\"current_role\"] == \"Data Engineer\"\n assert payload[\"security\"][\"permissions\"][0][\"key\"] == \"migration:run\"\n service.get_my_preference.assert_called_once_with(mock_user)\n# [/DEF:test_get_profile_preferences_returns_self_payload:Function]\n\n\n# [DEF:test_patch_profile_preferences_success:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies PATCH /api/profile/preferences persists valid payload through route mapping.\n# @PRE: Valid request payload and authenticated user.\n# @POST: Response status is 200 with saved preference payload.\ndef test_patch_profile_preferences_success(profile_route_deps_fixture):\n mock_user, _, _ = profile_route_deps_fixture\n service = MagicMock()\n service.update_my_preference.return_value = _build_preference_response(user_id=mock_user.id)\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"John_Doe\",\n \"show_only_my_dashboards\": True,\n \"show_only_slug_dashboards\": True,\n \"git_username\": \"ivan.ivanov\",\n \"git_email\": \"ivan@company.local\",\n \"git_personal_access_token\": \"ghp_1234567890\",\n \"start_page\": \"reports-logs\",\n \"auto_open_task_drawer\": False,\n \"dashboards_table_density\": \"free\",\n },\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"preference\"][\"superset_username\"] == \"John_Doe\"\n assert payload[\"preference\"][\"show_only_my_dashboards\"] is True\n assert payload[\"preference\"][\"show_only_slug_dashboards\"] is True\n assert payload[\"preference\"][\"git_username\"] == \"ivan.ivanov\"\n assert payload[\"preference\"][\"git_email\"] == \"ivan@company.local\"\n assert payload[\"preference\"][\"start_page\"] == \"reports\"\n assert payload[\"preference\"][\"auto_open_task_drawer\"] is False\n assert payload[\"preference\"][\"dashboards_table_density\"] == \"compact\"\n service.update_my_preference.assert_called_once()\n\n called_kwargs = service.update_my_preference.call_args.kwargs\n assert called_kwargs[\"current_user\"] == mock_user\n assert called_kwargs[\"payload\"].git_username == \"ivan.ivanov\"\n assert called_kwargs[\"payload\"].git_email == \"ivan@company.local\"\n assert called_kwargs[\"payload\"].git_personal_access_token == \"ghp_1234567890\"\n assert called_kwargs[\"payload\"].show_only_slug_dashboards is True\n assert called_kwargs[\"payload\"].start_page == \"reports-logs\"\n assert called_kwargs[\"payload\"].auto_open_task_drawer is False\n assert called_kwargs[\"payload\"].dashboards_table_density == \"free\"\n# [/DEF:test_patch_profile_preferences_success:Function]\n\n\n# [DEF:test_patch_profile_preferences_validation_error:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies route maps domain validation failure to HTTP 422 with actionable details.\n# @PRE: Service raises ProfileValidationError.\n# @POST: Response status is 422 and includes validation messages.\ndef test_patch_profile_preferences_validation_error(profile_route_deps_fixture):\n service = MagicMock()\n service.update_my_preference.side_effect = ProfileValidationError(\n [\"Superset username is required when default filter is enabled.\"]\n )\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"\",\n \"show_only_my_dashboards\": True,\n },\n )\n\n assert response.status_code == 422\n payload = response.json()\n assert \"detail\" in payload\n assert \"Superset username is required when default filter is enabled.\" in payload[\"detail\"]\n# [/DEF:test_patch_profile_preferences_validation_error:Function]\n\n\n# [DEF:test_patch_profile_preferences_cross_user_denied:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies route maps domain authorization guard failure to HTTP 403.\n# @PRE: Service raises ProfileAuthorizationError.\n# @POST: Response status is 403 with denial message.\ndef test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture):\n service = MagicMock()\n service.update_my_preference.side_effect = ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"john_doe\",\n \"show_only_my_dashboards\": True,\n },\n )\n\n assert response.status_code == 403\n payload = response.json()\n assert payload[\"detail\"] == \"Cross-user preference mutation is forbidden\"\n# [/DEF:test_patch_profile_preferences_cross_user_denied:Function]\n\n\n# [DEF:test_lookup_superset_accounts_success:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies lookup route returns success payload with normalized candidates.\n# @PRE: Valid environment_id and service success response.\n# @POST: Response status is 200 and items list is returned.\ndef test_lookup_superset_accounts_success(profile_route_deps_fixture):\n service = MagicMock()\n service.lookup_superset_accounts.return_value = SupersetAccountLookupResponse(\n status=\"success\",\n environment_id=\"dev\",\n page_index=0,\n page_size=20,\n total=1,\n warning=None,\n items=[\n SupersetAccountCandidate(\n environment_id=\"dev\",\n username=\"john_doe\",\n display_name=\"John Doe\",\n email=\"john@example.local\",\n is_active=True,\n )\n ],\n )\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/superset-accounts?environment_id=dev\")\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"environment_id\"] == \"dev\"\n assert payload[\"total\"] == 1\n assert payload[\"items\"][0][\"username\"] == \"john_doe\"\n# [/DEF:test_lookup_superset_accounts_success:Function]\n\n\n# [DEF:test_lookup_superset_accounts_env_not_found:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies lookup route maps missing environment to HTTP 404.\n# @PRE: Service raises EnvironmentNotFoundError.\n# @POST: Response status is 404 with explicit message.\ndef test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):\n service = MagicMock()\n service.lookup_superset_accounts.side_effect = EnvironmentNotFoundError(\n \"Environment 'missing-env' not found\"\n )\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/superset-accounts?environment_id=missing-env\")\n\n assert response.status_code == 404\n payload = response.json()\n assert payload[\"detail\"] == \"Environment 'missing-env' not found\"\n# [/DEF:test_lookup_superset_accounts_env_not_found:Function]\n\n# [/DEF:TestProfileApi:Module]\n" + "anchor_syntax": "region", + "body": "# #region _make_sync_config_manager [TYPE Function]\n# @RELATION BINDS_TO -> TestMigrationRoutes\ndef _make_sync_config_manager(environments):\n \"\"\"Creates a mock config manager with environments list.\"\"\"\n settings = MagicMock()\n settings.migration_sync_cron = \"0 2 * * *\"\n config = MagicMock()\n config.settings = settings\n config.environments = environments\n cm = MagicMock()\n cm.get_config.return_value = config\n cm.get_environments.return_value = environments\n return cm\n# #endregion _make_sync_config_manager\n" }, { "contract_id": "mock_profile_route_dependencies", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 36, - "end_line": 54, + "start_line": 30, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -6645,15 +5744,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:mock_profile_route_dependencies:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Provides deterministic dependency overrides for profile route tests.\n# @PRE: App instance is initialized.\n# @POST: Dependencies are overridden for current test and restored afterward.\ndef mock_profile_route_dependencies():\n mock_user = MagicMock()\n mock_user.id = \"u-1\"\n mock_user.username = \"test-user\"\n\n mock_db = MagicMock()\n mock_config_manager = MagicMock()\n\n app.dependency_overrides[get_current_user] = lambda: mock_user\n app.dependency_overrides[get_db] = lambda: mock_db\n app.dependency_overrides[get_config_manager] = lambda: mock_config_manager\n\n return mock_user, mock_db, mock_config_manager\n# [/DEF:mock_profile_route_dependencies:Function]\n" + "anchor_syntax": "region", + "body": "# #region mock_profile_route_dependencies [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Provides deterministic dependency overrides for profile route tests.\n# @PRE: App instance is initialized.\n# @POST: Dependencies are overridden for current test and restored afterward.\ndef mock_profile_route_dependencies():\n mock_user = MagicMock()\n mock_user.id = \"u-1\"\n mock_user.username = \"test-user\"\n mock_db = MagicMock()\n mock_config_manager = MagicMock()\n app.dependency_overrides[get_current_user] = lambda: mock_user\n app.dependency_overrides[get_db] = lambda: mock_db\n app.dependency_overrides[get_config_manager] = lambda: mock_config_manager\n return mock_user, mock_db, mock_config_manager\n# #endregion mock_profile_route_dependencies\n" }, { "contract_id": "profile_route_deps_fixture", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 57, - "end_line": 70, + "start_line": 46, + "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -6707,15 +5806,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:profile_route_deps_fixture:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Pytest fixture wrapper for profile route dependency overrides.\n# @PRE: None.\n# @POST: Yields overridden dependencies and clears overrides after test.\nimport pytest\n\n\n@pytest.fixture(autouse=True)\ndef profile_route_deps_fixture():\n yielded = mock_profile_route_dependencies()\n yield yielded\n app.dependency_overrides.clear()\n# [/DEF:profile_route_deps_fixture:Function]\n" + "anchor_syntax": "region", + "body": "# #region profile_route_deps_fixture [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Pytest fixture wrapper for profile route dependency overrides.\n# @PRE: None.\n# @POST: Yields overridden dependencies and clears overrides after test.\nimport pytest\n\n\n@pytest.fixture(autouse=True)\ndef profile_route_deps_fixture():\n yielded = mock_profile_route_dependencies()\n yield yielded\n app.dependency_overrides.clear()\n# #endregion profile_route_deps_fixture\n" }, { "contract_id": "_build_preference_response", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 73, - "end_line": 111, + "start_line": 60, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -6769,15 +5868,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_build_preference_response:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Builds stable profile preference response payload for route tests.\n# @PRE: user_id is provided.\n# @POST: Returns ProfilePreferenceResponse object with deterministic timestamps.\ndef _build_preference_response(user_id: str = \"u-1\") -> ProfilePreferenceResponse:\n now = datetime.now(timezone.utc)\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference loaded\",\n preference=ProfilePreference(\n user_id=user_id,\n superset_username=\"John_Doe\",\n superset_username_normalized=\"john_doe\",\n show_only_my_dashboards=True,\n show_only_slug_dashboards=True,\n git_username=\"ivan.ivanov\",\n git_email=\"ivan@company.local\",\n has_git_personal_access_token=True,\n git_personal_access_token_masked=\"iv***al\",\n start_page=\"reports\",\n auto_open_task_drawer=False,\n dashboards_table_density=\"compact\",\n created_at=now,\n updated_at=now,\n ),\n security=ProfileSecuritySummary(\n read_only=True,\n auth_source=\"adfs\",\n current_role=\"Data Engineer\",\n role_source=\"adfs\",\n roles=[\"Data Engineer\"],\n permissions=[\n ProfilePermissionState(key=\"migration:run\", allowed=True),\n ProfilePermissionState(key=\"admin:users\", allowed=False),\n ],\n ),\n )\n# [/DEF:_build_preference_response:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_preference_response [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Builds stable profile preference response payload for route tests.\n# @PRE: user_id is provided.\n# @POST: Returns ProfilePreferenceResponse object with deterministic timestamps.\ndef _build_preference_response(user_id: str = \"u-1\") -> ProfilePreferenceResponse:\n now = datetime.now(UTC)\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference loaded\",\n preference=ProfilePreference(\n user_id=user_id,\n superset_username=\"John_Doe\",\n superset_username_normalized=\"john_doe\",\n show_only_my_dashboards=True,\n show_only_slug_dashboards=True,\n git_username=\"ivan.ivanov\",\n git_email=\"ivan@company.local\",\n has_git_personal_access_token=True,\n git_personal_access_token_masked=\"iv***al\",\n start_page=\"reports\",\n auto_open_task_drawer=False,\n dashboards_table_density=\"compact\",\n created_at=now,\n updated_at=now,\n ),\n security=ProfileSecuritySummary(\n read_only=True,\n auth_source=\"adfs\",\n current_role=\"Data Engineer\",\n role_source=\"adfs\",\n roles=[\"Data Engineer\"],\n permissions=[\n ProfilePermissionState(key=\"migration:run\", allowed=True),\n ProfilePermissionState(key=\"admin:users\", allowed=False),\n ],\n ),\n )\n# #endregion _build_preference_response\n" }, { "contract_id": "test_get_profile_preferences_returns_self_payload", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 114, - "end_line": 144, + "start_line": 99, + "end_line": 127, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6822,15 +5921,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_profile_preferences_returns_self_payload:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies GET /api/profile/preferences returns stable self-scoped payload.\n# @PRE: Authenticated user context is available.\n# @POST: Response status is 200 and payload contains current user preference.\ndef test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture):\n mock_user, _, _ = profile_route_deps_fixture\n service = MagicMock()\n service.get_my_preference.return_value = _build_preference_response(user_id=mock_user.id)\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/preferences\")\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"preference\"][\"user_id\"] == mock_user.id\n assert payload[\"preference\"][\"superset_username_normalized\"] == \"john_doe\"\n assert payload[\"preference\"][\"git_username\"] == \"ivan.ivanov\"\n assert payload[\"preference\"][\"git_email\"] == \"ivan@company.local\"\n assert payload[\"preference\"][\"show_only_slug_dashboards\"] is True\n assert payload[\"preference\"][\"has_git_personal_access_token\"] is True\n assert payload[\"preference\"][\"git_personal_access_token_masked\"] == \"iv***al\"\n assert payload[\"preference\"][\"start_page\"] == \"reports\"\n assert payload[\"preference\"][\"auto_open_task_drawer\"] is False\n assert payload[\"preference\"][\"dashboards_table_density\"] == \"compact\"\n assert payload[\"security\"][\"read_only\"] is True\n assert payload[\"security\"][\"current_role\"] == \"Data Engineer\"\n assert payload[\"security\"][\"permissions\"][0][\"key\"] == \"migration:run\"\n service.get_my_preference.assert_called_once_with(mock_user)\n# [/DEF:test_get_profile_preferences_returns_self_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_profile_preferences_returns_self_payload [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Verifies GET /api/profile/preferences returns stable self-scoped payload.\n# @PRE: Authenticated user context is available.\n# @POST: Response status is 200 and payload contains current user preference.\ndef test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture):\n mock_user, _, _ = profile_route_deps_fixture\n service = MagicMock()\n service.get_my_preference.return_value = _build_preference_response(user_id=mock_user.id)\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/preferences\")\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"preference\"][\"user_id\"] == mock_user.id\n assert payload[\"preference\"][\"superset_username_normalized\"] == \"john_doe\"\n assert payload[\"preference\"][\"git_username\"] == \"ivan.ivanov\"\n assert payload[\"preference\"][\"git_email\"] == \"ivan@company.local\"\n assert payload[\"preference\"][\"show_only_slug_dashboards\"] is True\n assert payload[\"preference\"][\"has_git_personal_access_token\"] is True\n assert payload[\"preference\"][\"git_personal_access_token_masked\"] == \"iv***al\"\n assert payload[\"preference\"][\"start_page\"] == \"reports\"\n assert payload[\"preference\"][\"auto_open_task_drawer\"] is False\n assert payload[\"preference\"][\"dashboards_table_density\"] == \"compact\"\n assert payload[\"security\"][\"read_only\"] is True\n assert payload[\"security\"][\"current_role\"] == \"Data Engineer\"\n assert payload[\"security\"][\"permissions\"][0][\"key\"] == \"migration:run\"\n service.get_my_preference.assert_called_once_with(mock_user)\n# #endregion test_get_profile_preferences_returns_self_payload\n" }, { "contract_id": "test_patch_profile_preferences_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 147, - "end_line": 195, + "start_line": 128, + "end_line": 173, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6875,15 +5974,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_patch_profile_preferences_success:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies PATCH /api/profile/preferences persists valid payload through route mapping.\n# @PRE: Valid request payload and authenticated user.\n# @POST: Response status is 200 with saved preference payload.\ndef test_patch_profile_preferences_success(profile_route_deps_fixture):\n mock_user, _, _ = profile_route_deps_fixture\n service = MagicMock()\n service.update_my_preference.return_value = _build_preference_response(user_id=mock_user.id)\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"John_Doe\",\n \"show_only_my_dashboards\": True,\n \"show_only_slug_dashboards\": True,\n \"git_username\": \"ivan.ivanov\",\n \"git_email\": \"ivan@company.local\",\n \"git_personal_access_token\": \"ghp_1234567890\",\n \"start_page\": \"reports-logs\",\n \"auto_open_task_drawer\": False,\n \"dashboards_table_density\": \"free\",\n },\n )\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"preference\"][\"superset_username\"] == \"John_Doe\"\n assert payload[\"preference\"][\"show_only_my_dashboards\"] is True\n assert payload[\"preference\"][\"show_only_slug_dashboards\"] is True\n assert payload[\"preference\"][\"git_username\"] == \"ivan.ivanov\"\n assert payload[\"preference\"][\"git_email\"] == \"ivan@company.local\"\n assert payload[\"preference\"][\"start_page\"] == \"reports\"\n assert payload[\"preference\"][\"auto_open_task_drawer\"] is False\n assert payload[\"preference\"][\"dashboards_table_density\"] == \"compact\"\n service.update_my_preference.assert_called_once()\n\n called_kwargs = service.update_my_preference.call_args.kwargs\n assert called_kwargs[\"current_user\"] == mock_user\n assert called_kwargs[\"payload\"].git_username == \"ivan.ivanov\"\n assert called_kwargs[\"payload\"].git_email == \"ivan@company.local\"\n assert called_kwargs[\"payload\"].git_personal_access_token == \"ghp_1234567890\"\n assert called_kwargs[\"payload\"].show_only_slug_dashboards is True\n assert called_kwargs[\"payload\"].start_page == \"reports-logs\"\n assert called_kwargs[\"payload\"].auto_open_task_drawer is False\n assert called_kwargs[\"payload\"].dashboards_table_density == \"free\"\n# [/DEF:test_patch_profile_preferences_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_patch_profile_preferences_success [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Verifies PATCH /api/profile/preferences persists valid payload through route mapping.\n# @PRE: Valid request payload and authenticated user.\n# @POST: Response status is 200 with saved preference payload.\ndef test_patch_profile_preferences_success(profile_route_deps_fixture):\n mock_user, _, _ = profile_route_deps_fixture\n service = MagicMock()\n service.update_my_preference.return_value = _build_preference_response(user_id=mock_user.id)\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"John_Doe\",\n \"show_only_my_dashboards\": True,\n \"show_only_slug_dashboards\": True,\n \"git_username\": \"ivan.ivanov\",\n \"git_email\": \"ivan@company.local\",\n \"git_personal_access_token\": \"ghp_1234567890\",\n \"start_page\": \"reports-logs\",\n \"auto_open_task_drawer\": False,\n \"dashboards_table_density\": \"free\",\n },\n )\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"preference\"][\"superset_username\"] == \"John_Doe\"\n assert payload[\"preference\"][\"show_only_my_dashboards\"] is True\n assert payload[\"preference\"][\"show_only_slug_dashboards\"] is True\n assert payload[\"preference\"][\"git_username\"] == \"ivan.ivanov\"\n assert payload[\"preference\"][\"git_email\"] == \"ivan@company.local\"\n assert payload[\"preference\"][\"start_page\"] == \"reports\"\n assert payload[\"preference\"][\"auto_open_task_drawer\"] is False\n assert payload[\"preference\"][\"dashboards_table_density\"] == \"compact\"\n service.update_my_preference.assert_called_once()\n called_kwargs = service.update_my_preference.call_args.kwargs\n assert called_kwargs[\"current_user\"] == mock_user\n assert called_kwargs[\"payload\"].git_username == \"ivan.ivanov\"\n assert called_kwargs[\"payload\"].git_email == \"ivan@company.local\"\n assert called_kwargs[\"payload\"].git_personal_access_token == \"ghp_1234567890\"\n assert called_kwargs[\"payload\"].show_only_slug_dashboards is True\n assert called_kwargs[\"payload\"].start_page == \"reports-logs\"\n assert called_kwargs[\"payload\"].auto_open_task_drawer is False\n assert called_kwargs[\"payload\"].dashboards_table_density == \"free\"\n# #endregion test_patch_profile_preferences_success\n" }, { "contract_id": "test_patch_profile_preferences_validation_error", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 198, - "end_line": 222, + "start_line": 174, + "end_line": 196, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6928,15 +6027,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_patch_profile_preferences_validation_error:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies route maps domain validation failure to HTTP 422 with actionable details.\n# @PRE: Service raises ProfileValidationError.\n# @POST: Response status is 422 and includes validation messages.\ndef test_patch_profile_preferences_validation_error(profile_route_deps_fixture):\n service = MagicMock()\n service.update_my_preference.side_effect = ProfileValidationError(\n [\"Superset username is required when default filter is enabled.\"]\n )\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"\",\n \"show_only_my_dashboards\": True,\n },\n )\n\n assert response.status_code == 422\n payload = response.json()\n assert \"detail\" in payload\n assert \"Superset username is required when default filter is enabled.\" in payload[\"detail\"]\n# [/DEF:test_patch_profile_preferences_validation_error:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_patch_profile_preferences_validation_error [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Verifies route maps domain validation failure to HTTP 422 with actionable details.\n# @PRE: Service raises ProfileValidationError.\n# @POST: Response status is 422 and includes validation messages.\ndef test_patch_profile_preferences_validation_error(profile_route_deps_fixture):\n service = MagicMock()\n service.update_my_preference.side_effect = ProfileValidationError(\n [\"Superset username is required when default filter is enabled.\"]\n )\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"\",\n \"show_only_my_dashboards\": True,\n },\n )\n assert response.status_code == 422\n payload = response.json()\n assert \"detail\" in payload\n assert \"Superset username is required when default filter is enabled.\" in payload[\"detail\"]\n# #endregion test_patch_profile_preferences_validation_error\n" }, { "contract_id": "test_patch_profile_preferences_cross_user_denied", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 225, - "end_line": 248, + "start_line": 197, + "end_line": 218, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6981,15 +6080,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_patch_profile_preferences_cross_user_denied:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies route maps domain authorization guard failure to HTTP 403.\n# @PRE: Service raises ProfileAuthorizationError.\n# @POST: Response status is 403 with denial message.\ndef test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture):\n service = MagicMock()\n service.update_my_preference.side_effect = ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"john_doe\",\n \"show_only_my_dashboards\": True,\n },\n )\n\n assert response.status_code == 403\n payload = response.json()\n assert payload[\"detail\"] == \"Cross-user preference mutation is forbidden\"\n# [/DEF:test_patch_profile_preferences_cross_user_denied:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_patch_profile_preferences_cross_user_denied [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Verifies route maps domain authorization guard failure to HTTP 403.\n# @PRE: Service raises ProfileAuthorizationError.\n# @POST: Response status is 403 with denial message.\ndef test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture):\n service = MagicMock()\n service.update_my_preference.side_effect = ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.patch(\n \"/api/profile/preferences\",\n json={\n \"superset_username\": \"john_doe\",\n \"show_only_my_dashboards\": True,\n },\n )\n assert response.status_code == 403\n payload = response.json()\n assert payload[\"detail\"] == \"Cross-user preference mutation is forbidden\"\n# #endregion test_patch_profile_preferences_cross_user_denied\n" }, { "contract_id": "test_lookup_superset_accounts_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 251, - "end_line": 285, + "start_line": 219, + "end_line": 251, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7034,15 +6133,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_lookup_superset_accounts_success:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies lookup route returns success payload with normalized candidates.\n# @PRE: Valid environment_id and service success response.\n# @POST: Response status is 200 and items list is returned.\ndef test_lookup_superset_accounts_success(profile_route_deps_fixture):\n service = MagicMock()\n service.lookup_superset_accounts.return_value = SupersetAccountLookupResponse(\n status=\"success\",\n environment_id=\"dev\",\n page_index=0,\n page_size=20,\n total=1,\n warning=None,\n items=[\n SupersetAccountCandidate(\n environment_id=\"dev\",\n username=\"john_doe\",\n display_name=\"John Doe\",\n email=\"john@example.local\",\n is_active=True,\n )\n ],\n )\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/superset-accounts?environment_id=dev\")\n\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"environment_id\"] == \"dev\"\n assert payload[\"total\"] == 1\n assert payload[\"items\"][0][\"username\"] == \"john_doe\"\n# [/DEF:test_lookup_superset_accounts_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_lookup_superset_accounts_success [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Verifies lookup route returns success payload with normalized candidates.\n# @PRE: Valid environment_id and service success response.\n# @POST: Response status is 200 and items list is returned.\ndef test_lookup_superset_accounts_success(profile_route_deps_fixture):\n service = MagicMock()\n service.lookup_superset_accounts.return_value = SupersetAccountLookupResponse(\n status=\"success\",\n environment_id=\"dev\",\n page_index=0,\n page_size=20,\n total=1,\n warning=None,\n items=[\n SupersetAccountCandidate(\n environment_id=\"dev\",\n username=\"john_doe\",\n display_name=\"John Doe\",\n email=\"john@example.local\",\n is_active=True,\n )\n ],\n )\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/superset-accounts?environment_id=dev\")\n assert response.status_code == 200\n payload = response.json()\n assert payload[\"status\"] == \"success\"\n assert payload[\"environment_id\"] == \"dev\"\n assert payload[\"total\"] == 1\n assert payload[\"items\"][0][\"username\"] == \"john_doe\"\n# #endregion test_lookup_superset_accounts_success\n" }, { "contract_id": "test_lookup_superset_accounts_env_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_profile_api.py", - "start_line": 288, - "end_line": 305, + "start_line": 252, + "end_line": 267, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7087,84 +6186,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_lookup_superset_accounts_env_not_found:Function]\n# @RELATION: BINDS_TO -> TestProfileApi\n# @PURPOSE: Verifies lookup route maps missing environment to HTTP 404.\n# @PRE: Service raises EnvironmentNotFoundError.\n# @POST: Response status is 404 with explicit message.\ndef test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):\n service = MagicMock()\n service.lookup_superset_accounts.side_effect = EnvironmentNotFoundError(\n \"Environment 'missing-env' not found\"\n )\n\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/superset-accounts?environment_id=missing-env\")\n\n assert response.status_code == 404\n payload = response.json()\n assert payload[\"detail\"] == \"Environment 'missing-env' not found\"\n# [/DEF:test_lookup_superset_accounts_env_not_found:Function]\n" - }, - { - "contract_id": "TestReportsApi", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_reports_api.py", - "start_line": 1, - "end_line": 249, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DEBT": "Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().", - "INVARIANT": "API response contract contains {items,total,page,page_size,has_next,applied_filters}.", - "LAYER": "Domain (Tests)", - "PURPOSE": "Contract tests for GET /api/reports defaults, pagination, and filtering behavior.", - "SEMANTICS": [ - "tests", - "reports", - "api", - "contract", - "pagination", - "filtering" - ] - }, - "relations": [ - { - "source_id": "TestReportsApi", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "DEBT", - "message": "@DEBT 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", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestReportsApi:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, reports, api, contract, pagination, filtering\n# @PURPOSE: Contract tests for GET /api/reports defaults, pagination, and filtering behavior.\n# @LAYER: Domain (Tests)\n# @INVARIANT: API response contract contains {items,total,page,page_size,has_next,applied_filters}.\n\nfrom datetime import datetime, timedelta, timezone\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# [DEF:_FakeTaskManager:Class]\n# @RELATION: BINDS_TO -> [TestReportsApi]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal task-manager double exposing only get_all_tasks used by reports route tests.\n# @INVARIANT: Returns pre-seeded tasks without mutation or side effects.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n\n def get_all_tasks(self):\n return self._tasks\n\n\n# [/DEF:_FakeTaskManager:Class]\n\n\n# [DEF:_admin_user:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Build deterministic admin principal accepted by reports authorization guard.\ndef _admin_user():\n admin_role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[admin_role])\n\n\n# [/DEF:_admin_user:Function]\n\n\n# [DEF:_make_task:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Build Task fixture with controlled timestamps/status for reports list/detail normalization.\ndef _make_task(\n task_id: str,\n plugin_id: str,\n status: TaskStatus,\n started_at: datetime,\n finished_at: datetime = None,\n result=None,\n):\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=started_at,\n finished_at=finished_at,\n params={\"environment_id\": \"env-1\"},\n result=result or {\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n\n\n# [/DEF:_make_task:Function]\n\n\n# [DEF:test_get_reports_default_pagination_contract:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Validate reports list endpoint default pagination and contract keys for mixed task statuses.\ndef test_get_reports_default_pagination_contract():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n _make_task(\n \"t-2\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=8),\n now - timedelta(minutes=7),\n ),\n _make_task(\n \"t-3\",\n \"llm_dashboard_validation\",\n TaskStatus.RUNNING,\n now - timedelta(minutes=6),\n None,\n ),\n ]\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n\n data = response.json()\n assert set(\n [\"items\", \"total\", \"page\", \"page_size\", \"has_next\", \"applied_filters\"]\n ).issubset(data.keys())\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n assert data[\"total\"] == 3\n assert isinstance(data[\"items\"], list)\n assert data[\"applied_filters\"][\"sort_by\"] == \"updated_at\"\n assert data[\"applied_filters\"][\"sort_order\"] == \"desc\"\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_reports_default_pagination_contract:Function]\n\n\n# [DEF:test_get_reports_filter_and_pagination:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Validate reports list endpoint applies task-type/status filters and pagination boundaries.\ndef test_get_reports_filter_and_pagination():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=30),\n now - timedelta(minutes=29),\n ),\n _make_task(\n \"t-2\",\n \"superset-backup\",\n TaskStatus.FAILED,\n now - timedelta(minutes=20),\n now - timedelta(minutes=19),\n ),\n _make_task(\n \"t-3\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n ]\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\n \"/api/reports?task_types=backup&statuses=failed&page=1&page_size=1\"\n )\n assert response.status_code == 200\n\n data = response.json()\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 1\n assert data[\"has_next\"] is False\n assert len(data[\"items\"]) == 1\n assert data[\"items\"][0][\"task_type\"] == \"backup\"\n assert data[\"items\"][0][\"status\"] == \"failed\"\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_reports_filter_and_pagination:Function]\n\n\n# [DEF:test_get_reports_handles_mixed_naive_and_aware_datetimes:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes.\ndef test_get_reports_handles_mixed_naive_and_aware_datetimes():\n naive_now = datetime.utcnow()\n aware_now = datetime.now(timezone.utc)\n tasks = [\n _make_task(\n \"t-naive\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n naive_now - timedelta(minutes=5),\n naive_now - timedelta(minutes=4),\n ),\n _make_task(\n \"t-aware\",\n \"superset-migration\",\n TaskStatus.FAILED,\n aware_now - timedelta(minutes=3),\n aware_now - timedelta(minutes=2),\n ),\n ]\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?sort_by=updated_at&sort_order=desc\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 2\n assert len(data[\"items\"]) == 2\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_reports_handles_mixed_naive_and_aware_datetimes:Function]\n\n\n# [DEF:test_get_reports_invalid_filter_returns_400:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Validate reports list endpoint rejects unsupported task type filters with HTTP 400.\ndef test_get_reports_invalid_filter_returns_400():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=5),\n now - timedelta(minutes=4),\n )\n ]\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?task_types=bad_type\")\n assert response.status_code == 400\n body = response.json()\n assert \"detail\" in body\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_reports_invalid_filter_returns_400:Function]\n# [/DEF:TestReportsApi:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_lookup_superset_accounts_env_not_found [TYPE Function]\n# @RELATION BINDS_TO -> TestProfileApi\n# @BRIEF Verifies lookup route maps missing environment to HTTP 404.\n# @PRE: Service raises EnvironmentNotFoundError.\n# @POST: Response status is 404 with explicit message.\ndef test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):\n service = MagicMock()\n service.lookup_superset_accounts.side_effect = EnvironmentNotFoundError(\n \"Environment 'missing-env' not found\"\n )\n with patch(\"src.api.routes.profile._get_profile_service\", return_value=service):\n response = client.get(\"/api/profile/superset-accounts?environment_id=missing-env\")\n assert response.status_code == 404\n payload = response.json()\n assert payload[\"detail\"] == \"Environment 'missing-env' not found\"\n# #endregion test_lookup_superset_accounts_env_not_found\n" }, { "contract_id": "_make_task", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_api.py", - "start_line": 47, - "end_line": 69, + "start_line": 34, + "end_line": 54, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -7198,15 +6228,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_task:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Build Task fixture with controlled timestamps/status for reports list/detail normalization.\ndef _make_task(\n task_id: str,\n plugin_id: str,\n status: TaskStatus,\n started_at: datetime,\n finished_at: datetime = None,\n result=None,\n):\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=started_at,\n finished_at=finished_at,\n params={\"environment_id\": \"env-1\"},\n result=result or {\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n\n\n# [/DEF:_make_task:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Build Task fixture with controlled timestamps/status for reports list/detail normalization.\ndef _make_task(\n task_id: str,\n plugin_id: str,\n status: TaskStatus,\n started_at: datetime,\n finished_at: datetime = None,\n result=None,\n):\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=started_at,\n finished_at=finished_at,\n params={\"environment_id\": \"env-1\"},\n result=result or {\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n# #endregion _make_task\n" }, { "contract_id": "test_get_reports_default_pagination_contract", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_api.py", - "start_line": 72, - "end_line": 123, + "start_line": 55, + "end_line": 101, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7231,15 +6261,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_reports_default_pagination_contract:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Validate reports list endpoint default pagination and contract keys for mixed task statuses.\ndef test_get_reports_default_pagination_contract():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n _make_task(\n \"t-2\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=8),\n now - timedelta(minutes=7),\n ),\n _make_task(\n \"t-3\",\n \"llm_dashboard_validation\",\n TaskStatus.RUNNING,\n now - timedelta(minutes=6),\n None,\n ),\n ]\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n\n data = response.json()\n assert set(\n [\"items\", \"total\", \"page\", \"page_size\", \"has_next\", \"applied_filters\"]\n ).issubset(data.keys())\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n assert data[\"total\"] == 3\n assert isinstance(data[\"items\"], list)\n assert data[\"applied_filters\"][\"sort_by\"] == \"updated_at\"\n assert data[\"applied_filters\"][\"sort_order\"] == \"desc\"\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_reports_default_pagination_contract:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_reports_default_pagination_contract [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint default pagination and contract keys for mixed task statuses.\ndef test_get_reports_default_pagination_contract():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n _make_task(\n \"t-2\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=8),\n now - timedelta(minutes=7),\n ),\n _make_task(\n \"t-3\",\n \"llm_dashboard_validation\",\n TaskStatus.RUNNING,\n now - timedelta(minutes=6),\n None,\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n data = response.json()\n assert set(\n [\"items\", \"total\", \"page\", \"page_size\", \"has_next\", \"applied_filters\"]\n ).issubset(data.keys())\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n assert data[\"total\"] == 3\n assert isinstance(data[\"items\"], list)\n assert data[\"applied_filters\"][\"sort_by\"] == \"updated_at\"\n assert data[\"applied_filters\"][\"sort_order\"] == \"desc\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_default_pagination_contract\n" }, { "contract_id": "test_get_reports_filter_and_pagination", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_api.py", - "start_line": 126, - "end_line": 177, + "start_line": 102, + "end_line": 148, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7264,15 +6294,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_reports_filter_and_pagination:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Validate reports list endpoint applies task-type/status filters and pagination boundaries.\ndef test_get_reports_filter_and_pagination():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=30),\n now - timedelta(minutes=29),\n ),\n _make_task(\n \"t-2\",\n \"superset-backup\",\n TaskStatus.FAILED,\n now - timedelta(minutes=20),\n now - timedelta(minutes=19),\n ),\n _make_task(\n \"t-3\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n ]\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\n \"/api/reports?task_types=backup&statuses=failed&page=1&page_size=1\"\n )\n assert response.status_code == 200\n\n data = response.json()\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 1\n assert data[\"has_next\"] is False\n assert len(data[\"items\"]) == 1\n assert data[\"items\"][0][\"task_type\"] == \"backup\"\n assert data[\"items\"][0][\"status\"] == \"failed\"\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_reports_filter_and_pagination:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_reports_filter_and_pagination [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint applies task-type/status filters and pagination boundaries.\ndef test_get_reports_filter_and_pagination():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=30),\n now - timedelta(minutes=29),\n ),\n _make_task(\n \"t-2\",\n \"superset-backup\",\n TaskStatus.FAILED,\n now - timedelta(minutes=20),\n now - timedelta(minutes=19),\n ),\n _make_task(\n \"t-3\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\n \"/api/reports?task_types=backup&statuses=failed&page=1&page_size=1\"\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 1\n assert data[\"has_next\"] is False\n assert len(data[\"items\"]) == 1\n assert data[\"items\"][0][\"task_type\"] == \"backup\"\n assert data[\"items\"][0][\"status\"] == \"failed\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_filter_and_pagination\n" }, { "contract_id": "test_get_reports_handles_mixed_naive_and_aware_datetimes", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_api.py", - "start_line": 180, - "end_line": 217, + "start_line": 149, + "end_line": 182, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7297,15 +6327,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_reports_handles_mixed_naive_and_aware_datetimes:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes.\ndef test_get_reports_handles_mixed_naive_and_aware_datetimes():\n naive_now = datetime.utcnow()\n aware_now = datetime.now(timezone.utc)\n tasks = [\n _make_task(\n \"t-naive\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n naive_now - timedelta(minutes=5),\n naive_now - timedelta(minutes=4),\n ),\n _make_task(\n \"t-aware\",\n \"superset-migration\",\n TaskStatus.FAILED,\n aware_now - timedelta(minutes=3),\n aware_now - timedelta(minutes=2),\n ),\n ]\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?sort_by=updated_at&sort_order=desc\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 2\n assert len(data[\"items\"]) == 2\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_reports_handles_mixed_naive_and_aware_datetimes:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_reports_handles_mixed_naive_and_aware_datetimes [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes.\ndef test_get_reports_handles_mixed_naive_and_aware_datetimes():\n naive_now = datetime.utcnow()\n aware_now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-naive\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n naive_now - timedelta(minutes=5),\n naive_now - timedelta(minutes=4),\n ),\n _make_task(\n \"t-aware\",\n \"superset-migration\",\n TaskStatus.FAILED,\n aware_now - timedelta(minutes=3),\n aware_now - timedelta(minutes=2),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?sort_by=updated_at&sort_order=desc\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 2\n assert len(data[\"items\"]) == 2\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_handles_mixed_naive_and_aware_datetimes\n" }, { "contract_id": "test_get_reports_invalid_filter_returns_400", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_api.py", - "start_line": 220, - "end_line": 248, + "start_line": 183, + "end_line": 207, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7330,83 +6360,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_reports_invalid_filter_returns_400:Function]\n# @RELATION: BINDS_TO -> TestReportsApi\n# @PURPOSE: Validate reports list endpoint rejects unsupported task type filters with HTTP 400.\ndef test_get_reports_invalid_filter_returns_400():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=5),\n now - timedelta(minutes=4),\n )\n ]\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?task_types=bad_type\")\n assert response.status_code == 400\n body = response.json()\n assert \"detail\" in body\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_reports_invalid_filter_returns_400:Function]\n" - }, - { - "contract_id": "TestReportsDetailApi", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_reports_detail_api.py", - "start_line": 1, - "end_line": 122, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DEBT": "Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().", - "INVARIANT": "Detail endpoint tests must keep deterministic assertions for success and not-found contracts.", - "LAYER": "Domain (Tests)", - "PURPOSE": "Contract tests for GET /api/reports/{report_id} detail endpoint behavior.", - "SEMANTICS": [ - "tests", - "reports", - "api", - "detail", - "diagnostics" - ] - }, - "relations": [ - { - "source_id": "TestReportsDetailApi", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "DEBT", - "message": "@DEBT 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", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestReportsDetailApi:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, reports, api, detail, diagnostics\n# @PURPOSE: Contract tests for GET /api/reports/{report_id} detail endpoint behavior.\n# @LAYER: Domain (Tests)\n# @INVARIANT: Detail endpoint tests must keep deterministic assertions for success and not-found contracts.\n\nfrom datetime import datetime, timedelta\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# [DEF:_FakeTaskManager:Class]\n# @RELATION: BINDS_TO -> [TestReportsDetailApi]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal task-manager double exposing pre-seeded tasks to detail endpoint under test.\n# @INVARIANT: get_all_tasks returns exactly seeded tasks list.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n\n def get_all_tasks(self):\n return self._tasks\n\n\n# [/DEF:_FakeTaskManager:Class]\n\n\n# [DEF:_admin_user:Function]\n# @RELATION: BINDS_TO -> TestReportsDetailApi\n# @PURPOSE: Provide admin principal fixture accepted by reports detail authorization policy.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[role])\n\n\n# [/DEF:_admin_user:Function]\n\n\n# [DEF:_make_task:Function]\n# @RELATION: BINDS_TO -> TestReportsDetailApi\n# @PURPOSE: Build deterministic Task payload for reports detail endpoint contract assertions.\ndef _make_task(task_id: str, plugin_id: str, status: TaskStatus, result=None):\n now = datetime.utcnow()\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now - timedelta(minutes=2),\n finished_at=now - timedelta(minutes=1)\n if status != TaskStatus.RUNNING\n else None,\n params={\"environment_id\": \"env-1\"},\n result=result or {\"summary\": f\"{plugin_id} result\"},\n )\n\n\n# [/DEF:_make_task:Function]\n\n\n# [DEF:test_get_report_detail_success:Function]\n# @RELATION: BINDS_TO -> TestReportsDetailApi\n# @PURPOSE: Validate report detail endpoint returns report body with diagnostics and next actions for existing task.\ndef test_get_report_detail_success():\n task = _make_task(\n \"detail-1\",\n \"superset-migration\",\n TaskStatus.FAILED,\n result={\n \"error\": {\n \"message\": \"Step failed\",\n \"next_actions\": [\"Check mapping\", \"Retry\"],\n }\n },\n )\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/detail-1\")\n assert response.status_code == 200\n\n data = response.json()\n assert \"report\" in data\n assert data[\"report\"][\"report_id\"] == \"detail-1\"\n assert \"diagnostics\" in data\n assert \"next_actions\" in data\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_report_detail_success:Function]\n\n\n# [DEF:test_get_report_detail_not_found:Function]\n# @RELATION: BINDS_TO -> TestReportsDetailApi\n# @PURPOSE: Validate report detail endpoint returns 404 when requested report identifier is absent.\ndef test_get_report_detail_not_found():\n task = _make_task(\"detail-2\", \"superset-backup\", TaskStatus.SUCCESS)\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/unknown-id\")\n assert response.status_code == 404\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_report_detail_not_found:Function]\n# [/DEF:TestReportsDetailApi:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_get_reports_invalid_filter_returns_400 [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint rejects unsupported task type filters with HTTP 400.\ndef test_get_reports_invalid_filter_returns_400():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=5),\n now - timedelta(minutes=4),\n )\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?task_types=bad_type\")\n assert response.status_code == 400\n body = response.json()\n assert \"detail\" in body\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_invalid_filter_returns_400\n" }, { "contract_id": "test_get_report_detail_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_detail_api.py", - "start_line": 68, - "end_line": 101, + "start_line": 51, + "end_line": 79, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7431,15 +6393,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_report_detail_success:Function]\n# @RELATION: BINDS_TO -> TestReportsDetailApi\n# @PURPOSE: Validate report detail endpoint returns report body with diagnostics and next actions for existing task.\ndef test_get_report_detail_success():\n task = _make_task(\n \"detail-1\",\n \"superset-migration\",\n TaskStatus.FAILED,\n result={\n \"error\": {\n \"message\": \"Step failed\",\n \"next_actions\": [\"Check mapping\", \"Retry\"],\n }\n },\n )\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/detail-1\")\n assert response.status_code == 200\n\n data = response.json()\n assert \"report\" in data\n assert data[\"report\"][\"report_id\"] == \"detail-1\"\n assert \"diagnostics\" in data\n assert \"next_actions\" in data\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_report_detail_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_report_detail_success [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Validate report detail endpoint returns report body with diagnostics and next actions for existing task.\ndef test_get_report_detail_success():\n task = _make_task(\n \"detail-1\",\n \"superset-migration\",\n TaskStatus.FAILED,\n result={\n \"error\": {\n \"message\": \"Step failed\",\n \"next_actions\": [\"Check mapping\", \"Retry\"],\n }\n },\n )\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/detail-1\")\n assert response.status_code == 200\n data = response.json()\n assert \"report\" in data\n assert data[\"report\"][\"report_id\"] == \"detail-1\"\n assert \"diagnostics\" in data\n assert \"next_actions\" in data\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_report_detail_success\n" }, { "contract_id": "test_get_report_detail_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_detail_api.py", - "start_line": 104, - "end_line": 121, + "start_line": 80, + "end_line": 93, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7464,128 +6426,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_report_detail_not_found:Function]\n# @RELATION: BINDS_TO -> TestReportsDetailApi\n# @PURPOSE: Validate report detail endpoint returns 404 when requested report identifier is absent.\ndef test_get_report_detail_not_found():\n task = _make_task(\"detail-2\", \"superset-backup\", TaskStatus.SUCCESS)\n\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/unknown-id\")\n assert response.status_code == 404\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_get_report_detail_not_found:Function]\n" - }, - { - "contract_id": "TestReportsOpenapiConformance", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py", - "start_line": 1, - "end_line": 118, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "List and detail payloads include required contract keys.", - "LAYER": "Domain (Tests)", - "PURPOSE": "Validate implemented reports payload shape against OpenAPI-required top-level contract fields.", - "SEMANTICS": [ - "tests", - "reports", - "openapi", - "conformance" - ] - }, - "relations": [ - { - "source_id": "TestReportsOpenapiConformance", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "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 BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestReportsOpenapiConformance:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, reports, openapi, conformance\n# @PURPOSE: Validate implemented reports payload shape against OpenAPI-required top-level contract fields.\n# @LAYER: Domain (Tests)\n# @INVARIANT: List and detail payloads include required contract keys.\n\nfrom datetime import datetime\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# [DEF:_FakeTaskManager:Class]\n# @RELATION: BINDS_TO -> [TestReportsOpenapiConformance]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal task-manager fake exposing static task list for OpenAPI conformance checks.\n# @INVARIANT: get_all_tasks returns seeded tasks unchanged.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n\n def get_all_tasks(self):\n return self._tasks\n\n\n# [/DEF:_FakeTaskManager:Class]\n\n\n# [DEF:_admin_user:Function]\n# @RELATION: BINDS_TO -> TestReportsOpenapiConformance\n# @PURPOSE: Provide admin principal fixture required by reports routes in conformance tests.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[role])\n\n\n# [/DEF:_admin_user:Function]\n\n\n# [DEF:_task:Function]\n# @RELATION: BINDS_TO -> TestReportsOpenapiConformance\n# @PURPOSE: Construct deterministic task fixture consumed by reports list/detail payload assertions.\ndef _task(task_id: str, plugin_id: str, status: TaskStatus):\n now = datetime.utcnow()\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now,\n finished_at=now if status != TaskStatus.RUNNING else None,\n params={\"environment_id\": \"env-1\"},\n result={\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n\n\n# [/DEF:_task:Function]\n\n\n# [DEF:test_reports_list_openapi_required_keys:Function]\n# @RELATION: BINDS_TO -> TestReportsOpenapiConformance\n# @PURPOSE: Verify reports list endpoint includes all required OpenAPI top-level keys.\ndef test_reports_list_openapi_required_keys():\n tasks = [\n _task(\"r-1\", \"superset-backup\", TaskStatus.SUCCESS),\n _task(\"r-2\", \"superset-migration\", TaskStatus.FAILED),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n\n body = response.json()\n required = {\n \"items\",\n \"total\",\n \"page\",\n \"page_size\",\n \"has_next\",\n \"applied_filters\",\n }\n assert required.issubset(body.keys())\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_reports_list_openapi_required_keys:Function]\n\n\n# [DEF:test_reports_detail_openapi_required_keys:Function]\n# @RELATION: BINDS_TO -> TestReportsOpenapiConformance\n# @PURPOSE: Verify reports detail endpoint returns payload containing the report object key.\ndef test_reports_detail_openapi_required_keys():\n tasks = [_task(\"r-3\", \"llm_dashboard_validation\", TaskStatus.SUCCESS)]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/r-3\")\n assert response.status_code == 200\n\n body = response.json()\n assert \"report\" in body\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_reports_detail_openapi_required_keys:Function]\n# [/DEF:TestReportsOpenapiConformance:Module]\n" - }, - { - "contract_id": "_FakeTaskManager", - "contract_type": "Class", - "file_path": "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py", - "start_line": 19, - "end_line": 32, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "get_all_tasks returns seeded tasks unchanged.", - "PURPOSE": "Minimal task-manager fake exposing static task list for OpenAPI conformance checks." - }, - "relations": [ - { - "source_id": "_FakeTaskManager", - "relation_type": "BINDS_TO", - "target_id": "TestReportsOpenapiConformance", - "target_ref": "[TestReportsOpenapiConformance]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_FakeTaskManager:Class]\n# @RELATION: BINDS_TO -> [TestReportsOpenapiConformance]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal task-manager fake exposing static task list for OpenAPI conformance checks.\n# @INVARIANT: get_all_tasks returns seeded tasks unchanged.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n\n def get_all_tasks(self):\n return self._tasks\n\n\n# [/DEF:_FakeTaskManager:Class]\n" + "anchor_syntax": "region", + "body": "# #region test_get_report_detail_not_found [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Validate report detail endpoint returns 404 when requested report identifier is absent.\ndef test_get_report_detail_not_found():\n task = _make_task(\"detail-2\", \"superset-backup\", TaskStatus.SUCCESS)\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/unknown-id\")\n assert response.status_code == 404\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_report_detail_not_found\n" }, { "contract_id": "_admin_user", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py", - "start_line": 35, - "end_line": 43, + "start_line": 26, + "end_line": 32, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -7619,15 +6468,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_admin_user:Function]\n# @RELATION: BINDS_TO -> TestReportsOpenapiConformance\n# @PURPOSE: Provide admin principal fixture required by reports routes in conformance tests.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[role])\n\n\n# [/DEF:_admin_user:Function]\n" + "anchor_syntax": "region", + "body": "# #region _admin_user [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Provide admin principal fixture required by reports routes in conformance tests.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[role])\n# #endregion _admin_user\n" }, { "contract_id": "_task", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py", - "start_line": 46, - "end_line": 62, + "start_line": 33, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -7661,15 +6510,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_task:Function]\n# @RELATION: BINDS_TO -> TestReportsOpenapiConformance\n# @PURPOSE: Construct deterministic task fixture consumed by reports list/detail payload assertions.\ndef _task(task_id: str, plugin_id: str, status: TaskStatus):\n now = datetime.utcnow()\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now,\n finished_at=now if status != TaskStatus.RUNNING else None,\n params={\"environment_id\": \"env-1\"},\n result={\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n\n\n# [/DEF:_task:Function]\n" + "anchor_syntax": "region", + "body": "# #region _task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Construct deterministic task fixture consumed by reports list/detail payload assertions.\ndef _task(task_id: str, plugin_id: str, status: TaskStatus):\n now = datetime.utcnow()\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now,\n finished_at=now if status != TaskStatus.RUNNING else None,\n params={\"environment_id\": \"env-1\"},\n result={\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n# #endregion _task\n" }, { "contract_id": "test_reports_list_openapi_required_keys", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py", - "start_line": 65, - "end_line": 95, + "start_line": 48, + "end_line": 74, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7694,15 +6543,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_reports_list_openapi_required_keys:Function]\n# @RELATION: BINDS_TO -> TestReportsOpenapiConformance\n# @PURPOSE: Verify reports list endpoint includes all required OpenAPI top-level keys.\ndef test_reports_list_openapi_required_keys():\n tasks = [\n _task(\"r-1\", \"superset-backup\", TaskStatus.SUCCESS),\n _task(\"r-2\", \"superset-migration\", TaskStatus.FAILED),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n\n body = response.json()\n required = {\n \"items\",\n \"total\",\n \"page\",\n \"page_size\",\n \"has_next\",\n \"applied_filters\",\n }\n assert required.issubset(body.keys())\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_reports_list_openapi_required_keys:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_reports_list_openapi_required_keys [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Verify reports list endpoint includes all required OpenAPI top-level keys.\ndef test_reports_list_openapi_required_keys():\n tasks = [\n _task(\"r-1\", \"superset-backup\", TaskStatus.SUCCESS),\n _task(\"r-2\", \"superset-migration\", TaskStatus.FAILED),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n body = response.json()\n required = {\n \"items\",\n \"total\",\n \"page\",\n \"page_size\",\n \"has_next\",\n \"applied_filters\",\n }\n assert required.issubset(body.keys())\n finally:\n app.dependency_overrides.clear()\n# #endregion test_reports_list_openapi_required_keys\n" }, { "contract_id": "test_reports_detail_openapi_required_keys", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py", - "start_line": 98, - "end_line": 117, + "start_line": 75, + "end_line": 90, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7727,79 +6576,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_reports_detail_openapi_required_keys:Function]\n# @RELATION: BINDS_TO -> TestReportsOpenapiConformance\n# @PURPOSE: Verify reports detail endpoint returns payload containing the report object key.\ndef test_reports_detail_openapi_required_keys():\n tasks = [_task(\"r-3\", \"llm_dashboard_validation\", TaskStatus.SUCCESS)]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/r-3\")\n assert response.status_code == 200\n\n body = response.json()\n assert \"report\" in body\n finally:\n app.dependency_overrides.clear()\n\n\n# [/DEF:test_reports_detail_openapi_required_keys:Function]\n" - }, - { - "contract_id": "test_tasks_logs_module", - "contract_type": "Module", - "file_path": "backend/src/api/routes/__tests__/test_tasks_logs.py", - "start_line": 1, - "end_line": 108, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "LAYER": "Domain (Tests)", - "PURPOSE": "Contract testing for task logs API endpoints.", - "SEMANTICS": [ - "tests", - "tasks", - "logs", - "api", - "contract", - "validation" - ], - "TEST_FIXTURE": "mock_app" - }, - "relations": [ - { - "source_id": "test_tasks_logs_module", - "relation_type": "VERIFIES", - "target_id": "src.api.routes.tasks:Module", - "target_ref": "[src.api.routes.tasks:Module]" - } - ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE 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": "def", - "body": "# [DEF:test_tasks_logs_module:Module]\n# @RELATION: VERIFIES -> [src.api.routes.tasks:Module]\n# @COMPLEXITY: 2\n# @SEMANTICS: tests, tasks, logs, api, contract, validation\n# @PURPOSE: Contract testing for task logs API endpoints.\n# @LAYER: Domain (Tests)\n\nimport pytest\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\nfrom unittest.mock import MagicMock\nfrom src.dependencies import get_task_manager, has_permission\nfrom src.api.routes.tasks import router\n\n\n# @TEST_FIXTURE: mock_app\n@pytest.fixture\ndef client():\n app = FastAPI()\n app.include_router(router, prefix=\"/tasks\")\n\n # Mock TaskManager\n # @INVARIANT: unconstrained mock — no spec= enforced\n mock_tm = MagicMock()\n app.dependency_overrides[get_task_manager] = lambda: mock_tm\n\n # Mock permissions (bypass for unit test)\n app.dependency_overrides[has_permission(\"tasks\", \"READ\")] = lambda: True\n\n return TestClient(app), mock_tm\n\n\n# @TEST_CONTRACT: get_task_logs_api -> Invariants\n# @TEST_FIXTURE: valid_task_logs_request\n# [DEF:test_get_task_logs_success:Function]\n# @RELATION: BINDS_TO -> test_tasks_logs_module\n# @PURPOSE: Validate task logs endpoint returns filtered logs for an existing task.\ndef test_get_task_logs_success(client):\n tc, tm = client\n\n # Setup mock task\n mock_task = MagicMock()\n tm.get_task.return_value = mock_task\n tm.get_task_logs.return_value = [{\"level\": \"INFO\", \"message\": \"msg1\"}]\n\n response = tc.get(\"/tasks/task-1/logs?level=INFO\")\n\n assert response.status_code == 200\n assert response.json() == [{\"level\": \"INFO\", \"message\": \"msg1\"}]\n tm.get_task.assert_called_with(\"task-1\")\n # Verify filter construction inside route\n args = tm.get_task_logs.call_args\n assert args[0][0] == \"task-1\"\n assert args[0][1].level == \"INFO\"\n\n\n# @TEST_EDGE: task_not_found\n# [/DEF:test_get_task_logs_success:Function]\n\n\n# [DEF:test_get_task_logs_not_found:Function]\n# @RELATION: BINDS_TO -> test_tasks_logs_module\n# @PURPOSE: Validate task logs endpoint returns 404 when the task identifier is missing.\ndef test_get_task_logs_not_found(client):\n tc, tm = client\n tm.get_task.return_value = None\n\n response = tc.get(\"/tasks/missing/logs\")\n assert response.status_code == 404\n assert response.json()[\"detail\"] == \"Task not found\"\n\n\n# @TEST_EDGE: invalid_limit\n# [/DEF:test_get_task_logs_not_found:Function]\n\n\n# [DEF:test_get_task_logs_invalid_limit:Function]\n# @RELATION: BINDS_TO -> test_tasks_logs_module\n# @PURPOSE: Validate task logs endpoint enforces query validation for limit lower bound.\ndef test_get_task_logs_invalid_limit(client):\n tc, tm = client\n # limit=0 is ge=1 in Query\n response = tc.get(\"/tasks/task-1/logs?limit=0\")\n assert response.status_code == 422\n\n\n# @TEST_INVARIANT: response_purity\n# [/DEF:test_get_task_logs_invalid_limit:Function]\n\n\n# [DEF:test_get_task_log_stats_success:Function]\n# @RELATION: BINDS_TO -> test_tasks_logs_module\n# @PURPOSE: Validate log stats endpoint returns success payload for an existing task.\ndef test_get_task_log_stats_success(client):\n tc, tm = client\n tm.get_task.return_value = MagicMock()\n tm.get_task_log_stats.return_value = {\"INFO\": 5, \"ERROR\": 1}\n\n response = tc.get(\"/tasks/task-1/logs/stats\")\n assert response.status_code == 200\n\n\n# response_model=LogStats might wrap this, but let's check basic structure\n# assuming tm.get_task_log_stats returns something compatible with LogStats\n\n\n# [/DEF:test_get_task_log_stats_success:Function]\n# [/DEF:test_tasks_logs_module:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_reports_detail_openapi_required_keys [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Verify reports detail endpoint returns payload containing the report object key.\ndef test_reports_detail_openapi_required_keys():\n tasks = [_task(\"r-3\", \"llm_dashboard_validation\", TaskStatus.SUCCESS)]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/r-3\")\n assert response.status_code == 200\n body = response.json()\n assert \"report\" in body\n finally:\n app.dependency_overrides.clear()\n# #endregion test_reports_detail_openapi_required_keys\n" }, { "contract_id": "test_get_task_logs_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_tasks_logs.py", - "start_line": 35, - "end_line": 58, + "start_line": 29, + "end_line": 47, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7824,15 +6609,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_task_logs_success:Function]\n# @RELATION: BINDS_TO -> test_tasks_logs_module\n# @PURPOSE: Validate task logs endpoint returns filtered logs for an existing task.\ndef test_get_task_logs_success(client):\n tc, tm = client\n\n # Setup mock task\n mock_task = MagicMock()\n tm.get_task.return_value = mock_task\n tm.get_task_logs.return_value = [{\"level\": \"INFO\", \"message\": \"msg1\"}]\n\n response = tc.get(\"/tasks/task-1/logs?level=INFO\")\n\n assert response.status_code == 200\n assert response.json() == [{\"level\": \"INFO\", \"message\": \"msg1\"}]\n tm.get_task.assert_called_with(\"task-1\")\n # Verify filter construction inside route\n args = tm.get_task_logs.call_args\n assert args[0][0] == \"task-1\"\n assert args[0][1].level == \"INFO\"\n\n\n# @TEST_EDGE: task_not_found\n# [/DEF:test_get_task_logs_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_task_logs_success [TYPE Function]\n# @RELATION BINDS_TO -> test_tasks_logs_module\n# @BRIEF Validate task logs endpoint returns filtered logs for an existing task.\ndef test_get_task_logs_success(client):\n tc, tm = client\n # Setup mock task\n mock_task = MagicMock()\n tm.get_task.return_value = mock_task\n tm.get_task_logs.return_value = [{\"level\": \"INFO\", \"message\": \"msg1\"}]\n response = tc.get(\"/tasks/task-1/logs?level=INFO\")\n assert response.status_code == 200\n assert response.json() == [{\"level\": \"INFO\", \"message\": \"msg1\"}]\n tm.get_task.assert_called_with(\"task-1\")\n # Verify filter construction inside route\n args = tm.get_task_logs.call_args\n assert args[0][0] == \"task-1\"\n assert args[0][1].level == \"INFO\"\n# @TEST_EDGE: task_not_found\n# #endregion test_get_task_logs_success\n" }, { "contract_id": "test_get_task_logs_not_found", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_tasks_logs.py", - "start_line": 61, - "end_line": 74, + "start_line": 48, + "end_line": 58, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7858,15 +6643,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_task_logs_not_found:Function]\n# @RELATION: BINDS_TO -> test_tasks_logs_module\n# @PURPOSE: Validate task logs endpoint returns 404 when the task identifier is missing.\ndef test_get_task_logs_not_found(client):\n tc, tm = client\n tm.get_task.return_value = None\n\n response = tc.get(\"/tasks/missing/logs\")\n assert response.status_code == 404\n assert response.json()[\"detail\"] == \"Task not found\"\n\n\n# @TEST_EDGE: invalid_limit\n# [/DEF:test_get_task_logs_not_found:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_task_logs_not_found [TYPE Function]\n# @RELATION BINDS_TO -> test_tasks_logs_module\n# @BRIEF Validate task logs endpoint returns 404 when the task identifier is missing.\ndef test_get_task_logs_not_found(client):\n tc, tm = client\n tm.get_task.return_value = None\n response = tc.get(\"/tasks/missing/logs\")\n assert response.status_code == 404\n assert response.json()[\"detail\"] == \"Task not found\"\n# @TEST_EDGE: invalid_limit\n# #endregion test_get_task_logs_not_found\n" }, { "contract_id": "test_get_task_logs_invalid_limit", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_tasks_logs.py", - "start_line": 77, - "end_line": 88, + "start_line": 59, + "end_line": 68, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7891,15 +6676,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_task_logs_invalid_limit:Function]\n# @RELATION: BINDS_TO -> test_tasks_logs_module\n# @PURPOSE: Validate task logs endpoint enforces query validation for limit lower bound.\ndef test_get_task_logs_invalid_limit(client):\n tc, tm = client\n # limit=0 is ge=1 in Query\n response = tc.get(\"/tasks/task-1/logs?limit=0\")\n assert response.status_code == 422\n\n\n# @TEST_INVARIANT: response_purity\n# [/DEF:test_get_task_logs_invalid_limit:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_task_logs_invalid_limit [TYPE Function]\n# @RELATION BINDS_TO -> test_tasks_logs_module\n# @BRIEF Validate task logs endpoint enforces query validation for limit lower bound.\ndef test_get_task_logs_invalid_limit(client):\n tc, tm = client\n # limit=0 is ge=1 in Query\n response = tc.get(\"/tasks/task-1/logs?limit=0\")\n assert response.status_code == 422\n# @TEST_INVARIANT: response_purity\n# #endregion test_get_task_logs_invalid_limit\n" }, { "contract_id": "test_get_task_log_stats_success", "contract_type": "Function", "file_path": "backend/src/api/routes/__tests__/test_tasks_logs.py", - "start_line": 91, - "end_line": 107, + "start_line": 69, + "end_line": 80, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -7924,46 +6709,39 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_task_log_stats_success:Function]\n# @RELATION: BINDS_TO -> test_tasks_logs_module\n# @PURPOSE: Validate log stats endpoint returns success payload for an existing task.\ndef test_get_task_log_stats_success(client):\n tc, tm = client\n tm.get_task.return_value = MagicMock()\n tm.get_task_log_stats.return_value = {\"INFO\": 5, \"ERROR\": 1}\n\n response = tc.get(\"/tasks/task-1/logs/stats\")\n assert response.status_code == 200\n\n\n# response_model=LogStats might wrap this, but let's check basic structure\n# assuming tm.get_task_log_stats returns something compatible with LogStats\n\n\n# [/DEF:test_get_task_log_stats_success:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_task_log_stats_success [TYPE Function]\n# @RELATION BINDS_TO -> test_tasks_logs_module\n# @BRIEF Validate log stats endpoint returns success payload for an existing task.\ndef test_get_task_log_stats_success(client):\n tc, tm = client\n tm.get_task.return_value = MagicMock()\n tm.get_task_log_stats.return_value = {\"INFO\": 5, \"ERROR\": 1}\n response = tc.get(\"/tasks/task-1/logs/stats\")\n assert response.status_code == 200\n# response_model=LogStats might wrap this, but let's check basic structure\n# assuming tm.get_task_log_stats returns something compatible with LogStats\n# #endregion test_get_task_log_stats_success\n" }, { "contract_id": "AdminApi", "contract_type": "Module", "file_path": "backend/src/api/routes/admin.py", "start_line": 1, - "end_line": 395, + "end_line": 366, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "All endpoints in this module require 'Admin' role or 'admin' scope.", "LAYER": "API", - "PURPOSE": "Admin API endpoints for user and role management.", - "SEMANTICS": [ - "api", - "admin", - "users", - "roles", - "permissions" - ] + "PURPOSE": "Admin API endpoints for user and role management." }, "relations": [ { "source_id": "AdminApi", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AuthRepository:Class", "target_ref": "[AuthRepository:Class]" }, { "source_id": "AdminApi", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "get_auth_db:Function", "target_ref": "[get_auth_db:Function]" }, { "source_id": "AdminApi", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "has_permission:Function", "target_ref": "[has_permission:Function]" } @@ -7977,80 +6755,24 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:AdminApi:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: api, admin, users, roles, permissions\n# @PURPOSE: 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# [SECTION: IMPORTS]\nfrom typing import List\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\nfrom ...core.database import get_auth_db\nfrom ...core.auth.repository import AuthRepository\nfrom ...core.auth.security import get_password_hash\nfrom ...schemas.auth import (\n User as UserSchema,\n UserCreate,\n UserUpdate,\n RoleSchema,\n RoleCreate,\n RoleUpdate,\n PermissionSchema,\n ADGroupMappingSchema,\n ADGroupMappingCreate,\n)\nfrom ...models.auth import User, Role, ADGroupMapping\nfrom ...dependencies import has_permission, get_plugin_loader\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.logger import logger, belief_scope\n\nlog = MarkerLogger(\"AdminApi\")\nfrom ...services.rbac_permission_catalog import (\n discover_declared_permissions,\n sync_permission_catalog,\n)\n# [/SECTION]\n\n# [DEF:router:Variable]\n# @RELATION: DEPENDS_ON -> fastapi.APIRouter\n# @PURPOSE: APIRouter instance for admin routes.\nrouter = APIRouter(prefix=\"/api/admin\", tags=[\"admin\"])\n# [/DEF:router:Variable]\n\n\n# [DEF:list_users:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Lists all registered users.\n# @PRE: Current user has 'Admin' role.\n# @POST: Returns a list of UserSchema objects.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: List[UserSchema] - List of users.\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# [/DEF:list_users:Function]\n\n\n# [DEF:create_user:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Creates a new local user.\n# @PRE: Current user has 'Admin' role.\n# @POST: New user is created in the database.\n# @PARAM: user_in (UserCreate) - New user data.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: UserSchema - The created user.\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# [/DEF:create_user:Function]\n\n\n# [DEF:update_user:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Updates an existing user.\n# @PRE: Current user has 'Admin' role.\n# @POST: User record is updated in the database.\n# @PARAM: user_id (str) - Target user UUID.\n# @PARAM: user_in (UserUpdate) - Updated user data.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: UserSchema - The updated user profile.\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# [/DEF:update_user:Function]\n\n\n# [DEF:delete_user:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Deletes a user.\n# @PRE: Current user has 'Admin' role.\n# @POST: User record is removed from the database.\n# @PARAM: user_id (str) - Target user UUID.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: None\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 log.reason(f\"Attempting to delete user {user_id}\")\n repo = AuthRepository(db)\n user = repo.get_user_by_id(user_id)\n if not user:\n log.explore(f\"User not found for deletion: {user_id}\", error=\"User not found\")\n raise HTTPException(status_code=404, detail=\"User not found\")\n\n log.reason(f\"Found user to delete: {user.username}\")\n db.delete(user)\n db.commit()\n log.reflect(f\"Successfully deleted user {user_id}\")\n return None\n\n\n# [/DEF:delete_user:Function]\n\n\n# [DEF:list_roles:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Lists all available roles.\n# @RETURN: List[RoleSchema] - List of 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# [/DEF:list_roles:Function]\n\n\n# [DEF:create_role:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: role_in (RoleCreate) - New role data.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: RoleSchema - The created role.\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# [/DEF:create_role:Function]\n\n\n# [DEF:update_role:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: role_id (str) - Target role identifier.\n# @PARAM: role_in (RoleUpdate) - Updated role data.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: RoleSchema - The updated role.\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# [/DEF:update_role:Function]\n\n\n# [DEF:delete_role:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: role_id (str) - Target role identifier.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: None\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# [/DEF:delete_role:Function]\n\n\n# [DEF:list_permissions:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Lists all available system permissions for assignment.\n# @POST: Returns a list of all PermissionSchema objects.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: List[PermissionSchema] - List of permissions.\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 log.reason(f\"Synchronized {inserted_count} missing RBAC permissions into auth catalog\")\n\n repo = AuthRepository(db)\n return repo.list_permissions()\n\n\n# [/DEF:list_permissions:Function]\n\n\n# [DEF:list_ad_mappings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:list_ad_mappings:Function]\n\n\n# [DEF:create_ad_mapping:Function]\n# @RELATION: [DEPENDS_ON] ->[ADGroupMapping:Class]\n# @RELATION: [DEPENDS_ON] ->[get_auth_db:Function]\n# @RELATION: [DEPENDS_ON] ->[has_permission:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:create_ad_mapping:Function]\n\n# [/DEF:AdminApi:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "list_users", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 50, - "end_line": 67, + "start_line": 46, + "end_line": 60, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "Returns a list of UserSchema objects.", "PRE": "Current user has 'Admin' role.", - "PURPOSE": "Lists all registered users.", - "RETURN": "List[UserSchema] - List of users." + "PURPOSE": "Lists all registered users." }, "relations": [ { @@ -8061,12 +6783,6 @@ } ], "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", @@ -8084,48 +6800,34 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:list_users:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Lists all registered users.\n# @PRE: Current user has 'Admin' role.\n# @POST: Returns a list of UserSchema objects.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: List[UserSchema] - List of users.\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# [/DEF:list_users:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "create_user", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 70, - "end_line": 109, + "start_line": 63, + "end_line": 98, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "New user is created in the database.", "PRE": "Current user has 'Admin' role.", - "PURPOSE": "Creates a new local user.", - "RETURN": "UserSchema - The created user." + "PURPOSE": "Creates a new local user." }, "relations": [ { "source_id": "create_user", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "AuthRepository:Class", "target_ref": "[AuthRepository:Class]" } ], "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", @@ -8143,50 +6845,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:create_user:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Creates a new local user.\n# @PRE: Current user has 'Admin' role.\n# @POST: New user is created in the database.\n# @PARAM: user_in (UserCreate) - New user data.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: UserSchema - The created user.\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# [/DEF:create_user:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "update_user", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 112, - "end_line": 154, + "start_line": 101, + "end_line": 138, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "User record is updated in the database.", "PRE": "Current user has 'Admin' role.", - "PURPOSE": "Updates an existing user.", - "RETURN": "UserSchema - The updated user profile." + "PURPOSE": "Updates an existing user." }, "relations": [ { @@ -8197,12 +6873,6 @@ } ], "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", @@ -8220,32 +6890,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:update_user:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Updates an existing user.\n# @PRE: Current user has 'Admin' role.\n# @POST: User record is updated in the database.\n# @PARAM: user_id (str) - Target user UUID.\n# @PARAM: user_in (UserUpdate) - Updated user data.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: UserSchema - The updated user profile.\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# [/DEF:update_user:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "delete_user", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 157, - "end_line": 187, + "start_line": 141, + "end_line": 175, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "User record is removed from the database.", "PRE": "Current user has 'Admin' role.", - "PURPOSE": "Deletes a user.", - "RETURN": "None" + "PURPOSE": "Deletes a user." }, "relations": [ { @@ -8256,12 +6918,6 @@ } ], "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", @@ -8279,99 +6935,59 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:delete_user:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Deletes a user.\n# @PRE: Current user has 'Admin' role.\n# @POST: User record is removed from the database.\n# @PARAM: user_id (str) - Target user UUID.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: None\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 log.reason(f\"Attempting to delete user {user_id}\")\n repo = AuthRepository(db)\n user = repo.get_user_by_id(user_id)\n if not user:\n log.explore(f\"User not found for deletion: {user_id}\", error=\"User not found\")\n raise HTTPException(status_code=404, detail=\"User not found\")\n\n log.reason(f\"Found user to delete: {user.username}\")\n db.delete(user)\n db.commit()\n log.reflect(f\"Successfully deleted user {user_id}\")\n return None\n\n\n# [/DEF:delete_user:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "list_roles", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 190, - "end_line": 203, + "start_line": 178, + "end_line": 189, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Lists all available roles.", - "RETURN": "List[RoleSchema] - List of roles." + "COMPLEXITY": 3, + "PURPOSE": "Lists all available roles." }, "relations": [ { "source_id": "list_roles", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "Role:Class", "target_ref": "[Role:Class]" } ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:list_roles:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Lists all available roles.\n# @RETURN: List[RoleSchema] - List of 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# [/DEF:list_roles:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "create_role", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 206, - "end_line": 244, + "start_line": 192, + "end_line": 226, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "New Role record is created in auth.db.", "PRE": "Role name must be unique.", "PURPOSE": "Creates a new system role with associated permissions.", - "RETURN": "RoleSchema - The created role.", "SIDE_EFFECT": "Commits new role and associations to auth.db." }, "relations": [ { "source_id": "create_role", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "get_permission_by_id:Function", "target_ref": "[get_permission_by_id:Function]" } ], "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", @@ -8390,12 +7006,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -8404,61 +7014,35 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:create_role:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: role_in (RoleCreate) - New role data.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: RoleSchema - The created role.\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# [/DEF:create_role:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "update_role", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 247, - "end_line": 292, + "start_line": 229, + "end_line": 269, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "Role record is updated in auth.db.", "PRE": "role_id must be a valid existing role UUID.", "PURPOSE": "Updates an existing role's metadata and permissions.", - "RETURN": "RoleSchema - The updated role.", "SIDE_EFFECT": "Commits updates to auth.db." }, "relations": [ { "source_id": "update_role", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "get_role_by_id:Function", "target_ref": "[get_role_by_id:Function]" } ], "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", @@ -8477,12 +7061,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -8491,61 +7069,35 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_role:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: role_id (str) - Target role identifier.\n# @PARAM: role_in (RoleUpdate) - Updated role data.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: RoleSchema - The updated role.\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# [/DEF:update_role:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "delete_role", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 295, - "end_line": 322, + "start_line": 272, + "end_line": 295, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "Role record is removed from auth.db.", "PRE": "role_id must be a valid existing role UUID.", "PURPOSE": "Removes a role from the system.", - "RETURN": "None", "SIDE_EFFECT": "Deletes record from auth.db and commits." }, "relations": [ { "source_id": "delete_role", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "get_role_by_id:Function", "target_ref": "[get_role_by_id:Function]" } ], "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", @@ -8564,12 +7116,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -8578,39 +7124,21 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:delete_role:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: role_id (str) - Target role identifier.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: None\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# [/DEF:delete_role:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "list_ad_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 355, - "end_line": 368, + "start_line": 328, + "end_line": 340, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Lists all AD Group to Role mappings." }, "relations": [ @@ -8622,37 +7150,37 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:list_ad_mappings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:list_ad_mappings:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "create_ad_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/admin.py", - "start_line": 371, - "end_line": 393, + "start_line": 343, + "end_line": 364, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Creates a new AD Group mapping." }, "relations": [ { "source_id": "create_ad_mapping", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ADGroupMapping:Class", "target_ref": "[ADGroupMapping:Class]" }, { "source_id": "create_ad_mapping", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "get_auth_db:Function", "target_ref": "[get_auth_db:Function]" }, { "source_id": "create_ad_mapping", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "has_permission:Function", "target_ref": "[has_permission:Function]" } @@ -8666,75 +7194,21 @@ "actual_complexity": 2, "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:create_ad_mapping:Function]\n# @RELATION: [DEPENDS_ON] ->[ADGroupMapping:Class]\n# @RELATION: [DEPENDS_ON] ->[get_auth_db:Function]\n# @RELATION: [DEPENDS_ON] ->[has_permission:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:create_ad_mapping:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "AssistantAdminRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_admin_routes.py", "start_line": 1, - "end_line": 305, + "end_line": 296, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "INVARIANT": "Audit endpoint requires tasks:READ permission.", "LAYER": "API", "PURPOSE": "FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit." @@ -8815,23 +7289,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantAdminRoutes:Module]\n# @COMPLEXITY: 5\n# @PURPOSE: FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [AssistantRoutes]\n# @RELATION: DEPENDS_ON -> [AssistantSchemas]\n# @RELATION: DEPENDS_ON -> [AssistantHistory]\n# @INVARIANT: Audit endpoint requires tasks:READ permission.\n\nfrom __future__ import annotations\n\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, Optional\n\nfrom fastapi import Depends, HTTPException, Query\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import desc\n\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_current_user, has_permission\nfrom src.core.database import get_db\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantMessageRecord,\n)\nfrom src.schemas.auth import User\nfrom ._schemas import (\n ASSISTANT_AUDIT,\n CONVERSATIONS,\n AssistantAction,\n AssistantMessageResponse,\n)\nfrom ._history import (\n _cleanup_history_ttl,\n _coerce_query_bool,\n _is_conversation_archived,\n _persist_audit,\n _resolve_or_create_conversation,\n)\nfrom ._routes import router\n\n\n# [DEF:list_conversations:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Return paginated conversation list for current user with archived flag and last message preview.\n# @PRE: Authenticated user context and valid pagination params.\n# @POST: Conversations are grouped by conversation_id sorted by latest activity descending.\n# @RETURN: Dict with items, paging metadata, and archive segmentation counts.\n@router.get(\"/conversations\")\nasync def list_conversations(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n include_archived: bool = Query(False),\n archived_only: bool = Query(False),\n search: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations\"):\n user_id = current_user.id\n include_archived = _coerce_query_bool(include_archived)\n archived_only = _coerce_query_bool(archived_only)\n _cleanup_history_ttl(db, user_id)\n\n rows = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .all()\n )\n\n summary: Dict[str, Dict[str, Any]] = {}\n for row in rows:\n conv_id = row.conversation_id\n if not conv_id:\n continue\n created_at = row.created_at or datetime.utcnow()\n if conv_id not in summary:\n summary[conv_id] = {\n \"conversation_id\": conv_id,\n \"title\": \"\",\n \"updated_at\": created_at,\n \"last_message\": row.text,\n \"last_role\": row.role,\n \"last_state\": row.state,\n \"last_task_id\": row.task_id,\n \"message_count\": 0,\n }\n item = summary[conv_id]\n item[\"message_count\"] += 1\n if row.role == \"user\" and row.text and not item[\"title\"]:\n item[\"title\"] = row.text.strip()[:80]\n\n items = []\n search_term = search.lower().strip() if search else \"\"\n archived_total = sum(\n 1\n for c in summary.values()\n if _is_conversation_archived(c.get(\"updated_at\"))\n )\n active_total = len(summary) - archived_total\n for conv in summary.values():\n conv[\"archived\"] = _is_conversation_archived(conv.get(\"updated_at\"))\n if not conv.get(\"title\"):\n conv[\"title\"] = f\"Conversation {conv['conversation_id'][:8]}\"\n if search_term:\n haystack = (\n f\"{conv.get('title', '')} {conv.get('last_message', '')}\".lower()\n )\n if search_term not in haystack:\n continue\n if archived_only and not conv[\"archived\"]:\n continue\n if not archived_only and not include_archived and conv[\"archived\"]:\n continue\n updated = conv.get(\"updated_at\")\n conv[\"updated_at\"] = (\n updated.isoformat() if isinstance(updated, datetime) else None\n )\n items.append(conv)\n\n items.sort(key=lambda x: x.get(\"updated_at\") or \"\", reverse=True)\n total = len(items)\n start = (page - 1) * page_size\n page_items = items[start : start + page_size]\n\n return {\n \"items\": page_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"active_total\": active_total,\n \"archived_total\": archived_total,\n }\n\n\n# [/DEF:list_conversations:Function]\n\n\n# [DEF:delete_conversation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Soft-delete or hard-delete a conversation and clear its in-memory trace.\n# @PRE: conversation_id belongs to current_user.\n# @POST: Conversation records are removed from DB and CONVERSATIONS cache.\n@router.delete(\"/conversations/{conversation_id}\")\nasync def delete_conversation(\n conversation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations.delete\"):\n user_id = current_user.id\n\n # 1. Remove from in-memory cache\n key = (user_id, conversation_id)\n if key in CONVERSATIONS:\n del CONVERSATIONS[key]\n\n # 2. Delete from database\n deleted_count = (\n db.query(AssistantMessageRecord)\n .filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conversation_id,\n )\n .delete()\n )\n\n db.commit()\n\n if deleted_count == 0:\n raise HTTPException(\n status_code=404, detail=\"Conversation not found or already deleted\"\n )\n\n return {\n \"status\": \"success\",\n \"deleted\": deleted_count,\n \"conversation_id\": conversation_id,\n }\n\n\n# [/DEF:delete_conversation:Function]\n\n\n@router.get(\"/history\")\n# [DEF:get_history:Function]\n# @PURPOSE: Retrieve paginated assistant conversation history for current user.\n# @PRE: Authenticated user is available and page params are valid.\n# @POST: Returns persistent messages and mirrored in-memory snapshot for diagnostics.\n# @RETURN: Dict with items, paging metadata, and resolved conversation_id.\nasync def get_history(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n conversation_id: Optional[str] = Query(None),\n from_latest: bool = Query(False),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.history\"):\n user_id = current_user.id\n _cleanup_history_ttl(db, user_id)\n conv_id = _resolve_or_create_conversation(user_id, conversation_id, db)\n\n base_query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conv_id,\n )\n total = base_query.count()\n start = (page - 1) * page_size\n if from_latest:\n rows = (\n base_query.order_by(desc(AssistantMessageRecord.created_at))\n .offset(start)\n .limit(page_size)\n .all()\n )\n rows = list(reversed(rows))\n else:\n rows = (\n base_query.order_by(AssistantMessageRecord.created_at.asc())\n .offset(start)\n .limit(page_size)\n .all()\n )\n\n persistent_items = [\n {\n \"message_id\": row.id,\n \"conversation_id\": row.conversation_id,\n \"role\": row.role,\n \"text\": row.text,\n \"state\": row.state,\n \"task_id\": row.task_id,\n \"confirmation_id\": row.confirmation_id,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n \"metadata\": row.payload,\n }\n for row in rows\n ]\n\n memory_items = CONVERSATIONS.get((user_id, conv_id), [])\n return {\n \"items\": persistent_items,\n \"memory_items\": memory_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"from_latest\": from_latest,\n \"conversation_id\": conv_id,\n }\n\n\n# [/DEF:get_history:Function]\n\n\n@router.get(\"/audit\")\n# [DEF:get_assistant_audit:Function]\n# @PURPOSE: Return assistant audit decisions for current user from persistent and in-memory stores.\n# @PRE: User has tasks:READ permission.\n# @POST: Audit payload is returned in reverse chronological order from DB.\n# @RETURN: Dict with persistent and memory audit slices.\nasync def get_assistant_audit(\n limit: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"assistant.audit\"):\n memory_rows = ASSISTANT_AUDIT.get(current_user.id, [])\n db_rows = (\n db.query(AssistantAuditRecord)\n .filter(AssistantAuditRecord.user_id == current_user.id)\n .order_by(AssistantAuditRecord.created_at.desc())\n .limit(limit)\n .all()\n )\n persistent = [\n {\n \"id\": row.id,\n \"user_id\": row.user_id,\n \"conversation_id\": row.conversation_id,\n \"decision\": row.decision,\n \"task_id\": row.task_id,\n \"message\": row.message,\n \"payload\": row.payload,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n }\n for row in db_rows\n ]\n return {\n \"items\": persistent,\n \"memory_items\": memory_rows[-limit:],\n \"total\": len(persistent),\n \"memory_total\": len(memory_rows),\n }\n\n\n# [/DEF:get_assistant_audit:Function]\n\n\n# [/DEF:AssistantAdminRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantAdminRoutes [C:5] [TYPE Module] [SEMANTICS assistant, admin, route, audit, conversation]\n# @BRIEF FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantRoutes]\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantHistory]\n# @INVARIANT: Audit endpoint requires tasks:READ permission.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom fastapi import Depends, HTTPException, Query\nfrom sqlalchemy import desc\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_current_user, has_permission\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantMessageRecord,\n)\nfrom src.schemas.auth import User\n\nfrom ._history import (\n _cleanup_history_ttl,\n _coerce_query_bool,\n _is_conversation_archived,\n _resolve_or_create_conversation,\n)\nfrom ._routes import router\nfrom ._schemas import (\n ASSISTANT_AUDIT,\n CONVERSATIONS,\n)\n\n\n# #region list_conversations [C:2] [TYPE Function]\n# @BRIEF Return paginated conversation list for current user with archived flag and last message preview.\n# @PRE: Authenticated user context and valid pagination params.\n# @POST: Conversations are grouped by conversation_id sorted by latest activity descending.\n@router.get(\"/conversations\")\nasync def list_conversations(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n include_archived: bool = Query(False),\n archived_only: bool = Query(False),\n search: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations\"):\n user_id = current_user.id\n include_archived = _coerce_query_bool(include_archived)\n archived_only = _coerce_query_bool(archived_only)\n _cleanup_history_ttl(db, user_id)\n\n rows = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .all()\n )\n\n summary: dict[str, dict[str, Any]] = {}\n for row in rows:\n conv_id = row.conversation_id\n if not conv_id:\n continue\n created_at = row.created_at or datetime.utcnow()\n if conv_id not in summary:\n summary[conv_id] = {\n \"conversation_id\": conv_id,\n \"title\": \"\",\n \"updated_at\": created_at,\n \"last_message\": row.text,\n \"last_role\": row.role,\n \"last_state\": row.state,\n \"last_task_id\": row.task_id,\n \"message_count\": 0,\n }\n item = summary[conv_id]\n item[\"message_count\"] += 1\n if row.role == \"user\" and row.text and not item[\"title\"]:\n item[\"title\"] = row.text.strip()[:80]\n\n items = []\n search_term = search.lower().strip() if search else \"\"\n archived_total = sum(\n 1\n for c in summary.values()\n if _is_conversation_archived(c.get(\"updated_at\"))\n )\n active_total = len(summary) - archived_total\n for conv in summary.values():\n conv[\"archived\"] = _is_conversation_archived(conv.get(\"updated_at\"))\n if not conv.get(\"title\"):\n conv[\"title\"] = f\"Conversation {conv['conversation_id'][:8]}\"\n if search_term:\n haystack = (\n f\"{conv.get('title', '')} {conv.get('last_message', '')}\".lower()\n )\n if search_term not in haystack:\n continue\n if archived_only and not conv[\"archived\"]:\n continue\n if not archived_only and not include_archived and conv[\"archived\"]:\n continue\n updated = conv.get(\"updated_at\")\n conv[\"updated_at\"] = (\n updated.isoformat() if isinstance(updated, datetime) else None\n )\n items.append(conv)\n\n items.sort(key=lambda x: x.get(\"updated_at\") or \"\", reverse=True)\n total = len(items)\n start = (page - 1) * page_size\n page_items = items[start : start + page_size]\n\n return {\n \"items\": page_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"active_total\": active_total,\n \"archived_total\": archived_total,\n }\n\n\n# #endregion list_conversations\n\n\n# #region delete_conversation [C:2] [TYPE Function]\n# @BRIEF Soft-delete or hard-delete a conversation and clear its in-memory trace.\n# @PRE: conversation_id belongs to current_user.\n# @POST: Conversation records are removed from DB and CONVERSATIONS cache.\n@router.delete(\"/conversations/{conversation_id}\")\nasync def delete_conversation(\n conversation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations.delete\"):\n user_id = current_user.id\n\n # 1. Remove from in-memory cache\n key = (user_id, conversation_id)\n if key in CONVERSATIONS:\n del CONVERSATIONS[key]\n\n # 2. Delete from database\n deleted_count = (\n db.query(AssistantMessageRecord)\n .filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conversation_id,\n )\n .delete()\n )\n\n db.commit()\n\n if deleted_count == 0:\n raise HTTPException(\n status_code=404, detail=\"Conversation not found or already deleted\"\n )\n\n return {\n \"status\": \"success\",\n \"deleted\": deleted_count,\n \"conversation_id\": conversation_id,\n }\n\n\n# #endregion delete_conversation\n\n\n@router.get(\"/history\")\n# #region get_history [TYPE Function]\n# @BRIEF Retrieve paginated assistant conversation history for current user.\n# @PRE: Authenticated user is available and page params are valid.\n# @POST: Returns persistent messages and mirrored in-memory snapshot for diagnostics.\nasync def get_history(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n conversation_id: str | None = Query(None),\n from_latest: bool = Query(False),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.history\"):\n user_id = current_user.id\n _cleanup_history_ttl(db, user_id)\n conv_id = _resolve_or_create_conversation(user_id, conversation_id, db)\n\n base_query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conv_id,\n )\n total = base_query.count()\n start = (page - 1) * page_size\n if from_latest:\n rows = (\n base_query.order_by(desc(AssistantMessageRecord.created_at))\n .offset(start)\n .limit(page_size)\n .all()\n )\n rows = list(reversed(rows))\n else:\n rows = (\n base_query.order_by(AssistantMessageRecord.created_at.asc())\n .offset(start)\n .limit(page_size)\n .all()\n )\n\n persistent_items = [\n {\n \"message_id\": row.id,\n \"conversation_id\": row.conversation_id,\n \"role\": row.role,\n \"text\": row.text,\n \"state\": row.state,\n \"task_id\": row.task_id,\n \"confirmation_id\": row.confirmation_id,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n \"metadata\": row.payload,\n }\n for row in rows\n ]\n\n memory_items = CONVERSATIONS.get((user_id, conv_id), [])\n return {\n \"items\": persistent_items,\n \"memory_items\": memory_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"from_latest\": from_latest,\n \"conversation_id\": conv_id,\n }\n\n\n# #endregion get_history\n\n\n@router.get(\"/audit\")\n# #region get_assistant_audit [TYPE Function]\n# @BRIEF Return assistant audit decisions for current user from persistent and in-memory stores.\n# @PRE: User has tasks:READ permission.\n# @POST: Audit payload is returned in reverse chronological order from DB.\nasync def get_assistant_audit(\n limit: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"assistant.audit\"):\n memory_rows = ASSISTANT_AUDIT.get(current_user.id, [])\n db_rows = (\n db.query(AssistantAuditRecord)\n .filter(AssistantAuditRecord.user_id == current_user.id)\n .order_by(AssistantAuditRecord.created_at.desc())\n .limit(limit)\n .all()\n )\n persistent = [\n {\n \"id\": row.id,\n \"user_id\": row.user_id,\n \"conversation_id\": row.conversation_id,\n \"decision\": row.decision,\n \"task_id\": row.task_id,\n \"message\": row.message,\n \"payload\": row.payload,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n }\n for row in db_rows\n ]\n return {\n \"items\": persistent,\n \"memory_items\": memory_rows[-limit:],\n \"total\": len(persistent),\n \"memory_total\": len(memory_rows),\n }\n\n\n# #endregion get_assistant_audit\n\n\n# #endregion AssistantAdminRoutes\n" }, { "contract_id": "list_conversations", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_admin_routes.py", - "start_line": 44, - "end_line": 139, + "start_line": 40, + "end_line": 133, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Conversations are grouped by conversation_id sorted by latest activity descending.", "PRE": "Authenticated user context and valid pagination params.", - "PURPOSE": "Return paginated conversation list for current user with archived flag and last message preview.", - "RETURN": "Dict with items, paging metadata, and archive segmentation counts." + "PURPOSE": "Return paginated conversation list for current user with archived flag and last message preview." }, "relations": [], "schema_warnings": [ @@ -8852,27 +7325,21 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:list_conversations:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Return paginated conversation list for current user with archived flag and last message preview.\n# @PRE: Authenticated user context and valid pagination params.\n# @POST: Conversations are grouped by conversation_id sorted by latest activity descending.\n# @RETURN: Dict with items, paging metadata, and archive segmentation counts.\n@router.get(\"/conversations\")\nasync def list_conversations(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n include_archived: bool = Query(False),\n archived_only: bool = Query(False),\n search: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations\"):\n user_id = current_user.id\n include_archived = _coerce_query_bool(include_archived)\n archived_only = _coerce_query_bool(archived_only)\n _cleanup_history_ttl(db, user_id)\n\n rows = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .all()\n )\n\n summary: Dict[str, Dict[str, Any]] = {}\n for row in rows:\n conv_id = row.conversation_id\n if not conv_id:\n continue\n created_at = row.created_at or datetime.utcnow()\n if conv_id not in summary:\n summary[conv_id] = {\n \"conversation_id\": conv_id,\n \"title\": \"\",\n \"updated_at\": created_at,\n \"last_message\": row.text,\n \"last_role\": row.role,\n \"last_state\": row.state,\n \"last_task_id\": row.task_id,\n \"message_count\": 0,\n }\n item = summary[conv_id]\n item[\"message_count\"] += 1\n if row.role == \"user\" and row.text and not item[\"title\"]:\n item[\"title\"] = row.text.strip()[:80]\n\n items = []\n search_term = search.lower().strip() if search else \"\"\n archived_total = sum(\n 1\n for c in summary.values()\n if _is_conversation_archived(c.get(\"updated_at\"))\n )\n active_total = len(summary) - archived_total\n for conv in summary.values():\n conv[\"archived\"] = _is_conversation_archived(conv.get(\"updated_at\"))\n if not conv.get(\"title\"):\n conv[\"title\"] = f\"Conversation {conv['conversation_id'][:8]}\"\n if search_term:\n haystack = (\n f\"{conv.get('title', '')} {conv.get('last_message', '')}\".lower()\n )\n if search_term not in haystack:\n continue\n if archived_only and not conv[\"archived\"]:\n continue\n if not archived_only and not include_archived and conv[\"archived\"]:\n continue\n updated = conv.get(\"updated_at\")\n conv[\"updated_at\"] = (\n updated.isoformat() if isinstance(updated, datetime) else None\n )\n items.append(conv)\n\n items.sort(key=lambda x: x.get(\"updated_at\") or \"\", reverse=True)\n total = len(items)\n start = (page - 1) * page_size\n page_items = items[start : start + page_size]\n\n return {\n \"items\": page_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"active_total\": active_total,\n \"archived_total\": archived_total,\n }\n\n\n# [/DEF:list_conversations:Function]\n" + "anchor_syntax": "region", + "body": "# #region list_conversations [C:2] [TYPE Function]\n# @BRIEF Return paginated conversation list for current user with archived flag and last message preview.\n# @PRE: Authenticated user context and valid pagination params.\n# @POST: Conversations are grouped by conversation_id sorted by latest activity descending.\n@router.get(\"/conversations\")\nasync def list_conversations(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n include_archived: bool = Query(False),\n archived_only: bool = Query(False),\n search: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations\"):\n user_id = current_user.id\n include_archived = _coerce_query_bool(include_archived)\n archived_only = _coerce_query_bool(archived_only)\n _cleanup_history_ttl(db, user_id)\n\n rows = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .all()\n )\n\n summary: dict[str, dict[str, Any]] = {}\n for row in rows:\n conv_id = row.conversation_id\n if not conv_id:\n continue\n created_at = row.created_at or datetime.utcnow()\n if conv_id not in summary:\n summary[conv_id] = {\n \"conversation_id\": conv_id,\n \"title\": \"\",\n \"updated_at\": created_at,\n \"last_message\": row.text,\n \"last_role\": row.role,\n \"last_state\": row.state,\n \"last_task_id\": row.task_id,\n \"message_count\": 0,\n }\n item = summary[conv_id]\n item[\"message_count\"] += 1\n if row.role == \"user\" and row.text and not item[\"title\"]:\n item[\"title\"] = row.text.strip()[:80]\n\n items = []\n search_term = search.lower().strip() if search else \"\"\n archived_total = sum(\n 1\n for c in summary.values()\n if _is_conversation_archived(c.get(\"updated_at\"))\n )\n active_total = len(summary) - archived_total\n for conv in summary.values():\n conv[\"archived\"] = _is_conversation_archived(conv.get(\"updated_at\"))\n if not conv.get(\"title\"):\n conv[\"title\"] = f\"Conversation {conv['conversation_id'][:8]}\"\n if search_term:\n haystack = (\n f\"{conv.get('title', '')} {conv.get('last_message', '')}\".lower()\n )\n if search_term not in haystack:\n continue\n if archived_only and not conv[\"archived\"]:\n continue\n if not archived_only and not include_archived and conv[\"archived\"]:\n continue\n updated = conv.get(\"updated_at\")\n conv[\"updated_at\"] = (\n updated.isoformat() if isinstance(updated, datetime) else None\n )\n items.append(conv)\n\n items.sort(key=lambda x: x.get(\"updated_at\") or \"\", reverse=True)\n total = len(items)\n start = (page - 1) * page_size\n page_items = items[start : start + page_size]\n\n return {\n \"items\": page_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"active_total\": active_total,\n \"archived_total\": archived_total,\n }\n\n\n# #endregion list_conversations\n" }, { "contract_id": "delete_conversation", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_admin_routes.py", - "start_line": 142, - "end_line": 185, + "start_line": 136, + "end_line": 178, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Conversation records are removed from DB and CONVERSATIONS cache.", "PRE": "conversation_id belongs to current_user.", "PURPOSE": "Soft-delete or hard-delete a conversation and clear its in-memory trace." @@ -8898,22 +7365,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:delete_conversation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Soft-delete or hard-delete a conversation and clear its in-memory trace.\n# @PRE: conversation_id belongs to current_user.\n# @POST: Conversation records are removed from DB and CONVERSATIONS cache.\n@router.delete(\"/conversations/{conversation_id}\")\nasync def delete_conversation(\n conversation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations.delete\"):\n user_id = current_user.id\n\n # 1. Remove from in-memory cache\n key = (user_id, conversation_id)\n if key in CONVERSATIONS:\n del CONVERSATIONS[key]\n\n # 2. Delete from database\n deleted_count = (\n db.query(AssistantMessageRecord)\n .filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conversation_id,\n )\n .delete()\n )\n\n db.commit()\n\n if deleted_count == 0:\n raise HTTPException(\n status_code=404, detail=\"Conversation not found or already deleted\"\n )\n\n return {\n \"status\": \"success\",\n \"deleted\": deleted_count,\n \"conversation_id\": conversation_id,\n }\n\n\n# [/DEF:delete_conversation:Function]\n" + "anchor_syntax": "region", + "body": "# #region delete_conversation [C:2] [TYPE Function]\n# @BRIEF Soft-delete or hard-delete a conversation and clear its in-memory trace.\n# @PRE: conversation_id belongs to current_user.\n# @POST: Conversation records are removed from DB and CONVERSATIONS cache.\n@router.delete(\"/conversations/{conversation_id}\")\nasync def delete_conversation(\n conversation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations.delete\"):\n user_id = current_user.id\n\n # 1. Remove from in-memory cache\n key = (user_id, conversation_id)\n if key in CONVERSATIONS:\n del CONVERSATIONS[key]\n\n # 2. Delete from database\n deleted_count = (\n db.query(AssistantMessageRecord)\n .filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conversation_id,\n )\n .delete()\n )\n\n db.commit()\n\n if deleted_count == 0:\n raise HTTPException(\n status_code=404, detail=\"Conversation not found or already deleted\"\n )\n\n return {\n \"status\": \"success\",\n \"deleted\": deleted_count,\n \"conversation_id\": conversation_id,\n }\n\n\n# #endregion delete_conversation\n" }, { "contract_id": "get_history", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_admin_routes.py", - "start_line": 189, - "end_line": 257, + "start_line": 182, + "end_line": 249, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Returns persistent messages and mirrored in-memory snapshot for diagnostics.", "PRE": "Authenticated user is available and page params are valid.", - "PURPOSE": "Retrieve paginated assistant conversation history for current user.", - "RETURN": "Dict with items, paging metadata, and resolved conversation_id." + "PURPOSE": "Retrieve paginated assistant conversation history for current user." }, "relations": [], "schema_warnings": [ @@ -8943,30 +7409,23 @@ "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": "def", - "body": "# [DEF:get_history:Function]\n# @PURPOSE: Retrieve paginated assistant conversation history for current user.\n# @PRE: Authenticated user is available and page params are valid.\n# @POST: Returns persistent messages and mirrored in-memory snapshot for diagnostics.\n# @RETURN: Dict with items, paging metadata, and resolved conversation_id.\nasync def get_history(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n conversation_id: Optional[str] = Query(None),\n from_latest: bool = Query(False),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.history\"):\n user_id = current_user.id\n _cleanup_history_ttl(db, user_id)\n conv_id = _resolve_or_create_conversation(user_id, conversation_id, db)\n\n base_query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conv_id,\n )\n total = base_query.count()\n start = (page - 1) * page_size\n if from_latest:\n rows = (\n base_query.order_by(desc(AssistantMessageRecord.created_at))\n .offset(start)\n .limit(page_size)\n .all()\n )\n rows = list(reversed(rows))\n else:\n rows = (\n base_query.order_by(AssistantMessageRecord.created_at.asc())\n .offset(start)\n .limit(page_size)\n .all()\n )\n\n persistent_items = [\n {\n \"message_id\": row.id,\n \"conversation_id\": row.conversation_id,\n \"role\": row.role,\n \"text\": row.text,\n \"state\": row.state,\n \"task_id\": row.task_id,\n \"confirmation_id\": row.confirmation_id,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n \"metadata\": row.payload,\n }\n for row in rows\n ]\n\n memory_items = CONVERSATIONS.get((user_id, conv_id), [])\n return {\n \"items\": persistent_items,\n \"memory_items\": memory_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"from_latest\": from_latest,\n \"conversation_id\": conv_id,\n }\n\n\n# [/DEF:get_history:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_history [TYPE Function]\n# @BRIEF Retrieve paginated assistant conversation history for current user.\n# @PRE: Authenticated user is available and page params are valid.\n# @POST: Returns persistent messages and mirrored in-memory snapshot for diagnostics.\nasync def get_history(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n conversation_id: str | None = Query(None),\n from_latest: bool = Query(False),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.history\"):\n user_id = current_user.id\n _cleanup_history_ttl(db, user_id)\n conv_id = _resolve_or_create_conversation(user_id, conversation_id, db)\n\n base_query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conv_id,\n )\n total = base_query.count()\n start = (page - 1) * page_size\n if from_latest:\n rows = (\n base_query.order_by(desc(AssistantMessageRecord.created_at))\n .offset(start)\n .limit(page_size)\n .all()\n )\n rows = list(reversed(rows))\n else:\n rows = (\n base_query.order_by(AssistantMessageRecord.created_at.asc())\n .offset(start)\n .limit(page_size)\n .all()\n )\n\n persistent_items = [\n {\n \"message_id\": row.id,\n \"conversation_id\": row.conversation_id,\n \"role\": row.role,\n \"text\": row.text,\n \"state\": row.state,\n \"task_id\": row.task_id,\n \"confirmation_id\": row.confirmation_id,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n \"metadata\": row.payload,\n }\n for row in rows\n ]\n\n memory_items = CONVERSATIONS.get((user_id, conv_id), [])\n return {\n \"items\": persistent_items,\n \"memory_items\": memory_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"from_latest\": from_latest,\n \"conversation_id\": conv_id,\n }\n\n\n# #endregion get_history\n" }, { "contract_id": "get_assistant_audit", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_admin_routes.py", - "start_line": 261, - "end_line": 302, + "start_line": 253, + "end_line": 293, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Audit payload is returned in reverse chronological order from DB.", "PRE": "User has tasks:READ permission.", - "PURPOSE": "Return assistant audit decisions for current user from persistent and in-memory stores.", - "RETURN": "Dict with persistent and memory audit slices." + "PURPOSE": "Return assistant audit decisions for current user from persistent and in-memory stores." }, "relations": [], "schema_warnings": [ @@ -8996,27 +7455,21 @@ "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": "def", - "body": "# [DEF:get_assistant_audit:Function]\n# @PURPOSE: Return assistant audit decisions for current user from persistent and in-memory stores.\n# @PRE: User has tasks:READ permission.\n# @POST: Audit payload is returned in reverse chronological order from DB.\n# @RETURN: Dict with persistent and memory audit slices.\nasync def get_assistant_audit(\n limit: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"assistant.audit\"):\n memory_rows = ASSISTANT_AUDIT.get(current_user.id, [])\n db_rows = (\n db.query(AssistantAuditRecord)\n .filter(AssistantAuditRecord.user_id == current_user.id)\n .order_by(AssistantAuditRecord.created_at.desc())\n .limit(limit)\n .all()\n )\n persistent = [\n {\n \"id\": row.id,\n \"user_id\": row.user_id,\n \"conversation_id\": row.conversation_id,\n \"decision\": row.decision,\n \"task_id\": row.task_id,\n \"message\": row.message,\n \"payload\": row.payload,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n }\n for row in db_rows\n ]\n return {\n \"items\": persistent,\n \"memory_items\": memory_rows[-limit:],\n \"total\": len(persistent),\n \"memory_total\": len(memory_rows),\n }\n\n\n# [/DEF:get_assistant_audit:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_assistant_audit [TYPE Function]\n# @BRIEF Return assistant audit decisions for current user from persistent and in-memory stores.\n# @PRE: User has tasks:READ permission.\n# @POST: Audit payload is returned in reverse chronological order from DB.\nasync def get_assistant_audit(\n limit: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"assistant.audit\"):\n memory_rows = ASSISTANT_AUDIT.get(current_user.id, [])\n db_rows = (\n db.query(AssistantAuditRecord)\n .filter(AssistantAuditRecord.user_id == current_user.id)\n .order_by(AssistantAuditRecord.created_at.desc())\n .limit(limit)\n .all()\n )\n persistent = [\n {\n \"id\": row.id,\n \"user_id\": row.user_id,\n \"conversation_id\": row.conversation_id,\n \"decision\": row.decision,\n \"task_id\": row.task_id,\n \"message\": row.message,\n \"payload\": row.payload,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n }\n for row in db_rows\n ]\n return {\n \"items\": persistent,\n \"memory_items\": memory_rows[-limit:],\n \"total\": len(persistent),\n \"memory_total\": len(memory_rows),\n }\n\n\n# #endregion get_assistant_audit\n" }, { "contract_id": "AssistantCommandParser", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_command_parser.py", "start_line": 1, - "end_line": 91, + "end_line": 90, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "INVARIANT": "Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.", "LAYER": "API", "PURPOSE": "Deterministic RU/EN command text parser that converts user messages into intent payloads." @@ -9067,19 +7520,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantCommandParser:Module]\n# @COMPLEXITY: 4\n# @PURPOSE: Deterministic RU/EN command text parser that converts user messages into intent payloads.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [AssistantResolvers]\n# @INVARIANT: Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any, Dict, List, Optional\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.config_manager import ConfigManager\nfrom ._resolvers import _extract_id, _is_production_env\n\n\n# [DEF:_parse_command:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Deterministically parse RU/EN command text into intent payload.\n# @DATA_CONTRACT: Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]\n# @RELATION: DEPENDS_ON -> [_extract_id]\n# @RELATION: DEPENDS_ON -> [_is_production_env]\n# @SIDE_EFFECT: None (pure parsing logic).\n# @PRE: message contains raw user text and config manager resolves environments.\n# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields.\n# @INVARIANT: every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\ndef _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any]:\n with belief_scope('_parse_command'):\n logger.reason('Belief protocol reasoning checkpoint for _parse_command')\n text = message.strip()\n lower = text.lower()\n if any((phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды'])):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}\n dashboard_id = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n dashboard_ref = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?([a-zа-я0-9._-]+)'])\n dataset_id = _extract_id(lower, ['(?:датасет\\\\w*|dataset)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])\n if any((k in lower for k in ['статус', 'status', 'state', 'проверь задачу'])):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}\n if any((k in lower for k in ['ветк', 'branch'])) and any((k in lower for k in ['созд', 'сделай', 'create'])):\n branch = _extract_id(lower, ['(?:ветк\\\\w*|branch)\\\\s+([a-z0-9._/-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any((k in lower for k in ['коммит', 'commit'])):\n quoted = re.search('\"([^\"]{3,120})\"', text)\n message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any((k in lower for k in ['деплой', 'deploy', 'разверн'])):\n env_match = _extract_id(lower, ['(?:в|to)\\\\s+([a-z0-9_-]+)'])\n is_dangerous = _is_production_env(env_match, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}\n if any((k in lower for k in ['миграц', 'migration', 'migrate'])):\n src = _extract_id(lower, ['(?:с|from)\\\\s+([a-z0-9_-]+)'])\n tgt = _extract_id(lower, ['(?:на|to)\\\\s+([a-z0-9_-]+)'])\n dry_run = '--dry-run' in lower or 'dry run' in lower\n replace_db_config = '--replace-db-config' in lower\n fix_cross_filters = '--no-fix-cross-filters' not in lower\n is_dangerous = _is_production_env(tgt, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}\n if any((k in lower for k in ['бэкап', 'backup', 'резерв'])):\n env_match = _extract_id(lower, ['(?:в|for|из|from)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any((k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы'])):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any((k in lower for k in ['валидац', 'validate', 'провер'])):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any((k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док'])):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_documentation', 'entities': {'dataset_id': int(dataset_id) if dataset_id else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dataset_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False}\n\n\n# [/DEF:_parse_command:Function]\n\n\n# [/DEF:AssistantCommandParser:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantCommandParser [C:4] [TYPE Module] [SEMANTICS assistant, command, parser, nlu, intent]\n# @BRIEF Deterministic RU/EN command text parser that converts user messages into intent payloads.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @INVARIANT: Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\n\nfrom ._resolvers import _extract_id, _is_production_env\n\n\n# #region _parse_command [C:4] [TYPE Function]\n# @BRIEF Deterministically parse RU/EN command text into intent payload.\n# @DATA_CONTRACT: Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]\n# @RELATION DEPENDS_ON -> [_extract_id]\n# @RELATION DEPENDS_ON -> [_is_production_env]\n# @SIDE_EFFECT: None (pure parsing logic).\n# @PRE: message contains raw user text and config manager resolves environments.\n# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields.\n# @INVARIANT: every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\ndef _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:\n with belief_scope('_parse_command'):\n logger.reason('Belief protocol reasoning checkpoint for _parse_command')\n text = message.strip()\n lower = text.lower()\n if any(phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}\n dashboard_id = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n dashboard_ref = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?([a-zа-я0-9._-]+)'])\n dataset_id = _extract_id(lower, ['(?:датасет\\\\w*|dataset)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])\n if any(k in lower for k in ['статус', 'status', 'state', 'проверь задачу']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['ветк', 'branch']) and any(k in lower for k in ['созд', 'сделай', 'create']):\n branch = _extract_id(lower, ['(?:ветк\\\\w*|branch)\\\\s+([a-z0-9._/-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['коммит', 'commit']):\n quoted = re.search('\"([^\"]{3,120})\"', text)\n message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['деплой', 'deploy', 'разверн']):\n env_match = _extract_id(lower, ['(?:в|to)\\\\s+([a-z0-9_-]+)'])\n is_dangerous = _is_production_env(env_match, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}\n if any(k in lower for k in ['миграц', 'migration', 'migrate']):\n src = _extract_id(lower, ['(?:с|from)\\\\s+([a-z0-9_-]+)'])\n tgt = _extract_id(lower, ['(?:на|to)\\\\s+([a-z0-9_-]+)'])\n dry_run = '--dry-run' in lower or 'dry run' in lower\n replace_db_config = '--replace-db-config' in lower\n fix_cross_filters = '--no-fix-cross-filters' not in lower\n is_dangerous = _is_production_env(tgt, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}\n if any(k in lower for k in ['бэкап', 'backup', 'резерв']):\n env_match = _extract_id(lower, ['(?:в|for|из|from)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['валидац', 'validate', 'провер']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_documentation', 'entities': {'dataset_id': int(dataset_id) if dataset_id else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dataset_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False}\n\n\n# #endregion _parse_command\n\n\n# #endregion AssistantCommandParser\n" }, { "contract_id": "_parse_command", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_command_parser.py", "start_line": 18, - "end_line": 88, + "end_line": 87, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "DATA_CONTRACT": "Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]", "INVARIANT": "every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.", "POST": "Returns intent dict with domain/operation/entities/confidence/risk fields.", @@ -9121,19 +7574,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_parse_command:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Deterministically parse RU/EN command text into intent payload.\n# @DATA_CONTRACT: Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]\n# @RELATION: DEPENDS_ON -> [_extract_id]\n# @RELATION: DEPENDS_ON -> [_is_production_env]\n# @SIDE_EFFECT: None (pure parsing logic).\n# @PRE: message contains raw user text and config manager resolves environments.\n# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields.\n# @INVARIANT: every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\ndef _parse_command(message: str, config_manager: ConfigManager) -> Dict[str, Any]:\n with belief_scope('_parse_command'):\n logger.reason('Belief protocol reasoning checkpoint for _parse_command')\n text = message.strip()\n lower = text.lower()\n if any((phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды'])):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}\n dashboard_id = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n dashboard_ref = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?([a-zа-я0-9._-]+)'])\n dataset_id = _extract_id(lower, ['(?:датасет\\\\w*|dataset)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])\n if any((k in lower for k in ['статус', 'status', 'state', 'проверь задачу'])):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}\n if any((k in lower for k in ['ветк', 'branch'])) and any((k in lower for k in ['созд', 'сделай', 'create'])):\n branch = _extract_id(lower, ['(?:ветк\\\\w*|branch)\\\\s+([a-z0-9._/-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any((k in lower for k in ['коммит', 'commit'])):\n quoted = re.search('\"([^\"]{3,120})\"', text)\n message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any((k in lower for k in ['деплой', 'deploy', 'разверн'])):\n env_match = _extract_id(lower, ['(?:в|to)\\\\s+([a-z0-9_-]+)'])\n is_dangerous = _is_production_env(env_match, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}\n if any((k in lower for k in ['миграц', 'migration', 'migrate'])):\n src = _extract_id(lower, ['(?:с|from)\\\\s+([a-z0-9_-]+)'])\n tgt = _extract_id(lower, ['(?:на|to)\\\\s+([a-z0-9_-]+)'])\n dry_run = '--dry-run' in lower or 'dry run' in lower\n replace_db_config = '--replace-db-config' in lower\n fix_cross_filters = '--no-fix-cross-filters' not in lower\n is_dangerous = _is_production_env(tgt, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}\n if any((k in lower for k in ['бэкап', 'backup', 'резерв'])):\n env_match = _extract_id(lower, ['(?:в|for|из|from)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any((k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы'])):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any((k in lower for k in ['валидац', 'validate', 'провер'])):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any((k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док'])):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_documentation', 'entities': {'dataset_id': int(dataset_id) if dataset_id else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dataset_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False}\n\n\n# [/DEF:_parse_command:Function]\n" + "anchor_syntax": "region", + "body": "# #region _parse_command [C:4] [TYPE Function]\n# @BRIEF Deterministically parse RU/EN command text into intent payload.\n# @DATA_CONTRACT: Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]\n# @RELATION DEPENDS_ON -> [_extract_id]\n# @RELATION DEPENDS_ON -> [_is_production_env]\n# @SIDE_EFFECT: None (pure parsing logic).\n# @PRE: message contains raw user text and config manager resolves environments.\n# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields.\n# @INVARIANT: every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\ndef _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:\n with belief_scope('_parse_command'):\n logger.reason('Belief protocol reasoning checkpoint for _parse_command')\n text = message.strip()\n lower = text.lower()\n if any(phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}\n dashboard_id = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n dashboard_ref = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?([a-zа-я0-9._-]+)'])\n dataset_id = _extract_id(lower, ['(?:датасет\\\\w*|dataset)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])\n if any(k in lower for k in ['статус', 'status', 'state', 'проверь задачу']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['ветк', 'branch']) and any(k in lower for k in ['созд', 'сделай', 'create']):\n branch = _extract_id(lower, ['(?:ветк\\\\w*|branch)\\\\s+([a-z0-9._/-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['коммит', 'commit']):\n quoted = re.search('\"([^\"]{3,120})\"', text)\n message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['деплой', 'deploy', 'разверн']):\n env_match = _extract_id(lower, ['(?:в|to)\\\\s+([a-z0-9_-]+)'])\n is_dangerous = _is_production_env(env_match, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}\n if any(k in lower for k in ['миграц', 'migration', 'migrate']):\n src = _extract_id(lower, ['(?:с|from)\\\\s+([a-z0-9_-]+)'])\n tgt = _extract_id(lower, ['(?:на|to)\\\\s+([a-z0-9_-]+)'])\n dry_run = '--dry-run' in lower or 'dry run' in lower\n replace_db_config = '--replace-db-config' in lower\n fix_cross_filters = '--no-fix-cross-filters' not in lower\n is_dangerous = _is_production_env(tgt, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}\n if any(k in lower for k in ['бэкап', 'backup', 'резерв']):\n env_match = _extract_id(lower, ['(?:в|for|из|from)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['валидац', 'validate', 'провер']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_documentation', 'entities': {'dataset_id': int(dataset_id) if dataset_id else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dataset_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False}\n\n\n# #endregion _parse_command\n" }, { "contract_id": "AssistantDatasetReview", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_dataset_review.py", "start_line": 1, - "end_line": 297, + "end_line": 283, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "INVARIANT": "Dataset review operations are always scoped to the owner's session.", "LAYER": "API", "PURPOSE": "Dataset review context loading and intent planning for the assistant API." @@ -9196,19 +7649,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantDatasetReview:Module]\n# @COMPLEXITY: 4\n# @PURPOSE: Dataset review context loading and intent planning for the assistant API.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DatasetReviewOrchestrator]\n# @RELATION: DEPENDS_ON -> [AssistantSchemas]\n# @RELATION: DISPATCHES -> [AssistantDatasetReviewDispatch]\n# @INVARIANT: Dataset review operations are always scoped to the owner's session.\n\nfrom __future__ import annotations\n\nimport re\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom fastapi import HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.utils.superset_context_extractor import (\n sanitize_imported_filter_for_assistant,\n)\nfrom src.models.dataset_review import (\n ApprovalState,\n DatasetReviewSession,\n ReadinessState,\n RecommendedAction,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\nfrom src.schemas.auth import User\nfrom ._schemas import (\n AssistantAction,\n)\n\n\n# [DEF:_serialize_dataset_review_context:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\n# @PRE: session_id is a valid active review session identifier.\n# @POST: Returns a serializable dictionary containing the complete review context.\n# @SIDE_EFFECT: Reads session data from the database.\ndef _serialize_dataset_review_context(session: DatasetReviewSession) -> Dict[str, Any]:\n with belief_scope('_serialize_dataset_review_context'):\n logger.reason('Belief protocol reasoning checkpoint for _serialize_dataset_review_context')\n latest_preview = None\n previews = getattr(session, 'previews', []) or []\n if previews:\n latest_preview = previews[-1]\n logger.reflect('Belief protocol postcondition checkpoint for _serialize_dataset_review_context')\n return {'session_id': session.session_id, 'version': int(getattr(session, 'version', 0) or 0), 'dataset_ref': session.dataset_ref, 'dataset_id': session.dataset_id, 'environment_id': session.environment_id, 'readiness_state': session.readiness_state.value, 'recommended_action': session.recommended_action.value, 'status': session.status.value, 'current_phase': session.current_phase.value, 'findings': [{'finding_id': item.finding_id, 'code': item.code, 'severity': item.severity.value, 'message': item.message, 'resolution_state': item.resolution_state.value} for item in getattr(session, 'findings', [])], 'imported_filters': [sanitize_imported_filter_for_assistant({'filter_id': item.filter_id, 'filter_name': item.filter_name, 'display_name': item.display_name, 'raw_value': item.raw_value, 'raw_value_masked': bool(getattr(item, 'raw_value_masked', False)), 'normalized_value': item.normalized_value, 'source': getattr(item.source, 'value', item.source), 'confidence_state': getattr(item.confidence_state, 'value', item.confidence_state), 'requires_confirmation': bool(item.requires_confirmation), 'recovery_status': getattr(item.recovery_status, 'value', item.recovery_status), 'notes': item.notes}) for item in getattr(session, 'imported_filters', [])], 'mappings': [{'mapping_id': item.mapping_id, 'filter_id': item.filter_id, 'variable_id': item.variable_id, 'mapping_method': getattr(item.mapping_method, 'value', item.mapping_method), 'effective_value': item.effective_value, 'approval_state': getattr(item.approval_state, 'value', item.approval_state), 'requires_explicit_approval': bool(item.requires_explicit_approval)} for item in getattr(session, 'execution_mappings', [])], 'semantic_fields': [{'field_id': item.field_id, 'field_name': item.field_name, 'verbose_name': item.verbose_name, 'description': item.description, 'display_format': item.display_format, 'provenance': getattr(item.provenance, 'value', item.provenance), 'is_locked': bool(item.is_locked), 'needs_review': bool(getattr(item, 'needs_review', False)), 'candidates': [{'candidate_id': c.candidate_id, 'field_id': c.field_id, 'verbose_name': c.proposed_verbose_name, 'description': c.proposed_description, 'display_format': c.proposed_display_format, 'status': getattr(c.status, 'value', c.status), 'source': c.source_id, 'score': c.confidence_score, 'created_at': c.created_at.isoformat() if c.created_at else None} for c in getattr(item, 'candidates', [])]} for item in getattr(session, 'semantic_fields', [])]}\n\n\n# [/DEF:_serialize_dataset_review_context:Function]\n\n\n# [DEF:_load_dataset_review_context:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Load owner-scoped dataset-review context for assistant planning and grounded response generation.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @PRE: session_id is a valid active review session identifier.\n# @POST: Returns a loaded context object with session data and findings.\n# @SIDE_EFFECT: Reads session data from the database.\ndef _load_dataset_review_context(dataset_review_session_id: Optional[str], current_user: User, db: Session) -> Optional[Dict[str, Any]]:\n with belief_scope('_load_dataset_review_context'):\n if not dataset_review_session_id:\n return None\n logger.reason('Belief protocol reasoning checkpoint for _load_dataset_review_context')\n repository = DatasetReviewSessionRepository(db)\n session = repository.load_session_detail(dataset_review_session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n raise HTTPException(status_code=404, detail='Dataset review session not found')\n logger.reflect('Belief protocol postcondition checkpoint for _load_dataset_review_context')\n return _serialize_dataset_review_context(session)\n\n\n# [/DEF:_load_dataset_review_context:Function]\n\n\n# [DEF:_extract_dataset_review_target:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Extract structured dataset-review focus target hints embedded in assistant prompts.\ndef _extract_dataset_review_target(message: str) -> Tuple[Optional[str], Optional[str]]:\n match = re.search(\n r\"(?:target|focus)\\s*[:=]\\s*(field|mapping|finding|filter)[:=]([A-Za-z0-9._-]+)\",\n str(message or \"\"),\n re.IGNORECASE,\n )\n if not match:\n return None, None\n return match.group(1).lower(), match.group(2)\n\n\n# [/DEF:_extract_dataset_review_target:Function]\n\n\n# [DEF:_match_dataset_review_field:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve one semantic field from assistant-visible context by id or user-visible label.\ndef _match_dataset_review_field(\n dataset_context: Dict[str, Any],\n message: str,\n) -> Optional[Dict[str, Any]]:\n target_kind, target_id = _extract_dataset_review_target(message)\n fields = dataset_context.get(\"semantic_fields\", []) or []\n if target_kind == \"field\" and target_id:\n return next(\n (item for item in fields if str(item.get(\"field_id\")) == str(target_id)),\n None,\n )\n\n normalized_message = str(message or \"\").lower()\n for field in fields:\n if str(field.get(\"field_id\", \"\")).lower() in normalized_message:\n return field\n field_name = str(field.get(\"field_name\", \"\")).lower()\n if field_name and field_name in normalized_message:\n return field\n verbose_name = str(field.get(\"verbose_name\", \"\")).lower()\n if verbose_name and verbose_name in normalized_message:\n return field\n return None\n\n\n# [/DEF:_match_dataset_review_field:Function]\n\n\n# [DEF:_extract_quoted_segment:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Extract one quoted assistant command segment after a label token.\ndef _extract_quoted_segment(message: str, label: str) -> Optional[str]:\n pattern = rf\"{label}\\s*[=:]?\\s*[\\\"']([^\\\"']+)[\\\"']\"\n match = re.search(pattern, str(message or \"\"), re.IGNORECASE)\n return match.group(1).strip() if match else None\n\n\n# [/DEF:_extract_quoted_segment:Function]\n\n\n# [DEF:_plan_dataset_review_intent:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Parse session-scoped dataset-review assistant commands before falling back to generic assistant tool routing.\n# @RELATION: CALLS -> DatasetReviewOrchestrator\ndef _plan_dataset_review_intent(\n message: str,\n dataset_context: Dict[str, Any],\n) -> Optional[Dict[str, Any]]:\n lower = message.strip().lower()\n session_id = dataset_context[\"session_id\"]\n session_version = int(dataset_context.get(\"version\", 0) or 0)\n target_kind, target_id = _extract_dataset_review_target(message)\n\n if any(\n token in lower\n for token in [\n \"approve mappings\",\n \"approve mapping\",\n \"подтверди мапп\",\n \"одобри мапп\",\n ]\n ):\n pending_mapping_ids = [\n item[\"mapping_id\"]\n for item in dataset_context.get(\"mappings\", [])\n if item.get(\"requires_explicit_approval\")\n and item.get(\"approval_state\") != ApprovalState.APPROVED.value\n ]\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_approve_mappings\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n \"mapping_ids\": pending_mapping_ids,\n },\n \"confidence\": 0.95,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"generate sql preview\",\n \"generate preview\",\n \"сгенерируй превью\",\n \"собери превью\",\n ]\n ):\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n },\n \"confidence\": 0.94,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"set field semantics\",\n \"apply field semantics\",\n \"semantic override\",\n \"update semantic field\",\n \"установи семантик\",\n \"обнови семантик\",\n ]\n ):\n field = _match_dataset_review_field(dataset_context, message)\n if field is None:\n return None\n candidate_id = None\n if any(\n token in lower for token in [\"accept candidate\", \"apply candidate\", \"прими кандидат\"]\n ):\n candidates = field.get(\"candidates\") or []\n if candidates:\n candidate_id = candidates[0].get(\"candidate_id\")\n verbose_name = _extract_quoted_segment(\n message, \"verbose_name|verbose name|label\"\n )\n description = _extract_quoted_segment(message, \"description|desc\")\n display_format = _extract_quoted_segment(\n message, \"display_format|display format|format\"\n )\n lock_field = any(\n token in lower for token in [\" lock\", \"lock it\", \"зафикс\", \"закреп\"]\n )\n if not any([candidate_id, verbose_name, description, display_format]):\n return None\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_set_field_semantics\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n \"field_id\": field.get(\"field_id\") or target_id,\n \"candidate_id\": candidate_id,\n \"verbose_name\": verbose_name,\n \"description\": description,\n \"display_format\": display_format,\n \"lock_field\": lock_field,\n },\n \"confidence\": 0.9,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"filters\",\n \"фильтр\",\n \"mapping\",\n \"маппинг\",\n \"preview\",\n \"превью\",\n \"finding\",\n \"ошиб\",\n ]\n ):\n findings_count = len(dataset_context.get(\"findings\", []))\n mappings_count = len(dataset_context.get(\"mappings\", []))\n filters_count = len(dataset_context.get(\"imported_filters\", []))\n preview = dataset_context.get(\"preview\") or {}\n preview_status = preview.get(\"preview_status\") or \"missing\"\n masked_filters = dataset_context.get(\"imported_filters\", [])\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_answer_context\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"summary\": (\n f\"Session {session_id}: readiness={dataset_context['readiness_state']}, \"\n f\"recommended_action={dataset_context['recommended_action']}, \"\n f\"filters={filters_count}, mappings={mappings_count}, findings={findings_count}, \"\n f\"preview_status={preview_status}, imported_filters={masked_filters}\"\n ),\n },\n \"confidence\": 0.8,\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n return None\n\n\n# [/DEF:_plan_dataset_review_intent:Function]\n\n\n# [/DEF:AssistantDatasetReview:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantDatasetReview [C:4] [TYPE Module] [SEMANTICS assistant, dataset, review, context, intent]\n# @BRIEF Dataset review context loading and intent planning for the assistant API.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator]\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DISPATCHES -> [AssistantDatasetReviewDispatch]\n# @INVARIANT: Dataset review operations are always scoped to the owner's session.\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.utils.superset_context_extractor import (\n sanitize_imported_filter_for_assistant,\n)\nfrom src.models.dataset_review import (\n ApprovalState,\n DatasetReviewSession,\n)\nfrom src.schemas.auth import User\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\n\n\n# #region _serialize_dataset_review_context [C:4] [TYPE Function]\n# @BRIEF Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @PRE: session_id is a valid active review session identifier.\n# @POST: Returns a serializable dictionary containing the complete review context.\n# @SIDE_EFFECT: Reads session data from the database.\ndef _serialize_dataset_review_context(session: DatasetReviewSession) -> dict[str, Any]:\n with belief_scope('_serialize_dataset_review_context'):\n logger.reason('Belief protocol reasoning checkpoint for _serialize_dataset_review_context')\n latest_preview = None\n previews = getattr(session, 'previews', []) or []\n if previews:\n latest_preview = previews[-1]\n logger.reflect('Belief protocol postcondition checkpoint for _serialize_dataset_review_context')\n return {'session_id': session.session_id, 'version': int(getattr(session, 'version', 0) or 0), 'dataset_ref': session.dataset_ref, 'dataset_id': session.dataset_id, 'environment_id': session.environment_id, 'readiness_state': session.readiness_state.value, 'recommended_action': session.recommended_action.value, 'status': session.status.value, 'current_phase': session.current_phase.value, 'findings': [{'finding_id': item.finding_id, 'code': item.code, 'severity': item.severity.value, 'message': item.message, 'resolution_state': item.resolution_state.value} for item in getattr(session, 'findings', [])], 'imported_filters': [sanitize_imported_filter_for_assistant({'filter_id': item.filter_id, 'filter_name': item.filter_name, 'display_name': item.display_name, 'raw_value': item.raw_value, 'raw_value_masked': bool(getattr(item, 'raw_value_masked', False)), 'normalized_value': item.normalized_value, 'source': getattr(item.source, 'value', item.source), 'confidence_state': getattr(item.confidence_state, 'value', item.confidence_state), 'requires_confirmation': bool(item.requires_confirmation), 'recovery_status': getattr(item.recovery_status, 'value', item.recovery_status), 'notes': item.notes}) for item in getattr(session, 'imported_filters', [])], 'mappings': [{'mapping_id': item.mapping_id, 'filter_id': item.filter_id, 'variable_id': item.variable_id, 'mapping_method': getattr(item.mapping_method, 'value', item.mapping_method), 'effective_value': item.effective_value, 'approval_state': getattr(item.approval_state, 'value', item.approval_state), 'requires_explicit_approval': bool(item.requires_explicit_approval)} for item in getattr(session, 'execution_mappings', [])], 'semantic_fields': [{'field_id': item.field_id, 'field_name': item.field_name, 'verbose_name': item.verbose_name, 'description': item.description, 'display_format': item.display_format, 'provenance': getattr(item.provenance, 'value', item.provenance), 'is_locked': bool(item.is_locked), 'needs_review': bool(getattr(item, 'needs_review', False)), 'candidates': [{'candidate_id': c.candidate_id, 'field_id': c.field_id, 'verbose_name': c.proposed_verbose_name, 'description': c.proposed_description, 'display_format': c.proposed_display_format, 'status': getattr(c.status, 'value', c.status), 'source': c.source_id, 'score': c.confidence_score, 'created_at': c.created_at.isoformat() if c.created_at else None} for c in getattr(item, 'candidates', [])]} for item in getattr(session, 'semantic_fields', [])]}\n\n\n# #endregion _serialize_dataset_review_context\n\n\n# #region _load_dataset_review_context [C:4] [TYPE Function]\n# @BRIEF Load owner-scoped dataset-review context for assistant planning and grounded response generation.\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @PRE: session_id is a valid active review session identifier.\n# @POST: Returns a loaded context object with session data and findings.\n# @SIDE_EFFECT: Reads session data from the database.\ndef _load_dataset_review_context(dataset_review_session_id: str | None, current_user: User, db: Session) -> dict[str, Any] | None:\n with belief_scope('_load_dataset_review_context'):\n if not dataset_review_session_id:\n return None\n logger.reason('Belief protocol reasoning checkpoint for _load_dataset_review_context')\n repository = DatasetReviewSessionRepository(db)\n session = repository.load_session_detail(dataset_review_session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n raise HTTPException(status_code=404, detail='Dataset review session not found')\n logger.reflect('Belief protocol postcondition checkpoint for _load_dataset_review_context')\n return _serialize_dataset_review_context(session)\n\n\n# #endregion _load_dataset_review_context\n\n\n# #region _extract_dataset_review_target [C:2] [TYPE Function]\n# @BRIEF Extract structured dataset-review focus target hints embedded in assistant prompts.\ndef _extract_dataset_review_target(message: str) -> tuple[str | None, str | None]:\n match = re.search(\n r\"(?:target|focus)\\s*[:=]\\s*(field|mapping|finding|filter)[:=]([A-Za-z0-9._-]+)\",\n str(message or \"\"),\n re.IGNORECASE,\n )\n if not match:\n return None, None\n return match.group(1).lower(), match.group(2)\n\n\n# #endregion _extract_dataset_review_target\n\n\n# #region _match_dataset_review_field [C:2] [TYPE Function]\n# @BRIEF Resolve one semantic field from assistant-visible context by id or user-visible label.\ndef _match_dataset_review_field(\n dataset_context: dict[str, Any],\n message: str,\n) -> dict[str, Any] | None:\n target_kind, target_id = _extract_dataset_review_target(message)\n fields = dataset_context.get(\"semantic_fields\", []) or []\n if target_kind == \"field\" and target_id:\n return next(\n (item for item in fields if str(item.get(\"field_id\")) == str(target_id)),\n None,\n )\n\n normalized_message = str(message or \"\").lower()\n for field in fields:\n if str(field.get(\"field_id\", \"\")).lower() in normalized_message:\n return field\n field_name = str(field.get(\"field_name\", \"\")).lower()\n if field_name and field_name in normalized_message:\n return field\n verbose_name = str(field.get(\"verbose_name\", \"\")).lower()\n if verbose_name and verbose_name in normalized_message:\n return field\n return None\n\n\n# #endregion _match_dataset_review_field\n\n\n# #region _extract_quoted_segment [C:2] [TYPE Function]\n# @BRIEF Extract one quoted assistant command segment after a label token.\ndef _extract_quoted_segment(message: str, label: str) -> str | None:\n pattern = rf\"{label}\\s*[=:]?\\s*[\\\"']([^\\\"']+)[\\\"']\"\n match = re.search(pattern, str(message or \"\"), re.IGNORECASE)\n return match.group(1).strip() if match else None\n\n\n# #endregion _extract_quoted_segment\n\n\n# #region _plan_dataset_review_intent [C:3] [TYPE Function]\n# @BRIEF Parse session-scoped dataset-review assistant commands before falling back to generic assistant tool routing.\n# @RELATION CALLS -> DatasetReviewOrchestrator\ndef _plan_dataset_review_intent(\n message: str,\n dataset_context: dict[str, Any],\n) -> dict[str, Any] | None:\n lower = message.strip().lower()\n session_id = dataset_context[\"session_id\"]\n session_version = int(dataset_context.get(\"version\", 0) or 0)\n target_kind, target_id = _extract_dataset_review_target(message)\n\n if any(\n token in lower\n for token in [\n \"approve mappings\",\n \"approve mapping\",\n \"подтверди мапп\",\n \"одобри мапп\",\n ]\n ):\n pending_mapping_ids = [\n item[\"mapping_id\"]\n for item in dataset_context.get(\"mappings\", [])\n if item.get(\"requires_explicit_approval\")\n and item.get(\"approval_state\") != ApprovalState.APPROVED.value\n ]\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_approve_mappings\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n \"mapping_ids\": pending_mapping_ids,\n },\n \"confidence\": 0.95,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"generate sql preview\",\n \"generate preview\",\n \"сгенерируй превью\",\n \"собери превью\",\n ]\n ):\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n },\n \"confidence\": 0.94,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"set field semantics\",\n \"apply field semantics\",\n \"semantic override\",\n \"update semantic field\",\n \"установи семантик\",\n \"обнови семантик\",\n ]\n ):\n field = _match_dataset_review_field(dataset_context, message)\n if field is None:\n return None\n candidate_id = None\n if any(\n token in lower for token in [\"accept candidate\", \"apply candidate\", \"прими кандидат\"]\n ):\n candidates = field.get(\"candidates\") or []\n if candidates:\n candidate_id = candidates[0].get(\"candidate_id\")\n verbose_name = _extract_quoted_segment(\n message, \"verbose_name|verbose name|label\"\n )\n description = _extract_quoted_segment(message, \"description|desc\")\n display_format = _extract_quoted_segment(\n message, \"display_format|display format|format\"\n )\n lock_field = any(\n token in lower for token in [\" lock\", \"lock it\", \"зафикс\", \"закреп\"]\n )\n if not any([candidate_id, verbose_name, description, display_format]):\n return None\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_set_field_semantics\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n \"field_id\": field.get(\"field_id\") or target_id,\n \"candidate_id\": candidate_id,\n \"verbose_name\": verbose_name,\n \"description\": description,\n \"display_format\": display_format,\n \"lock_field\": lock_field,\n },\n \"confidence\": 0.9,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"filters\",\n \"фильтр\",\n \"mapping\",\n \"маппинг\",\n \"preview\",\n \"превью\",\n \"finding\",\n \"ошиб\",\n ]\n ):\n findings_count = len(dataset_context.get(\"findings\", []))\n mappings_count = len(dataset_context.get(\"mappings\", []))\n filters_count = len(dataset_context.get(\"imported_filters\", []))\n preview = dataset_context.get(\"preview\") or {}\n preview_status = preview.get(\"preview_status\") or \"missing\"\n masked_filters = dataset_context.get(\"imported_filters\", [])\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_answer_context\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"summary\": (\n f\"Session {session_id}: readiness={dataset_context['readiness_state']}, \"\n f\"recommended_action={dataset_context['recommended_action']}, \"\n f\"filters={filters_count}, mappings={mappings_count}, findings={findings_count}, \"\n f\"preview_status={preview_status}, imported_filters={masked_filters}\"\n ),\n },\n \"confidence\": 0.8,\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n return None\n\n\n# #endregion _plan_dataset_review_intent\n\n\n# #endregion AssistantDatasetReview\n" }, { "contract_id": "_serialize_dataset_review_context", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dataset_review.py", - "start_line": 39, - "end_line": 57, + "start_line": 31, + "end_line": 48, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "POST": "Returns a serializable dictionary containing the complete review context.", "PRE": "session_id is a valid active review session identifier.", "PURPOSE": "Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing.", @@ -9223,19 +7676,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_dataset_review_context:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\n# @PRE: session_id is a valid active review session identifier.\n# @POST: Returns a serializable dictionary containing the complete review context.\n# @SIDE_EFFECT: Reads session data from the database.\ndef _serialize_dataset_review_context(session: DatasetReviewSession) -> Dict[str, Any]:\n with belief_scope('_serialize_dataset_review_context'):\n logger.reason('Belief protocol reasoning checkpoint for _serialize_dataset_review_context')\n latest_preview = None\n previews = getattr(session, 'previews', []) or []\n if previews:\n latest_preview = previews[-1]\n logger.reflect('Belief protocol postcondition checkpoint for _serialize_dataset_review_context')\n return {'session_id': session.session_id, 'version': int(getattr(session, 'version', 0) or 0), 'dataset_ref': session.dataset_ref, 'dataset_id': session.dataset_id, 'environment_id': session.environment_id, 'readiness_state': session.readiness_state.value, 'recommended_action': session.recommended_action.value, 'status': session.status.value, 'current_phase': session.current_phase.value, 'findings': [{'finding_id': item.finding_id, 'code': item.code, 'severity': item.severity.value, 'message': item.message, 'resolution_state': item.resolution_state.value} for item in getattr(session, 'findings', [])], 'imported_filters': [sanitize_imported_filter_for_assistant({'filter_id': item.filter_id, 'filter_name': item.filter_name, 'display_name': item.display_name, 'raw_value': item.raw_value, 'raw_value_masked': bool(getattr(item, 'raw_value_masked', False)), 'normalized_value': item.normalized_value, 'source': getattr(item.source, 'value', item.source), 'confidence_state': getattr(item.confidence_state, 'value', item.confidence_state), 'requires_confirmation': bool(item.requires_confirmation), 'recovery_status': getattr(item.recovery_status, 'value', item.recovery_status), 'notes': item.notes}) for item in getattr(session, 'imported_filters', [])], 'mappings': [{'mapping_id': item.mapping_id, 'filter_id': item.filter_id, 'variable_id': item.variable_id, 'mapping_method': getattr(item.mapping_method, 'value', item.mapping_method), 'effective_value': item.effective_value, 'approval_state': getattr(item.approval_state, 'value', item.approval_state), 'requires_explicit_approval': bool(item.requires_explicit_approval)} for item in getattr(session, 'execution_mappings', [])], 'semantic_fields': [{'field_id': item.field_id, 'field_name': item.field_name, 'verbose_name': item.verbose_name, 'description': item.description, 'display_format': item.display_format, 'provenance': getattr(item.provenance, 'value', item.provenance), 'is_locked': bool(item.is_locked), 'needs_review': bool(getattr(item, 'needs_review', False)), 'candidates': [{'candidate_id': c.candidate_id, 'field_id': c.field_id, 'verbose_name': c.proposed_verbose_name, 'description': c.proposed_description, 'display_format': c.proposed_display_format, 'status': getattr(c.status, 'value', c.status), 'source': c.source_id, 'score': c.confidence_score, 'created_at': c.created_at.isoformat() if c.created_at else None} for c in getattr(item, 'candidates', [])]} for item in getattr(session, 'semantic_fields', [])]}\n\n\n# [/DEF:_serialize_dataset_review_context:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_dataset_review_context [C:4] [TYPE Function]\n# @BRIEF Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @PRE: session_id is a valid active review session identifier.\n# @POST: Returns a serializable dictionary containing the complete review context.\n# @SIDE_EFFECT: Reads session data from the database.\ndef _serialize_dataset_review_context(session: DatasetReviewSession) -> dict[str, Any]:\n with belief_scope('_serialize_dataset_review_context'):\n logger.reason('Belief protocol reasoning checkpoint for _serialize_dataset_review_context')\n latest_preview = None\n previews = getattr(session, 'previews', []) or []\n if previews:\n latest_preview = previews[-1]\n logger.reflect('Belief protocol postcondition checkpoint for _serialize_dataset_review_context')\n return {'session_id': session.session_id, 'version': int(getattr(session, 'version', 0) or 0), 'dataset_ref': session.dataset_ref, 'dataset_id': session.dataset_id, 'environment_id': session.environment_id, 'readiness_state': session.readiness_state.value, 'recommended_action': session.recommended_action.value, 'status': session.status.value, 'current_phase': session.current_phase.value, 'findings': [{'finding_id': item.finding_id, 'code': item.code, 'severity': item.severity.value, 'message': item.message, 'resolution_state': item.resolution_state.value} for item in getattr(session, 'findings', [])], 'imported_filters': [sanitize_imported_filter_for_assistant({'filter_id': item.filter_id, 'filter_name': item.filter_name, 'display_name': item.display_name, 'raw_value': item.raw_value, 'raw_value_masked': bool(getattr(item, 'raw_value_masked', False)), 'normalized_value': item.normalized_value, 'source': getattr(item.source, 'value', item.source), 'confidence_state': getattr(item.confidence_state, 'value', item.confidence_state), 'requires_confirmation': bool(item.requires_confirmation), 'recovery_status': getattr(item.recovery_status, 'value', item.recovery_status), 'notes': item.notes}) for item in getattr(session, 'imported_filters', [])], 'mappings': [{'mapping_id': item.mapping_id, 'filter_id': item.filter_id, 'variable_id': item.variable_id, 'mapping_method': getattr(item.mapping_method, 'value', item.mapping_method), 'effective_value': item.effective_value, 'approval_state': getattr(item.approval_state, 'value', item.approval_state), 'requires_explicit_approval': bool(item.requires_explicit_approval)} for item in getattr(session, 'execution_mappings', [])], 'semantic_fields': [{'field_id': item.field_id, 'field_name': item.field_name, 'verbose_name': item.verbose_name, 'description': item.description, 'display_format': item.display_format, 'provenance': getattr(item.provenance, 'value', item.provenance), 'is_locked': bool(item.is_locked), 'needs_review': bool(getattr(item, 'needs_review', False)), 'candidates': [{'candidate_id': c.candidate_id, 'field_id': c.field_id, 'verbose_name': c.proposed_verbose_name, 'description': c.proposed_description, 'display_format': c.proposed_display_format, 'status': getattr(c.status, 'value', c.status), 'source': c.source_id, 'score': c.confidence_score, 'created_at': c.created_at.isoformat() if c.created_at else None} for c in getattr(item, 'candidates', [])]} for item in getattr(session, 'semantic_fields', [])]}\n\n\n# #endregion _serialize_dataset_review_context\n" }, { "contract_id": "_load_dataset_review_context", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dataset_review.py", - "start_line": 60, - "end_line": 80, + "start_line": 51, + "end_line": 70, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "POST": "Returns a loaded context object with session data and findings.", "PRE": "session_id is a valid active review session identifier.", "PURPOSE": "Load owner-scoped dataset-review context for assistant planning and grounded response generation.", @@ -9250,70 +7703,70 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_load_dataset_review_context:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Load owner-scoped dataset-review context for assistant planning and grounded response generation.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @PRE: session_id is a valid active review session identifier.\n# @POST: Returns a loaded context object with session data and findings.\n# @SIDE_EFFECT: Reads session data from the database.\ndef _load_dataset_review_context(dataset_review_session_id: Optional[str], current_user: User, db: Session) -> Optional[Dict[str, Any]]:\n with belief_scope('_load_dataset_review_context'):\n if not dataset_review_session_id:\n return None\n logger.reason('Belief protocol reasoning checkpoint for _load_dataset_review_context')\n repository = DatasetReviewSessionRepository(db)\n session = repository.load_session_detail(dataset_review_session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n raise HTTPException(status_code=404, detail='Dataset review session not found')\n logger.reflect('Belief protocol postcondition checkpoint for _load_dataset_review_context')\n return _serialize_dataset_review_context(session)\n\n\n# [/DEF:_load_dataset_review_context:Function]\n" + "anchor_syntax": "region", + "body": "# #region _load_dataset_review_context [C:4] [TYPE Function]\n# @BRIEF Load owner-scoped dataset-review context for assistant planning and grounded response generation.\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @PRE: session_id is a valid active review session identifier.\n# @POST: Returns a loaded context object with session data and findings.\n# @SIDE_EFFECT: Reads session data from the database.\ndef _load_dataset_review_context(dataset_review_session_id: str | None, current_user: User, db: Session) -> dict[str, Any] | None:\n with belief_scope('_load_dataset_review_context'):\n if not dataset_review_session_id:\n return None\n logger.reason('Belief protocol reasoning checkpoint for _load_dataset_review_context')\n repository = DatasetReviewSessionRepository(db)\n session = repository.load_session_detail(dataset_review_session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n raise HTTPException(status_code=404, detail='Dataset review session not found')\n logger.reflect('Belief protocol postcondition checkpoint for _load_dataset_review_context')\n return _serialize_dataset_review_context(session)\n\n\n# #endregion _load_dataset_review_context\n" }, { "contract_id": "_extract_dataset_review_target", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dataset_review.py", - "start_line": 83, - "end_line": 97, + "start_line": 73, + "end_line": 86, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Extract structured dataset-review focus target hints embedded in assistant prompts." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_extract_dataset_review_target:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Extract structured dataset-review focus target hints embedded in assistant prompts.\ndef _extract_dataset_review_target(message: str) -> Tuple[Optional[str], Optional[str]]:\n match = re.search(\n r\"(?:target|focus)\\s*[:=]\\s*(field|mapping|finding|filter)[:=]([A-Za-z0-9._-]+)\",\n str(message or \"\"),\n re.IGNORECASE,\n )\n if not match:\n return None, None\n return match.group(1).lower(), match.group(2)\n\n\n# [/DEF:_extract_dataset_review_target:Function]\n" + "anchor_syntax": "region", + "body": "# #region _extract_dataset_review_target [C:2] [TYPE Function]\n# @BRIEF Extract structured dataset-review focus target hints embedded in assistant prompts.\ndef _extract_dataset_review_target(message: str) -> tuple[str | None, str | None]:\n match = re.search(\n r\"(?:target|focus)\\s*[:=]\\s*(field|mapping|finding|filter)[:=]([A-Za-z0-9._-]+)\",\n str(message or \"\"),\n re.IGNORECASE,\n )\n if not match:\n return None, None\n return match.group(1).lower(), match.group(2)\n\n\n# #endregion _extract_dataset_review_target\n" }, { "contract_id": "_match_dataset_review_field", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dataset_review.py", - "start_line": 100, - "end_line": 128, + "start_line": 89, + "end_line": 116, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Resolve one semantic field from assistant-visible context by id or user-visible label." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_match_dataset_review_field:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve one semantic field from assistant-visible context by id or user-visible label.\ndef _match_dataset_review_field(\n dataset_context: Dict[str, Any],\n message: str,\n) -> Optional[Dict[str, Any]]:\n target_kind, target_id = _extract_dataset_review_target(message)\n fields = dataset_context.get(\"semantic_fields\", []) or []\n if target_kind == \"field\" and target_id:\n return next(\n (item for item in fields if str(item.get(\"field_id\")) == str(target_id)),\n None,\n )\n\n normalized_message = str(message or \"\").lower()\n for field in fields:\n if str(field.get(\"field_id\", \"\")).lower() in normalized_message:\n return field\n field_name = str(field.get(\"field_name\", \"\")).lower()\n if field_name and field_name in normalized_message:\n return field\n verbose_name = str(field.get(\"verbose_name\", \"\")).lower()\n if verbose_name and verbose_name in normalized_message:\n return field\n return None\n\n\n# [/DEF:_match_dataset_review_field:Function]\n" + "anchor_syntax": "region", + "body": "# #region _match_dataset_review_field [C:2] [TYPE Function]\n# @BRIEF Resolve one semantic field from assistant-visible context by id or user-visible label.\ndef _match_dataset_review_field(\n dataset_context: dict[str, Any],\n message: str,\n) -> dict[str, Any] | None:\n target_kind, target_id = _extract_dataset_review_target(message)\n fields = dataset_context.get(\"semantic_fields\", []) or []\n if target_kind == \"field\" and target_id:\n return next(\n (item for item in fields if str(item.get(\"field_id\")) == str(target_id)),\n None,\n )\n\n normalized_message = str(message or \"\").lower()\n for field in fields:\n if str(field.get(\"field_id\", \"\")).lower() in normalized_message:\n return field\n field_name = str(field.get(\"field_name\", \"\")).lower()\n if field_name and field_name in normalized_message:\n return field\n verbose_name = str(field.get(\"verbose_name\", \"\")).lower()\n if verbose_name and verbose_name in normalized_message:\n return field\n return None\n\n\n# #endregion _match_dataset_review_field\n" }, { "contract_id": "_extract_quoted_segment", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dataset_review.py", - "start_line": 131, - "end_line": 140, + "start_line": 119, + "end_line": 127, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Extract one quoted assistant command segment after a label token." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_extract_quoted_segment:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Extract one quoted assistant command segment after a label token.\ndef _extract_quoted_segment(message: str, label: str) -> Optional[str]:\n pattern = rf\"{label}\\s*[=:]?\\s*[\\\"']([^\\\"']+)[\\\"']\"\n match = re.search(pattern, str(message or \"\"), re.IGNORECASE)\n return match.group(1).strip() if match else None\n\n\n# [/DEF:_extract_quoted_segment:Function]\n" + "anchor_syntax": "region", + "body": "# #region _extract_quoted_segment [C:2] [TYPE Function]\n# @BRIEF Extract one quoted assistant command segment after a label token.\ndef _extract_quoted_segment(message: str, label: str) -> str | None:\n pattern = rf\"{label}\\s*[=:]?\\s*[\\\"']([^\\\"']+)[\\\"']\"\n match = re.search(pattern, str(message or \"\"), re.IGNORECASE)\n return match.group(1).strip() if match else None\n\n\n# #endregion _extract_quoted_segment\n" }, { "contract_id": "_plan_dataset_review_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dataset_review.py", - "start_line": 143, - "end_line": 294, + "start_line": 130, + "end_line": 280, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Parse session-scoped dataset-review assistant commands before falling back to generic assistant tool routing." }, "relations": [ @@ -9325,19 +7778,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_plan_dataset_review_intent:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Parse session-scoped dataset-review assistant commands before falling back to generic assistant tool routing.\n# @RELATION: CALLS -> DatasetReviewOrchestrator\ndef _plan_dataset_review_intent(\n message: str,\n dataset_context: Dict[str, Any],\n) -> Optional[Dict[str, Any]]:\n lower = message.strip().lower()\n session_id = dataset_context[\"session_id\"]\n session_version = int(dataset_context.get(\"version\", 0) or 0)\n target_kind, target_id = _extract_dataset_review_target(message)\n\n if any(\n token in lower\n for token in [\n \"approve mappings\",\n \"approve mapping\",\n \"подтверди мапп\",\n \"одобри мапп\",\n ]\n ):\n pending_mapping_ids = [\n item[\"mapping_id\"]\n for item in dataset_context.get(\"mappings\", [])\n if item.get(\"requires_explicit_approval\")\n and item.get(\"approval_state\") != ApprovalState.APPROVED.value\n ]\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_approve_mappings\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n \"mapping_ids\": pending_mapping_ids,\n },\n \"confidence\": 0.95,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"generate sql preview\",\n \"generate preview\",\n \"сгенерируй превью\",\n \"собери превью\",\n ]\n ):\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n },\n \"confidence\": 0.94,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"set field semantics\",\n \"apply field semantics\",\n \"semantic override\",\n \"update semantic field\",\n \"установи семантик\",\n \"обнови семантик\",\n ]\n ):\n field = _match_dataset_review_field(dataset_context, message)\n if field is None:\n return None\n candidate_id = None\n if any(\n token in lower for token in [\"accept candidate\", \"apply candidate\", \"прими кандидат\"]\n ):\n candidates = field.get(\"candidates\") or []\n if candidates:\n candidate_id = candidates[0].get(\"candidate_id\")\n verbose_name = _extract_quoted_segment(\n message, \"verbose_name|verbose name|label\"\n )\n description = _extract_quoted_segment(message, \"description|desc\")\n display_format = _extract_quoted_segment(\n message, \"display_format|display format|format\"\n )\n lock_field = any(\n token in lower for token in [\" lock\", \"lock it\", \"зафикс\", \"закреп\"]\n )\n if not any([candidate_id, verbose_name, description, display_format]):\n return None\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_set_field_semantics\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n \"field_id\": field.get(\"field_id\") or target_id,\n \"candidate_id\": candidate_id,\n \"verbose_name\": verbose_name,\n \"description\": description,\n \"display_format\": display_format,\n \"lock_field\": lock_field,\n },\n \"confidence\": 0.9,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"filters\",\n \"фильтр\",\n \"mapping\",\n \"маппинг\",\n \"preview\",\n \"превью\",\n \"finding\",\n \"ошиб\",\n ]\n ):\n findings_count = len(dataset_context.get(\"findings\", []))\n mappings_count = len(dataset_context.get(\"mappings\", []))\n filters_count = len(dataset_context.get(\"imported_filters\", []))\n preview = dataset_context.get(\"preview\") or {}\n preview_status = preview.get(\"preview_status\") or \"missing\"\n masked_filters = dataset_context.get(\"imported_filters\", [])\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_answer_context\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"summary\": (\n f\"Session {session_id}: readiness={dataset_context['readiness_state']}, \"\n f\"recommended_action={dataset_context['recommended_action']}, \"\n f\"filters={filters_count}, mappings={mappings_count}, findings={findings_count}, \"\n f\"preview_status={preview_status}, imported_filters={masked_filters}\"\n ),\n },\n \"confidence\": 0.8,\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n return None\n\n\n# [/DEF:_plan_dataset_review_intent:Function]\n" + "anchor_syntax": "region", + "body": "# #region _plan_dataset_review_intent [C:3] [TYPE Function]\n# @BRIEF Parse session-scoped dataset-review assistant commands before falling back to generic assistant tool routing.\n# @RELATION CALLS -> DatasetReviewOrchestrator\ndef _plan_dataset_review_intent(\n message: str,\n dataset_context: dict[str, Any],\n) -> dict[str, Any] | None:\n lower = message.strip().lower()\n session_id = dataset_context[\"session_id\"]\n session_version = int(dataset_context.get(\"version\", 0) or 0)\n target_kind, target_id = _extract_dataset_review_target(message)\n\n if any(\n token in lower\n for token in [\n \"approve mappings\",\n \"approve mapping\",\n \"подтверди мапп\",\n \"одобри мапп\",\n ]\n ):\n pending_mapping_ids = [\n item[\"mapping_id\"]\n for item in dataset_context.get(\"mappings\", [])\n if item.get(\"requires_explicit_approval\")\n and item.get(\"approval_state\") != ApprovalState.APPROVED.value\n ]\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_approve_mappings\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n \"mapping_ids\": pending_mapping_ids,\n },\n \"confidence\": 0.95,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"generate sql preview\",\n \"generate preview\",\n \"сгенерируй превью\",\n \"собери превью\",\n ]\n ):\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n },\n \"confidence\": 0.94,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"set field semantics\",\n \"apply field semantics\",\n \"semantic override\",\n \"update semantic field\",\n \"установи семантик\",\n \"обнови семантик\",\n ]\n ):\n field = _match_dataset_review_field(dataset_context, message)\n if field is None:\n return None\n candidate_id = None\n if any(\n token in lower for token in [\"accept candidate\", \"apply candidate\", \"прими кандидат\"]\n ):\n candidates = field.get(\"candidates\") or []\n if candidates:\n candidate_id = candidates[0].get(\"candidate_id\")\n verbose_name = _extract_quoted_segment(\n message, \"verbose_name|verbose name|label\"\n )\n description = _extract_quoted_segment(message, \"description|desc\")\n display_format = _extract_quoted_segment(\n message, \"display_format|display format|format\"\n )\n lock_field = any(\n token in lower for token in [\" lock\", \"lock it\", \"зафикс\", \"закреп\"]\n )\n if not any([candidate_id, verbose_name, description, display_format]):\n return None\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_set_field_semantics\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"session_version\": session_version,\n \"field_id\": field.get(\"field_id\") or target_id,\n \"candidate_id\": candidate_id,\n \"verbose_name\": verbose_name,\n \"description\": description,\n \"display_format\": display_format,\n \"lock_field\": lock_field,\n },\n \"confidence\": 0.9,\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n }\n\n if any(\n token in lower\n for token in [\n \"filters\",\n \"фильтр\",\n \"mapping\",\n \"маппинг\",\n \"preview\",\n \"превью\",\n \"finding\",\n \"ошиб\",\n ]\n ):\n findings_count = len(dataset_context.get(\"findings\", []))\n mappings_count = len(dataset_context.get(\"mappings\", []))\n filters_count = len(dataset_context.get(\"imported_filters\", []))\n preview = dataset_context.get(\"preview\") or {}\n preview_status = preview.get(\"preview_status\") or \"missing\"\n masked_filters = dataset_context.get(\"imported_filters\", [])\n return {\n \"domain\": \"dataset_review\",\n \"operation\": \"dataset_review_answer_context\",\n \"entities\": {\n \"dataset_review_session_id\": session_id,\n \"summary\": (\n f\"Session {session_id}: readiness={dataset_context['readiness_state']}, \"\n f\"recommended_action={dataset_context['recommended_action']}, \"\n f\"filters={filters_count}, mappings={mappings_count}, findings={findings_count}, \"\n f\"preview_status={preview_status}, imported_filters={masked_filters}\"\n ),\n },\n \"confidence\": 0.8,\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n return None\n\n\n# #endregion _plan_dataset_review_intent\n" }, { "contract_id": "AssistantDatasetReviewDispatch", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_dataset_review_dispatch.py", "start_line": 1, - "end_line": 290, + "end_line": 288, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "INVARIANT": "Dataset review dispatch requires valid session version for write operations.", "LAYER": "API", "PURPOSE": "Dispatch and confirmation handling for dataset-review assistant intents." @@ -9400,36 +7853,36 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantDatasetReviewDispatch:Module]\n# @COMPLEXITY: 4\n# @PURPOSE: Dispatch and confirmation handling for dataset-review assistant intents.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [AssistantDatasetReview]\n# @RELATION: DEPENDS_ON -> [DatasetReviewOrchestrator]\n# @RELATION: DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT: Dataset review dispatch requires valid session version for write operations.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom fastapi import HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.config_manager import ConfigManager\nfrom src.models.dataset_review import (\n ApprovalState,\n ReadinessState,\n RecommendedAction,\n)\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n PreparePreviewCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\nfrom src.schemas.auth import User\nfrom src.api.routes.dataset_review import FieldSemanticUpdateRequest, _update_semantic_field_state\nfrom ._schemas import (\n AssistantAction,\n)\n\n\n# [DEF:_dataset_review_conflict_http_exception:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics.\ndef _dataset_review_conflict_http_exception(\n exc: DatasetReviewSessionVersionConflictError,\n) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\n \"error_code\": \"session_version_conflict\",\n \"message\": str(exc),\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n },\n )\n\n\n# [/DEF:_dataset_review_conflict_http_exception:Function]\n\n\n# [DEF:_dispatch_dataset_review_intent:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.\n# @RELATION: CALLS -> DatasetReviewOrchestrator\n# @PRE: context contains valid session data and user intent.\n# @POST: Returns a structured response with planned actions and confirmations.\n# @SIDE_EFFECT: May update session state and enqueue tasks.\nasync def _dispatch_dataset_review_intent(\n intent: Dict[str, Any],\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> Tuple[str, Optional[str], List[AssistantAction]]:\n with belief_scope(\"_dispatch_dataset_review_intent\"):\n logger.reason(\n \"Dispatching assistant dataset-review intent\",\n extra={\"operation\": intent.get(\"operation\")},\n )\n entities = intent.get(\"entities\", {})\n session_id = entities.get(\"dataset_review_session_id\")\n session_version = entities.get(\"session_version\")\n if not session_id or session_version is None:\n raise HTTPException(\n status_code=422,\n detail=\"Missing dataset_review_session_id/session_version\",\n )\n\n operation = str(intent.get(\"operation\") or \"\")\n repository = DatasetReviewSessionRepository(db)\n if operation == \"dataset_review_answer_context\":\n summary = str(entities.get(\"summary\") or \"\")\n logger.reflect(\n \"Returned assistant-safe dataset review context summary\",\n extra={\"session_id\": session_id, \"operation\": operation},\n )\n return summary, None, []\n\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n logger.explore(\n \"Assistant dataset-review intent rejected because session was not found\",\n extra={\"session_id\": session_id, \"user_id\": current_user.id},\n )\n raise HTTPException(\n status_code=404, detail=\"Dataset review session not found\"\n )\n\n try:\n repository.require_session_version(session, int(session_version))\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\n \"Assistant dataset-review intent rejected due to stale session version\",\n extra={\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n \"operation\": operation,\n },\n )\n raise _dataset_review_conflict_http_exception(exc) from exc\n\n logger.reason(\n \"Dispatching confirmed assistant dataset-review intent\",\n extra={\n \"session_id\": session_id,\n \"session_version\": session_version,\n \"operation\": operation,\n },\n )\n\n if operation == \"dataset_review_approve_mappings\":\n mapping_ids = list(dict.fromkeys(entities.get(\"mapping_ids\") or []))\n if not mapping_ids:\n raise HTTPException(\n status_code=409, detail=\"No pending mappings to approve\"\n )\n updated_count = 0\n for mapping in session.execution_mappings:\n if mapping.mapping_id not in mapping_ids:\n continue\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n updated_count += 1\n if updated_count == 0:\n raise HTTPException(\n status_code=409, detail=\"No matching mappings available to approve\"\n )\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_mapping_approval\",\n event_summary=\"Assistant-approved warning-sensitive mappings persisted\",\n event_details={\n \"mapping_ids\": mapping_ids,\n \"count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant mapping approval persisted within optimistic-lock boundary\",\n extra={\n \"session_id\": session_id,\n \"updated_count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Approved {updated_count} mapping(s) for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open mapping review\",\n target=\"mapping\",\n )\n ],\n )\n\n if operation == \"dataset_review_set_field_semantics\":\n field_id = str(entities.get(\"field_id\") or \"\").strip()\n if not field_id:\n raise HTTPException(status_code=422, detail=\"Missing field_id\")\n field = next(\n (item for item in session.semantic_fields if item.field_id == field_id),\n None,\n )\n if field is None:\n raise HTTPException(status_code=404, detail=\"Semantic field not found\")\n update_request = FieldSemanticUpdateRequest(\n candidate_id=entities.get(\"candidate_id\"),\n verbose_name=entities.get(\"verbose_name\"),\n description=entities.get(\"description\"),\n display_format=entities.get(\"display_format\"),\n lock_field=bool(entities.get(\"lock_field\", False)),\n )\n try:\n _update_semantic_field_state(\n field, update_request, changed_by=\"assistant\"\n )\n except HTTPException:\n raise\n except ValueError as exc:\n raise HTTPException(status_code=400, detail=str(exc)) from exc\n session.last_activity_at = datetime.utcnow()\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.db.refresh(field)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_field_semantics_updated\",\n event_summary=\"Assistant semantic field update persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"candidate_id\": entities.get(\"candidate_id\"),\n \"lock_field\": bool(entities.get(\"lock_field\", False)),\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant semantic field update committed safely\",\n extra={\n \"session_id\": session_id,\n \"field_id\": field_id,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Updated semantic field {field.field_name} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open semantic review\",\n target=f\"field:{field.field_id}\",\n )\n ],\n )\n\n if operation == \"dataset_review_generate_sql_preview\":\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=config_manager,\n )\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=int(session_version),\n )\n )\n preview_status = getattr(\n result.preview.preview_status, \"value\", result.preview.preview_status\n )\n logger.reflect(\n \"Assistant-triggered Superset preview generation completed\",\n extra={\n \"session_id\": session_id,\n \"preview_status\": preview_status,\n },\n )\n return (\n f\"SQL preview {preview_status} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open SQL preview\",\n target=\"sql-preview\",\n )\n ],\n )\n\n raise HTTPException(\n status_code=400, detail=\"Unsupported dataset review operation\"\n )\n\n\n# [/DEF:_dispatch_dataset_review_intent:Function]\n\n\n# [/DEF:AssistantDatasetReviewDispatch:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantDatasetReviewDispatch [C:4] [TYPE Module] [SEMANTICS assistant, dataset, review, dispatch, confirm]\n# @BRIEF Dispatch and confirmation handling for dataset-review assistant intents.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantDatasetReview]\n# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator]\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT: Dataset review dispatch requires valid session version for write operations.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom fastapi import HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.dataset_review import FieldSemanticUpdateRequest, _update_semantic_field_state\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n ApprovalState,\n ReadinessState,\n RecommendedAction,\n)\nfrom src.schemas.auth import User\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n PreparePreviewCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\n\nfrom ._schemas import (\n AssistantAction,\n)\n\n\n# #region _dataset_review_conflict_http_exception [C:2] [TYPE Function]\n# @BRIEF Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics.\ndef _dataset_review_conflict_http_exception(\n exc: DatasetReviewSessionVersionConflictError,\n) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\n \"error_code\": \"session_version_conflict\",\n \"message\": str(exc),\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n },\n )\n\n\n# #endregion _dataset_review_conflict_http_exception\n\n\n# #region _dispatch_dataset_review_intent [C:4] [TYPE Function]\n# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.\n# @RELATION CALLS -> DatasetReviewOrchestrator\n# @PRE: context contains valid session data and user intent.\n# @POST: Returns a structured response with planned actions and confirmations.\n# @SIDE_EFFECT: May update session state and enqueue tasks.\nasync def _dispatch_dataset_review_intent(\n intent: dict[str, Any],\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n with belief_scope(\"_dispatch_dataset_review_intent\"):\n logger.reason(\n \"Dispatching assistant dataset-review intent\",\n extra={\"operation\": intent.get(\"operation\")},\n )\n entities = intent.get(\"entities\", {})\n session_id = entities.get(\"dataset_review_session_id\")\n session_version = entities.get(\"session_version\")\n if not session_id or session_version is None:\n raise HTTPException(\n status_code=422,\n detail=\"Missing dataset_review_session_id/session_version\",\n )\n\n operation = str(intent.get(\"operation\") or \"\")\n repository = DatasetReviewSessionRepository(db)\n if operation == \"dataset_review_answer_context\":\n summary = str(entities.get(\"summary\") or \"\")\n logger.reflect(\n \"Returned assistant-safe dataset review context summary\",\n extra={\"session_id\": session_id, \"operation\": operation},\n )\n return summary, None, []\n\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n logger.explore(\n \"Assistant dataset-review intent rejected because session was not found\",\n extra={\"session_id\": session_id, \"user_id\": current_user.id},\n )\n raise HTTPException(\n status_code=404, detail=\"Dataset review session not found\"\n )\n\n try:\n repository.require_session_version(session, int(session_version))\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\n \"Assistant dataset-review intent rejected due to stale session version\",\n extra={\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n \"operation\": operation,\n },\n )\n raise _dataset_review_conflict_http_exception(exc) from exc\n\n logger.reason(\n \"Dispatching confirmed assistant dataset-review intent\",\n extra={\n \"session_id\": session_id,\n \"session_version\": session_version,\n \"operation\": operation,\n },\n )\n\n if operation == \"dataset_review_approve_mappings\":\n mapping_ids = list(dict.fromkeys(entities.get(\"mapping_ids\") or []))\n if not mapping_ids:\n raise HTTPException(\n status_code=409, detail=\"No pending mappings to approve\"\n )\n updated_count = 0\n for mapping in session.execution_mappings:\n if mapping.mapping_id not in mapping_ids:\n continue\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n updated_count += 1\n if updated_count == 0:\n raise HTTPException(\n status_code=409, detail=\"No matching mappings available to approve\"\n )\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_mapping_approval\",\n event_summary=\"Assistant-approved warning-sensitive mappings persisted\",\n event_details={\n \"mapping_ids\": mapping_ids,\n \"count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant mapping approval persisted within optimistic-lock boundary\",\n extra={\n \"session_id\": session_id,\n \"updated_count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Approved {updated_count} mapping(s) for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open mapping review\",\n target=\"mapping\",\n )\n ],\n )\n\n if operation == \"dataset_review_set_field_semantics\":\n field_id = str(entities.get(\"field_id\") or \"\").strip()\n if not field_id:\n raise HTTPException(status_code=422, detail=\"Missing field_id\")\n field = next(\n (item for item in session.semantic_fields if item.field_id == field_id),\n None,\n )\n if field is None:\n raise HTTPException(status_code=404, detail=\"Semantic field not found\")\n update_request = FieldSemanticUpdateRequest(\n candidate_id=entities.get(\"candidate_id\"),\n verbose_name=entities.get(\"verbose_name\"),\n description=entities.get(\"description\"),\n display_format=entities.get(\"display_format\"),\n lock_field=bool(entities.get(\"lock_field\", False)),\n )\n try:\n _update_semantic_field_state(\n field, update_request, changed_by=\"assistant\"\n )\n except HTTPException:\n raise\n except ValueError as exc:\n raise HTTPException(status_code=400, detail=str(exc)) from exc\n session.last_activity_at = datetime.utcnow()\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.db.refresh(field)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_field_semantics_updated\",\n event_summary=\"Assistant semantic field update persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"candidate_id\": entities.get(\"candidate_id\"),\n \"lock_field\": bool(entities.get(\"lock_field\", False)),\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant semantic field update committed safely\",\n extra={\n \"session_id\": session_id,\n \"field_id\": field_id,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Updated semantic field {field.field_name} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open semantic review\",\n target=f\"field:{field.field_id}\",\n )\n ],\n )\n\n if operation == \"dataset_review_generate_sql_preview\":\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=config_manager,\n )\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=int(session_version),\n )\n )\n preview_status = getattr(\n result.preview.preview_status, \"value\", result.preview.preview_status\n )\n logger.reflect(\n \"Assistant-triggered Superset preview generation completed\",\n extra={\n \"session_id\": session_id,\n \"preview_status\": preview_status,\n },\n )\n return (\n f\"SQL preview {preview_status} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open SQL preview\",\n target=\"sql-preview\",\n )\n ],\n )\n\n raise HTTPException(\n status_code=400, detail=\"Unsupported dataset review operation\"\n )\n\n\n# #endregion _dispatch_dataset_review_intent\n\n\n# #endregion AssistantDatasetReviewDispatch\n" }, { "contract_id": "_dataset_review_conflict_http_exception", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dataset_review_dispatch.py", "start_line": 40, - "end_line": 58, + "end_line": 57, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_dataset_review_conflict_http_exception:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics.\ndef _dataset_review_conflict_http_exception(\n exc: DatasetReviewSessionVersionConflictError,\n) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\n \"error_code\": \"session_version_conflict\",\n \"message\": str(exc),\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n },\n )\n\n\n# [/DEF:_dataset_review_conflict_http_exception:Function]\n" + "anchor_syntax": "region", + "body": "# #region _dataset_review_conflict_http_exception [C:2] [TYPE Function]\n# @BRIEF Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics.\ndef _dataset_review_conflict_http_exception(\n exc: DatasetReviewSessionVersionConflictError,\n) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\n \"error_code\": \"session_version_conflict\",\n \"message\": str(exc),\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n },\n )\n\n\n# #endregion _dataset_review_conflict_http_exception\n" }, { "contract_id": "_dispatch_dataset_review_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dataset_review_dispatch.py", - "start_line": 61, - "end_line": 287, + "start_line": 60, + "end_line": 285, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "POST": "Returns a structured response with planned actions and confirmations.", "PRE": "context contains valid session data and user intent.", "PURPOSE": "Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.", @@ -9444,19 +7897,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_dispatch_dataset_review_intent:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.\n# @RELATION: CALLS -> DatasetReviewOrchestrator\n# @PRE: context contains valid session data and user intent.\n# @POST: Returns a structured response with planned actions and confirmations.\n# @SIDE_EFFECT: May update session state and enqueue tasks.\nasync def _dispatch_dataset_review_intent(\n intent: Dict[str, Any],\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> Tuple[str, Optional[str], List[AssistantAction]]:\n with belief_scope(\"_dispatch_dataset_review_intent\"):\n logger.reason(\n \"Dispatching assistant dataset-review intent\",\n extra={\"operation\": intent.get(\"operation\")},\n )\n entities = intent.get(\"entities\", {})\n session_id = entities.get(\"dataset_review_session_id\")\n session_version = entities.get(\"session_version\")\n if not session_id or session_version is None:\n raise HTTPException(\n status_code=422,\n detail=\"Missing dataset_review_session_id/session_version\",\n )\n\n operation = str(intent.get(\"operation\") or \"\")\n repository = DatasetReviewSessionRepository(db)\n if operation == \"dataset_review_answer_context\":\n summary = str(entities.get(\"summary\") or \"\")\n logger.reflect(\n \"Returned assistant-safe dataset review context summary\",\n extra={\"session_id\": session_id, \"operation\": operation},\n )\n return summary, None, []\n\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n logger.explore(\n \"Assistant dataset-review intent rejected because session was not found\",\n extra={\"session_id\": session_id, \"user_id\": current_user.id},\n )\n raise HTTPException(\n status_code=404, detail=\"Dataset review session not found\"\n )\n\n try:\n repository.require_session_version(session, int(session_version))\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\n \"Assistant dataset-review intent rejected due to stale session version\",\n extra={\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n \"operation\": operation,\n },\n )\n raise _dataset_review_conflict_http_exception(exc) from exc\n\n logger.reason(\n \"Dispatching confirmed assistant dataset-review intent\",\n extra={\n \"session_id\": session_id,\n \"session_version\": session_version,\n \"operation\": operation,\n },\n )\n\n if operation == \"dataset_review_approve_mappings\":\n mapping_ids = list(dict.fromkeys(entities.get(\"mapping_ids\") or []))\n if not mapping_ids:\n raise HTTPException(\n status_code=409, detail=\"No pending mappings to approve\"\n )\n updated_count = 0\n for mapping in session.execution_mappings:\n if mapping.mapping_id not in mapping_ids:\n continue\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n updated_count += 1\n if updated_count == 0:\n raise HTTPException(\n status_code=409, detail=\"No matching mappings available to approve\"\n )\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_mapping_approval\",\n event_summary=\"Assistant-approved warning-sensitive mappings persisted\",\n event_details={\n \"mapping_ids\": mapping_ids,\n \"count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant mapping approval persisted within optimistic-lock boundary\",\n extra={\n \"session_id\": session_id,\n \"updated_count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Approved {updated_count} mapping(s) for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open mapping review\",\n target=\"mapping\",\n )\n ],\n )\n\n if operation == \"dataset_review_set_field_semantics\":\n field_id = str(entities.get(\"field_id\") or \"\").strip()\n if not field_id:\n raise HTTPException(status_code=422, detail=\"Missing field_id\")\n field = next(\n (item for item in session.semantic_fields if item.field_id == field_id),\n None,\n )\n if field is None:\n raise HTTPException(status_code=404, detail=\"Semantic field not found\")\n update_request = FieldSemanticUpdateRequest(\n candidate_id=entities.get(\"candidate_id\"),\n verbose_name=entities.get(\"verbose_name\"),\n description=entities.get(\"description\"),\n display_format=entities.get(\"display_format\"),\n lock_field=bool(entities.get(\"lock_field\", False)),\n )\n try:\n _update_semantic_field_state(\n field, update_request, changed_by=\"assistant\"\n )\n except HTTPException:\n raise\n except ValueError as exc:\n raise HTTPException(status_code=400, detail=str(exc)) from exc\n session.last_activity_at = datetime.utcnow()\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.db.refresh(field)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_field_semantics_updated\",\n event_summary=\"Assistant semantic field update persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"candidate_id\": entities.get(\"candidate_id\"),\n \"lock_field\": bool(entities.get(\"lock_field\", False)),\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant semantic field update committed safely\",\n extra={\n \"session_id\": session_id,\n \"field_id\": field_id,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Updated semantic field {field.field_name} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open semantic review\",\n target=f\"field:{field.field_id}\",\n )\n ],\n )\n\n if operation == \"dataset_review_generate_sql_preview\":\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=config_manager,\n )\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=int(session_version),\n )\n )\n preview_status = getattr(\n result.preview.preview_status, \"value\", result.preview.preview_status\n )\n logger.reflect(\n \"Assistant-triggered Superset preview generation completed\",\n extra={\n \"session_id\": session_id,\n \"preview_status\": preview_status,\n },\n )\n return (\n f\"SQL preview {preview_status} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open SQL preview\",\n target=\"sql-preview\",\n )\n ],\n )\n\n raise HTTPException(\n status_code=400, detail=\"Unsupported dataset review operation\"\n )\n\n\n# [/DEF:_dispatch_dataset_review_intent:Function]\n" + "anchor_syntax": "region", + "body": "# #region _dispatch_dataset_review_intent [C:4] [TYPE Function]\n# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.\n# @RELATION CALLS -> DatasetReviewOrchestrator\n# @PRE: context contains valid session data and user intent.\n# @POST: Returns a structured response with planned actions and confirmations.\n# @SIDE_EFFECT: May update session state and enqueue tasks.\nasync def _dispatch_dataset_review_intent(\n intent: dict[str, Any],\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n with belief_scope(\"_dispatch_dataset_review_intent\"):\n logger.reason(\n \"Dispatching assistant dataset-review intent\",\n extra={\"operation\": intent.get(\"operation\")},\n )\n entities = intent.get(\"entities\", {})\n session_id = entities.get(\"dataset_review_session_id\")\n session_version = entities.get(\"session_version\")\n if not session_id or session_version is None:\n raise HTTPException(\n status_code=422,\n detail=\"Missing dataset_review_session_id/session_version\",\n )\n\n operation = str(intent.get(\"operation\") or \"\")\n repository = DatasetReviewSessionRepository(db)\n if operation == \"dataset_review_answer_context\":\n summary = str(entities.get(\"summary\") or \"\")\n logger.reflect(\n \"Returned assistant-safe dataset review context summary\",\n extra={\"session_id\": session_id, \"operation\": operation},\n )\n return summary, None, []\n\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n logger.explore(\n \"Assistant dataset-review intent rejected because session was not found\",\n extra={\"session_id\": session_id, \"user_id\": current_user.id},\n )\n raise HTTPException(\n status_code=404, detail=\"Dataset review session not found\"\n )\n\n try:\n repository.require_session_version(session, int(session_version))\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\n \"Assistant dataset-review intent rejected due to stale session version\",\n extra={\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n \"operation\": operation,\n },\n )\n raise _dataset_review_conflict_http_exception(exc) from exc\n\n logger.reason(\n \"Dispatching confirmed assistant dataset-review intent\",\n extra={\n \"session_id\": session_id,\n \"session_version\": session_version,\n \"operation\": operation,\n },\n )\n\n if operation == \"dataset_review_approve_mappings\":\n mapping_ids = list(dict.fromkeys(entities.get(\"mapping_ids\") or []))\n if not mapping_ids:\n raise HTTPException(\n status_code=409, detail=\"No pending mappings to approve\"\n )\n updated_count = 0\n for mapping in session.execution_mappings:\n if mapping.mapping_id not in mapping_ids:\n continue\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n updated_count += 1\n if updated_count == 0:\n raise HTTPException(\n status_code=409, detail=\"No matching mappings available to approve\"\n )\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_mapping_approval\",\n event_summary=\"Assistant-approved warning-sensitive mappings persisted\",\n event_details={\n \"mapping_ids\": mapping_ids,\n \"count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant mapping approval persisted within optimistic-lock boundary\",\n extra={\n \"session_id\": session_id,\n \"updated_count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Approved {updated_count} mapping(s) for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open mapping review\",\n target=\"mapping\",\n )\n ],\n )\n\n if operation == \"dataset_review_set_field_semantics\":\n field_id = str(entities.get(\"field_id\") or \"\").strip()\n if not field_id:\n raise HTTPException(status_code=422, detail=\"Missing field_id\")\n field = next(\n (item for item in session.semantic_fields if item.field_id == field_id),\n None,\n )\n if field is None:\n raise HTTPException(status_code=404, detail=\"Semantic field not found\")\n update_request = FieldSemanticUpdateRequest(\n candidate_id=entities.get(\"candidate_id\"),\n verbose_name=entities.get(\"verbose_name\"),\n description=entities.get(\"description\"),\n display_format=entities.get(\"display_format\"),\n lock_field=bool(entities.get(\"lock_field\", False)),\n )\n try:\n _update_semantic_field_state(\n field, update_request, changed_by=\"assistant\"\n )\n except HTTPException:\n raise\n except ValueError as exc:\n raise HTTPException(status_code=400, detail=str(exc)) from exc\n session.last_activity_at = datetime.utcnow()\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.db.refresh(field)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_field_semantics_updated\",\n event_summary=\"Assistant semantic field update persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"candidate_id\": entities.get(\"candidate_id\"),\n \"lock_field\": bool(entities.get(\"lock_field\", False)),\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant semantic field update committed safely\",\n extra={\n \"session_id\": session_id,\n \"field_id\": field_id,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Updated semantic field {field.field_name} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open semantic review\",\n target=f\"field:{field.field_id}\",\n )\n ],\n )\n\n if operation == \"dataset_review_generate_sql_preview\":\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=config_manager,\n )\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=int(session_version),\n )\n )\n preview_status = getattr(\n result.preview.preview_status, \"value\", result.preview.preview_status\n )\n logger.reflect(\n \"Assistant-triggered Superset preview generation completed\",\n extra={\n \"session_id\": session_id,\n \"preview_status\": preview_status,\n },\n )\n return (\n f\"SQL preview {preview_status} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open SQL preview\",\n target=\"sql-preview\",\n )\n ],\n )\n\n raise HTTPException(\n status_code=400, detail=\"Unsupported dataset review operation\"\n )\n\n\n# #endregion _dispatch_dataset_review_intent\n" }, { "contract_id": "AssistantDispatch", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_dispatch.py", "start_line": 1, - "end_line": 309, + "end_line": 306, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "INVARIANT": "Unsupported operations are rejected via HTTPException(400).", "LAYER": "API", "PURPOSE": "Intent dispatch engine, confirmation summary, and clarification text for the assistant API." @@ -9543,19 +7996,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantDispatch:Module]\n# @COMPLEXITY: 5\n# @PURPOSE: Intent dispatch engine, confirmation summary, and clarification text for the assistant API.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [AssistantSchemas]\n# @RELATION: DEPENDS_ON -> [AssistantResolvers]\n# @RELATION: DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION: DEPENDS_ON -> [AssistantDatasetReview]\n# @INVARIANT: Unsupported operations are rejected via HTTPException(400).\n\nfrom __future__ import annotations\n\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.config_manager import ConfigManager\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\nfrom src.services.git_service import GitService\nfrom src.services.llm_provider import LLMProviderService\nfrom src.services.llm_prompt_templates import is_multimodal_model\nfrom ._schemas import (\n _DATASET_REVIEW_OPS,\n AssistantAction,\n)\nfrom ._resolvers import (\n _build_task_observability_summary,\n _extract_result_deep_links,\n _get_environment_name_by_id,\n _resolve_dashboard_id_entity,\n _resolve_env_id,\n _resolve_provider_id,\n)\nfrom ._history import _coerce_query_bool\nfrom ._llm_planner import _check_any_permission\nfrom ._dataset_review_dispatch import _dispatch_dataset_review_intent\n\ngit_service = GitService()\n\n\n# [DEF:_clarification_text_for_intent:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Convert technical missing-parameter errors into user-facing clarification prompts.\n# @PRE: state was classified as needs_clarification for current intent/error combination.\n# @POST: Returned text is human-readable and actionable for target operation.\ndef _clarification_text_for_intent(\n intent: Optional[Dict[str, Any]], detail_text: str\n) -> str:\n operation = (intent or {}).get(\"operation\")\n guidance_by_operation: Dict[str, str] = {\n \"run_llm_validation\": (\n \"Нужно уточнение для запуска LLM-валидации: Укажите дашборд (id или slug), окружение и провайдер LLM.\"\n ),\n \"run_llm_documentation\": (\n \"Нужно уточнение для генерации документации: Укажите dataset_id, окружение и провайдер LLM.\"\n ),\n \"create_branch\": \"Нужно уточнение: укажите дашборд (id/slug/title) и имя ветки.\",\n \"commit_changes\": \"Нужно уточнение: укажите дашборд (id/slug/title) для коммита.\",\n \"deploy_dashboard\": \"Нужно уточнение: укажите дашборд (id/slug/title) и целевое окружение.\",\n \"execute_migration\": \"Нужно уточнение: укажите дашборд (id/slug/title), source_env и target_env.\",\n \"run_backup\": \"Нужно уточнение: укажите окружение и при необходимости дашборд (id/slug/title).\",\n }\n return guidance_by_operation.get(operation, detail_text)\n\n\n# [/DEF:_clarification_text_for_intent:Function]\n\n\n# [DEF:_async_confirmation_summary:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Build human-readable confirmation prompt for an intent before execution.\n# @PRE: actions is a non-empty list of planned review actions.\n# @POST: Returns a formatted summary string suitable for display to the user.\n# @SIDE_EFFECT: None - pure formatting function.\nasync def _async_confirmation_summary(intent: Dict[str, Any], config_manager: ConfigManager, db: Session) -> str:\n with belief_scope('_confirmation_summary'):\n logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')\n operation = intent.get('operation', '')\n entities = intent.get('entities', {})\n descriptions: Dict[str, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'}\n template = descriptions.get(operation)\n if not template:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return 'Подтвердите выполнение операции или отмените.'\n\n def _label(value: Any, prefix: str=' ') -> str:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'{prefix}{value}' if value else ''\n dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref')\n text = template.format(branch=_label(entities.get('branch_name')), dashboard=_label(dashboard), env=_label(entities.get('environment') or entities.get('target_env')), src=_label(entities.get('source_env')), tgt=_label(entities.get('target_env')), dataset=_label(entities.get('dataset_id')))\n if operation == 'execute_migration':\n flags = []\n flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ'))\n flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ'))\n dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False))\n flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ'))\n text += f\" ({', '.join(flags)})\"\n if dry_run_enabled:\n try:\n from src.core.migration.dry_run_orchestrator import MigrationDryRunService\n from src.models.dashboard import DashboardSelection\n from src.core.superset_client import SupersetClient\n src_token = entities.get('source_env')\n tgt_token = entities.get('target_env')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n if dashboard_id and src_token and tgt_token:\n src_env_id = _resolve_env_id(src_token, config_manager)\n tgt_env_id = _resolve_env_id(tgt_token, config_manager)\n if src_env_id and tgt_env_id:\n env_map = {env.id: env for env in config_manager.get_environments()}\n source_env = env_map.get(src_env_id)\n target_env = env_map.get(tgt_env_id)\n if source_env and target_env and (source_env.id != target_env.id):\n selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], replace_db_config=_coerce_query_bool(entities.get('replace_db_config', False)), fix_cross_filters=_coerce_query_bool(entities.get('fix_cross_filters', True)))\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n report = service.run(selection, source_client, target_client, db)\n s = report.get('summary', {})\n dash_s = s.get('dashboards', {})\n charts_s = s.get('charts', {})\n ds_s = s.get('datasets', {})\n creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0)\n updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0)\n deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0)\n text += f'\\n\\nОтчет dry-run:\\n- Будет создано новых объектов: {creates}\\n- Будет обновлено: {updates}\\n- Будет удалено: {deletes}'\n else:\n text += '\\n\\n(Не удалось загрузить отчет dry-run: неверные окружения).'\n except Exception as e:\n import traceback\n logger.warning('[assistant.dry_run_summary][failed] Exception: %s\\n%s', e, traceback.format_exc())\n text += f'\\n\\n(Не удалось загрузить отчет dry-run: {e}).'\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'Выполнить: {text}. Подтвердите или отмените.'\n\n\n# [/DEF:_async_confirmation_summary:Function]\n\n\n# [DEF:_dispatch_intent:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Execute parsed assistant intent via existing task/plugin/git services.\n# @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]\n# @RELATION: DEPENDS_ON -> [_check_any_permission]\n# @RELATION: DEPENDS_ON -> [_resolve_dashboard_id_entity]\n# @RELATION: DEPENDS_ON -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [GitService]\n# @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state.\n# @PRE: intent operation is known and actor permissions are validated per operation.\n# @POST: Returns response text, optional task id, and UI actions for follow-up.\n# @INVARIANT: unsupported operations are rejected via HTTPException(400).\nasync def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> Tuple[str, Optional[str], List[AssistantAction]]:\n with belief_scope('_dispatch_intent'):\n logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent')\n operation = intent.get('operation')\n entities = intent.get('entities', {})\n if operation in _DATASET_REVIEW_OPS or operation == 'dataset_review_answer_context':\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return await _dispatch_dataset_review_intent(intent, current_user, config_manager, db)\n if operation == 'show_capabilities':\n from ._llm_planner import _build_tool_catalog\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n labels = {'create_branch': 'Git: создание ветки', 'commit_changes': 'Git: коммит', 'deploy_dashboard': 'Git: деплой дашборда', 'execute_migration': 'Миграции: запуск переноса', 'run_backup': 'Бэкапы: запуск резервного копирования', 'run_llm_validation': 'LLM: валидация дашборда', 'run_llm_documentation': 'LLM: генерация документации', 'get_task_status': 'Статус: проверка задачи', 'get_health_summary': 'Здоровье: сводка по дашбордам'}\n available = [labels[t['operation']] for t in tools_catalog if t['operation'] in labels]\n if not available:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Сейчас нет доступных для вас операций ассистента.', None, [])\n commands = '\\n'.join((f'- {item}' for item in available))\n text = f'Вот что я могу сделать для вас:\\n{commands}\\n\\nПример: `запусти миграцию с dev на prod для дашборда 42`.'\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, [])\n if operation == 'get_health_summary':\n from src.services.health_service import HealthService\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=env_id)\n env_name = _get_environment_name_by_id(env_id, config_manager) if env_id else 'всех окружений'\n text = f'Сводка здоровья дашбордов для {env_name}:\\n- ✅ Прошли проверку: {summary.pass_count}\\n- ⚠️ С предупреждениями: {summary.warn_count}\\n- ❌ Ошибки валидации: {summary.fail_count}\\n- ❓ Неизвестно: {summary.unknown_count}'\n actions = [AssistantAction(type='open_route', label='Открыть Health Center', target='/dashboards/health')]\n if summary.fail_count > 0:\n text += '\\n\\nОбнаружены ошибки в следующих дашбордах:'\n for item in summary.items:\n if item.status == 'FAIL':\n text += f\"\\n- {item.dashboard_id} ({item.environment_id}): {item.summary or 'Нет деталей'}\"\n actions.append(AssistantAction(type='open_route', label=f'Отчет {item.dashboard_id}', target=f'/reports/llm/{item.task_id}'))\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, actions[:5])\n if operation == 'get_task_status':\n _check_any_permission(current_user, [('tasks', 'READ')])\n task_id = entities.get('task_id')\n if not task_id:\n recent = [t for t in task_manager.get_tasks(limit=20, offset=0) if t.user_id == current_user.id]\n if not recent:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('У вас пока нет задач в истории.', None, [])\n task = recent[0]\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Последняя задача: {task.id}, статус: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(status_code=404, detail=f'Task {task_id} not found')\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Статус задачи {task.id}: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n if operation == 'create_branch':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n branch_name = entities.get('branch_name')\n if not dashboard_id or not branch_name:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or branch_name')\n git_service.create_branch(dashboard_id, branch_name, 'main')\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Ветка `{branch_name}` создана для дашборда {dashboard_id}.', None, [])\n if operation == 'commit_changes':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n commit_message = entities.get('message')\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n git_service.commit_changes(dashboard_id, commit_message, None)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Коммит выполнен успешно.', None, [])\n if operation == 'deploy_dashboard':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id or not env_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or environment')\n task = await task_manager.create_task(plugin_id='git-integration', params={'operation': 'deploy', 'dashboard_id': dashboard_id, 'environment_id': env_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Деплой запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'execute_migration':\n _check_any_permission(current_user, [('plugin:migration', 'EXECUTE'), ('plugin:superset-migration', 'EXECUTE')])\n src_token = entities.get('source_env')\n dashboard_ref = entities.get('dashboard_ref')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n src = _resolve_env_id(src_token, config_manager)\n tgt = _resolve_env_id(entities.get('target_env'), config_manager)\n if not src or not tgt:\n raise HTTPException(status_code=422, detail='Missing source_env/target_env')\n if not dashboard_id and (not dashboard_ref):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n migration_params: Dict[str, Any] = {'source_env_id': src, 'target_env_id': tgt, 'replace_db_config': _coerce_query_bool(entities.get('replace_db_config', False)), 'fix_cross_filters': _coerce_query_bool(entities.get('fix_cross_filters', True))}\n if dashboard_id:\n migration_params['selected_ids'] = [dashboard_id]\n else:\n migration_params['dashboard_regex'] = str(dashboard_ref)\n task = await task_manager.create_task(plugin_id='superset-migration', params=migration_params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Миграция запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={tgt}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if dashboard_id else [])])\n if operation == 'run_backup':\n _check_any_permission(current_user, [('plugin:superset-backup', 'EXECUTE'), ('plugin:backup', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n if not env_id:\n raise HTTPException(status_code=400, detail='Missing or unknown environment')\n params: Dict[str, Any] = {'environment_id': env_id}\n if entities.get('dashboard_id') or entities.get('dashboard_ref'):\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n params['dashboard_ids'] = [dashboard_id]\n task = await task_manager.create_task(plugin_id='superset-backup', params=params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Бэкап запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={env_id}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if entities.get('dashboard_id') or entities.get('dashboard_ref') else [])])\n if operation == 'run_llm_validation':\n _check_any_permission(current_user, [('plugin:llm_dashboard_validation', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager) or _resolve_env_id(None, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='dashboard_validation')\n if not dashboard_id or not env_id or (not provider_id):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.')\n provider = LLMProviderService(db).get_provider(provider_id)\n provider_model = provider.default_model if provider else ''\n if not is_multimodal_model(provider_model, provider.provider_type if provider else None):\n raise HTTPException(status_code=422, detail='Selected provider model is not multimodal for dashboard validation. Выберите мультимодальную модель (например, gpt-4o).')\n task = await task_manager.create_task(plugin_id='llm_dashboard_validation', params={'dashboard_id': str(dashboard_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'LLM-валидация запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'run_llm_documentation':\n _check_any_permission(current_user, [('plugin:llm_documentation', 'EXECUTE')])\n dataset_id = entities.get('dataset_id')\n env_id = _resolve_env_id(entities.get('environment'), config_manager)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='documentation')\n if not dataset_id or not env_id or (not provider_id):\n raise HTTPException(status_code=400, detail='Missing dataset_id/environment/provider')\n task = await task_manager.create_task(plugin_id='llm_documentation', params={'dataset_id': str(dataset_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Генерация документации запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n raise HTTPException(status_code=400, detail='Unsupported operation')\n\n\n# [/DEF:_dispatch_intent:Function]\n\n\n# [/DEF:AssistantDispatch:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantDispatch [C:5] [TYPE Module] [SEMANTICS assistant, dispatch, confirm, execution, orchestration]\n# @BRIEF Intent dispatch engine, confirmation summary, and clarification text for the assistant API.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantDatasetReview]\n# @INVARIANT: Unsupported operations are rejected via HTTPException(400).\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\nfrom src.services.git_service import GitService\nfrom src.services.llm_prompt_templates import is_multimodal_model\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._dataset_review_dispatch import _dispatch_dataset_review_intent\nfrom ._history import _coerce_query_bool\nfrom ._llm_planner import _check_any_permission\nfrom ._resolvers import (\n _build_task_observability_summary,\n _extract_result_deep_links,\n _get_environment_name_by_id,\n _resolve_dashboard_id_entity,\n _resolve_env_id,\n _resolve_provider_id,\n)\nfrom ._schemas import (\n _DATASET_REVIEW_OPS,\n AssistantAction,\n)\n\ngit_service = GitService()\n\n\n# #region _clarification_text_for_intent [C:2] [TYPE Function]\n# @BRIEF Convert technical missing-parameter errors into user-facing clarification prompts.\n# @PRE: state was classified as needs_clarification for current intent/error combination.\n# @POST: Returned text is human-readable and actionable for target operation.\ndef _clarification_text_for_intent(\n intent: dict[str, Any] | None, detail_text: str\n) -> str:\n operation = (intent or {}).get(\"operation\")\n guidance_by_operation: dict[str, str] = {\n \"run_llm_validation\": (\n \"Нужно уточнение для запуска LLM-валидации: Укажите дашборд (id или slug), окружение и провайдер LLM.\"\n ),\n \"run_llm_documentation\": (\n \"Нужно уточнение для генерации документации: Укажите dataset_id, окружение и провайдер LLM.\"\n ),\n \"create_branch\": \"Нужно уточнение: укажите дашборд (id/slug/title) и имя ветки.\",\n \"commit_changes\": \"Нужно уточнение: укажите дашборд (id/slug/title) для коммита.\",\n \"deploy_dashboard\": \"Нужно уточнение: укажите дашборд (id/slug/title) и целевое окружение.\",\n \"execute_migration\": \"Нужно уточнение: укажите дашборд (id/slug/title), source_env и target_env.\",\n \"run_backup\": \"Нужно уточнение: укажите окружение и при необходимости дашборд (id/slug/title).\",\n }\n return guidance_by_operation.get(operation, detail_text)\n\n\n# #endregion _clarification_text_for_intent\n\n\n# #region _async_confirmation_summary [C:4] [TYPE Function]\n# @BRIEF Build human-readable confirmation prompt for an intent before execution.\n# @PRE: actions is a non-empty list of planned review actions.\n# @POST: Returns a formatted summary string suitable for display to the user.\n# @SIDE_EFFECT: None - pure formatting function.\nasync def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:\n with belief_scope('_confirmation_summary'):\n logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')\n operation = intent.get('operation', '')\n entities = intent.get('entities', {})\n descriptions: dict[str, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'}\n template = descriptions.get(operation)\n if not template:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return 'Подтвердите выполнение операции или отмените.'\n\n def _label(value: Any, prefix: str=' ') -> str:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'{prefix}{value}' if value else ''\n dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref')\n text = template.format(branch=_label(entities.get('branch_name')), dashboard=_label(dashboard), env=_label(entities.get('environment') or entities.get('target_env')), src=_label(entities.get('source_env')), tgt=_label(entities.get('target_env')), dataset=_label(entities.get('dataset_id')))\n if operation == 'execute_migration':\n flags = []\n flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ'))\n flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ'))\n dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False))\n flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ'))\n text += f\" ({', '.join(flags)})\"\n if dry_run_enabled:\n try:\n from src.core.migration.dry_run_orchestrator import MigrationDryRunService\n from src.core.superset_client import SupersetClient\n from src.models.dashboard import DashboardSelection\n src_token = entities.get('source_env')\n tgt_token = entities.get('target_env')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n if dashboard_id and src_token and tgt_token:\n src_env_id = _resolve_env_id(src_token, config_manager)\n tgt_env_id = _resolve_env_id(tgt_token, config_manager)\n if src_env_id and tgt_env_id:\n env_map = {env.id: env for env in config_manager.get_environments()}\n source_env = env_map.get(src_env_id)\n target_env = env_map.get(tgt_env_id)\n if source_env and target_env and (source_env.id != target_env.id):\n selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], replace_db_config=_coerce_query_bool(entities.get('replace_db_config', False)), fix_cross_filters=_coerce_query_bool(entities.get('fix_cross_filters', True)))\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n report = service.run(selection, source_client, target_client, db)\n s = report.get('summary', {})\n dash_s = s.get('dashboards', {})\n charts_s = s.get('charts', {})\n ds_s = s.get('datasets', {})\n creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0)\n updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0)\n deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0)\n text += f'\\n\\nОтчет dry-run:\\n- Будет создано новых объектов: {creates}\\n- Будет обновлено: {updates}\\n- Будет удалено: {deletes}'\n else:\n text += '\\n\\n(Не удалось загрузить отчет dry-run: неверные окружения).'\n except Exception as e:\n import traceback\n logger.warning('[assistant.dry_run_summary][failed] Exception: %s\\n%s', e, traceback.format_exc())\n text += f'\\n\\n(Не удалось загрузить отчет dry-run: {e}).'\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'Выполнить: {text}. Подтвердите или отмените.'\n\n\n# #endregion _async_confirmation_summary\n\n\n# #region _dispatch_intent [C:5] [TYPE Function]\n# @BRIEF Execute parsed assistant intent via existing task/plugin/git services.\n# @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]\n# @RELATION DEPENDS_ON -> [_check_any_permission]\n# @RELATION DEPENDS_ON -> [_resolve_dashboard_id_entity]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [GitService]\n# @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state.\n# @PRE: intent operation is known and actor permissions are validated per operation.\n# @POST: Returns response text, optional task id, and UI actions for follow-up.\n# @INVARIANT: unsupported operations are rejected via HTTPException(400).\nasync def _dispatch_intent(intent: dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> tuple[str, str | None, list[AssistantAction]]:\n with belief_scope('_dispatch_intent'):\n logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent')\n operation = intent.get('operation')\n entities = intent.get('entities', {})\n if operation in _DATASET_REVIEW_OPS or operation == 'dataset_review_answer_context':\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return await _dispatch_dataset_review_intent(intent, current_user, config_manager, db)\n if operation == 'show_capabilities':\n from ._llm_planner import _build_tool_catalog\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n labels = {'create_branch': 'Git: создание ветки', 'commit_changes': 'Git: коммит', 'deploy_dashboard': 'Git: деплой дашборда', 'execute_migration': 'Миграции: запуск переноса', 'run_backup': 'Бэкапы: запуск резервного копирования', 'run_llm_validation': 'LLM: валидация дашборда', 'run_llm_documentation': 'LLM: генерация документации', 'get_task_status': 'Статус: проверка задачи', 'get_health_summary': 'Здоровье: сводка по дашбордам'}\n available = [labels[t['operation']] for t in tools_catalog if t['operation'] in labels]\n if not available:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Сейчас нет доступных для вас операций ассистента.', None, [])\n commands = '\\n'.join(f'- {item}' for item in available)\n text = f'Вот что я могу сделать для вас:\\n{commands}\\n\\nПример: `запусти миграцию с dev на prod для дашборда 42`.'\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, [])\n if operation == 'get_health_summary':\n from src.services.health_service import HealthService\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=env_id)\n env_name = _get_environment_name_by_id(env_id, config_manager) if env_id else 'всех окружений'\n text = f'Сводка здоровья дашбордов для {env_name}:\\n- ✅ Прошли проверку: {summary.pass_count}\\n- ⚠️ С предупреждениями: {summary.warn_count}\\n- ❌ Ошибки валидации: {summary.fail_count}\\n- ❓ Неизвестно: {summary.unknown_count}'\n actions = [AssistantAction(type='open_route', label='Открыть Health Center', target='/dashboards/health')]\n if summary.fail_count > 0:\n text += '\\n\\nОбнаружены ошибки в следующих дашбордах:'\n for item in summary.items:\n if item.status == 'FAIL':\n text += f\"\\n- {item.dashboard_id} ({item.environment_id}): {item.summary or 'Нет деталей'}\"\n actions.append(AssistantAction(type='open_route', label=f'Отчет {item.dashboard_id}', target=f'/reports/llm/{item.task_id}'))\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, actions[:5])\n if operation == 'get_task_status':\n _check_any_permission(current_user, [('tasks', 'READ')])\n task_id = entities.get('task_id')\n if not task_id:\n recent = [t for t in task_manager.get_tasks(limit=20, offset=0) if t.user_id == current_user.id]\n if not recent:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('У вас пока нет задач в истории.', None, [])\n task = recent[0]\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Последняя задача: {task.id}, статус: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(status_code=404, detail=f'Task {task_id} not found')\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Статус задачи {task.id}: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n if operation == 'create_branch':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n branch_name = entities.get('branch_name')\n if not dashboard_id or not branch_name:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or branch_name')\n git_service.create_branch(dashboard_id, branch_name, 'main')\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Ветка `{branch_name}` создана для дашборда {dashboard_id}.', None, [])\n if operation == 'commit_changes':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n commit_message = entities.get('message')\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n git_service.commit_changes(dashboard_id, commit_message, None)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Коммит выполнен успешно.', None, [])\n if operation == 'deploy_dashboard':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id or not env_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or environment')\n task = await task_manager.create_task(plugin_id='git-integration', params={'operation': 'deploy', 'dashboard_id': dashboard_id, 'environment_id': env_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Деплой запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'execute_migration':\n _check_any_permission(current_user, [('plugin:migration', 'EXECUTE'), ('plugin:superset-migration', 'EXECUTE')])\n src_token = entities.get('source_env')\n dashboard_ref = entities.get('dashboard_ref')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n src = _resolve_env_id(src_token, config_manager)\n tgt = _resolve_env_id(entities.get('target_env'), config_manager)\n if not src or not tgt:\n raise HTTPException(status_code=422, detail='Missing source_env/target_env')\n if not dashboard_id and (not dashboard_ref):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n migration_params: dict[str, Any] = {'source_env_id': src, 'target_env_id': tgt, 'replace_db_config': _coerce_query_bool(entities.get('replace_db_config', False)), 'fix_cross_filters': _coerce_query_bool(entities.get('fix_cross_filters', True))}\n if dashboard_id:\n migration_params['selected_ids'] = [dashboard_id]\n else:\n migration_params['dashboard_regex'] = str(dashboard_ref)\n task = await task_manager.create_task(plugin_id='superset-migration', params=migration_params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Миграция запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={tgt}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if dashboard_id else [])])\n if operation == 'run_backup':\n _check_any_permission(current_user, [('plugin:superset-backup', 'EXECUTE'), ('plugin:backup', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n if not env_id:\n raise HTTPException(status_code=400, detail='Missing or unknown environment')\n params: dict[str, Any] = {'environment_id': env_id}\n if entities.get('dashboard_id') or entities.get('dashboard_ref'):\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n params['dashboard_ids'] = [dashboard_id]\n task = await task_manager.create_task(plugin_id='superset-backup', params=params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Бэкап запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={env_id}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if entities.get('dashboard_id') or entities.get('dashboard_ref') else [])])\n if operation == 'run_llm_validation':\n _check_any_permission(current_user, [('plugin:llm_dashboard_validation', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager) or _resolve_env_id(None, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='dashboard_validation')\n if not dashboard_id or not env_id or (not provider_id):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.')\n provider = LLMProviderService(db).get_provider(provider_id)\n provider_model = provider.default_model if provider else ''\n if not is_multimodal_model(provider_model, provider.provider_type if provider else None):\n raise HTTPException(status_code=422, detail='Selected provider model is not multimodal for dashboard validation. Выберите мультимодальную модель (например, gpt-4o).')\n task = await task_manager.create_task(plugin_id='llm_dashboard_validation', params={'dashboard_id': str(dashboard_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'LLM-валидация запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'run_llm_documentation':\n _check_any_permission(current_user, [('plugin:llm_documentation', 'EXECUTE')])\n dataset_id = entities.get('dataset_id')\n env_id = _resolve_env_id(entities.get('environment'), config_manager)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='documentation')\n if not dataset_id or not env_id or (not provider_id):\n raise HTTPException(status_code=400, detail='Missing dataset_id/environment/provider')\n task = await task_manager.create_task(plugin_id='llm_documentation', params={'dataset_id': str(dataset_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Генерация документации запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n raise HTTPException(status_code=400, detail='Unsupported operation')\n\n\n# #endregion _dispatch_intent\n\n\n# #endregion AssistantDispatch\n" }, { "contract_id": "_clarification_text_for_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dispatch.py", "start_line": 44, - "end_line": 69, + "end_line": 68, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returned text is human-readable and actionable for target operation.", "PRE": "state was classified as needs_clarification for current intent/error combination.", "PURPOSE": "Convert technical missing-parameter errors into user-facing clarification prompts." @@ -9581,19 +8034,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_clarification_text_for_intent:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Convert technical missing-parameter errors into user-facing clarification prompts.\n# @PRE: state was classified as needs_clarification for current intent/error combination.\n# @POST: Returned text is human-readable and actionable for target operation.\ndef _clarification_text_for_intent(\n intent: Optional[Dict[str, Any]], detail_text: str\n) -> str:\n operation = (intent or {}).get(\"operation\")\n guidance_by_operation: Dict[str, str] = {\n \"run_llm_validation\": (\n \"Нужно уточнение для запуска LLM-валидации: Укажите дашборд (id или slug), окружение и провайдер LLM.\"\n ),\n \"run_llm_documentation\": (\n \"Нужно уточнение для генерации документации: Укажите dataset_id, окружение и провайдер LLM.\"\n ),\n \"create_branch\": \"Нужно уточнение: укажите дашборд (id/slug/title) и имя ветки.\",\n \"commit_changes\": \"Нужно уточнение: укажите дашборд (id/slug/title) для коммита.\",\n \"deploy_dashboard\": \"Нужно уточнение: укажите дашборд (id/slug/title) и целевое окружение.\",\n \"execute_migration\": \"Нужно уточнение: укажите дашборд (id/slug/title), source_env и target_env.\",\n \"run_backup\": \"Нужно уточнение: укажите окружение и при необходимости дашборд (id/slug/title).\",\n }\n return guidance_by_operation.get(operation, detail_text)\n\n\n# [/DEF:_clarification_text_for_intent:Function]\n" + "anchor_syntax": "region", + "body": "# #region _clarification_text_for_intent [C:2] [TYPE Function]\n# @BRIEF Convert technical missing-parameter errors into user-facing clarification prompts.\n# @PRE: state was classified as needs_clarification for current intent/error combination.\n# @POST: Returned text is human-readable and actionable for target operation.\ndef _clarification_text_for_intent(\n intent: dict[str, Any] | None, detail_text: str\n) -> str:\n operation = (intent or {}).get(\"operation\")\n guidance_by_operation: dict[str, str] = {\n \"run_llm_validation\": (\n \"Нужно уточнение для запуска LLM-валидации: Укажите дашборд (id или slug), окружение и провайдер LLM.\"\n ),\n \"run_llm_documentation\": (\n \"Нужно уточнение для генерации документации: Укажите dataset_id, окружение и провайдер LLM.\"\n ),\n \"create_branch\": \"Нужно уточнение: укажите дашборд (id/slug/title) и имя ветки.\",\n \"commit_changes\": \"Нужно уточнение: укажите дашборд (id/slug/title) для коммита.\",\n \"deploy_dashboard\": \"Нужно уточнение: укажите дашборд (id/slug/title) и целевое окружение.\",\n \"execute_migration\": \"Нужно уточнение: укажите дашборд (id/slug/title), source_env и target_env.\",\n \"run_backup\": \"Нужно уточнение: укажите окружение и при необходимости дашборд (id/slug/title).\",\n }\n return guidance_by_operation.get(operation, detail_text)\n\n\n# #endregion _clarification_text_for_intent\n" }, { "contract_id": "_async_confirmation_summary", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dispatch.py", - "start_line": 72, - "end_line": 140, + "start_line": 71, + "end_line": 138, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "POST": "Returns a formatted summary string suitable for display to the user.", "PRE": "actions is a non-empty list of planned review actions.", "PURPOSE": "Build human-readable confirmation prompt for an intent before execution.", @@ -9611,19 +8064,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_async_confirmation_summary:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Build human-readable confirmation prompt for an intent before execution.\n# @PRE: actions is a non-empty list of planned review actions.\n# @POST: Returns a formatted summary string suitable for display to the user.\n# @SIDE_EFFECT: None - pure formatting function.\nasync def _async_confirmation_summary(intent: Dict[str, Any], config_manager: ConfigManager, db: Session) -> str:\n with belief_scope('_confirmation_summary'):\n logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')\n operation = intent.get('operation', '')\n entities = intent.get('entities', {})\n descriptions: Dict[str, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'}\n template = descriptions.get(operation)\n if not template:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return 'Подтвердите выполнение операции или отмените.'\n\n def _label(value: Any, prefix: str=' ') -> str:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'{prefix}{value}' if value else ''\n dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref')\n text = template.format(branch=_label(entities.get('branch_name')), dashboard=_label(dashboard), env=_label(entities.get('environment') or entities.get('target_env')), src=_label(entities.get('source_env')), tgt=_label(entities.get('target_env')), dataset=_label(entities.get('dataset_id')))\n if operation == 'execute_migration':\n flags = []\n flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ'))\n flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ'))\n dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False))\n flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ'))\n text += f\" ({', '.join(flags)})\"\n if dry_run_enabled:\n try:\n from src.core.migration.dry_run_orchestrator import MigrationDryRunService\n from src.models.dashboard import DashboardSelection\n from src.core.superset_client import SupersetClient\n src_token = entities.get('source_env')\n tgt_token = entities.get('target_env')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n if dashboard_id and src_token and tgt_token:\n src_env_id = _resolve_env_id(src_token, config_manager)\n tgt_env_id = _resolve_env_id(tgt_token, config_manager)\n if src_env_id and tgt_env_id:\n env_map = {env.id: env for env in config_manager.get_environments()}\n source_env = env_map.get(src_env_id)\n target_env = env_map.get(tgt_env_id)\n if source_env and target_env and (source_env.id != target_env.id):\n selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], replace_db_config=_coerce_query_bool(entities.get('replace_db_config', False)), fix_cross_filters=_coerce_query_bool(entities.get('fix_cross_filters', True)))\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n report = service.run(selection, source_client, target_client, db)\n s = report.get('summary', {})\n dash_s = s.get('dashboards', {})\n charts_s = s.get('charts', {})\n ds_s = s.get('datasets', {})\n creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0)\n updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0)\n deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0)\n text += f'\\n\\nОтчет dry-run:\\n- Будет создано новых объектов: {creates}\\n- Будет обновлено: {updates}\\n- Будет удалено: {deletes}'\n else:\n text += '\\n\\n(Не удалось загрузить отчет dry-run: неверные окружения).'\n except Exception as e:\n import traceback\n logger.warning('[assistant.dry_run_summary][failed] Exception: %s\\n%s', e, traceback.format_exc())\n text += f'\\n\\n(Не удалось загрузить отчет dry-run: {e}).'\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'Выполнить: {text}. Подтвердите или отмените.'\n\n\n# [/DEF:_async_confirmation_summary:Function]\n" + "anchor_syntax": "region", + "body": "# #region _async_confirmation_summary [C:4] [TYPE Function]\n# @BRIEF Build human-readable confirmation prompt for an intent before execution.\n# @PRE: actions is a non-empty list of planned review actions.\n# @POST: Returns a formatted summary string suitable for display to the user.\n# @SIDE_EFFECT: None - pure formatting function.\nasync def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:\n with belief_scope('_confirmation_summary'):\n logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')\n operation = intent.get('operation', '')\n entities = intent.get('entities', {})\n descriptions: dict[str, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'}\n template = descriptions.get(operation)\n if not template:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return 'Подтвердите выполнение операции или отмените.'\n\n def _label(value: Any, prefix: str=' ') -> str:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'{prefix}{value}' if value else ''\n dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref')\n text = template.format(branch=_label(entities.get('branch_name')), dashboard=_label(dashboard), env=_label(entities.get('environment') or entities.get('target_env')), src=_label(entities.get('source_env')), tgt=_label(entities.get('target_env')), dataset=_label(entities.get('dataset_id')))\n if operation == 'execute_migration':\n flags = []\n flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ'))\n flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ'))\n dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False))\n flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ'))\n text += f\" ({', '.join(flags)})\"\n if dry_run_enabled:\n try:\n from src.core.migration.dry_run_orchestrator import MigrationDryRunService\n from src.core.superset_client import SupersetClient\n from src.models.dashboard import DashboardSelection\n src_token = entities.get('source_env')\n tgt_token = entities.get('target_env')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n if dashboard_id and src_token and tgt_token:\n src_env_id = _resolve_env_id(src_token, config_manager)\n tgt_env_id = _resolve_env_id(tgt_token, config_manager)\n if src_env_id and tgt_env_id:\n env_map = {env.id: env for env in config_manager.get_environments()}\n source_env = env_map.get(src_env_id)\n target_env = env_map.get(tgt_env_id)\n if source_env and target_env and (source_env.id != target_env.id):\n selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], replace_db_config=_coerce_query_bool(entities.get('replace_db_config', False)), fix_cross_filters=_coerce_query_bool(entities.get('fix_cross_filters', True)))\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n report = service.run(selection, source_client, target_client, db)\n s = report.get('summary', {})\n dash_s = s.get('dashboards', {})\n charts_s = s.get('charts', {})\n ds_s = s.get('datasets', {})\n creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0)\n updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0)\n deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0)\n text += f'\\n\\nОтчет dry-run:\\n- Будет создано новых объектов: {creates}\\n- Будет обновлено: {updates}\\n- Будет удалено: {deletes}'\n else:\n text += '\\n\\n(Не удалось загрузить отчет dry-run: неверные окружения).'\n except Exception as e:\n import traceback\n logger.warning('[assistant.dry_run_summary][failed] Exception: %s\\n%s', e, traceback.format_exc())\n text += f'\\n\\n(Не удалось загрузить отчет dry-run: {e}).'\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'Выполнить: {text}. Подтвердите или отмените.'\n\n\n# #endregion _async_confirmation_summary\n" }, { "contract_id": "_dispatch_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dispatch.py", - "start_line": 143, - "end_line": 306, + "start_line": 141, + "end_line": 303, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]", "INVARIANT": "unsupported operations are rejected via HTTPException(400).", "POST": "Returns response text, optional task id, and UI actions for follow-up.", @@ -9677,19 +8130,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_dispatch_intent:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Execute parsed assistant intent via existing task/plugin/git services.\n# @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]\n# @RELATION: DEPENDS_ON -> [_check_any_permission]\n# @RELATION: DEPENDS_ON -> [_resolve_dashboard_id_entity]\n# @RELATION: DEPENDS_ON -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [GitService]\n# @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state.\n# @PRE: intent operation is known and actor permissions are validated per operation.\n# @POST: Returns response text, optional task id, and UI actions for follow-up.\n# @INVARIANT: unsupported operations are rejected via HTTPException(400).\nasync def _dispatch_intent(intent: Dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> Tuple[str, Optional[str], List[AssistantAction]]:\n with belief_scope('_dispatch_intent'):\n logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent')\n operation = intent.get('operation')\n entities = intent.get('entities', {})\n if operation in _DATASET_REVIEW_OPS or operation == 'dataset_review_answer_context':\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return await _dispatch_dataset_review_intent(intent, current_user, config_manager, db)\n if operation == 'show_capabilities':\n from ._llm_planner import _build_tool_catalog\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n labels = {'create_branch': 'Git: создание ветки', 'commit_changes': 'Git: коммит', 'deploy_dashboard': 'Git: деплой дашборда', 'execute_migration': 'Миграции: запуск переноса', 'run_backup': 'Бэкапы: запуск резервного копирования', 'run_llm_validation': 'LLM: валидация дашборда', 'run_llm_documentation': 'LLM: генерация документации', 'get_task_status': 'Статус: проверка задачи', 'get_health_summary': 'Здоровье: сводка по дашбордам'}\n available = [labels[t['operation']] for t in tools_catalog if t['operation'] in labels]\n if not available:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Сейчас нет доступных для вас операций ассистента.', None, [])\n commands = '\\n'.join((f'- {item}' for item in available))\n text = f'Вот что я могу сделать для вас:\\n{commands}\\n\\nПример: `запусти миграцию с dev на prod для дашборда 42`.'\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, [])\n if operation == 'get_health_summary':\n from src.services.health_service import HealthService\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=env_id)\n env_name = _get_environment_name_by_id(env_id, config_manager) if env_id else 'всех окружений'\n text = f'Сводка здоровья дашбордов для {env_name}:\\n- ✅ Прошли проверку: {summary.pass_count}\\n- ⚠️ С предупреждениями: {summary.warn_count}\\n- ❌ Ошибки валидации: {summary.fail_count}\\n- ❓ Неизвестно: {summary.unknown_count}'\n actions = [AssistantAction(type='open_route', label='Открыть Health Center', target='/dashboards/health')]\n if summary.fail_count > 0:\n text += '\\n\\nОбнаружены ошибки в следующих дашбордах:'\n for item in summary.items:\n if item.status == 'FAIL':\n text += f\"\\n- {item.dashboard_id} ({item.environment_id}): {item.summary or 'Нет деталей'}\"\n actions.append(AssistantAction(type='open_route', label=f'Отчет {item.dashboard_id}', target=f'/reports/llm/{item.task_id}'))\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, actions[:5])\n if operation == 'get_task_status':\n _check_any_permission(current_user, [('tasks', 'READ')])\n task_id = entities.get('task_id')\n if not task_id:\n recent = [t for t in task_manager.get_tasks(limit=20, offset=0) if t.user_id == current_user.id]\n if not recent:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('У вас пока нет задач в истории.', None, [])\n task = recent[0]\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Последняя задача: {task.id}, статус: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(status_code=404, detail=f'Task {task_id} not found')\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Статус задачи {task.id}: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n if operation == 'create_branch':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n branch_name = entities.get('branch_name')\n if not dashboard_id or not branch_name:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or branch_name')\n git_service.create_branch(dashboard_id, branch_name, 'main')\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Ветка `{branch_name}` создана для дашборда {dashboard_id}.', None, [])\n if operation == 'commit_changes':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n commit_message = entities.get('message')\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n git_service.commit_changes(dashboard_id, commit_message, None)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Коммит выполнен успешно.', None, [])\n if operation == 'deploy_dashboard':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id or not env_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or environment')\n task = await task_manager.create_task(plugin_id='git-integration', params={'operation': 'deploy', 'dashboard_id': dashboard_id, 'environment_id': env_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Деплой запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'execute_migration':\n _check_any_permission(current_user, [('plugin:migration', 'EXECUTE'), ('plugin:superset-migration', 'EXECUTE')])\n src_token = entities.get('source_env')\n dashboard_ref = entities.get('dashboard_ref')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n src = _resolve_env_id(src_token, config_manager)\n tgt = _resolve_env_id(entities.get('target_env'), config_manager)\n if not src or not tgt:\n raise HTTPException(status_code=422, detail='Missing source_env/target_env')\n if not dashboard_id and (not dashboard_ref):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n migration_params: Dict[str, Any] = {'source_env_id': src, 'target_env_id': tgt, 'replace_db_config': _coerce_query_bool(entities.get('replace_db_config', False)), 'fix_cross_filters': _coerce_query_bool(entities.get('fix_cross_filters', True))}\n if dashboard_id:\n migration_params['selected_ids'] = [dashboard_id]\n else:\n migration_params['dashboard_regex'] = str(dashboard_ref)\n task = await task_manager.create_task(plugin_id='superset-migration', params=migration_params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Миграция запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={tgt}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if dashboard_id else [])])\n if operation == 'run_backup':\n _check_any_permission(current_user, [('plugin:superset-backup', 'EXECUTE'), ('plugin:backup', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n if not env_id:\n raise HTTPException(status_code=400, detail='Missing or unknown environment')\n params: Dict[str, Any] = {'environment_id': env_id}\n if entities.get('dashboard_id') or entities.get('dashboard_ref'):\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n params['dashboard_ids'] = [dashboard_id]\n task = await task_manager.create_task(plugin_id='superset-backup', params=params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Бэкап запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={env_id}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if entities.get('dashboard_id') or entities.get('dashboard_ref') else [])])\n if operation == 'run_llm_validation':\n _check_any_permission(current_user, [('plugin:llm_dashboard_validation', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager) or _resolve_env_id(None, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='dashboard_validation')\n if not dashboard_id or not env_id or (not provider_id):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.')\n provider = LLMProviderService(db).get_provider(provider_id)\n provider_model = provider.default_model if provider else ''\n if not is_multimodal_model(provider_model, provider.provider_type if provider else None):\n raise HTTPException(status_code=422, detail='Selected provider model is not multimodal for dashboard validation. Выберите мультимодальную модель (например, gpt-4o).')\n task = await task_manager.create_task(plugin_id='llm_dashboard_validation', params={'dashboard_id': str(dashboard_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'LLM-валидация запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'run_llm_documentation':\n _check_any_permission(current_user, [('plugin:llm_documentation', 'EXECUTE')])\n dataset_id = entities.get('dataset_id')\n env_id = _resolve_env_id(entities.get('environment'), config_manager)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='documentation')\n if not dataset_id or not env_id or (not provider_id):\n raise HTTPException(status_code=400, detail='Missing dataset_id/environment/provider')\n task = await task_manager.create_task(plugin_id='llm_documentation', params={'dataset_id': str(dataset_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Генерация документации запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n raise HTTPException(status_code=400, detail='Unsupported operation')\n\n\n# [/DEF:_dispatch_intent:Function]\n" + "anchor_syntax": "region", + "body": "# #region _dispatch_intent [C:5] [TYPE Function]\n# @BRIEF Execute parsed assistant intent via existing task/plugin/git services.\n# @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]\n# @RELATION DEPENDS_ON -> [_check_any_permission]\n# @RELATION DEPENDS_ON -> [_resolve_dashboard_id_entity]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [GitService]\n# @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state.\n# @PRE: intent operation is known and actor permissions are validated per operation.\n# @POST: Returns response text, optional task id, and UI actions for follow-up.\n# @INVARIANT: unsupported operations are rejected via HTTPException(400).\nasync def _dispatch_intent(intent: dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> tuple[str, str | None, list[AssistantAction]]:\n with belief_scope('_dispatch_intent'):\n logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent')\n operation = intent.get('operation')\n entities = intent.get('entities', {})\n if operation in _DATASET_REVIEW_OPS or operation == 'dataset_review_answer_context':\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return await _dispatch_dataset_review_intent(intent, current_user, config_manager, db)\n if operation == 'show_capabilities':\n from ._llm_planner import _build_tool_catalog\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n labels = {'create_branch': 'Git: создание ветки', 'commit_changes': 'Git: коммит', 'deploy_dashboard': 'Git: деплой дашборда', 'execute_migration': 'Миграции: запуск переноса', 'run_backup': 'Бэкапы: запуск резервного копирования', 'run_llm_validation': 'LLM: валидация дашборда', 'run_llm_documentation': 'LLM: генерация документации', 'get_task_status': 'Статус: проверка задачи', 'get_health_summary': 'Здоровье: сводка по дашбордам'}\n available = [labels[t['operation']] for t in tools_catalog if t['operation'] in labels]\n if not available:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Сейчас нет доступных для вас операций ассистента.', None, [])\n commands = '\\n'.join(f'- {item}' for item in available)\n text = f'Вот что я могу сделать для вас:\\n{commands}\\n\\nПример: `запусти миграцию с dev на prod для дашборда 42`.'\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, [])\n if operation == 'get_health_summary':\n from src.services.health_service import HealthService\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=env_id)\n env_name = _get_environment_name_by_id(env_id, config_manager) if env_id else 'всех окружений'\n text = f'Сводка здоровья дашбордов для {env_name}:\\n- ✅ Прошли проверку: {summary.pass_count}\\n- ⚠️ С предупреждениями: {summary.warn_count}\\n- ❌ Ошибки валидации: {summary.fail_count}\\n- ❓ Неизвестно: {summary.unknown_count}'\n actions = [AssistantAction(type='open_route', label='Открыть Health Center', target='/dashboards/health')]\n if summary.fail_count > 0:\n text += '\\n\\nОбнаружены ошибки в следующих дашбордах:'\n for item in summary.items:\n if item.status == 'FAIL':\n text += f\"\\n- {item.dashboard_id} ({item.environment_id}): {item.summary or 'Нет деталей'}\"\n actions.append(AssistantAction(type='open_route', label=f'Отчет {item.dashboard_id}', target=f'/reports/llm/{item.task_id}'))\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (text, None, actions[:5])\n if operation == 'get_task_status':\n _check_any_permission(current_user, [('tasks', 'READ')])\n task_id = entities.get('task_id')\n if not task_id:\n recent = [t for t in task_manager.get_tasks(limit=20, offset=0) if t.user_id == current_user.id]\n if not recent:\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('У вас пока нет задач в истории.', None, [])\n task = recent[0]\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Последняя задача: {task.id}, статус: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(status_code=404, detail=f'Task {task_id} not found')\n actions = [AssistantAction(type='open_task', label='Open Task', target=task.id)]\n if str(task.status).upper() in {'SUCCESS', 'FAILED'}:\n actions.extend(_extract_result_deep_links(task, config_manager))\n summary_line = _build_task_observability_summary(task, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Статус задачи {task.id}: {task.status}.' + (f'\\n{summary_line}' if summary_line else ''), task.id, actions)\n if operation == 'create_branch':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n branch_name = entities.get('branch_name')\n if not dashboard_id or not branch_name:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or branch_name')\n git_service.create_branch(dashboard_id, branch_name, 'main')\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Ветка `{branch_name}` создана для дашборда {dashboard_id}.', None, [])\n if operation == 'commit_changes':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager)\n commit_message = entities.get('message')\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n git_service.commit_changes(dashboard_id, commit_message, None)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return ('Коммит выполнен успешно.', None, [])\n if operation == 'deploy_dashboard':\n _check_any_permission(current_user, [('plugin:git', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id or not env_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref or environment')\n task = await task_manager.create_task(plugin_id='git-integration', params={'operation': 'deploy', 'dashboard_id': dashboard_id, 'environment_id': env_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Деплой запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'execute_migration':\n _check_any_permission(current_user, [('plugin:migration', 'EXECUTE'), ('plugin:superset-migration', 'EXECUTE')])\n src_token = entities.get('source_env')\n dashboard_ref = entities.get('dashboard_ref')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n src = _resolve_env_id(src_token, config_manager)\n tgt = _resolve_env_id(entities.get('target_env'), config_manager)\n if not src or not tgt:\n raise HTTPException(status_code=422, detail='Missing source_env/target_env')\n if not dashboard_id and (not dashboard_ref):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n migration_params: dict[str, Any] = {'source_env_id': src, 'target_env_id': tgt, 'replace_db_config': _coerce_query_bool(entities.get('replace_db_config', False)), 'fix_cross_filters': _coerce_query_bool(entities.get('fix_cross_filters', True))}\n if dashboard_id:\n migration_params['selected_ids'] = [dashboard_id]\n else:\n migration_params['dashboard_regex'] = str(dashboard_ref)\n task = await task_manager.create_task(plugin_id='superset-migration', params=migration_params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Миграция запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={tgt}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if dashboard_id else [])])\n if operation == 'run_backup':\n _check_any_permission(current_user, [('plugin:superset-backup', 'EXECUTE'), ('plugin:backup', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager)\n if not env_id:\n raise HTTPException(status_code=400, detail='Missing or unknown environment')\n params: dict[str, Any] = {'environment_id': env_id}\n if entities.get('dashboard_id') or entities.get('dashboard_ref'):\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n if not dashboard_id:\n raise HTTPException(status_code=422, detail='Missing dashboard_id/dashboard_ref')\n params['dashboard_ids'] = [dashboard_id]\n task = await task_manager.create_task(plugin_id='superset-backup', params=params, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Бэкап запущен. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports'), *([AssistantAction(type='open_route', label=f'Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}', target=f'/dashboards/{dashboard_id}?env_id={env_id}'), AssistantAction(type='open_diff', label='Показать Diff', target=str(dashboard_id))] if entities.get('dashboard_id') or entities.get('dashboard_ref') else [])])\n if operation == 'run_llm_validation':\n _check_any_permission(current_user, [('plugin:llm_dashboard_validation', 'EXECUTE')])\n env_token = entities.get('environment')\n env_id = _resolve_env_id(env_token, config_manager) or _resolve_env_id(None, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=env_token)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='dashboard_validation')\n if not dashboard_id or not env_id or (not provider_id):\n raise HTTPException(status_code=422, detail='Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.')\n provider = LLMProviderService(db).get_provider(provider_id)\n provider_model = provider.default_model if provider else ''\n if not is_multimodal_model(provider_model, provider.provider_type if provider else None):\n raise HTTPException(status_code=422, detail='Selected provider model is not multimodal for dashboard validation. Выберите мультимодальную модель (например, gpt-4o).')\n task = await task_manager.create_task(plugin_id='llm_dashboard_validation', params={'dashboard_id': str(dashboard_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'LLM-валидация запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n if operation == 'run_llm_documentation':\n _check_any_permission(current_user, [('plugin:llm_documentation', 'EXECUTE')])\n dataset_id = entities.get('dataset_id')\n env_id = _resolve_env_id(entities.get('environment'), config_manager)\n provider_id = _resolve_provider_id(entities.get('provider'), db, config_manager=config_manager, task_key='documentation')\n if not dataset_id or not env_id or (not provider_id):\n raise HTTPException(status_code=400, detail='Missing dataset_id/environment/provider')\n task = await task_manager.create_task(plugin_id='llm_documentation', params={'dataset_id': str(dataset_id), 'environment_id': env_id, 'provider_id': provider_id}, user_id=current_user.id)\n logger.reflect('Belief protocol postcondition checkpoint for _dispatch_intent')\n return (f'Генерация документации запущена. task_id={task.id}', task.id, [AssistantAction(type='open_task', label='Open Task', target=task.id), AssistantAction(type='open_reports', label='Open Reports', target='/reports')])\n raise HTTPException(status_code=400, detail='Unsupported operation')\n\n\n# #endregion _dispatch_intent\n" }, { "contract_id": "AssistantHistory", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_history.py", "start_line": 1, - "end_line": 382, + "end_line": 369, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "Failed persistence attempts always rollback before returning.", "LAYER": "API", "PURPOSE": "Conversation history, audit trail, and confirmation persistence helpers for the assistant API." @@ -9722,19 +8175,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantHistory:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Conversation history, audit trail, and confirmation persistence helpers for the assistant API.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT: Failed persistence attempts always rollback before returning.\n\nfrom __future__ import annotations\n\nimport uuid\nfrom datetime import datetime, timedelta\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantConfirmationRecord,\n AssistantMessageRecord,\n)\nfrom ._schemas import (\n ASSISTANT_ARCHIVE_AFTER_DAYS,\n ASSISTANT_MESSAGE_TTL_DAYS,\n ASSISTANT_AUDIT,\n CONFIRMATIONS,\n CONVERSATIONS,\n ConfirmationRecord,\n)\n\nlogger = logger\n\n\n# [DEF:_append_history:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Append conversation message to in-memory history buffer.\n# @DATA_CONTRACT: Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]\n# @RELATION: UPDATES -> [CONVERSATIONS]\n# @SIDE_EFFECT: Mutates in-memory CONVERSATIONS store for user conversation history.\n# @PRE: user_id and conversation_id identify target conversation bucket.\n# @POST: Message entry is appended to CONVERSATIONS key list.\n# @INVARIANT: every appended entry includes generated message_id and created_at timestamp.\ndef _append_history(\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: Optional[str] = None,\n task_id: Optional[str] = None,\n confirmation_id: Optional[str] = None,\n):\n key = (user_id, conversation_id)\n if key not in CONVERSATIONS:\n CONVERSATIONS[key] = []\n CONVERSATIONS[key].append(\n {\n \"message_id\": str(uuid.uuid4()),\n \"conversation_id\": conversation_id,\n \"role\": role,\n \"text\": text,\n \"state\": state,\n \"task_id\": task_id,\n \"confirmation_id\": confirmation_id,\n \"created_at\": datetime.utcnow(),\n }\n )\n\n\n# [/DEF:_append_history:Function]\n\n\n# [DEF:_persist_message:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Persist assistant/user message record to database.\n# @DATA_CONTRACT: Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]\n# @RELATION: DEPENDS_ON -> [AssistantMessageRecord]\n# @SIDE_EFFECT: Writes AssistantMessageRecord rows and commits or rollbacks the DB session.\n# @PRE: db session is writable and message payload is serializable.\n# @POST: Message row is committed or persistence failure is logged.\n# @INVARIANT: failed persistence attempts always rollback before returning.\ndef _persist_message(\n db: Session,\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: Optional[str] = None,\n task_id: Optional[str] = None,\n confirmation_id: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n):\n try:\n row = AssistantMessageRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n role=role,\n text=text,\n state=state,\n task_id=task_id,\n confirmation_id=confirmation_id,\n payload=metadata,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.message][persist_failed] {exc}\")\n\n\n# [/DEF:_persist_message:Function]\n\n\n# [DEF:_audit:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Append in-memory audit record for assistant decision trace.\n# @DATA_CONTRACT: Input[user_id,payload:Dict[str,Any]] -> Output[None]\n# @RELATION: UPDATES -> [ASSISTANT_AUDIT]\n# @SIDE_EFFECT: Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.\n# @PRE: payload describes decision/outcome fields.\n# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry.\n# @INVARIANT: persisted in-memory audit entry always contains created_at in ISO format.\ndef _audit(user_id: str, payload: Dict[str, Any]):\n if user_id not in ASSISTANT_AUDIT:\n ASSISTANT_AUDIT[user_id] = []\n ASSISTANT_AUDIT[user_id].append(\n {**payload, \"created_at\": datetime.utcnow().isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# [/DEF:_audit:Function]\n\n\n# [DEF:_persist_audit:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Persist structured assistant audit payload in database.\n# @PRE: db session is writable and payload is JSON-serializable.\n# @POST: Audit row is committed or failure is logged with rollback.\ndef _persist_audit(\n db: Session, user_id: str, payload: Dict[str, Any], conversation_id: Optional[str]\n):\n try:\n row = AssistantAuditRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n decision=payload.get(\"decision\"),\n task_id=payload.get(\"task_id\"),\n message=payload.get(\"message\"),\n payload=payload,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.audit][persist_failed] {exc}\")\n\n\n# [/DEF:_persist_audit:Function]\n\n\n# [DEF:_persist_confirmation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Persist confirmation token record to database.\n# @PRE: record contains id/user/intent/dispatch/expiry fields.\n# @POST: Confirmation row exists in persistent storage.\ndef _persist_confirmation(db: Session, record: ConfirmationRecord):\n try:\n row = AssistantConfirmationRecord(\n id=record.id,\n user_id=record.user_id,\n conversation_id=record.conversation_id,\n state=record.state,\n intent=record.intent,\n dispatch=record.dispatch,\n expires_at=record.expires_at,\n created_at=record.created_at,\n consumed_at=None,\n )\n db.merge(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][persist_failed] {exc}\")\n\n\n# [/DEF:_persist_confirmation:Function]\n\n\n# [DEF:_update_confirmation_state:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Update persistent confirmation token lifecycle state.\n# @PRE: confirmation_id references existing row.\n# @POST: State and consumed_at fields are updated when applicable.\ndef _update_confirmation_state(db: Session, confirmation_id: str, state: str):\n try:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return\n row.state = state\n if state in {\"consumed\", \"expired\", \"cancelled\"}:\n row.consumed_at = datetime.utcnow()\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][update_failed] {exc}\")\n\n\n# [/DEF:_update_confirmation_state:Function]\n\n\n# [DEF:_load_confirmation_from_db:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Load confirmation token from database into in-memory model.\n# @PRE: confirmation_id may or may not exist in storage.\n# @POST: Returns ConfirmationRecord when found, otherwise None.\ndef _load_confirmation_from_db(\n db: Session, confirmation_id: str\n) -> Optional[ConfirmationRecord]:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return None\n return ConfirmationRecord(\n id=row.id,\n user_id=row.user_id,\n conversation_id=row.conversation_id,\n intent=row.intent or {},\n dispatch=row.dispatch or {},\n expires_at=row.expires_at,\n state=row.state,\n created_at=row.created_at,\n )\n\n\n# [/DEF:_load_confirmation_from_db:Function]\n\n\n# [DEF:_ensure_conversation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve active conversation id in memory or create a new one.\n# @PRE: user_id identifies current actor.\n# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.\ndef _ensure_conversation(user_id: str, conversation_id: Optional[str]) -> str:\n if conversation_id:\n from ._schemas import USER_ACTIVE_CONVERSATION\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n from ._schemas import USER_ACTIVE_CONVERSATION\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# [/DEF:_ensure_conversation:Function]\n\n\n# [DEF:_resolve_or_create_conversation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve active conversation using explicit id, memory cache, or persisted history.\n# @PRE: user_id and db session are available.\n# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.\ndef _resolve_or_create_conversation(\n user_id: str, conversation_id: Optional[str], db: Session\n) -> str:\n from ._schemas import USER_ACTIVE_CONVERSATION\n\n if conversation_id:\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n from sqlalchemy import desc\n\n last_message = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .first()\n )\n if last_message:\n USER_ACTIVE_CONVERSATION[user_id] = last_message.conversation_id\n return last_message.conversation_id\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# [/DEF:_resolve_or_create_conversation:Function]\n\n\n# [DEF:_cleanup_history_ttl:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Enforce assistant message retention window by deleting expired rows and in-memory records.\n# @PRE: db session is available and user_id references current actor scope.\n# @POST: Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.\ndef _cleanup_history_ttl(db: Session, user_id: str):\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)\n try:\n query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.created_at < cutoff,\n )\n if hasattr(query, \"delete\"):\n query.delete(synchronize_session=False)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(\n f\"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}\"\n )\n\n stale_keys: List[Tuple[str, str]] = []\n for key, items in CONVERSATIONS.items():\n if key[0] != user_id:\n continue\n kept = []\n for item in items:\n created_at = item.get(\"created_at\")\n if isinstance(created_at, datetime) and created_at < cutoff:\n continue\n kept.append(item)\n if kept:\n CONVERSATIONS[key] = kept\n else:\n stale_keys.append(key)\n for key in stale_keys:\n CONVERSATIONS.pop(key, None)\n\n\n# [/DEF:_cleanup_history_ttl:Function]\n\n\n# [DEF:_is_conversation_archived:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Determine archived state for a conversation based on last update timestamp.\n# @PRE: updated_at can be null for empty conversations.\n# @POST: Returns True when conversation inactivity exceeds archive threshold.\ndef _is_conversation_archived(updated_at: Optional[datetime]) -> bool:\n if not updated_at:\n return False\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)\n return updated_at < cutoff\n\n\n# [/DEF:_is_conversation_archived:Function]\n\n\n# [DEF:_coerce_query_bool:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize bool-like query values for compatibility in direct handler invocations/tests.\n# @PRE: value may be bool, string, or FastAPI Query metadata object.\n# @POST: Returns deterministic boolean flag.\ndef _coerce_query_bool(value: Any) -> bool:\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n return False\n\n\n# [/DEF:_coerce_query_bool:Function]\n\n\n# [/DEF:AssistantHistory:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantHistory [C:2] [TYPE Module] [SEMANTICS assistant, history, audit, persistence, conversation]\n# @BRIEF Conversation history, audit trail, and confirmation persistence helpers for the assistant API.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT: Failed persistence attempts always rollback before returning.\n\nfrom __future__ import annotations\n\nimport uuid\nfrom datetime import datetime, timedelta\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import logger\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantConfirmationRecord,\n AssistantMessageRecord,\n)\n\nfrom ._schemas import (\n ASSISTANT_ARCHIVE_AFTER_DAYS,\n ASSISTANT_AUDIT,\n ASSISTANT_MESSAGE_TTL_DAYS,\n CONVERSATIONS,\n ConfirmationRecord,\n)\n\nlogger = logger\n\n\n# #region _append_history [C:2] [TYPE Function]\n# @BRIEF Append conversation message to in-memory history buffer.\n# @DATA_CONTRACT: Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]\n# @RELATION UPDATES -> [CONVERSATIONS]\n# @SIDE_EFFECT: Mutates in-memory CONVERSATIONS store for user conversation history.\n# @PRE: user_id and conversation_id identify target conversation bucket.\n# @POST: Message entry is appended to CONVERSATIONS key list.\n# @INVARIANT: every appended entry includes generated message_id and created_at timestamp.\ndef _append_history(\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n):\n key = (user_id, conversation_id)\n if key not in CONVERSATIONS:\n CONVERSATIONS[key] = []\n CONVERSATIONS[key].append(\n {\n \"message_id\": str(uuid.uuid4()),\n \"conversation_id\": conversation_id,\n \"role\": role,\n \"text\": text,\n \"state\": state,\n \"task_id\": task_id,\n \"confirmation_id\": confirmation_id,\n \"created_at\": datetime.utcnow(),\n }\n )\n\n\n# #endregion _append_history\n\n\n# #region _persist_message [C:2] [TYPE Function]\n# @BRIEF Persist assistant/user message record to database.\n# @DATA_CONTRACT: Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]\n# @RELATION DEPENDS_ON -> [AssistantMessageRecord]\n# @SIDE_EFFECT: Writes AssistantMessageRecord rows and commits or rollbacks the DB session.\n# @PRE: db session is writable and message payload is serializable.\n# @POST: Message row is committed or persistence failure is logged.\n# @INVARIANT: failed persistence attempts always rollback before returning.\ndef _persist_message(\n db: Session,\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n metadata: dict[str, Any] | None = None,\n):\n try:\n row = AssistantMessageRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n role=role,\n text=text,\n state=state,\n task_id=task_id,\n confirmation_id=confirmation_id,\n payload=metadata,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.message][persist_failed] {exc}\")\n\n\n# #endregion _persist_message\n\n\n# #region _audit [C:2] [TYPE Function]\n# @BRIEF Append in-memory audit record for assistant decision trace.\n# @DATA_CONTRACT: Input[user_id,payload:Dict[str,Any]] -> Output[None]\n# @RELATION UPDATES -> [ASSISTANT_AUDIT]\n# @SIDE_EFFECT: Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.\n# @PRE: payload describes decision/outcome fields.\n# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry.\n# @INVARIANT: persisted in-memory audit entry always contains created_at in ISO format.\ndef _audit(user_id: str, payload: dict[str, Any]):\n if user_id not in ASSISTANT_AUDIT:\n ASSISTANT_AUDIT[user_id] = []\n ASSISTANT_AUDIT[user_id].append(\n {**payload, \"created_at\": datetime.utcnow().isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# #endregion _audit\n\n\n# #region _persist_audit [C:2] [TYPE Function]\n# @BRIEF Persist structured assistant audit payload in database.\n# @PRE: db session is writable and payload is JSON-serializable.\n# @POST: Audit row is committed or failure is logged with rollback.\ndef _persist_audit(\n db: Session, user_id: str, payload: dict[str, Any], conversation_id: str | None\n):\n try:\n row = AssistantAuditRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n decision=payload.get(\"decision\"),\n task_id=payload.get(\"task_id\"),\n message=payload.get(\"message\"),\n payload=payload,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.audit][persist_failed] {exc}\")\n\n\n# #endregion _persist_audit\n\n\n# #region _persist_confirmation [C:2] [TYPE Function]\n# @BRIEF Persist confirmation token record to database.\n# @PRE: record contains id/user/intent/dispatch/expiry fields.\n# @POST: Confirmation row exists in persistent storage.\ndef _persist_confirmation(db: Session, record: ConfirmationRecord):\n try:\n row = AssistantConfirmationRecord(\n id=record.id,\n user_id=record.user_id,\n conversation_id=record.conversation_id,\n state=record.state,\n intent=record.intent,\n dispatch=record.dispatch,\n expires_at=record.expires_at,\n created_at=record.created_at,\n consumed_at=None,\n )\n db.merge(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][persist_failed] {exc}\")\n\n\n# #endregion _persist_confirmation\n\n\n# #region _update_confirmation_state [C:2] [TYPE Function]\n# @BRIEF Update persistent confirmation token lifecycle state.\n# @PRE: confirmation_id references existing row.\n# @POST: State and consumed_at fields are updated when applicable.\ndef _update_confirmation_state(db: Session, confirmation_id: str, state: str):\n try:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return\n row.state = state\n if state in {\"consumed\", \"expired\", \"cancelled\"}:\n row.consumed_at = datetime.utcnow()\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][update_failed] {exc}\")\n\n\n# #endregion _update_confirmation_state\n\n\n# #region _load_confirmation_from_db [C:2] [TYPE Function]\n# @BRIEF Load confirmation token from database into in-memory model.\n# @PRE: confirmation_id may or may not exist in storage.\n# @POST: Returns ConfirmationRecord when found, otherwise None.\ndef _load_confirmation_from_db(\n db: Session, confirmation_id: str\n) -> ConfirmationRecord | None:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return None\n return ConfirmationRecord(\n id=row.id,\n user_id=row.user_id,\n conversation_id=row.conversation_id,\n intent=row.intent or {},\n dispatch=row.dispatch or {},\n expires_at=row.expires_at,\n state=row.state,\n created_at=row.created_at,\n )\n\n\n# #endregion _load_confirmation_from_db\n\n\n# #region _ensure_conversation [C:2] [TYPE Function]\n# @BRIEF Resolve active conversation id in memory or create a new one.\n# @PRE: user_id identifies current actor.\n# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.\ndef _ensure_conversation(user_id: str, conversation_id: str | None) -> str:\n if conversation_id:\n from ._schemas import USER_ACTIVE_CONVERSATION\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n from ._schemas import USER_ACTIVE_CONVERSATION\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# #endregion _ensure_conversation\n\n\n# #region _resolve_or_create_conversation [C:2] [TYPE Function]\n# @BRIEF Resolve active conversation using explicit id, memory cache, or persisted history.\n# @PRE: user_id and db session are available.\n# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.\ndef _resolve_or_create_conversation(\n user_id: str, conversation_id: str | None, db: Session\n) -> str:\n from ._schemas import USER_ACTIVE_CONVERSATION\n\n if conversation_id:\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n from sqlalchemy import desc\n\n last_message = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .first()\n )\n if last_message:\n USER_ACTIVE_CONVERSATION[user_id] = last_message.conversation_id\n return last_message.conversation_id\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# #endregion _resolve_or_create_conversation\n\n\n# #region _cleanup_history_ttl [C:2] [TYPE Function]\n# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.\n# @PRE: db session is available and user_id references current actor scope.\n# @POST: Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.\ndef _cleanup_history_ttl(db: Session, user_id: str):\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)\n try:\n query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.created_at < cutoff,\n )\n if hasattr(query, \"delete\"):\n query.delete(synchronize_session=False)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(\n f\"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}\"\n )\n\n stale_keys: list[tuple[str, str]] = []\n for key, items in CONVERSATIONS.items():\n if key[0] != user_id:\n continue\n kept = []\n for item in items:\n created_at = item.get(\"created_at\")\n if isinstance(created_at, datetime) and created_at < cutoff:\n continue\n kept.append(item)\n if kept:\n CONVERSATIONS[key] = kept\n else:\n stale_keys.append(key)\n for key in stale_keys:\n CONVERSATIONS.pop(key, None)\n\n\n# #endregion _cleanup_history_ttl\n\n\n# #region _is_conversation_archived [C:2] [TYPE Function]\n# @BRIEF Determine archived state for a conversation based on last update timestamp.\n# @PRE: updated_at can be null for empty conversations.\n# @POST: Returns True when conversation inactivity exceeds archive threshold.\ndef _is_conversation_archived(updated_at: datetime | None) -> bool:\n if not updated_at:\n return False\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)\n return updated_at < cutoff\n\n\n# #endregion _is_conversation_archived\n\n\n# #region _coerce_query_bool [C:2] [TYPE Function]\n# @BRIEF Normalize bool-like query values for compatibility in direct handler invocations/tests.\n# @PRE: value may be bool, string, or FastAPI Query metadata object.\n# @POST: Returns deterministic boolean flag.\ndef _coerce_query_bool(value: Any) -> bool:\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n return False\n\n\n# #endregion _coerce_query_bool\n\n\n# #endregion AssistantHistory\n" }, { "contract_id": "_append_history", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 34, - "end_line": 69, + "start_line": 33, + "end_line": 67, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "DATA_CONTRACT": "Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]", "INVARIANT": "every appended entry includes generated message_id and created_at timestamp.", "POST": "Message entry is appended to CONVERSATIONS key list.", @@ -9824,19 +8277,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_append_history:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Append conversation message to in-memory history buffer.\n# @DATA_CONTRACT: Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]\n# @RELATION: UPDATES -> [CONVERSATIONS]\n# @SIDE_EFFECT: Mutates in-memory CONVERSATIONS store for user conversation history.\n# @PRE: user_id and conversation_id identify target conversation bucket.\n# @POST: Message entry is appended to CONVERSATIONS key list.\n# @INVARIANT: every appended entry includes generated message_id and created_at timestamp.\ndef _append_history(\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: Optional[str] = None,\n task_id: Optional[str] = None,\n confirmation_id: Optional[str] = None,\n):\n key = (user_id, conversation_id)\n if key not in CONVERSATIONS:\n CONVERSATIONS[key] = []\n CONVERSATIONS[key].append(\n {\n \"message_id\": str(uuid.uuid4()),\n \"conversation_id\": conversation_id,\n \"role\": role,\n \"text\": text,\n \"state\": state,\n \"task_id\": task_id,\n \"confirmation_id\": confirmation_id,\n \"created_at\": datetime.utcnow(),\n }\n )\n\n\n# [/DEF:_append_history:Function]\n" + "anchor_syntax": "region", + "body": "# #region _append_history [C:2] [TYPE Function]\n# @BRIEF Append conversation message to in-memory history buffer.\n# @DATA_CONTRACT: Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]\n# @RELATION UPDATES -> [CONVERSATIONS]\n# @SIDE_EFFECT: Mutates in-memory CONVERSATIONS store for user conversation history.\n# @PRE: user_id and conversation_id identify target conversation bucket.\n# @POST: Message entry is appended to CONVERSATIONS key list.\n# @INVARIANT: every appended entry includes generated message_id and created_at timestamp.\ndef _append_history(\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n):\n key = (user_id, conversation_id)\n if key not in CONVERSATIONS:\n CONVERSATIONS[key] = []\n CONVERSATIONS[key].append(\n {\n \"message_id\": str(uuid.uuid4()),\n \"conversation_id\": conversation_id,\n \"role\": role,\n \"text\": text,\n \"state\": state,\n \"task_id\": task_id,\n \"confirmation_id\": confirmation_id,\n \"created_at\": datetime.utcnow(),\n }\n )\n\n\n# #endregion _append_history\n" }, { "contract_id": "_persist_message", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 72, - "end_line": 111, + "start_line": 70, + "end_line": 108, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "DATA_CONTRACT": "Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]", "INVARIANT": "failed persistence attempts always rollback before returning.", "POST": "Message row is committed or persistence failure is logged.", @@ -9908,19 +8361,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_persist_message:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Persist assistant/user message record to database.\n# @DATA_CONTRACT: Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]\n# @RELATION: DEPENDS_ON -> [AssistantMessageRecord]\n# @SIDE_EFFECT: Writes AssistantMessageRecord rows and commits or rollbacks the DB session.\n# @PRE: db session is writable and message payload is serializable.\n# @POST: Message row is committed or persistence failure is logged.\n# @INVARIANT: failed persistence attempts always rollback before returning.\ndef _persist_message(\n db: Session,\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: Optional[str] = None,\n task_id: Optional[str] = None,\n confirmation_id: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n):\n try:\n row = AssistantMessageRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n role=role,\n text=text,\n state=state,\n task_id=task_id,\n confirmation_id=confirmation_id,\n payload=metadata,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.message][persist_failed] {exc}\")\n\n\n# [/DEF:_persist_message:Function]\n" + "anchor_syntax": "region", + "body": "# #region _persist_message [C:2] [TYPE Function]\n# @BRIEF Persist assistant/user message record to database.\n# @DATA_CONTRACT: Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]\n# @RELATION DEPENDS_ON -> [AssistantMessageRecord]\n# @SIDE_EFFECT: Writes AssistantMessageRecord rows and commits or rollbacks the DB session.\n# @PRE: db session is writable and message payload is serializable.\n# @POST: Message row is committed or persistence failure is logged.\n# @INVARIANT: failed persistence attempts always rollback before returning.\ndef _persist_message(\n db: Session,\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n metadata: dict[str, Any] | None = None,\n):\n try:\n row = AssistantMessageRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n role=role,\n text=text,\n state=state,\n task_id=task_id,\n confirmation_id=confirmation_id,\n payload=metadata,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.message][persist_failed] {exc}\")\n\n\n# #endregion _persist_message\n" }, { "contract_id": "_audit", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 114, - "end_line": 132, + "start_line": 111, + "end_line": 128, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "DATA_CONTRACT": "Input[user_id,payload:Dict[str,Any]] -> Output[None]", "INVARIANT": "persisted in-memory audit entry always contains created_at in ISO format.", "POST": "ASSISTANT_AUDIT list for user contains new timestamped entry.", @@ -10010,19 +8463,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_audit:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Append in-memory audit record for assistant decision trace.\n# @DATA_CONTRACT: Input[user_id,payload:Dict[str,Any]] -> Output[None]\n# @RELATION: UPDATES -> [ASSISTANT_AUDIT]\n# @SIDE_EFFECT: Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.\n# @PRE: payload describes decision/outcome fields.\n# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry.\n# @INVARIANT: persisted in-memory audit entry always contains created_at in ISO format.\ndef _audit(user_id: str, payload: Dict[str, Any]):\n if user_id not in ASSISTANT_AUDIT:\n ASSISTANT_AUDIT[user_id] = []\n ASSISTANT_AUDIT[user_id].append(\n {**payload, \"created_at\": datetime.utcnow().isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# [/DEF:_audit:Function]\n" + "anchor_syntax": "region", + "body": "# #region _audit [C:2] [TYPE Function]\n# @BRIEF Append in-memory audit record for assistant decision trace.\n# @DATA_CONTRACT: Input[user_id,payload:Dict[str,Any]] -> Output[None]\n# @RELATION UPDATES -> [ASSISTANT_AUDIT]\n# @SIDE_EFFECT: Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.\n# @PRE: payload describes decision/outcome fields.\n# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry.\n# @INVARIANT: persisted in-memory audit entry always contains created_at in ISO format.\ndef _audit(user_id: str, payload: dict[str, Any]):\n if user_id not in ASSISTANT_AUDIT:\n ASSISTANT_AUDIT[user_id] = []\n ASSISTANT_AUDIT[user_id].append(\n {**payload, \"created_at\": datetime.utcnow().isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# #endregion _audit\n" }, { "contract_id": "_persist_audit", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 135, - "end_line": 160, + "start_line": 131, + "end_line": 155, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Audit row is committed or failure is logged with rollback.", "PRE": "db session is writable and payload is JSON-serializable.", "PURPOSE": "Persist structured assistant audit payload in database." @@ -10048,19 +8501,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_persist_audit:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Persist structured assistant audit payload in database.\n# @PRE: db session is writable and payload is JSON-serializable.\n# @POST: Audit row is committed or failure is logged with rollback.\ndef _persist_audit(\n db: Session, user_id: str, payload: Dict[str, Any], conversation_id: Optional[str]\n):\n try:\n row = AssistantAuditRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n decision=payload.get(\"decision\"),\n task_id=payload.get(\"task_id\"),\n message=payload.get(\"message\"),\n payload=payload,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.audit][persist_failed] {exc}\")\n\n\n# [/DEF:_persist_audit:Function]\n" + "anchor_syntax": "region", + "body": "# #region _persist_audit [C:2] [TYPE Function]\n# @BRIEF Persist structured assistant audit payload in database.\n# @PRE: db session is writable and payload is JSON-serializable.\n# @POST: Audit row is committed or failure is logged with rollback.\ndef _persist_audit(\n db: Session, user_id: str, payload: dict[str, Any], conversation_id: str | None\n):\n try:\n row = AssistantAuditRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n decision=payload.get(\"decision\"),\n task_id=payload.get(\"task_id\"),\n message=payload.get(\"message\"),\n payload=payload,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.audit][persist_failed] {exc}\")\n\n\n# #endregion _persist_audit\n" }, { "contract_id": "_persist_confirmation", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 163, - "end_line": 188, + "start_line": 158, + "end_line": 182, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Confirmation row exists in persistent storage.", "PRE": "record contains id/user/intent/dispatch/expiry fields.", "PURPOSE": "Persist confirmation token record to database." @@ -10086,19 +8539,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_persist_confirmation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Persist confirmation token record to database.\n# @PRE: record contains id/user/intent/dispatch/expiry fields.\n# @POST: Confirmation row exists in persistent storage.\ndef _persist_confirmation(db: Session, record: ConfirmationRecord):\n try:\n row = AssistantConfirmationRecord(\n id=record.id,\n user_id=record.user_id,\n conversation_id=record.conversation_id,\n state=record.state,\n intent=record.intent,\n dispatch=record.dispatch,\n expires_at=record.expires_at,\n created_at=record.created_at,\n consumed_at=None,\n )\n db.merge(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][persist_failed] {exc}\")\n\n\n# [/DEF:_persist_confirmation:Function]\n" + "anchor_syntax": "region", + "body": "# #region _persist_confirmation [C:2] [TYPE Function]\n# @BRIEF Persist confirmation token record to database.\n# @PRE: record contains id/user/intent/dispatch/expiry fields.\n# @POST: Confirmation row exists in persistent storage.\ndef _persist_confirmation(db: Session, record: ConfirmationRecord):\n try:\n row = AssistantConfirmationRecord(\n id=record.id,\n user_id=record.user_id,\n conversation_id=record.conversation_id,\n state=record.state,\n intent=record.intent,\n dispatch=record.dispatch,\n expires_at=record.expires_at,\n created_at=record.created_at,\n consumed_at=None,\n )\n db.merge(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][persist_failed] {exc}\")\n\n\n# #endregion _persist_confirmation\n" }, { "contract_id": "_update_confirmation_state", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 191, - "end_line": 214, + "start_line": 185, + "end_line": 207, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "State and consumed_at fields are updated when applicable.", "PRE": "confirmation_id references existing row.", "PURPOSE": "Update persistent confirmation token lifecycle state." @@ -10124,19 +8577,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_update_confirmation_state:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Update persistent confirmation token lifecycle state.\n# @PRE: confirmation_id references existing row.\n# @POST: State and consumed_at fields are updated when applicable.\ndef _update_confirmation_state(db: Session, confirmation_id: str, state: str):\n try:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return\n row.state = state\n if state in {\"consumed\", \"expired\", \"cancelled\"}:\n row.consumed_at = datetime.utcnow()\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][update_failed] {exc}\")\n\n\n# [/DEF:_update_confirmation_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region _update_confirmation_state [C:2] [TYPE Function]\n# @BRIEF Update persistent confirmation token lifecycle state.\n# @PRE: confirmation_id references existing row.\n# @POST: State and consumed_at fields are updated when applicable.\ndef _update_confirmation_state(db: Session, confirmation_id: str, state: str):\n try:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return\n row.state = state\n if state in {\"consumed\", \"expired\", \"cancelled\"}:\n row.consumed_at = datetime.utcnow()\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][update_failed] {exc}\")\n\n\n# #endregion _update_confirmation_state\n" }, { "contract_id": "_load_confirmation_from_db", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 217, - "end_line": 244, + "start_line": 210, + "end_line": 236, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns ConfirmationRecord when found, otherwise None.", "PRE": "confirmation_id may or may not exist in storage.", "PURPOSE": "Load confirmation token from database into in-memory model." @@ -10162,19 +8615,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_load_confirmation_from_db:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Load confirmation token from database into in-memory model.\n# @PRE: confirmation_id may or may not exist in storage.\n# @POST: Returns ConfirmationRecord when found, otherwise None.\ndef _load_confirmation_from_db(\n db: Session, confirmation_id: str\n) -> Optional[ConfirmationRecord]:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return None\n return ConfirmationRecord(\n id=row.id,\n user_id=row.user_id,\n conversation_id=row.conversation_id,\n intent=row.intent or {},\n dispatch=row.dispatch or {},\n expires_at=row.expires_at,\n state=row.state,\n created_at=row.created_at,\n )\n\n\n# [/DEF:_load_confirmation_from_db:Function]\n" + "anchor_syntax": "region", + "body": "# #region _load_confirmation_from_db [C:2] [TYPE Function]\n# @BRIEF Load confirmation token from database into in-memory model.\n# @PRE: confirmation_id may or may not exist in storage.\n# @POST: Returns ConfirmationRecord when found, otherwise None.\ndef _load_confirmation_from_db(\n db: Session, confirmation_id: str\n) -> ConfirmationRecord | None:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return None\n return ConfirmationRecord(\n id=row.id,\n user_id=row.user_id,\n conversation_id=row.conversation_id,\n intent=row.intent or {},\n dispatch=row.dispatch or {},\n expires_at=row.expires_at,\n state=row.state,\n created_at=row.created_at,\n )\n\n\n# #endregion _load_confirmation_from_db\n" }, { "contract_id": "_ensure_conversation", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 247, - "end_line": 268, + "start_line": 239, + "end_line": 259, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.", "PRE": "user_id identifies current actor.", "PURPOSE": "Resolve active conversation id in memory or create a new one." @@ -10200,19 +8653,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_conversation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve active conversation id in memory or create a new one.\n# @PRE: user_id identifies current actor.\n# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.\ndef _ensure_conversation(user_id: str, conversation_id: Optional[str]) -> str:\n if conversation_id:\n from ._schemas import USER_ACTIVE_CONVERSATION\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n from ._schemas import USER_ACTIVE_CONVERSATION\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# [/DEF:_ensure_conversation:Function]\n" + "anchor_syntax": "region", + "body": "# #region _ensure_conversation [C:2] [TYPE Function]\n# @BRIEF Resolve active conversation id in memory or create a new one.\n# @PRE: user_id identifies current actor.\n# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.\ndef _ensure_conversation(user_id: str, conversation_id: str | None) -> str:\n if conversation_id:\n from ._schemas import USER_ACTIVE_CONVERSATION\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n from ._schemas import USER_ACTIVE_CONVERSATION\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# #endregion _ensure_conversation\n" }, { "contract_id": "_resolve_or_create_conversation", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 271, - "end_line": 306, + "start_line": 262, + "end_line": 296, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.", "PRE": "user_id and db session are available.", "PURPOSE": "Resolve active conversation using explicit id, memory cache, or persisted history." @@ -10238,19 +8691,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_or_create_conversation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve active conversation using explicit id, memory cache, or persisted history.\n# @PRE: user_id and db session are available.\n# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.\ndef _resolve_or_create_conversation(\n user_id: str, conversation_id: Optional[str], db: Session\n) -> str:\n from ._schemas import USER_ACTIVE_CONVERSATION\n\n if conversation_id:\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n from sqlalchemy import desc\n\n last_message = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .first()\n )\n if last_message:\n USER_ACTIVE_CONVERSATION[user_id] = last_message.conversation_id\n return last_message.conversation_id\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# [/DEF:_resolve_or_create_conversation:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_or_create_conversation [C:2] [TYPE Function]\n# @BRIEF Resolve active conversation using explicit id, memory cache, or persisted history.\n# @PRE: user_id and db session are available.\n# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.\ndef _resolve_or_create_conversation(\n user_id: str, conversation_id: str | None, db: Session\n) -> str:\n from ._schemas import USER_ACTIVE_CONVERSATION\n\n if conversation_id:\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n from sqlalchemy import desc\n\n last_message = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .first()\n )\n if last_message:\n USER_ACTIVE_CONVERSATION[user_id] = last_message.conversation_id\n return last_message.conversation_id\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# #endregion _resolve_or_create_conversation\n" }, { "contract_id": "_cleanup_history_ttl", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 309, - "end_line": 348, + "start_line": 299, + "end_line": 337, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.", "PRE": "db session is available and user_id references current actor scope.", "PURPOSE": "Enforce assistant message retention window by deleting expired rows and in-memory records." @@ -10276,19 +8729,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_cleanup_history_ttl:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Enforce assistant message retention window by deleting expired rows and in-memory records.\n# @PRE: db session is available and user_id references current actor scope.\n# @POST: Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.\ndef _cleanup_history_ttl(db: Session, user_id: str):\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)\n try:\n query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.created_at < cutoff,\n )\n if hasattr(query, \"delete\"):\n query.delete(synchronize_session=False)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(\n f\"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}\"\n )\n\n stale_keys: List[Tuple[str, str]] = []\n for key, items in CONVERSATIONS.items():\n if key[0] != user_id:\n continue\n kept = []\n for item in items:\n created_at = item.get(\"created_at\")\n if isinstance(created_at, datetime) and created_at < cutoff:\n continue\n kept.append(item)\n if kept:\n CONVERSATIONS[key] = kept\n else:\n stale_keys.append(key)\n for key in stale_keys:\n CONVERSATIONS.pop(key, None)\n\n\n# [/DEF:_cleanup_history_ttl:Function]\n" + "anchor_syntax": "region", + "body": "# #region _cleanup_history_ttl [C:2] [TYPE Function]\n# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.\n# @PRE: db session is available and user_id references current actor scope.\n# @POST: Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.\ndef _cleanup_history_ttl(db: Session, user_id: str):\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)\n try:\n query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.created_at < cutoff,\n )\n if hasattr(query, \"delete\"):\n query.delete(synchronize_session=False)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(\n f\"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}\"\n )\n\n stale_keys: list[tuple[str, str]] = []\n for key, items in CONVERSATIONS.items():\n if key[0] != user_id:\n continue\n kept = []\n for item in items:\n created_at = item.get(\"created_at\")\n if isinstance(created_at, datetime) and created_at < cutoff:\n continue\n kept.append(item)\n if kept:\n CONVERSATIONS[key] = kept\n else:\n stale_keys.append(key)\n for key in stale_keys:\n CONVERSATIONS.pop(key, None)\n\n\n# #endregion _cleanup_history_ttl\n" }, { "contract_id": "_is_conversation_archived", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 351, - "end_line": 363, + "start_line": 340, + "end_line": 351, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns True when conversation inactivity exceeds archive threshold.", "PRE": "updated_at can be null for empty conversations.", "PURPOSE": "Determine archived state for a conversation based on last update timestamp." @@ -10314,19 +8767,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_is_conversation_archived:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Determine archived state for a conversation based on last update timestamp.\n# @PRE: updated_at can be null for empty conversations.\n# @POST: Returns True when conversation inactivity exceeds archive threshold.\ndef _is_conversation_archived(updated_at: Optional[datetime]) -> bool:\n if not updated_at:\n return False\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)\n return updated_at < cutoff\n\n\n# [/DEF:_is_conversation_archived:Function]\n" + "anchor_syntax": "region", + "body": "# #region _is_conversation_archived [C:2] [TYPE Function]\n# @BRIEF Determine archived state for a conversation based on last update timestamp.\n# @PRE: updated_at can be null for empty conversations.\n# @POST: Returns True when conversation inactivity exceeds archive threshold.\ndef _is_conversation_archived(updated_at: datetime | None) -> bool:\n if not updated_at:\n return False\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)\n return updated_at < cutoff\n\n\n# #endregion _is_conversation_archived\n" }, { "contract_id": "_coerce_query_bool", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 366, - "end_line": 379, + "start_line": 354, + "end_line": 366, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns deterministic boolean flag.", "PRE": "value may be bool, string, or FastAPI Query metadata object.", "PURPOSE": "Normalize bool-like query values for compatibility in direct handler invocations/tests." @@ -10352,19 +8805,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_coerce_query_bool:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize bool-like query values for compatibility in direct handler invocations/tests.\n# @PRE: value may be bool, string, or FastAPI Query metadata object.\n# @POST: Returns deterministic boolean flag.\ndef _coerce_query_bool(value: Any) -> bool:\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n return False\n\n\n# [/DEF:_coerce_query_bool:Function]\n" + "anchor_syntax": "region", + "body": "# #region _coerce_query_bool [C:2] [TYPE Function]\n# @BRIEF Normalize bool-like query values for compatibility in direct handler invocations/tests.\n# @PRE: value may be bool, string, or FastAPI Query metadata object.\n# @POST: Returns deterministic boolean flag.\ndef _coerce_query_bool(value: Any) -> bool:\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n return False\n\n\n# #endregion _coerce_query_bool\n" }, { "contract_id": "AssistantLlmPlanner", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", "start_line": 1, - "end_line": 298, + "end_line": 292, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Tool catalog is filtered by user permissions before being sent to LLM.", "LAYER": "API", "PURPOSE": "LLM-based intent planning, tool catalog construction, and authorization for the assistant API." @@ -10400,19 +8853,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantLlmPlanner:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: 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, Dict, List, Optional, Tuple\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.config_manager import ConfigManager\nfrom src.dependencies import has_permission\nfrom src.schemas.auth import User\nfrom src.services.llm_provider import LLMProviderService\nfrom src.services.llm_prompt_templates import resolve_bound_provider_id\nfrom ._schemas import (\n INTENT_PERMISSION_CHECKS,\n AssistantAction,\n)\nfrom ._resolvers import (\n _get_default_environment_id,\n)\n\n\n# [DEF:_check_any_permission:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_check_any_permission:Function]\n\n\n# [DEF:_has_any_permission:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_has_any_permission:Function]\n\n\n# [DEF:_build_tool_catalog:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[Dict[str, Any]] = 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# [/DEF:_build_tool_catalog:Function]\n\n\n# [DEF:_coerce_intent_entities:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_coerce_intent_entities:Function]\n\n\n# [/DEF:AssistantLlmPlanner:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_check_any_permission", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 32, - "end_line": 53, + "start_line": 30, + "end_line": 50, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns on first successful permission; raises 403-like HTTPException otherwise.", "PRE": "checks list contains resource-action tuples.", "PURPOSE": "Validate user against alternative permission checks (logical OR)." @@ -10438,19 +8891,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_check_any_permission:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_check_any_permission:Function]\n" + "anchor_syntax": "region", + "body": "# #region _check_any_permission [C:2] [TYPE Function]\n# @BRIEF Validate user against alternative permission checks (logical OR).\n# @PRE: checks list contains resource-action tuples.\n# @POST: Returns on first successful permission; raises 403-like HTTPException otherwise.\ndef _check_any_permission(current_user: User, checks: list[tuple[str, str]]):\n errors: list[HTTPException] = []\n for resource, action in checks:\n try:\n has_permission(resource, action)(current_user)\n return\n except HTTPException as exc:\n errors.append(exc)\n\n raise (\n errors[-1]\n if errors\n else HTTPException(status_code=403, detail=\"Permission denied\")\n )\n\n\n# #endregion _check_any_permission\n" }, { "contract_id": "_has_any_permission", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 56, - "end_line": 69, + "start_line": 53, + "end_line": 65, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns True when at least one permission check passes.", "PRE": "current_user and checks list are valid.", "PURPOSE": "Check whether user has at least one permission tuple from the provided list." @@ -10476,19 +8929,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_has_any_permission:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_has_any_permission:Function]\n" + "anchor_syntax": "region", + "body": "# #region _has_any_permission [C:2] [TYPE Function]\n# @BRIEF Check whether user has at least one permission tuple from the provided list.\n# @PRE: current_user and checks list are valid.\n# @POST: Returns True when at least one permission check passes.\ndef _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:\n try:\n _check_any_permission(current_user, checks)\n return True\n except HTTPException:\n return False\n\n\n# #endregion _has_any_permission\n" }, { "contract_id": "_build_tool_catalog", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 72, - "end_line": 272, + "start_line": 68, + "end_line": 267, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Returns list of executable tools filtered by permission and runtime availability.", "PRE": "current_user is authenticated; config/db are available.", "PURPOSE": "Build current-user tool catalog for LLM planner with operation contracts and defaults." @@ -10521,19 +8974,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_build_tool_catalog:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[Dict[str, Any]] = 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# [/DEF:_build_tool_catalog:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_tool_catalog [C:3] [TYPE Function]\n# @BRIEF Build current-user tool catalog for LLM planner with operation contracts and defaults.\n# @PRE: current_user is authenticated; config/db are available.\n# @POST: Returns list of executable tools filtered by permission and runtime availability.\n# @RELATION CALLS -> LLMProviderService\ndef _build_tool_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n dataset_review_context: dict[str, Any] | None = None,\n) -> list[dict[str, Any]]:\n envs = config_manager.get_environments()\n default_env_id = _get_default_environment_id(config_manager)\n providers = LLMProviderService(db).get_all_providers()\n llm_settings = {}\n try:\n llm_settings = config_manager.get_config().settings.llm\n except Exception:\n llm_settings = {}\n active_provider = next((p.id for p in providers if p.is_active), None)\n fallback_provider = active_provider or (providers[0].id if providers else None)\n validation_provider = (\n resolve_bound_provider_id(llm_settings, \"dashboard_validation\")\n or fallback_provider\n )\n documentation_provider = (\n resolve_bound_provider_id(llm_settings, \"documentation\") or fallback_provider\n )\n\n candidates: list[dict[str, Any]] = [\n {\n \"operation\": \"show_capabilities\",\n \"domain\": \"assistant\",\n \"description\": \"Show available assistant commands and examples\",\n \"required_entities\": [],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_task_status\",\n \"domain\": \"status\",\n \"description\": \"Get task status by task_id or latest user task\",\n \"required_entities\": [],\n \"optional_entities\": [\"task_id\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"create_branch\",\n \"domain\": \"git\",\n \"description\": \"Create git branch for dashboard by id/slug/title\",\n \"required_entities\": [\"branch_name\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"commit_changes\",\n \"domain\": \"git\",\n \"description\": \"Commit dashboard repository changes by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\", \"message\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"deploy_dashboard\",\n \"domain\": \"git\",\n \"description\": \"Deploy dashboard (id/slug/title) to target environment\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"execute_migration\",\n \"domain\": \"migration\",\n \"description\": \"Run dashboard migration (id/slug/title) between environments. Optional boolean flags: replace_db_config, fix_cross_filters\",\n \"required_entities\": [\"source_env\", \"target_env\"],\n \"optional_entities\": [\n \"dashboard_id\",\n \"dashboard_ref\",\n \"replace_db_config\",\n \"fix_cross_filters\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_backup\",\n \"domain\": \"backup\",\n \"description\": \"Run backup for environment or specific dashboard by id/slug/title\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_validation\",\n \"domain\": \"llm\",\n \"description\": \"Run LLM dashboard validation by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_ref\", \"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": validation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_documentation\",\n \"domain\": \"llm\",\n \"description\": \"Generate dataset documentation via LLM\",\n \"required_entities\": [\"dataset_id\"],\n \"optional_entities\": [\"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": documentation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_health_summary\",\n \"domain\": \"health\",\n \"description\": \"Get summary of dashboard health and failing validations\",\n \"required_entities\": [],\n \"optional_entities\": [\"environment\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n ]\n\n available: list[dict[str, Any]] = []\n for tool in candidates:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n\n if dataset_review_context is not None:\n dataset_tools: list[dict[str, Any]] = [\n {\n \"operation\": \"dataset_review_answer_context\",\n \"domain\": \"dataset_review\",\n \"description\": \"Answer questions using the currently bound dataset review session context\",\n \"required_entities\": [\"dataset_review_session_id\"],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"dataset_review_approve_mappings\",\n \"domain\": \"dataset_review\",\n \"description\": \"Approve warning-sensitive execution mappings in the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [\"mapping_ids\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_set_field_semantics\",\n \"domain\": \"dataset_review\",\n \"description\": \"Apply explicit semantic field override or candidate selection in the current dataset review session\",\n \"required_entities\": [\n \"dataset_review_session_id\",\n \"session_version\",\n \"field_id\",\n ],\n \"optional_entities\": [\n \"candidate_id\",\n \"verbose_name\",\n \"description\",\n \"display_format\",\n \"lock_field\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"domain\": \"dataset_review\",\n \"description\": \"Generate a Superset-compiled SQL preview for the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n ]\n for tool in dataset_tools:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n return available\n\n\n# #endregion _build_tool_catalog\n" }, { "contract_id": "_coerce_intent_entities", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 275, - "end_line": 295, + "start_line": 270, + "end_line": 289, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returned intent has numeric ids coerced where possible and string values stripped.", "PRE": "intent contains entities dict or missing entities.", "PURPOSE": "Normalize intent entity value types from LLM output to route-compatible values." @@ -10559,19 +9012,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_coerce_intent_entities:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_coerce_intent_entities:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "AssistantLlmPlannerIntent", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", "start_line": 1, - "end_line": 173, + "end_line": 170, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Production deployments always require confirmation.", "LAYER": "API", "PURPOSE": "LLM-based intent planning and authorization for the assistant API — separated from tool catalog." @@ -10601,19 +9054,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantLlmPlannerIntent:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: 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, Dict, List, Optional\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.config_manager import ConfigManager\nfrom src.schemas.auth import User\nfrom src.services.llm_provider import LLMProviderService\nfrom src.services.llm_prompt_templates import (\n normalize_llm_settings,\n resolve_bound_provider_id,\n)\nfrom src.plugins.llm_analysis.service import LLMClient\nfrom src.plugins.llm_analysis.models import LLMProviderType\nfrom ._schemas import INTENT_PERMISSION_CHECKS\nfrom ._resolvers import (\n _is_production_env,\n _resolve_provider_id,\n)\nfrom ._llm_planner import (\n _check_any_permission,\n _coerce_intent_entities,\n)\n\n\n# [DEF:_plan_intent_with_llm:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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) -> Optional[Dict[str, Any]]:\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# [/DEF:_plan_intent_with_llm:Function]\n\n\n# [DEF:_authorize_intent:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_authorize_intent:Function]\n\n\n# [/DEF:AssistantLlmPlannerIntent:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_plan_intent_with_llm", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", - "start_line": 37, - "end_line": 156, + "start_line": 36, + "end_line": 154, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns normalized intent dict when planning succeeds; otherwise None.", "PRE": "tools list contains allowed operations for current user.", "PURPOSE": "Use active LLM provider to select best tool/operation from dynamic catalog." @@ -10639,19 +9092,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_plan_intent_with_llm:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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) -> Optional[Dict[str, Any]]:\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# [/DEF:_plan_intent_with_llm:Function]\n" + "anchor_syntax": "region", + "body": "# #region _plan_intent_with_llm [C:2] [TYPE Function]\n# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.\n# @PRE: tools list contains allowed operations for current user.\n# @POST: Returns normalized intent dict when planning succeeds; otherwise None.\nasync def _plan_intent_with_llm(\n message: str,\n tools: list[dict[str, Any]],\n db: Session,\n config_manager: ConfigManager,\n) -> dict[str, Any] | None:\n if not tools:\n return None\n\n llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)\n planner_provider_token = llm_settings.get(\"assistant_planner_provider\")\n planner_model_override = llm_settings.get(\"assistant_planner_model\")\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n provider_id = _resolve_provider_id(planner_provider_token, db)\n provider = next((p for p in providers if p.id == provider_id), None)\n if not provider:\n return None\n api_key = llm_service.get_decrypted_api_key(provider.id)\n if not api_key:\n return None\n\n planner = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=planner_model_override or provider.default_model,\n )\n\n system_instruction = (\n \"You are a deterministic intent planner for backend tools.\\n\"\n \"Choose exactly one operation from available_tools or return clarify.\\n\"\n \"Output strict JSON object:\\n\"\n \"{\"\n '\"domain\": string, '\n '\"operation\": string, '\n '\"entities\": object, '\n '\"confidence\": number, '\n '\"risk_level\": \"safe\"|\"guarded\"|\"dangerous\", '\n '\"requires_confirmation\": boolean'\n \"}\\n\"\n \"Rules:\\n\"\n \"- Use only operation names from available_tools.\\n\"\n '- If input is ambiguous, operation must be \"clarify\" with low confidence.\\n'\n \"- If dashboard is provided as name/slug (e.g., COVID), put it into entities.dashboard_ref.\\n\"\n \"- Keep entities minimal and factual.\\n\"\n )\n payload = {\n \"available_tools\": tools,\n \"user_message\": message,\n \"known_environments\": [\n {\"id\": e.id, \"name\": e.name} for e in config_manager.get_environments()\n ],\n }\n try:\n response = await planner.get_json_completion(\n [\n {\"role\": \"system\", \"content\": system_instruction},\n {\"role\": \"user\", \"content\": json.dumps(payload, ensure_ascii=False)},\n ]\n )\n except Exception as exc:\n import traceback\n\n logger.warning(\n f\"[assistant.planner][fallback] LLM planner unavailable: {exc}\\n{traceback.format_exc()}\"\n )\n return None\n if not isinstance(response, dict):\n return None\n\n operation = response.get(\"operation\")\n valid_ops = {tool[\"operation\"] for tool in tools}\n if operation == \"clarify\":\n return {\n \"domain\": \"unknown\",\n \"operation\": \"clarify\",\n \"entities\": {},\n \"confidence\": float(response.get(\"confidence\", 0.3)),\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n if operation not in valid_ops:\n return None\n\n by_operation = {tool[\"operation\"]: tool for tool in tools}\n selected = by_operation[operation]\n intent = {\n \"domain\": response.get(\"domain\") or selected[\"domain\"],\n \"operation\": operation,\n \"entities\": response.get(\"entities\", {}),\n \"confidence\": float(response.get(\"confidence\", 0.75)),\n \"risk_level\": response.get(\"risk_level\") or selected[\"risk_level\"],\n \"requires_confirmation\": bool(\n response.get(\"requires_confirmation\", selected[\"requires_confirmation\"])\n ),\n }\n intent = _coerce_intent_entities(intent)\n\n defaults = selected.get(\"defaults\") or {}\n for key, value in defaults.items():\n if value and not intent[\"entities\"].get(key):\n intent[\"entities\"][key] = value\n\n if operation in {\"deploy_dashboard\", \"execute_migration\"}:\n env_token = intent[\"entities\"].get(\"environment\") or intent[\"entities\"].get(\n \"target_env\"\n )\n if _is_production_env(env_token, config_manager):\n intent[\"risk_level\"] = \"dangerous\"\n intent[\"requires_confirmation\"] = True\n return intent\n\n\n# #endregion _plan_intent_with_llm\n" }, { "contract_id": "_authorize_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", - "start_line": 159, - "end_line": 170, + "start_line": 157, + "end_line": 167, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns if authorized; raises HTTPException(403) when denied.", "PRE": "intent.operation is present for known assistant command domains.", "PURPOSE": "Validate user permissions for parsed intent before confirmation/dispatch." @@ -10677,19 +9130,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_authorize_intent:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_authorize_intent:Function]\n" + "anchor_syntax": "region", + "body": "# #region _authorize_intent [C:2] [TYPE Function]\n# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.\n# @PRE: intent.operation is present for known assistant command domains.\n# @POST: Returns if authorized; raises HTTPException(403) when denied.\ndef _authorize_intent(intent: dict[str, Any], current_user: User):\n operation = intent.get(\"operation\")\n if operation in INTENT_PERMISSION_CHECKS:\n _check_any_permission(current_user, INTENT_PERMISSION_CHECKS[operation])\n\n\n# #endregion _authorize_intent\n" }, { "contract_id": "AssistantResolvers", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_resolvers.py", "start_line": 1, - "end_line": 397, + "end_line": 385, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "Resolution functions never raise; they return None on failure.", "LAYER": "API", "PURPOSE": "Environment, dashboard, provider, and task resolution utilities for the assistant API." @@ -10728,19 +9181,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantResolvers:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Environment, dashboard, provider, and task resolution utilities for the assistant API.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [ConfigManager]\n# @RELATION: DEPENDS_ON -> [SupersetClient]\n# @INVARIANT: Resolution functions never raise; they return None on failure.\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any, Dict, List, Optional, Tuple, cast\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.config_manager import ConfigManager\nfrom src.core.superset_client import SupersetClient\nfrom src.services.llm_provider import LLMProviderService\nfrom src.services.llm_prompt_templates import resolve_bound_provider_id\nfrom src.schemas.auth import User\n\nlogger = cast(Any, logger)\n\n\n# [DEF:_extract_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Extract first regex match group from text by ordered pattern list.\n# @PRE: patterns contain at least one capture group.\n# @POST: Returns first matched token or None.\ndef _extract_id(text: str, patterns: List[str]) -> Optional[str]:\n for p in patterns:\n m = re.search(p, text, flags=re.IGNORECASE)\n if m:\n return m.group(1)\n return None\n\n\n# [/DEF:_extract_id:Function]\n\n# [DEF:_resolve_env_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve environment identifier/name token to canonical environment id.\n# @PRE: config_manager provides environment list.\n# @POST: Returns matched environment id or None.\ndef _resolve_env_id(\n token: Optional[str], config_manager: ConfigManager\n) -> Optional[str]:\n if not token:\n return None\n\n normalized = token.strip().lower()\n envs = config_manager.get_environments()\n for env in envs:\n if env.id.lower() == normalized or env.name.lower() == normalized:\n return env.id\n return None\n\n\n# [/DEF:_resolve_env_id:Function]\n\n# [DEF:_is_production_env:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Determine whether environment token resolves to production-like target.\n# @PRE: config_manager provides environments or token text is provided.\n# @POST: Returns True for production/prod synonyms, else False.\ndef _is_production_env(token: Optional[str], config_manager: ConfigManager) -> bool:\n env_id = _resolve_env_id(token, config_manager)\n if not env_id:\n return (token or \"\").strip().lower() in {\"prod\", \"production\", \"прод\"}\n\n env = next((e for e in config_manager.get_environments() if e.id == env_id), None)\n if not env:\n return False\n target = f\"{env.id} {env.name}\".lower()\n return \"prod\" in target or \"production\" in target or \"прод\" in target\n\n\n# [/DEF:_is_production_env:Function]\n\n# [DEF:_resolve_provider_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve provider token to provider id with active/default fallback.\n# @PRE: db session can load provider list through LLMProviderService.\n# @POST: Returns provider id or None when no providers configured.\ndef _resolve_provider_id(\n provider_token: Optional[str],\n db: Session,\n config_manager: Optional[ConfigManager] = None,\n task_key: Optional[str] = None,\n) -> Optional[str]:\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n if not providers:\n return None\n\n if provider_token:\n needle = provider_token.strip().lower()\n for p in providers:\n if p.id.lower() == needle or p.name.lower() == needle:\n return p.id\n\n if config_manager and task_key:\n try:\n llm_settings = config_manager.get_config().settings.llm\n bound_provider_id = resolve_bound_provider_id(llm_settings, task_key)\n if bound_provider_id and any(p.id == bound_provider_id for p in providers):\n return bound_provider_id\n except Exception:\n pass\n\n active = next((p for p in providers if p.is_active), None)\n return active.id if active else providers[0].id\n\n\n# [/DEF:_resolve_provider_id:Function]\n\n# [DEF:_get_default_environment_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve default environment id from settings or first configured environment.\n# @PRE: config_manager returns environments list.\n# @POST: Returns default environment id or None when environment list is empty.\ndef _get_default_environment_id(config_manager: ConfigManager) -> Optional[str]:\n configured = config_manager.get_environments()\n if not configured:\n return None\n preferred = None\n if hasattr(config_manager, \"get_config\"):\n try:\n preferred = config_manager.get_config().settings.default_environment_id\n except Exception:\n preferred = None\n if preferred and any(env.id == preferred for env in configured):\n return preferred\n explicit_default = next(\n (env.id for env in configured if getattr(env, \"is_default\", False)), None\n )\n return explicit_default or configured[0].id\n\n\n# [/DEF:_get_default_environment_id:Function]\n\n# [DEF:_resolve_dashboard_id_by_ref:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard id by title or slug reference in selected environment.\n# @PRE: dashboard_ref is a non-empty string-like token.\n# @POST: Returns dashboard id when uniquely matched, otherwise None.\ndef _resolve_dashboard_id_by_ref(\n dashboard_ref: Optional[str],\n env_id: Optional[str],\n config_manager: ConfigManager,\n) -> Optional[int]:\n if not dashboard_ref or not env_id:\n return None\n env = next(\n (item for item in config_manager.get_environments() if item.id == env_id), None\n )\n if not env:\n return None\n\n needle = dashboard_ref.strip().lower()\n try:\n client = SupersetClient(env)\n _, dashboards = client.get_dashboards(query={\"page_size\": 200})\n except Exception as exc:\n logger.warning(\n f\"[assistant.dashboard_resolve][failed] ref={dashboard_ref} env={env_id} error={exc}\"\n )\n return None\n\n exact = next(\n (\n d\n for d in dashboards\n if str(d.get(\"slug\", \"\")).lower() == needle\n or str(d.get(\"dashboard_title\", \"\")).lower() == needle\n or str(d.get(\"title\", \"\")).lower() == needle\n ),\n None,\n )\n if exact:\n return int(exact.get(\"id\"))\n\n partial = [\n d\n for d in dashboards\n if needle in str(d.get(\"dashboard_title\", d.get(\"title\", \"\"))).lower()\n ]\n if len(partial) == 1 and partial[0].get(\"id\") is not None:\n return int(partial[0][\"id\"])\n return None\n\n\n# [/DEF:_resolve_dashboard_id_by_ref:Function]\n\n# [DEF:_resolve_dashboard_id_entity:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.\n# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref.\n# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable.\ndef _resolve_dashboard_id_entity(\n entities: Dict[str, Any],\n config_manager: ConfigManager,\n env_hint: Optional[str] = None,\n) -> Optional[int]:\n raw_dashboard_id = entities.get(\"dashboard_id\")\n dashboard_ref = entities.get(\"dashboard_ref\")\n\n if isinstance(raw_dashboard_id, int):\n return raw_dashboard_id\n\n if isinstance(raw_dashboard_id, str):\n token = raw_dashboard_id.strip()\n if token.isdigit():\n return int(token)\n if token and not dashboard_ref:\n dashboard_ref = token\n\n if not dashboard_ref:\n return None\n\n env_token = (\n env_hint\n or entities.get(\"environment\")\n or entities.get(\"source_env\")\n or entities.get(\"target_env\")\n )\n env_id = (\n _resolve_env_id(env_token, config_manager)\n if env_token\n else _get_default_environment_id(config_manager)\n )\n return _resolve_dashboard_id_by_ref(str(dashboard_ref), env_id, config_manager)\n\n\n# [/DEF:_resolve_dashboard_id_entity:Function]\n\n# [DEF:_get_environment_name_by_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve human-readable environment name by id.\n# @PRE: environment id may be None.\n# @POST: Returns matching environment name or fallback id.\ndef _get_environment_name_by_id(\n env_id: Optional[str], config_manager: ConfigManager\n) -> str:\n if not env_id:\n return \"unknown\"\n env = next(\n (item for item in config_manager.get_environments() if item.id == env_id), None\n )\n return env.name if env else env_id\n\n\n# [/DEF:_get_environment_name_by_id:Function]\n\n# [DEF:_extract_result_deep_links:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build deep-link actions to verify task result from assistant chat.\n# @PRE: task object is available.\n# @POST: Returns zero or more assistant actions for dashboard open/diff.\ndef _extract_result_deep_links(\n task: Any, config_manager: ConfigManager\n) -> List:\n from ._schemas import AssistantAction\n\n plugin_id = getattr(task, \"plugin_id\", None)\n params = getattr(task, \"params\", {}) or {}\n result = getattr(task, \"result\", {}) or {}\n actions: list = []\n dashboard_id: Optional[int] = None\n env_id: Optional[str] = None\n\n if plugin_id == \"superset-migration\":\n migrated = (\n result.get(\"migrated_dashboards\") if isinstance(result, dict) else None\n )\n if isinstance(migrated, list) and migrated:\n first = migrated[0]\n if isinstance(first, dict) and first.get(\"id\") is not None:\n dashboard_id = int(first.get(\"id\"))\n if (\n dashboard_id is None\n and isinstance(params.get(\"selected_ids\"), list)\n and params[\"selected_ids\"]\n ):\n dashboard_id = int(params[\"selected_ids\"][0])\n env_id = params.get(\"target_env_id\")\n elif plugin_id == \"superset-backup\":\n dashboards = result.get(\"dashboards\") if isinstance(result, dict) else None\n if isinstance(dashboards, list) and dashboards:\n first = dashboards[0]\n if isinstance(first, dict) and first.get(\"id\") is not None:\n dashboard_id = int(first.get(\"id\"))\n if (\n dashboard_id is None\n and isinstance(params.get(\"dashboard_ids\"), list)\n and params[\"dashboard_ids\"]\n ):\n dashboard_id = int(params[\"dashboard_ids\"][0])\n env_id = params.get(\"environment_id\") or _resolve_env_id(\n result.get(\"environment\"), config_manager\n )\n elif plugin_id == \"llm_dashboard_validation\":\n if params.get(\"dashboard_id\") is not None:\n dashboard_id = int(params[\"dashboard_id\"])\n env_id = params.get(\"environment_id\")\n\n if dashboard_id is not None and env_id:\n env_name = _get_environment_name_by_id(env_id, config_manager)\n actions.append(\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборд в {env_name}\",\n target=f\"/dashboards/{dashboard_id}?env_id={env_id}\",\n )\n )\n if dashboard_id is not None:\n actions.append(\n AssistantAction(\n type=\"open_diff\",\n label=\"Показать Diff\",\n target=str(dashboard_id),\n )\n )\n return actions\n\n\n# [/DEF:_extract_result_deep_links:Function]\n\n# [DEF:_build_task_observability_summary:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build compact textual summary for completed tasks to reduce \"black box\" effect.\n# @PRE: task may contain plugin-specific result payload.\n# @POST: Returns non-empty summary line for known task types or empty string fallback.\ndef _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:\n plugin_id = getattr(task, \"plugin_id\", None)\n status = str(getattr(task, \"status\", \"\")).upper()\n params = getattr(task, \"params\", {}) or {}\n result = getattr(task, \"result\", {}) or {}\n\n if plugin_id == \"superset-migration\" and isinstance(result, dict):\n migrated = len(result.get(\"migrated_dashboards\") or [])\n failed_rows = result.get(\"failed_dashboards\") or []\n failed = len(failed_rows)\n selected = result.get(\"selected_dashboards\", migrated + failed)\n mappings = result.get(\"mapping_count\", 0)\n target_env_id = params.get(\"target_env_id\")\n target_env_name = _get_environment_name_by_id(target_env_id, config_manager)\n warning = \"\"\n if failed_rows:\n first = failed_rows[0]\n warning = (\n f\" Внимание: {first.get('title') or first.get('id')}: \"\n f\"{first.get('error') or 'ошибка'}.\"\n )\n return (\n f\"Сводка миграции: выбрано {selected}, перенесено {migrated}, \"\n f\"с ошибками {failed}, маппингов {mappings}, целевая среда {target_env_name}.\"\n f\"{warning}\"\n )\n\n if plugin_id == \"superset-backup\" and isinstance(result, dict):\n total = int(result.get(\"total_dashboards\", 0) or 0)\n ok = int(result.get(\"backed_up_dashboards\", 0) or 0)\n failed = int(result.get(\"failed_dashboards\", 0) or 0)\n env_id = params.get(\"environment_id\") or _resolve_env_id(\n result.get(\"environment\"), config_manager\n )\n env_name = _get_environment_name_by_id(env_id, config_manager)\n failures = result.get(\"failures\") or []\n warning = \"\"\n if failures:\n first = failures[0]\n warning = (\n f\" Внимание: {first.get('title') or first.get('id')}: \"\n f\"{first.get('error') or 'ошибка'}.\"\n )\n return (\n f\"Сводка бэкапа: среда {env_name}, всего {total}, успешно {ok}, \"\n f\"с ошибками {failed}. {status}.{warning}\"\n )\n\n if plugin_id == \"llm_dashboard_validation\" and isinstance(result, dict):\n report_status = result.get(\"status\") or status\n report_summary = result.get(\"summary\") or \"Итог недоступен.\"\n issues = result.get(\"issues\") or []\n return f\"Сводка валидации: статус {report_status}, проблем {len(issues)}. {report_summary}\"\n\n # Fallback for unknown task payloads.\n if status in {\"SUCCESS\", \"FAILED\"}:\n return f\"Задача завершена со статусом {status}.\"\n return \"\"\n\n\n# [/DEF:_build_task_observability_summary:Function]\n\n# [/DEF:AssistantResolvers:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantResolvers [C:2] [TYPE Module] [SEMANTICS assistant, resolver, lookup, environment, mapper]\n# @BRIEF Environment, dashboard, provider, and task resolution utilities for the assistant API.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @INVARIANT: Resolution functions never raise; they return None on failure.\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any, cast\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import logger\nfrom src.core.superset_client import SupersetClient\nfrom src.services.llm_prompt_templates import resolve_bound_provider_id\nfrom src.services.llm_provider import LLMProviderService\n\nlogger = cast(Any, logger)\n\n\n# #region _extract_id [C:2] [TYPE Function]\n# @BRIEF Extract first regex match group from text by ordered pattern list.\n# @PRE: patterns contain at least one capture group.\n# @POST: Returns first matched token or None.\ndef _extract_id(text: str, patterns: list[str]) -> str | None:\n for p in patterns:\n m = re.search(p, text, flags=re.IGNORECASE)\n if m:\n return m.group(1)\n return None\n\n\n# #endregion _extract_id\n\n# #region _resolve_env_id [C:2] [TYPE Function]\n# @BRIEF Resolve environment identifier/name token to canonical environment id.\n# @PRE: config_manager provides environment list.\n# @POST: Returns matched environment id or None.\ndef _resolve_env_id(\n token: str | None, config_manager: ConfigManager\n) -> str | None:\n if not token:\n return None\n\n normalized = token.strip().lower()\n envs = config_manager.get_environments()\n for env in envs:\n if env.id.lower() == normalized or env.name.lower() == normalized:\n return env.id\n return None\n\n\n# #endregion _resolve_env_id\n\n# #region _is_production_env [C:2] [TYPE Function]\n# @BRIEF Determine whether environment token resolves to production-like target.\n# @PRE: config_manager provides environments or token text is provided.\n# @POST: Returns True for production/prod synonyms, else False.\ndef _is_production_env(token: str | None, config_manager: ConfigManager) -> bool:\n env_id = _resolve_env_id(token, config_manager)\n if not env_id:\n return (token or \"\").strip().lower() in {\"prod\", \"production\", \"прод\"}\n\n env = next((e for e in config_manager.get_environments() if e.id == env_id), None)\n if not env:\n return False\n target = f\"{env.id} {env.name}\".lower()\n return \"prod\" in target or \"production\" in target or \"прод\" in target\n\n\n# #endregion _is_production_env\n\n# #region _resolve_provider_id [C:2] [TYPE Function]\n# @BRIEF Resolve provider token to provider id with active/default fallback.\n# @PRE: db session can load provider list through LLMProviderService.\n# @POST: Returns provider id or None when no providers configured.\ndef _resolve_provider_id(\n provider_token: str | None,\n db: Session,\n config_manager: ConfigManager | None = None,\n task_key: str | None = None,\n) -> str | None:\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n if not providers:\n return None\n\n if provider_token:\n needle = provider_token.strip().lower()\n for p in providers:\n if p.id.lower() == needle or p.name.lower() == needle:\n return p.id\n\n if config_manager and task_key:\n try:\n llm_settings = config_manager.get_config().settings.llm\n bound_provider_id = resolve_bound_provider_id(llm_settings, task_key)\n if bound_provider_id and any(p.id == bound_provider_id for p in providers):\n return bound_provider_id\n except Exception:\n pass\n\n active = next((p for p in providers if p.is_active), None)\n return active.id if active else providers[0].id\n\n\n# #endregion _resolve_provider_id\n\n# #region _get_default_environment_id [C:2] [TYPE Function]\n# @BRIEF Resolve default environment id from settings or first configured environment.\n# @PRE: config_manager returns environments list.\n# @POST: Returns default environment id or None when environment list is empty.\ndef _get_default_environment_id(config_manager: ConfigManager) -> str | None:\n configured = config_manager.get_environments()\n if not configured:\n return None\n preferred = None\n if hasattr(config_manager, \"get_config\"):\n try:\n preferred = config_manager.get_config().settings.default_environment_id\n except Exception:\n preferred = None\n if preferred and any(env.id == preferred for env in configured):\n return preferred\n explicit_default = next(\n (env.id for env in configured if getattr(env, \"is_default\", False)), None\n )\n return explicit_default or configured[0].id\n\n\n# #endregion _get_default_environment_id\n\n# #region _resolve_dashboard_id_by_ref [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard id by title or slug reference in selected environment.\n# @PRE: dashboard_ref is a non-empty string-like token.\n# @POST: Returns dashboard id when uniquely matched, otherwise None.\ndef _resolve_dashboard_id_by_ref(\n dashboard_ref: str | None,\n env_id: str | None,\n config_manager: ConfigManager,\n) -> int | None:\n if not dashboard_ref or not env_id:\n return None\n env = next(\n (item for item in config_manager.get_environments() if item.id == env_id), None\n )\n if not env:\n return None\n\n needle = dashboard_ref.strip().lower()\n try:\n client = SupersetClient(env)\n _, dashboards = client.get_dashboards(query={\"page_size\": 200})\n except Exception as exc:\n logger.warning(\n f\"[assistant.dashboard_resolve][failed] ref={dashboard_ref} env={env_id} error={exc}\"\n )\n return None\n\n exact = next(\n (\n d\n for d in dashboards\n if str(d.get(\"slug\", \"\")).lower() == needle\n or str(d.get(\"dashboard_title\", \"\")).lower() == needle\n or str(d.get(\"title\", \"\")).lower() == needle\n ),\n None,\n )\n if exact:\n return int(exact.get(\"id\"))\n\n partial = [\n d\n for d in dashboards\n if needle in str(d.get(\"dashboard_title\", d.get(\"title\", \"\"))).lower()\n ]\n if len(partial) == 1 and partial[0].get(\"id\") is not None:\n return int(partial[0][\"id\"])\n return None\n\n\n# #endregion _resolve_dashboard_id_by_ref\n\n# #region _resolve_dashboard_id_entity [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.\n# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref.\n# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable.\ndef _resolve_dashboard_id_entity(\n entities: dict[str, Any],\n config_manager: ConfigManager,\n env_hint: str | None = None,\n) -> int | None:\n raw_dashboard_id = entities.get(\"dashboard_id\")\n dashboard_ref = entities.get(\"dashboard_ref\")\n\n if isinstance(raw_dashboard_id, int):\n return raw_dashboard_id\n\n if isinstance(raw_dashboard_id, str):\n token = raw_dashboard_id.strip()\n if token.isdigit():\n return int(token)\n if token and not dashboard_ref:\n dashboard_ref = token\n\n if not dashboard_ref:\n return None\n\n env_token = (\n env_hint\n or entities.get(\"environment\")\n or entities.get(\"source_env\")\n or entities.get(\"target_env\")\n )\n env_id = (\n _resolve_env_id(env_token, config_manager)\n if env_token\n else _get_default_environment_id(config_manager)\n )\n return _resolve_dashboard_id_by_ref(str(dashboard_ref), env_id, config_manager)\n\n\n# #endregion _resolve_dashboard_id_entity\n\n# #region _get_environment_name_by_id [C:2] [TYPE Function]\n# @BRIEF Resolve human-readable environment name by id.\n# @PRE: environment id may be None.\n# @POST: Returns matching environment name or fallback id.\ndef _get_environment_name_by_id(\n env_id: str | None, config_manager: ConfigManager\n) -> str:\n if not env_id:\n return \"unknown\"\n env = next(\n (item for item in config_manager.get_environments() if item.id == env_id), None\n )\n return env.name if env else env_id\n\n\n# #endregion _get_environment_name_by_id\n\n# #region _extract_result_deep_links [C:2] [TYPE Function]\n# @BRIEF Build deep-link actions to verify task result from assistant chat.\n# @PRE: task object is available.\n# @POST: Returns zero or more assistant actions for dashboard open/diff.\ndef _extract_result_deep_links(\n task: Any, config_manager: ConfigManager\n) -> list:\n from ._schemas import AssistantAction\n\n plugin_id = getattr(task, \"plugin_id\", None)\n params = getattr(task, \"params\", {}) or {}\n result = getattr(task, \"result\", {}) or {}\n actions: list = []\n dashboard_id: int | None = None\n env_id: str | None = None\n\n if plugin_id == \"superset-migration\":\n migrated = (\n result.get(\"migrated_dashboards\") if isinstance(result, dict) else None\n )\n if isinstance(migrated, list) and migrated:\n first = migrated[0]\n if isinstance(first, dict) and first.get(\"id\") is not None:\n dashboard_id = int(first.get(\"id\"))\n if (\n dashboard_id is None\n and isinstance(params.get(\"selected_ids\"), list)\n and params[\"selected_ids\"]\n ):\n dashboard_id = int(params[\"selected_ids\"][0])\n env_id = params.get(\"target_env_id\")\n elif plugin_id == \"superset-backup\":\n dashboards = result.get(\"dashboards\") if isinstance(result, dict) else None\n if isinstance(dashboards, list) and dashboards:\n first = dashboards[0]\n if isinstance(first, dict) and first.get(\"id\") is not None:\n dashboard_id = int(first.get(\"id\"))\n if (\n dashboard_id is None\n and isinstance(params.get(\"dashboard_ids\"), list)\n and params[\"dashboard_ids\"]\n ):\n dashboard_id = int(params[\"dashboard_ids\"][0])\n env_id = params.get(\"environment_id\") or _resolve_env_id(\n result.get(\"environment\"), config_manager\n )\n elif plugin_id == \"llm_dashboard_validation\":\n if params.get(\"dashboard_id\") is not None:\n dashboard_id = int(params[\"dashboard_id\"])\n env_id = params.get(\"environment_id\")\n\n if dashboard_id is not None and env_id:\n env_name = _get_environment_name_by_id(env_id, config_manager)\n actions.append(\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборд в {env_name}\",\n target=f\"/dashboards/{dashboard_id}?env_id={env_id}\",\n )\n )\n if dashboard_id is not None:\n actions.append(\n AssistantAction(\n type=\"open_diff\",\n label=\"Показать Diff\",\n target=str(dashboard_id),\n )\n )\n return actions\n\n\n# #endregion _extract_result_deep_links\n\n# #region _build_task_observability_summary [C:2] [TYPE Function]\n# @BRIEF Build compact textual summary for completed tasks to reduce \"black box\" effect.\n# @PRE: task may contain plugin-specific result payload.\n# @POST: Returns non-empty summary line for known task types or empty string fallback.\ndef _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:\n plugin_id = getattr(task, \"plugin_id\", None)\n status = str(getattr(task, \"status\", \"\")).upper()\n params = getattr(task, \"params\", {}) or {}\n result = getattr(task, \"result\", {}) or {}\n\n if plugin_id == \"superset-migration\" and isinstance(result, dict):\n migrated = len(result.get(\"migrated_dashboards\") or [])\n failed_rows = result.get(\"failed_dashboards\") or []\n failed = len(failed_rows)\n selected = result.get(\"selected_dashboards\", migrated + failed)\n mappings = result.get(\"mapping_count\", 0)\n target_env_id = params.get(\"target_env_id\")\n target_env_name = _get_environment_name_by_id(target_env_id, config_manager)\n warning = \"\"\n if failed_rows:\n first = failed_rows[0]\n warning = (\n f\" Внимание: {first.get('title') or first.get('id')}: \"\n f\"{first.get('error') or 'ошибка'}.\"\n )\n return (\n f\"Сводка миграции: выбрано {selected}, перенесено {migrated}, \"\n f\"с ошибками {failed}, маппингов {mappings}, целевая среда {target_env_name}.\"\n f\"{warning}\"\n )\n\n if plugin_id == \"superset-backup\" and isinstance(result, dict):\n total = int(result.get(\"total_dashboards\", 0) or 0)\n ok = int(result.get(\"backed_up_dashboards\", 0) or 0)\n failed = int(result.get(\"failed_dashboards\", 0) or 0)\n env_id = params.get(\"environment_id\") or _resolve_env_id(\n result.get(\"environment\"), config_manager\n )\n env_name = _get_environment_name_by_id(env_id, config_manager)\n failures = result.get(\"failures\") or []\n warning = \"\"\n if failures:\n first = failures[0]\n warning = (\n f\" Внимание: {first.get('title') or first.get('id')}: \"\n f\"{first.get('error') or 'ошибка'}.\"\n )\n return (\n f\"Сводка бэкапа: среда {env_name}, всего {total}, успешно {ok}, \"\n f\"с ошибками {failed}. {status}.{warning}\"\n )\n\n if plugin_id == \"llm_dashboard_validation\" and isinstance(result, dict):\n report_status = result.get(\"status\") or status\n report_summary = result.get(\"summary\") or \"Итог недоступен.\"\n issues = result.get(\"issues\") or []\n return f\"Сводка валидации: статус {report_status}, проблем {len(issues)}. {report_summary}\"\n\n # Fallback for unknown task payloads.\n if status in {\"SUCCESS\", \"FAILED\"}:\n return f\"Задача завершена со статусом {status}.\"\n return \"\"\n\n\n# #endregion _build_task_observability_summary\n\n# #endregion AssistantResolvers\n" }, { "contract_id": "_extract_id", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 26, - "end_line": 39, + "start_line": 24, + "end_line": 36, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns first matched token or None.", "PRE": "patterns contain at least one capture group.", "PURPOSE": "Extract first regex match group from text by ordered pattern list." @@ -10766,19 +9219,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_extract_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Extract first regex match group from text by ordered pattern list.\n# @PRE: patterns contain at least one capture group.\n# @POST: Returns first matched token or None.\ndef _extract_id(text: str, patterns: List[str]) -> Optional[str]:\n for p in patterns:\n m = re.search(p, text, flags=re.IGNORECASE)\n if m:\n return m.group(1)\n return None\n\n\n# [/DEF:_extract_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region _extract_id [C:2] [TYPE Function]\n# @BRIEF Extract first regex match group from text by ordered pattern list.\n# @PRE: patterns contain at least one capture group.\n# @POST: Returns first matched token or None.\ndef _extract_id(text: str, patterns: list[str]) -> str | None:\n for p in patterns:\n m = re.search(p, text, flags=re.IGNORECASE)\n if m:\n return m.group(1)\n return None\n\n\n# #endregion _extract_id\n" }, { "contract_id": "_resolve_env_id", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 41, - "end_line": 60, + "start_line": 38, + "end_line": 56, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns matched environment id or None.", "PRE": "config_manager provides environment list.", "PURPOSE": "Resolve environment identifier/name token to canonical environment id." @@ -10804,19 +9257,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_env_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve environment identifier/name token to canonical environment id.\n# @PRE: config_manager provides environment list.\n# @POST: Returns matched environment id or None.\ndef _resolve_env_id(\n token: Optional[str], config_manager: ConfigManager\n) -> Optional[str]:\n if not token:\n return None\n\n normalized = token.strip().lower()\n envs = config_manager.get_environments()\n for env in envs:\n if env.id.lower() == normalized or env.name.lower() == normalized:\n return env.id\n return None\n\n\n# [/DEF:_resolve_env_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_env_id [C:2] [TYPE Function]\n# @BRIEF Resolve environment identifier/name token to canonical environment id.\n# @PRE: config_manager provides environment list.\n# @POST: Returns matched environment id or None.\ndef _resolve_env_id(\n token: str | None, config_manager: ConfigManager\n) -> str | None:\n if not token:\n return None\n\n normalized = token.strip().lower()\n envs = config_manager.get_environments()\n for env in envs:\n if env.id.lower() == normalized or env.name.lower() == normalized:\n return env.id\n return None\n\n\n# #endregion _resolve_env_id\n" }, { "contract_id": "_is_production_env", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 62, - "end_line": 79, + "start_line": 58, + "end_line": 74, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns True for production/prod synonyms, else False.", "PRE": "config_manager provides environments or token text is provided.", "PURPOSE": "Determine whether environment token resolves to production-like target." @@ -10842,19 +9295,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_is_production_env:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Determine whether environment token resolves to production-like target.\n# @PRE: config_manager provides environments or token text is provided.\n# @POST: Returns True for production/prod synonyms, else False.\ndef _is_production_env(token: Optional[str], config_manager: ConfigManager) -> bool:\n env_id = _resolve_env_id(token, config_manager)\n if not env_id:\n return (token or \"\").strip().lower() in {\"prod\", \"production\", \"прод\"}\n\n env = next((e for e in config_manager.get_environments() if e.id == env_id), None)\n if not env:\n return False\n target = f\"{env.id} {env.name}\".lower()\n return \"prod\" in target or \"production\" in target or \"прод\" in target\n\n\n# [/DEF:_is_production_env:Function]\n" + "anchor_syntax": "region", + "body": "# #region _is_production_env [C:2] [TYPE Function]\n# @BRIEF Determine whether environment token resolves to production-like target.\n# @PRE: config_manager provides environments or token text is provided.\n# @POST: Returns True for production/prod synonyms, else False.\ndef _is_production_env(token: str | None, config_manager: ConfigManager) -> bool:\n env_id = _resolve_env_id(token, config_manager)\n if not env_id:\n return (token or \"\").strip().lower() in {\"prod\", \"production\", \"прод\"}\n\n env = next((e for e in config_manager.get_environments() if e.id == env_id), None)\n if not env:\n return False\n target = f\"{env.id} {env.name}\".lower()\n return \"prod\" in target or \"production\" in target or \"прод\" in target\n\n\n# #endregion _is_production_env\n" }, { "contract_id": "_resolve_provider_id", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 81, - "end_line": 116, + "start_line": 76, + "end_line": 110, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns provider id or None when no providers configured.", "PRE": "db session can load provider list through LLMProviderService.", "PURPOSE": "Resolve provider token to provider id with active/default fallback." @@ -10880,19 +9333,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_provider_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve provider token to provider id with active/default fallback.\n# @PRE: db session can load provider list through LLMProviderService.\n# @POST: Returns provider id or None when no providers configured.\ndef _resolve_provider_id(\n provider_token: Optional[str],\n db: Session,\n config_manager: Optional[ConfigManager] = None,\n task_key: Optional[str] = None,\n) -> Optional[str]:\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n if not providers:\n return None\n\n if provider_token:\n needle = provider_token.strip().lower()\n for p in providers:\n if p.id.lower() == needle or p.name.lower() == needle:\n return p.id\n\n if config_manager and task_key:\n try:\n llm_settings = config_manager.get_config().settings.llm\n bound_provider_id = resolve_bound_provider_id(llm_settings, task_key)\n if bound_provider_id and any(p.id == bound_provider_id for p in providers):\n return bound_provider_id\n except Exception:\n pass\n\n active = next((p for p in providers if p.is_active), None)\n return active.id if active else providers[0].id\n\n\n# [/DEF:_resolve_provider_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_provider_id [C:2] [TYPE Function]\n# @BRIEF Resolve provider token to provider id with active/default fallback.\n# @PRE: db session can load provider list through LLMProviderService.\n# @POST: Returns provider id or None when no providers configured.\ndef _resolve_provider_id(\n provider_token: str | None,\n db: Session,\n config_manager: ConfigManager | None = None,\n task_key: str | None = None,\n) -> str | None:\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n if not providers:\n return None\n\n if provider_token:\n needle = provider_token.strip().lower()\n for p in providers:\n if p.id.lower() == needle or p.name.lower() == needle:\n return p.id\n\n if config_manager and task_key:\n try:\n llm_settings = config_manager.get_config().settings.llm\n bound_provider_id = resolve_bound_provider_id(llm_settings, task_key)\n if bound_provider_id and any(p.id == bound_provider_id for p in providers):\n return bound_provider_id\n except Exception:\n pass\n\n active = next((p for p in providers if p.is_active), None)\n return active.id if active else providers[0].id\n\n\n# #endregion _resolve_provider_id\n" }, { "contract_id": "_get_default_environment_id", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 118, - "end_line": 141, + "start_line": 112, + "end_line": 134, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns default environment id or None when environment list is empty.", "PRE": "config_manager returns environments list.", "PURPOSE": "Resolve default environment id from settings or first configured environment." @@ -10918,19 +9371,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_default_environment_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve default environment id from settings or first configured environment.\n# @PRE: config_manager returns environments list.\n# @POST: Returns default environment id or None when environment list is empty.\ndef _get_default_environment_id(config_manager: ConfigManager) -> Optional[str]:\n configured = config_manager.get_environments()\n if not configured:\n return None\n preferred = None\n if hasattr(config_manager, \"get_config\"):\n try:\n preferred = config_manager.get_config().settings.default_environment_id\n except Exception:\n preferred = None\n if preferred and any(env.id == preferred for env in configured):\n return preferred\n explicit_default = next(\n (env.id for env in configured if getattr(env, \"is_default\", False)), None\n )\n return explicit_default or configured[0].id\n\n\n# [/DEF:_get_default_environment_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_default_environment_id [C:2] [TYPE Function]\n# @BRIEF Resolve default environment id from settings or first configured environment.\n# @PRE: config_manager returns environments list.\n# @POST: Returns default environment id or None when environment list is empty.\ndef _get_default_environment_id(config_manager: ConfigManager) -> str | None:\n configured = config_manager.get_environments()\n if not configured:\n return None\n preferred = None\n if hasattr(config_manager, \"get_config\"):\n try:\n preferred = config_manager.get_config().settings.default_environment_id\n except Exception:\n preferred = None\n if preferred and any(env.id == preferred for env in configured):\n return preferred\n explicit_default = next(\n (env.id for env in configured if getattr(env, \"is_default\", False)), None\n )\n return explicit_default or configured[0].id\n\n\n# #endregion _get_default_environment_id\n" }, { "contract_id": "_resolve_dashboard_id_by_ref", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 143, - "end_line": 194, + "start_line": 136, + "end_line": 186, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns dashboard id when uniquely matched, otherwise None.", "PRE": "dashboard_ref is a non-empty string-like token.", "PURPOSE": "Resolve dashboard id by title or slug reference in selected environment." @@ -10956,19 +9409,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_dashboard_id_by_ref:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard id by title or slug reference in selected environment.\n# @PRE: dashboard_ref is a non-empty string-like token.\n# @POST: Returns dashboard id when uniquely matched, otherwise None.\ndef _resolve_dashboard_id_by_ref(\n dashboard_ref: Optional[str],\n env_id: Optional[str],\n config_manager: ConfigManager,\n) -> Optional[int]:\n if not dashboard_ref or not env_id:\n return None\n env = next(\n (item for item in config_manager.get_environments() if item.id == env_id), None\n )\n if not env:\n return None\n\n needle = dashboard_ref.strip().lower()\n try:\n client = SupersetClient(env)\n _, dashboards = client.get_dashboards(query={\"page_size\": 200})\n except Exception as exc:\n logger.warning(\n f\"[assistant.dashboard_resolve][failed] ref={dashboard_ref} env={env_id} error={exc}\"\n )\n return None\n\n exact = next(\n (\n d\n for d in dashboards\n if str(d.get(\"slug\", \"\")).lower() == needle\n or str(d.get(\"dashboard_title\", \"\")).lower() == needle\n or str(d.get(\"title\", \"\")).lower() == needle\n ),\n None,\n )\n if exact:\n return int(exact.get(\"id\"))\n\n partial = [\n d\n for d in dashboards\n if needle in str(d.get(\"dashboard_title\", d.get(\"title\", \"\"))).lower()\n ]\n if len(partial) == 1 and partial[0].get(\"id\") is not None:\n return int(partial[0][\"id\"])\n return None\n\n\n# [/DEF:_resolve_dashboard_id_by_ref:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_dashboard_id_by_ref [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard id by title or slug reference in selected environment.\n# @PRE: dashboard_ref is a non-empty string-like token.\n# @POST: Returns dashboard id when uniquely matched, otherwise None.\ndef _resolve_dashboard_id_by_ref(\n dashboard_ref: str | None,\n env_id: str | None,\n config_manager: ConfigManager,\n) -> int | None:\n if not dashboard_ref or not env_id:\n return None\n env = next(\n (item for item in config_manager.get_environments() if item.id == env_id), None\n )\n if not env:\n return None\n\n needle = dashboard_ref.strip().lower()\n try:\n client = SupersetClient(env)\n _, dashboards = client.get_dashboards(query={\"page_size\": 200})\n except Exception as exc:\n logger.warning(\n f\"[assistant.dashboard_resolve][failed] ref={dashboard_ref} env={env_id} error={exc}\"\n )\n return None\n\n exact = next(\n (\n d\n for d in dashboards\n if str(d.get(\"slug\", \"\")).lower() == needle\n or str(d.get(\"dashboard_title\", \"\")).lower() == needle\n or str(d.get(\"title\", \"\")).lower() == needle\n ),\n None,\n )\n if exact:\n return int(exact.get(\"id\"))\n\n partial = [\n d\n for d in dashboards\n if needle in str(d.get(\"dashboard_title\", d.get(\"title\", \"\"))).lower()\n ]\n if len(partial) == 1 and partial[0].get(\"id\") is not None:\n return int(partial[0][\"id\"])\n return None\n\n\n# #endregion _resolve_dashboard_id_by_ref\n" }, { "contract_id": "_resolve_dashboard_id_entity", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 196, - "end_line": 236, + "start_line": 188, + "end_line": 227, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns resolved dashboard id or None when ambiguous/unresolvable.", "PRE": "entities may contain dashboard_id as int/str and optional dashboard_ref.", "PURPOSE": "Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback." @@ -10994,19 +9447,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_dashboard_id_entity:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.\n# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref.\n# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable.\ndef _resolve_dashboard_id_entity(\n entities: Dict[str, Any],\n config_manager: ConfigManager,\n env_hint: Optional[str] = None,\n) -> Optional[int]:\n raw_dashboard_id = entities.get(\"dashboard_id\")\n dashboard_ref = entities.get(\"dashboard_ref\")\n\n if isinstance(raw_dashboard_id, int):\n return raw_dashboard_id\n\n if isinstance(raw_dashboard_id, str):\n token = raw_dashboard_id.strip()\n if token.isdigit():\n return int(token)\n if token and not dashboard_ref:\n dashboard_ref = token\n\n if not dashboard_ref:\n return None\n\n env_token = (\n env_hint\n or entities.get(\"environment\")\n or entities.get(\"source_env\")\n or entities.get(\"target_env\")\n )\n env_id = (\n _resolve_env_id(env_token, config_manager)\n if env_token\n else _get_default_environment_id(config_manager)\n )\n return _resolve_dashboard_id_by_ref(str(dashboard_ref), env_id, config_manager)\n\n\n# [/DEF:_resolve_dashboard_id_entity:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_dashboard_id_entity [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.\n# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref.\n# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable.\ndef _resolve_dashboard_id_entity(\n entities: dict[str, Any],\n config_manager: ConfigManager,\n env_hint: str | None = None,\n) -> int | None:\n raw_dashboard_id = entities.get(\"dashboard_id\")\n dashboard_ref = entities.get(\"dashboard_ref\")\n\n if isinstance(raw_dashboard_id, int):\n return raw_dashboard_id\n\n if isinstance(raw_dashboard_id, str):\n token = raw_dashboard_id.strip()\n if token.isdigit():\n return int(token)\n if token and not dashboard_ref:\n dashboard_ref = token\n\n if not dashboard_ref:\n return None\n\n env_token = (\n env_hint\n or entities.get(\"environment\")\n or entities.get(\"source_env\")\n or entities.get(\"target_env\")\n )\n env_id = (\n _resolve_env_id(env_token, config_manager)\n if env_token\n else _get_default_environment_id(config_manager)\n )\n return _resolve_dashboard_id_by_ref(str(dashboard_ref), env_id, config_manager)\n\n\n# #endregion _resolve_dashboard_id_entity\n" }, { "contract_id": "_get_environment_name_by_id", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 238, - "end_line": 254, + "start_line": 229, + "end_line": 244, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns matching environment name or fallback id.", "PRE": "environment id may be None.", "PURPOSE": "Resolve human-readable environment name by id." @@ -11032,19 +9485,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_environment_name_by_id:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve human-readable environment name by id.\n# @PRE: environment id may be None.\n# @POST: Returns matching environment name or fallback id.\ndef _get_environment_name_by_id(\n env_id: Optional[str], config_manager: ConfigManager\n) -> str:\n if not env_id:\n return \"unknown\"\n env = next(\n (item for item in config_manager.get_environments() if item.id == env_id), None\n )\n return env.name if env else env_id\n\n\n# [/DEF:_get_environment_name_by_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_environment_name_by_id [C:2] [TYPE Function]\n# @BRIEF Resolve human-readable environment name by id.\n# @PRE: environment id may be None.\n# @POST: Returns matching environment name or fallback id.\ndef _get_environment_name_by_id(\n env_id: str | None, config_manager: ConfigManager\n) -> str:\n if not env_id:\n return \"unknown\"\n env = next(\n (item for item in config_manager.get_environments() if item.id == env_id), None\n )\n return env.name if env else env_id\n\n\n# #endregion _get_environment_name_by_id\n" }, { "contract_id": "_extract_result_deep_links", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 256, - "end_line": 328, + "start_line": 246, + "end_line": 317, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns zero or more assistant actions for dashboard open/diff.", "PRE": "task object is available.", "PURPOSE": "Build deep-link actions to verify task result from assistant chat." @@ -11070,19 +9523,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_extract_result_deep_links:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build deep-link actions to verify task result from assistant chat.\n# @PRE: task object is available.\n# @POST: Returns zero or more assistant actions for dashboard open/diff.\ndef _extract_result_deep_links(\n task: Any, config_manager: ConfigManager\n) -> List:\n from ._schemas import AssistantAction\n\n plugin_id = getattr(task, \"plugin_id\", None)\n params = getattr(task, \"params\", {}) or {}\n result = getattr(task, \"result\", {}) or {}\n actions: list = []\n dashboard_id: Optional[int] = None\n env_id: Optional[str] = None\n\n if plugin_id == \"superset-migration\":\n migrated = (\n result.get(\"migrated_dashboards\") if isinstance(result, dict) else None\n )\n if isinstance(migrated, list) and migrated:\n first = migrated[0]\n if isinstance(first, dict) and first.get(\"id\") is not None:\n dashboard_id = int(first.get(\"id\"))\n if (\n dashboard_id is None\n and isinstance(params.get(\"selected_ids\"), list)\n and params[\"selected_ids\"]\n ):\n dashboard_id = int(params[\"selected_ids\"][0])\n env_id = params.get(\"target_env_id\")\n elif plugin_id == \"superset-backup\":\n dashboards = result.get(\"dashboards\") if isinstance(result, dict) else None\n if isinstance(dashboards, list) and dashboards:\n first = dashboards[0]\n if isinstance(first, dict) and first.get(\"id\") is not None:\n dashboard_id = int(first.get(\"id\"))\n if (\n dashboard_id is None\n and isinstance(params.get(\"dashboard_ids\"), list)\n and params[\"dashboard_ids\"]\n ):\n dashboard_id = int(params[\"dashboard_ids\"][0])\n env_id = params.get(\"environment_id\") or _resolve_env_id(\n result.get(\"environment\"), config_manager\n )\n elif plugin_id == \"llm_dashboard_validation\":\n if params.get(\"dashboard_id\") is not None:\n dashboard_id = int(params[\"dashboard_id\"])\n env_id = params.get(\"environment_id\")\n\n if dashboard_id is not None and env_id:\n env_name = _get_environment_name_by_id(env_id, config_manager)\n actions.append(\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборд в {env_name}\",\n target=f\"/dashboards/{dashboard_id}?env_id={env_id}\",\n )\n )\n if dashboard_id is not None:\n actions.append(\n AssistantAction(\n type=\"open_diff\",\n label=\"Показать Diff\",\n target=str(dashboard_id),\n )\n )\n return actions\n\n\n# [/DEF:_extract_result_deep_links:Function]\n" + "anchor_syntax": "region", + "body": "# #region _extract_result_deep_links [C:2] [TYPE Function]\n# @BRIEF Build deep-link actions to verify task result from assistant chat.\n# @PRE: task object is available.\n# @POST: Returns zero or more assistant actions for dashboard open/diff.\ndef _extract_result_deep_links(\n task: Any, config_manager: ConfigManager\n) -> list:\n from ._schemas import AssistantAction\n\n plugin_id = getattr(task, \"plugin_id\", None)\n params = getattr(task, \"params\", {}) or {}\n result = getattr(task, \"result\", {}) or {}\n actions: list = []\n dashboard_id: int | None = None\n env_id: str | None = None\n\n if plugin_id == \"superset-migration\":\n migrated = (\n result.get(\"migrated_dashboards\") if isinstance(result, dict) else None\n )\n if isinstance(migrated, list) and migrated:\n first = migrated[0]\n if isinstance(first, dict) and first.get(\"id\") is not None:\n dashboard_id = int(first.get(\"id\"))\n if (\n dashboard_id is None\n and isinstance(params.get(\"selected_ids\"), list)\n and params[\"selected_ids\"]\n ):\n dashboard_id = int(params[\"selected_ids\"][0])\n env_id = params.get(\"target_env_id\")\n elif plugin_id == \"superset-backup\":\n dashboards = result.get(\"dashboards\") if isinstance(result, dict) else None\n if isinstance(dashboards, list) and dashboards:\n first = dashboards[0]\n if isinstance(first, dict) and first.get(\"id\") is not None:\n dashboard_id = int(first.get(\"id\"))\n if (\n dashboard_id is None\n and isinstance(params.get(\"dashboard_ids\"), list)\n and params[\"dashboard_ids\"]\n ):\n dashboard_id = int(params[\"dashboard_ids\"][0])\n env_id = params.get(\"environment_id\") or _resolve_env_id(\n result.get(\"environment\"), config_manager\n )\n elif plugin_id == \"llm_dashboard_validation\":\n if params.get(\"dashboard_id\") is not None:\n dashboard_id = int(params[\"dashboard_id\"])\n env_id = params.get(\"environment_id\")\n\n if dashboard_id is not None and env_id:\n env_name = _get_environment_name_by_id(env_id, config_manager)\n actions.append(\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборд в {env_name}\",\n target=f\"/dashboards/{dashboard_id}?env_id={env_id}\",\n )\n )\n if dashboard_id is not None:\n actions.append(\n AssistantAction(\n type=\"open_diff\",\n label=\"Показать Diff\",\n target=str(dashboard_id),\n )\n )\n return actions\n\n\n# #endregion _extract_result_deep_links\n" }, { "contract_id": "_build_task_observability_summary", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_resolvers.py", - "start_line": 330, - "end_line": 395, + "start_line": 319, + "end_line": 383, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns non-empty summary line for known task types or empty string fallback.", "PRE": "task may contain plugin-specific result payload.", "PURPOSE": "Build compact textual summary for completed tasks to reduce \"black box\" effect." @@ -11108,19 +9561,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_build_task_observability_summary:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build compact textual summary for completed tasks to reduce \"black box\" effect.\n# @PRE: task may contain plugin-specific result payload.\n# @POST: Returns non-empty summary line for known task types or empty string fallback.\ndef _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:\n plugin_id = getattr(task, \"plugin_id\", None)\n status = str(getattr(task, \"status\", \"\")).upper()\n params = getattr(task, \"params\", {}) or {}\n result = getattr(task, \"result\", {}) or {}\n\n if plugin_id == \"superset-migration\" and isinstance(result, dict):\n migrated = len(result.get(\"migrated_dashboards\") or [])\n failed_rows = result.get(\"failed_dashboards\") or []\n failed = len(failed_rows)\n selected = result.get(\"selected_dashboards\", migrated + failed)\n mappings = result.get(\"mapping_count\", 0)\n target_env_id = params.get(\"target_env_id\")\n target_env_name = _get_environment_name_by_id(target_env_id, config_manager)\n warning = \"\"\n if failed_rows:\n first = failed_rows[0]\n warning = (\n f\" Внимание: {first.get('title') or first.get('id')}: \"\n f\"{first.get('error') or 'ошибка'}.\"\n )\n return (\n f\"Сводка миграции: выбрано {selected}, перенесено {migrated}, \"\n f\"с ошибками {failed}, маппингов {mappings}, целевая среда {target_env_name}.\"\n f\"{warning}\"\n )\n\n if plugin_id == \"superset-backup\" and isinstance(result, dict):\n total = int(result.get(\"total_dashboards\", 0) or 0)\n ok = int(result.get(\"backed_up_dashboards\", 0) or 0)\n failed = int(result.get(\"failed_dashboards\", 0) or 0)\n env_id = params.get(\"environment_id\") or _resolve_env_id(\n result.get(\"environment\"), config_manager\n )\n env_name = _get_environment_name_by_id(env_id, config_manager)\n failures = result.get(\"failures\") or []\n warning = \"\"\n if failures:\n first = failures[0]\n warning = (\n f\" Внимание: {first.get('title') or first.get('id')}: \"\n f\"{first.get('error') or 'ошибка'}.\"\n )\n return (\n f\"Сводка бэкапа: среда {env_name}, всего {total}, успешно {ok}, \"\n f\"с ошибками {failed}. {status}.{warning}\"\n )\n\n if plugin_id == \"llm_dashboard_validation\" and isinstance(result, dict):\n report_status = result.get(\"status\") or status\n report_summary = result.get(\"summary\") or \"Итог недоступен.\"\n issues = result.get(\"issues\") or []\n return f\"Сводка валидации: статус {report_status}, проблем {len(issues)}. {report_summary}\"\n\n # Fallback for unknown task payloads.\n if status in {\"SUCCESS\", \"FAILED\"}:\n return f\"Задача завершена со статусом {status}.\"\n return \"\"\n\n\n# [/DEF:_build_task_observability_summary:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_task_observability_summary [C:2] [TYPE Function]\n# @BRIEF Build compact textual summary for completed tasks to reduce \"black box\" effect.\n# @PRE: task may contain plugin-specific result payload.\n# @POST: Returns non-empty summary line for known task types or empty string fallback.\ndef _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:\n plugin_id = getattr(task, \"plugin_id\", None)\n status = str(getattr(task, \"status\", \"\")).upper()\n params = getattr(task, \"params\", {}) or {}\n result = getattr(task, \"result\", {}) or {}\n\n if plugin_id == \"superset-migration\" and isinstance(result, dict):\n migrated = len(result.get(\"migrated_dashboards\") or [])\n failed_rows = result.get(\"failed_dashboards\") or []\n failed = len(failed_rows)\n selected = result.get(\"selected_dashboards\", migrated + failed)\n mappings = result.get(\"mapping_count\", 0)\n target_env_id = params.get(\"target_env_id\")\n target_env_name = _get_environment_name_by_id(target_env_id, config_manager)\n warning = \"\"\n if failed_rows:\n first = failed_rows[0]\n warning = (\n f\" Внимание: {first.get('title') or first.get('id')}: \"\n f\"{first.get('error') or 'ошибка'}.\"\n )\n return (\n f\"Сводка миграции: выбрано {selected}, перенесено {migrated}, \"\n f\"с ошибками {failed}, маппингов {mappings}, целевая среда {target_env_name}.\"\n f\"{warning}\"\n )\n\n if plugin_id == \"superset-backup\" and isinstance(result, dict):\n total = int(result.get(\"total_dashboards\", 0) or 0)\n ok = int(result.get(\"backed_up_dashboards\", 0) or 0)\n failed = int(result.get(\"failed_dashboards\", 0) or 0)\n env_id = params.get(\"environment_id\") or _resolve_env_id(\n result.get(\"environment\"), config_manager\n )\n env_name = _get_environment_name_by_id(env_id, config_manager)\n failures = result.get(\"failures\") or []\n warning = \"\"\n if failures:\n first = failures[0]\n warning = (\n f\" Внимание: {first.get('title') or first.get('id')}: \"\n f\"{first.get('error') or 'ошибка'}.\"\n )\n return (\n f\"Сводка бэкапа: среда {env_name}, всего {total}, успешно {ok}, \"\n f\"с ошибками {failed}. {status}.{warning}\"\n )\n\n if plugin_id == \"llm_dashboard_validation\" and isinstance(result, dict):\n report_status = result.get(\"status\") or status\n report_summary = result.get(\"summary\") or \"Итог недоступен.\"\n issues = result.get(\"issues\") or []\n return f\"Сводка валидации: статус {report_status}, проблем {len(issues)}. {report_summary}\"\n\n # Fallback for unknown task payloads.\n if status in {\"SUCCESS\", \"FAILED\"}:\n return f\"Задача завершена со статусом {status}.\"\n return \"\"\n\n\n# #endregion _build_task_observability_summary\n" }, { "contract_id": "AssistantRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_routes.py", "start_line": 1, - "end_line": 338, + "end_line": 319, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "INVARIANT": "Risky operations are never executed without valid confirmation token.", "LAYER": "API", "PURPOSE": "FastAPI route handlers for the assistant API — message sending, confirmation, conversation management." @@ -11225,25 +9678,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantRoutes:Module]\n# @COMPLEXITY: 5\n# @PURPOSE: FastAPI route handlers for the assistant API — message sending, confirmation, conversation management.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [AssistantSchemas]\n# @RELATION: DEPENDS_ON -> [AssistantHistory]\n# @RELATION: DEPENDS_ON -> [AssistantCommandParser]\n# @RELATION: DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION: DEPENDS_ON -> [AssistantDatasetReview]\n# @RELATION: DEPENDS_ON -> [AssistantDispatch]\n# @RELATION: DISPATCHES -> [AssistantAdminRoutes]\n# @INVARIANT: Risky operations are never executed without valid confirmation token.\n\nfrom __future__ import annotations\n\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, Optional\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import desc\n\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.dependencies import (\n get_current_user,\n get_task_manager,\n get_config_manager,\n has_permission,\n)\nfrom src.core.config_manager import ConfigManager\nfrom src.core.database import get_db\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantMessageRecord,\n)\nfrom src.schemas.auth import User\nfrom ._schemas import (\n _SAFE_OPS,\n ASSISTANT_AUDIT,\n CONFIRMATIONS,\n CONVERSATIONS,\n USER_ACTIVE_CONVERSATION,\n AssistantAction,\n AssistantMessageRequest,\n AssistantMessageResponse,\n ConfirmationRecord,\n)\nfrom ._history import (\n _append_history,\n _audit,\n _cleanup_history_ttl,\n _coerce_query_bool,\n _is_conversation_archived,\n _load_confirmation_from_db,\n _persist_audit,\n _persist_confirmation,\n _persist_message,\n _resolve_or_create_conversation,\n _update_confirmation_state,\n)\nfrom ._command_parser import _parse_command\nfrom ._llm_planner import _build_tool_catalog\nfrom ._llm_planner_intent import _authorize_intent, _plan_intent_with_llm\nfrom ._dataset_review import (\n _load_dataset_review_context,\n _plan_dataset_review_intent,\n)\nfrom ._dispatch import (\n _async_confirmation_summary,\n _clarification_text_for_intent,\n _dispatch_intent,\n)\n\nrouter = APIRouter(tags=[\"Assistant\"])\n\n\n@router.post(\"/messages\", response_model=AssistantMessageResponse)\n# [DEF:send_message:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Parse assistant command, enforce safety gates, and dispatch executable intent.\n# @DATA_CONTRACT: Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]\n# @RELATION: DEPENDS_ON -> [_plan_intent_with_llm]\n# @RELATION: DEPENDS_ON -> [_parse_command]\n# @RELATION: DEPENDS_ON -> [_dispatch_intent]\n# @RELATION: DEPENDS_ON -> [_append_history]\n# @RELATION: DEPENDS_ON -> [_persist_message]\n# @RELATION: DEPENDS_ON -> [_audit]\n# @SIDE_EFFECT: Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.\n# @PRE: Authenticated user is available and message text is non-empty.\n# @POST: Response state is one of clarification/confirmation/started/success/denied/failed.\n# @RETURN: AssistantMessageResponse with operation feedback and optional actions.\n# @INVARIANT: non-safe operations are gated with confirmation before execution from this endpoint.\nasync def send_message(request: AssistantMessageRequest, current_user: User=Depends(get_current_user), task_manager: TaskManager=Depends(get_task_manager), config_manager: ConfigManager=Depends(get_config_manager), db: Session=Depends(get_db)):\n with belief_scope('send_message'):\n logger.reason('Belief protocol reasoning checkpoint for send_message')\n user_id = current_user.id\n dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)\n conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)\n _append_history(user_id, conversation_id, 'user', request.message)\n _persist_message(db, user_id, conversation_id, 'user', request.message)\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n intent = None\n try:\n intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)\n except Exception as exc:\n logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')\n if not intent:\n intent = _parse_command(request.message, config_manager)\n if dataset_review_context:\n dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)\n if dataset_review_intent is not None:\n intent = dataset_review_intent\n confidence = float(intent.get('confidence', 0.0))\n if intent.get('domain') == 'unknown' or confidence < 0.6:\n text = 'Команда неоднозначна. Уточните действие: git / migration / backup / llm / status.'\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})\n audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.utcnow())\n try:\n _authorize_intent(intent, current_user)\n operation = intent.get('operation')\n if operation not in _SAFE_OPS:\n confirmation_id = str(uuid.uuid4())\n confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.utcnow() + __import__('datetime').timedelta(minutes=5), created_at=datetime.utcnow())\n CONFIRMATIONS[confirmation_id] = confirm\n _persist_confirmation(db, confirm)\n text = await _async_confirmation_summary(intent, config_manager, db)\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})\n audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.utcnow())\n text, task_id, actions = await _dispatch_intent(intent, current_user, task_manager, config_manager, db)\n state = 'started' if task_id else 'success'\n _append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})\n audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.utcnow())\n except HTTPException as exc:\n detail_text = str(exc.detail)\n is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())\n if exc.status_code == status.HTTP_403_FORBIDDEN:\n state = 'denied'\n elif is_clarification_error:\n state = 'needs_clarification'\n else:\n state = 'failed'\n text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text\n _append_history(user_id, conversation_id, 'assistant', text, state=state)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})\n audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')] if state == 'needs_clarification' else [], created_at=datetime.utcnow())\n\n\n# [/DEF:send_message:Function]\n\n\n@router.post(\n \"/confirmations/{confirmation_id}/confirm\", response_model=AssistantMessageResponse\n)\n# [DEF:confirm_operation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Execute previously requested risky operation after explicit user confirmation.\n# @PRE: confirmation_id exists, belongs to current user, is pending, and not expired.\n# @POST: Confirmation state becomes consumed and operation result is persisted in history.\n# @RETURN: AssistantMessageResponse with task details when async execution starts.\nasync def confirm_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n task_manager: TaskManager = Depends(get_task_manager),\n config_manager: ConfigManager = Depends(get_config_manager),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.confirm\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n if datetime.utcnow() > record.expires_at:\n record.state = \"expired\"\n _update_confirmation_state(db, confirmation_id, \"expired\")\n raise HTTPException(status_code=400, detail=\"Confirmation expired\")\n\n intent = record.intent\n text, task_id, actions = await _dispatch_intent(\n intent, current_user, task_manager, config_manager, db\n )\n record.state = \"consumed\"\n _update_confirmation_state(db, confirmation_id, \"consumed\")\n\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n metadata={\"intent\": intent, \"confirmation_id\": confirmation_id},\n )\n audit_payload = {\n \"decision\": \"confirmed_execute\",\n \"confirmation_id\": confirmation_id,\n \"task_id\": task_id,\n \"intent\": intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"started\" if task_id else \"success\",\n text=text,\n intent=intent,\n task_id=task_id,\n actions=actions,\n created_at=datetime.utcnow(),\n )\n\n\n# [/DEF:confirm_operation:Function]\n\n\n@router.post(\n \"/confirmations/{confirmation_id}/cancel\", response_model=AssistantMessageResponse\n)\n# [DEF:cancel_operation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Cancel pending risky operation and mark confirmation token as cancelled.\n# @PRE: confirmation_id exists, belongs to current user, and is still pending.\n# @POST: Confirmation becomes cancelled and cannot be executed anymore.\n# @RETURN: AssistantMessageResponse confirming cancellation.\nasync def cancel_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.cancel\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n record.state = \"cancelled\"\n _update_confirmation_state(db, confirmation_id, \"cancelled\")\n text = \"Операция отменена. Выполнение не запускалось.\"\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n metadata={\"intent\": record.intent},\n )\n audit_payload = {\n \"decision\": \"cancelled\",\n \"confirmation_id\": confirmation_id,\n \"intent\": record.intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"success\",\n text=text,\n intent=record.intent,\n confirmation_id=confirmation_id,\n actions=[],\n created_at=datetime.utcnow(),\n )\n\n\n# [/DEF:cancel_operation:Function]\n\n\n# [/DEF:AssistantRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantRoutes [C:5] [TYPE Module] [SEMANTICS assistant, api, route, chat, execution]\n# @BRIEF FastAPI route handlers for the assistant API — message sending, confirmation, conversation management.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantHistory]\n# @RELATION DEPENDS_ON -> [AssistantCommandParser]\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantDatasetReview]\n# @RELATION DEPENDS_ON -> [AssistantDispatch]\n# @RELATION DISPATCHES -> [AssistantAdminRoutes]\n# @INVARIANT: Risky operations are never executed without valid confirmation token.\n\nfrom __future__ import annotations\n\nimport uuid\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_task_manager,\n)\nfrom src.schemas.auth import User\n\nfrom ._command_parser import _parse_command\nfrom ._dataset_review import (\n _load_dataset_review_context,\n _plan_dataset_review_intent,\n)\nfrom ._dispatch import (\n _async_confirmation_summary,\n _clarification_text_for_intent,\n _dispatch_intent,\n)\nfrom ._history import (\n _append_history,\n _audit,\n _load_confirmation_from_db,\n _persist_audit,\n _persist_confirmation,\n _persist_message,\n _resolve_or_create_conversation,\n _update_confirmation_state,\n)\nfrom ._llm_planner import _build_tool_catalog\nfrom ._llm_planner_intent import _authorize_intent, _plan_intent_with_llm\nfrom ._schemas import (\n _SAFE_OPS,\n CONFIRMATIONS,\n AssistantAction,\n AssistantMessageRequest,\n AssistantMessageResponse,\n ConfirmationRecord,\n)\n\nrouter = APIRouter(tags=[\"Assistant\"])\n\n\n@router.post(\"/messages\", response_model=AssistantMessageResponse)\n# #region send_message [C:5] [TYPE Function]\n# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.\n# @DATA_CONTRACT: Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]\n# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]\n# @RELATION DEPENDS_ON -> [_parse_command]\n# @RELATION DEPENDS_ON -> [_dispatch_intent]\n# @RELATION DEPENDS_ON -> [_append_history]\n# @RELATION DEPENDS_ON -> [_persist_message]\n# @RELATION DEPENDS_ON -> [_audit]\n# @SIDE_EFFECT: Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.\n# @PRE: Authenticated user is available and message text is non-empty.\n# @POST: Response state is one of clarification/confirmation/started/success/denied/failed.\n# @INVARIANT: non-safe operations are gated with confirmation before execution from this endpoint.\nasync def send_message(request: AssistantMessageRequest, current_user: User=Depends(get_current_user), task_manager: TaskManager=Depends(get_task_manager), config_manager: ConfigManager=Depends(get_config_manager), db: Session=Depends(get_db)):\n with belief_scope('send_message'):\n logger.reason('Belief protocol reasoning checkpoint for send_message')\n user_id = current_user.id\n dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)\n conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)\n _append_history(user_id, conversation_id, 'user', request.message)\n _persist_message(db, user_id, conversation_id, 'user', request.message)\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n intent = None\n try:\n intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)\n except Exception as exc:\n logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')\n if not intent:\n intent = _parse_command(request.message, config_manager)\n if dataset_review_context:\n dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)\n if dataset_review_intent is not None:\n intent = dataset_review_intent\n confidence = float(intent.get('confidence', 0.0))\n if intent.get('domain') == 'unknown' or confidence < 0.6:\n text = 'Команда неоднозначна. Уточните действие: git / migration / backup / llm / status.'\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})\n audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.utcnow())\n try:\n _authorize_intent(intent, current_user)\n operation = intent.get('operation')\n if operation not in _SAFE_OPS:\n confirmation_id = str(uuid.uuid4())\n confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.utcnow() + __import__('datetime').timedelta(minutes=5), created_at=datetime.utcnow())\n CONFIRMATIONS[confirmation_id] = confirm\n _persist_confirmation(db, confirm)\n text = await _async_confirmation_summary(intent, config_manager, db)\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})\n audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.utcnow())\n text, task_id, actions = await _dispatch_intent(intent, current_user, task_manager, config_manager, db)\n state = 'started' if task_id else 'success'\n _append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})\n audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.utcnow())\n except HTTPException as exc:\n detail_text = str(exc.detail)\n is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())\n if exc.status_code == status.HTTP_403_FORBIDDEN:\n state = 'denied'\n elif is_clarification_error:\n state = 'needs_clarification'\n else:\n state = 'failed'\n text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text\n _append_history(user_id, conversation_id, 'assistant', text, state=state)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})\n audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')] if state == 'needs_clarification' else [], created_at=datetime.utcnow())\n\n\n# #endregion send_message\n\n\n@router.post(\n \"/confirmations/{confirmation_id}/confirm\", response_model=AssistantMessageResponse\n)\n# #region confirm_operation [C:2] [TYPE Function]\n# @BRIEF Execute previously requested risky operation after explicit user confirmation.\n# @PRE: confirmation_id exists, belongs to current user, is pending, and not expired.\n# @POST: Confirmation state becomes consumed and operation result is persisted in history.\nasync def confirm_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n task_manager: TaskManager = Depends(get_task_manager),\n config_manager: ConfigManager = Depends(get_config_manager),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.confirm\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n if datetime.utcnow() > record.expires_at:\n record.state = \"expired\"\n _update_confirmation_state(db, confirmation_id, \"expired\")\n raise HTTPException(status_code=400, detail=\"Confirmation expired\")\n\n intent = record.intent\n text, task_id, actions = await _dispatch_intent(\n intent, current_user, task_manager, config_manager, db\n )\n record.state = \"consumed\"\n _update_confirmation_state(db, confirmation_id, \"consumed\")\n\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n metadata={\"intent\": intent, \"confirmation_id\": confirmation_id},\n )\n audit_payload = {\n \"decision\": \"confirmed_execute\",\n \"confirmation_id\": confirmation_id,\n \"task_id\": task_id,\n \"intent\": intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"started\" if task_id else \"success\",\n text=text,\n intent=intent,\n task_id=task_id,\n actions=actions,\n created_at=datetime.utcnow(),\n )\n\n\n# #endregion confirm_operation\n\n\n@router.post(\n \"/confirmations/{confirmation_id}/cancel\", response_model=AssistantMessageResponse\n)\n# #region cancel_operation [C:2] [TYPE Function]\n# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.\n# @PRE: confirmation_id exists, belongs to current user, and is still pending.\n# @POST: Confirmation becomes cancelled and cannot be executed anymore.\nasync def cancel_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.cancel\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n record.state = \"cancelled\"\n _update_confirmation_state(db, confirmation_id, \"cancelled\")\n text = \"Операция отменена. Выполнение не запускалось.\"\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n metadata={\"intent\": record.intent},\n )\n audit_payload = {\n \"decision\": \"cancelled\",\n \"confirmation_id\": confirmation_id,\n \"intent\": record.intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"success\",\n text=text,\n intent=record.intent,\n confirmation_id=confirmation_id,\n actions=[],\n created_at=datetime.utcnow(),\n )\n\n\n# #endregion cancel_operation\n\n\n# #endregion AssistantRoutes\n" }, { "contract_id": "send_message", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_routes.py", - "start_line": 80, - "end_line": 169, + "start_line": 67, + "end_line": 154, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]", "INVARIANT": "non-safe operations are gated with confirmation before execution from this endpoint.", "POST": "Response state is one of clarification/confirmation/started/success/denied/failed.", "PRE": "Authenticated user is available and message text is non-empty.", "PURPOSE": "Parse assistant command, enforce safety gates, and dispatch executable intent.", - "RETURN": "AssistantMessageResponse with operation feedback and optional actions.", "SIDE_EFFECT": "Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records." }, "relations": [ @@ -11285,12 +9737,6 @@ } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "missing_required_tag", "tag": "RATIONALE", @@ -11310,23 +9756,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:send_message:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Parse assistant command, enforce safety gates, and dispatch executable intent.\n# @DATA_CONTRACT: Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]\n# @RELATION: DEPENDS_ON -> [_plan_intent_with_llm]\n# @RELATION: DEPENDS_ON -> [_parse_command]\n# @RELATION: DEPENDS_ON -> [_dispatch_intent]\n# @RELATION: DEPENDS_ON -> [_append_history]\n# @RELATION: DEPENDS_ON -> [_persist_message]\n# @RELATION: DEPENDS_ON -> [_audit]\n# @SIDE_EFFECT: Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.\n# @PRE: Authenticated user is available and message text is non-empty.\n# @POST: Response state is one of clarification/confirmation/started/success/denied/failed.\n# @RETURN: AssistantMessageResponse with operation feedback and optional actions.\n# @INVARIANT: non-safe operations are gated with confirmation before execution from this endpoint.\nasync def send_message(request: AssistantMessageRequest, current_user: User=Depends(get_current_user), task_manager: TaskManager=Depends(get_task_manager), config_manager: ConfigManager=Depends(get_config_manager), db: Session=Depends(get_db)):\n with belief_scope('send_message'):\n logger.reason('Belief protocol reasoning checkpoint for send_message')\n user_id = current_user.id\n dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)\n conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)\n _append_history(user_id, conversation_id, 'user', request.message)\n _persist_message(db, user_id, conversation_id, 'user', request.message)\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n intent = None\n try:\n intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)\n except Exception as exc:\n logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')\n if not intent:\n intent = _parse_command(request.message, config_manager)\n if dataset_review_context:\n dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)\n if dataset_review_intent is not None:\n intent = dataset_review_intent\n confidence = float(intent.get('confidence', 0.0))\n if intent.get('domain') == 'unknown' or confidence < 0.6:\n text = 'Команда неоднозначна. Уточните действие: git / migration / backup / llm / status.'\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})\n audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.utcnow())\n try:\n _authorize_intent(intent, current_user)\n operation = intent.get('operation')\n if operation not in _SAFE_OPS:\n confirmation_id = str(uuid.uuid4())\n confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.utcnow() + __import__('datetime').timedelta(minutes=5), created_at=datetime.utcnow())\n CONFIRMATIONS[confirmation_id] = confirm\n _persist_confirmation(db, confirm)\n text = await _async_confirmation_summary(intent, config_manager, db)\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})\n audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.utcnow())\n text, task_id, actions = await _dispatch_intent(intent, current_user, task_manager, config_manager, db)\n state = 'started' if task_id else 'success'\n _append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})\n audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.utcnow())\n except HTTPException as exc:\n detail_text = str(exc.detail)\n is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())\n if exc.status_code == status.HTTP_403_FORBIDDEN:\n state = 'denied'\n elif is_clarification_error:\n state = 'needs_clarification'\n else:\n state = 'failed'\n text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text\n _append_history(user_id, conversation_id, 'assistant', text, state=state)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})\n audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')] if state == 'needs_clarification' else [], created_at=datetime.utcnow())\n\n\n# [/DEF:send_message:Function]\n" + "anchor_syntax": "region", + "body": "# #region send_message [C:5] [TYPE Function]\n# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.\n# @DATA_CONTRACT: Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]\n# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]\n# @RELATION DEPENDS_ON -> [_parse_command]\n# @RELATION DEPENDS_ON -> [_dispatch_intent]\n# @RELATION DEPENDS_ON -> [_append_history]\n# @RELATION DEPENDS_ON -> [_persist_message]\n# @RELATION DEPENDS_ON -> [_audit]\n# @SIDE_EFFECT: Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.\n# @PRE: Authenticated user is available and message text is non-empty.\n# @POST: Response state is one of clarification/confirmation/started/success/denied/failed.\n# @INVARIANT: non-safe operations are gated with confirmation before execution from this endpoint.\nasync def send_message(request: AssistantMessageRequest, current_user: User=Depends(get_current_user), task_manager: TaskManager=Depends(get_task_manager), config_manager: ConfigManager=Depends(get_config_manager), db: Session=Depends(get_db)):\n with belief_scope('send_message'):\n logger.reason('Belief protocol reasoning checkpoint for send_message')\n user_id = current_user.id\n dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)\n conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)\n _append_history(user_id, conversation_id, 'user', request.message)\n _persist_message(db, user_id, conversation_id, 'user', request.message)\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n intent = None\n try:\n intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)\n except Exception as exc:\n logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')\n if not intent:\n intent = _parse_command(request.message, config_manager)\n if dataset_review_context:\n dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)\n if dataset_review_intent is not None:\n intent = dataset_review_intent\n confidence = float(intent.get('confidence', 0.0))\n if intent.get('domain') == 'unknown' or confidence < 0.6:\n text = 'Команда неоднозначна. Уточните действие: git / migration / backup / llm / status.'\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})\n audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.utcnow())\n try:\n _authorize_intent(intent, current_user)\n operation = intent.get('operation')\n if operation not in _SAFE_OPS:\n confirmation_id = str(uuid.uuid4())\n confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.utcnow() + __import__('datetime').timedelta(minutes=5), created_at=datetime.utcnow())\n CONFIRMATIONS[confirmation_id] = confirm\n _persist_confirmation(db, confirm)\n text = await _async_confirmation_summary(intent, config_manager, db)\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})\n audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.utcnow())\n text, task_id, actions = await _dispatch_intent(intent, current_user, task_manager, config_manager, db)\n state = 'started' if task_id else 'success'\n _append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})\n audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.utcnow())\n except HTTPException as exc:\n detail_text = str(exc.detail)\n is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())\n if exc.status_code == status.HTTP_403_FORBIDDEN:\n state = 'denied'\n elif is_clarification_error:\n state = 'needs_clarification'\n else:\n state = 'failed'\n text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text\n _append_history(user_id, conversation_id, 'assistant', text, state=state)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})\n audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')] if state == 'needs_clarification' else [], created_at=datetime.utcnow())\n\n\n# #endregion send_message\n" }, { "contract_id": "confirm_operation", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_routes.py", - "start_line": 175, - "end_line": 258, + "start_line": 160, + "end_line": 241, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Confirmation state becomes consumed and operation result is persisted in history.", "PRE": "confirmation_id exists, belongs to current user, is pending, and not expired.", - "PURPOSE": "Execute previously requested risky operation after explicit user confirmation.", - "RETURN": "AssistantMessageResponse with task details when async execution starts." + "PURPOSE": "Execute previously requested risky operation after explicit user confirmation." }, "relations": [], "schema_warnings": [ @@ -11347,31 +9792,24 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:confirm_operation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Execute previously requested risky operation after explicit user confirmation.\n# @PRE: confirmation_id exists, belongs to current user, is pending, and not expired.\n# @POST: Confirmation state becomes consumed and operation result is persisted in history.\n# @RETURN: AssistantMessageResponse with task details when async execution starts.\nasync def confirm_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n task_manager: TaskManager = Depends(get_task_manager),\n config_manager: ConfigManager = Depends(get_config_manager),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.confirm\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n if datetime.utcnow() > record.expires_at:\n record.state = \"expired\"\n _update_confirmation_state(db, confirmation_id, \"expired\")\n raise HTTPException(status_code=400, detail=\"Confirmation expired\")\n\n intent = record.intent\n text, task_id, actions = await _dispatch_intent(\n intent, current_user, task_manager, config_manager, db\n )\n record.state = \"consumed\"\n _update_confirmation_state(db, confirmation_id, \"consumed\")\n\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n metadata={\"intent\": intent, \"confirmation_id\": confirmation_id},\n )\n audit_payload = {\n \"decision\": \"confirmed_execute\",\n \"confirmation_id\": confirmation_id,\n \"task_id\": task_id,\n \"intent\": intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"started\" if task_id else \"success\",\n text=text,\n intent=intent,\n task_id=task_id,\n actions=actions,\n created_at=datetime.utcnow(),\n )\n\n\n# [/DEF:confirm_operation:Function]\n" + "anchor_syntax": "region", + "body": "# #region confirm_operation [C:2] [TYPE Function]\n# @BRIEF Execute previously requested risky operation after explicit user confirmation.\n# @PRE: confirmation_id exists, belongs to current user, is pending, and not expired.\n# @POST: Confirmation state becomes consumed and operation result is persisted in history.\nasync def confirm_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n task_manager: TaskManager = Depends(get_task_manager),\n config_manager: ConfigManager = Depends(get_config_manager),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.confirm\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n if datetime.utcnow() > record.expires_at:\n record.state = \"expired\"\n _update_confirmation_state(db, confirmation_id, \"expired\")\n raise HTTPException(status_code=400, detail=\"Confirmation expired\")\n\n intent = record.intent\n text, task_id, actions = await _dispatch_intent(\n intent, current_user, task_manager, config_manager, db\n )\n record.state = \"consumed\"\n _update_confirmation_state(db, confirmation_id, \"consumed\")\n\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n metadata={\"intent\": intent, \"confirmation_id\": confirmation_id},\n )\n audit_payload = {\n \"decision\": \"confirmed_execute\",\n \"confirmation_id\": confirmation_id,\n \"task_id\": task_id,\n \"intent\": intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"started\" if task_id else \"success\",\n text=text,\n intent=intent,\n task_id=task_id,\n actions=actions,\n created_at=datetime.utcnow(),\n )\n\n\n# #endregion confirm_operation\n" }, { "contract_id": "cancel_operation", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_routes.py", - "start_line": 264, - "end_line": 335, + "start_line": 247, + "end_line": 316, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Confirmation becomes cancelled and cannot be executed anymore.", "PRE": "confirmation_id exists, belongs to current user, and is still pending.", - "PURPOSE": "Cancel pending risky operation and mark confirmation token as cancelled.", - "RETURN": "AssistantMessageResponse confirming cancellation." + "PURPOSE": "Cancel pending risky operation and mark confirmation token as cancelled." }, "relations": [], "schema_warnings": [ @@ -11392,27 +9830,21 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:cancel_operation:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Cancel pending risky operation and mark confirmation token as cancelled.\n# @PRE: confirmation_id exists, belongs to current user, and is still pending.\n# @POST: Confirmation becomes cancelled and cannot be executed anymore.\n# @RETURN: AssistantMessageResponse confirming cancellation.\nasync def cancel_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.cancel\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n record.state = \"cancelled\"\n _update_confirmation_state(db, confirmation_id, \"cancelled\")\n text = \"Операция отменена. Выполнение не запускалось.\"\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n metadata={\"intent\": record.intent},\n )\n audit_payload = {\n \"decision\": \"cancelled\",\n \"confirmation_id\": confirmation_id,\n \"intent\": record.intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"success\",\n text=text,\n intent=record.intent,\n confirmation_id=confirmation_id,\n actions=[],\n created_at=datetime.utcnow(),\n )\n\n\n# [/DEF:cancel_operation:Function]\n" + "anchor_syntax": "region", + "body": "# #region cancel_operation [C:2] [TYPE Function]\n# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.\n# @PRE: confirmation_id exists, belongs to current user, and is still pending.\n# @POST: Confirmation becomes cancelled and cannot be executed anymore.\nasync def cancel_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.cancel\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n record.state = \"cancelled\"\n _update_confirmation_state(db, confirmation_id, \"cancelled\")\n text = \"Операция отменена. Выполнение не запускалось.\"\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n metadata={\"intent\": record.intent},\n )\n audit_payload = {\n \"decision\": \"cancelled\",\n \"confirmation_id\": confirmation_id,\n \"intent\": record.intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"success\",\n text=text,\n intent=record.intent,\n confirmation_id=confirmation_id,\n actions=[],\n created_at=datetime.utcnow(),\n )\n\n\n# #endregion cancel_operation\n" }, { "contract_id": "AssistantSchemas", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_schemas.py", "start_line": 1, - "end_line": 149, + "end_line": 142, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "In-memory stores are module-level singletons shared across the assistant package.", "LAYER": "API", "PURPOSE": "Pydantic models, in-memory stores, and permission mappings for the assistant API." @@ -11487,19 +9919,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantSchemas:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Pydantic models, in-memory stores, and permission mappings for the assistant API.\n# @LAYER: API\n# @RELATION: USED_BY -> [AssistantRoutes]\n# @RELATION: USED_BY -> [AssistantHistory]\n# @INVARIANT: In-memory stores are module-level singletons shared across the assistant package.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom pydantic import BaseModel, Field\n\nfrom src.schemas.auth import User\n\n\n# [DEF:AssistantMessageRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Input payload for assistant message endpoint.\n# @DATA_CONTRACT: Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]\n# @RELATION: USED_BY -> [send_message]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: message length is within accepted bounds.\n# @POST: Request object provides message text and optional conversation binding.\n# @INVARIANT: message is always non-empty and no longer than 4000 characters.\nclass AssistantMessageRequest(BaseModel):\n conversation_id: Optional[str] = None\n message: str = Field(..., min_length=1, max_length=4000)\n dataset_review_session_id: Optional[str] = None\n\n\n# [/DEF:AssistantMessageRequest:Class]\n\n\n# [DEF:AssistantAction:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: UI action descriptor returned with assistant responses.\n# @DATA_CONTRACT: Input[type:str, label:str, target?:str] -> Output[AssistantAction]\n# @RELATION: USED_BY -> [AssistantMessageResponse]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: type and label are provided by orchestration logic.\n# @POST: Action can be rendered as button on frontend.\n# @INVARIANT: type and label are required for every UI action.\nclass AssistantAction(BaseModel):\n type: str\n label: str\n target: Optional[str] = None\n\n\n# [/DEF:AssistantAction:Class]\n\n\n# [DEF:AssistantMessageResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Output payload contract for assistant interaction endpoints.\n# @DATA_CONTRACT: Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]\n# @RELATION: RETURNED_BY -> [send_message]\n# @RELATION: RETURNED_BY -> [confirm_operation]\n# @RELATION: RETURNED_BY -> [cancel_operation]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: Response includes deterministic state and text.\n# @POST: Payload may include task_id/confirmation_id/actions for UI follow-up.\n# @INVARIANT: created_at and state are always present in endpoint responses.\nclass AssistantMessageResponse(BaseModel):\n conversation_id: str\n response_id: str\n state: str\n text: str\n intent: Optional[Dict[str, Any]] = None\n confirmation_id: Optional[str] = None\n task_id: Optional[str] = None\n actions: List[AssistantAction] = Field(default_factory=list)\n created_at: datetime\n\n\n# [/DEF:AssistantMessageResponse:Class]\n\n\n# [DEF:ConfirmationRecord:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: In-memory confirmation token model for risky operation dispatch.\n# @DATA_CONTRACT: Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]\n# @RELATION: USED_BY -> [send_message]\n# @RELATION: USED_BY -> [confirm_operation]\n# @RELATION: USED_BY -> [cancel_operation]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: intent/dispatch/user_id are populated at confirmation request time.\n# @POST: Record tracks lifecycle state and expiry timestamp.\n# @INVARIANT: state defaults to \"pending\" and expires_at bounds confirmation validity.\nclass ConfirmationRecord(BaseModel):\n id: str\n user_id: str\n conversation_id: str\n intent: Dict[str, Any]\n dispatch: Dict[str, Any]\n expires_at: datetime\n state: str = \"pending\"\n created_at: datetime\n\n\n# [/DEF:ConfirmationRecord:Class]\n\n\n# --- In-memory stores ---\n\nCONVERSATIONS: Dict[Tuple[str, str], List[Dict[str, Any]]] = {}\nUSER_ACTIVE_CONVERSATION: Dict[str, str] = {}\nCONFIRMATIONS: Dict[str, ConfirmationRecord] = {}\nASSISTANT_AUDIT: Dict[str, List[Dict[str, Any]]] = {}\nASSISTANT_ARCHIVE_AFTER_DAYS = 14\nASSISTANT_MESSAGE_TTL_DAYS = 90\n\n# Operations that are read-only and do not require confirmation.\n_SAFE_OPS = {\n \"show_capabilities\",\n \"get_task_status\",\n \"get_health_summary\",\n \"dataset_review_answer_context\",\n}\n\n_DATASET_REVIEW_OPS = {\n \"dataset_review_approve_mappings\",\n \"dataset_review_set_field_semantics\",\n \"dataset_review_generate_sql_preview\",\n}\n\nINTENT_PERMISSION_CHECKS: Dict[str, List[Tuple[str, str]]] = {\n \"get_task_status\": [(\"tasks\", \"READ\")],\n \"create_branch\": [(\"plugin:git\", \"EXECUTE\")],\n \"commit_changes\": [(\"plugin:git\", \"EXECUTE\")],\n \"deploy_dashboard\": [(\"plugin:git\", \"EXECUTE\")],\n \"execute_migration\": [\n (\"plugin:migration\", \"EXECUTE\"),\n (\"plugin:superset-migration\", \"EXECUTE\"),\n ],\n \"run_backup\": [(\"plugin:superset-backup\", \"EXECUTE\"), (\"plugin:backup\", \"EXECUTE\")],\n \"run_llm_validation\": [(\"plugin:llm_dashboard_validation\", \"EXECUTE\")],\n \"run_llm_documentation\": [(\"plugin:llm_documentation\", \"EXECUTE\")],\n \"get_health_summary\": [(\"plugin:migration\", \"READ\")],\n \"dataset_review_answer_context\": [(\"dataset:session\", \"READ\")],\n \"dataset_review_approve_mappings\": [(\"dataset:session\", \"MANAGE\")],\n \"dataset_review_set_field_semantics\": [(\"dataset:session\", \"MANAGE\")],\n \"dataset_review_generate_sql_preview\": [(\"dataset:session\", \"MANAGE\")],\n}\n\n\n# [/DEF:AssistantSchemas:Module]\n" + "anchor_syntax": "region", + "body": "# #region AssistantSchemas [C:2] [TYPE Module] [SEMANTICS assistant, pydantic, schema, store, permission]\n# @BRIEF Pydantic models, in-memory stores, and permission mappings for the assistant API.\n# @LAYER: API\n# @RELATION USED_BY -> [AssistantRoutes]\n# @RELATION USED_BY -> [AssistantHistory]\n# @INVARIANT: In-memory stores are module-level singletons shared across the assistant package.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region AssistantMessageRequest [C:1] [TYPE Class]\n# @BRIEF Input payload for assistant message endpoint.\n# @DATA_CONTRACT: Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]\n# @RELATION USED_BY -> [send_message]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: message length is within accepted bounds.\n# @POST: Request object provides message text and optional conversation binding.\n# @INVARIANT: message is always non-empty and no longer than 4000 characters.\nclass AssistantMessageRequest(BaseModel):\n conversation_id: str | None = None\n message: str = Field(..., min_length=1, max_length=4000)\n dataset_review_session_id: str | None = None\n\n\n# #endregion AssistantMessageRequest\n\n\n# #region AssistantAction [C:1] [TYPE Class]\n# @BRIEF UI action descriptor returned with assistant responses.\n# @DATA_CONTRACT: Input[type:str, label:str, target?:str] -> Output[AssistantAction]\n# @RELATION USED_BY -> [AssistantMessageResponse]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: type and label are provided by orchestration logic.\n# @POST: Action can be rendered as button on frontend.\n# @INVARIANT: type and label are required for every UI action.\nclass AssistantAction(BaseModel):\n type: str\n label: str\n target: str | None = None\n\n\n# #endregion AssistantAction\n\n\n# #region AssistantMessageResponse [C:1] [TYPE Class]\n# @BRIEF Output payload contract for assistant interaction endpoints.\n# @DATA_CONTRACT: Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]\n# @RELATION RETURNED_BY -> [send_message]\n# @RELATION RETURNED_BY -> [confirm_operation]\n# @RELATION RETURNED_BY -> [cancel_operation]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: Response includes deterministic state and text.\n# @POST: Payload may include task_id/confirmation_id/actions for UI follow-up.\n# @INVARIANT: created_at and state are always present in endpoint responses.\nclass AssistantMessageResponse(BaseModel):\n conversation_id: str\n response_id: str\n state: str\n text: str\n intent: dict[str, Any] | None = None\n confirmation_id: str | None = None\n task_id: str | None = None\n actions: list[AssistantAction] = Field(default_factory=list)\n created_at: datetime\n\n\n# #endregion AssistantMessageResponse\n\n\n# #region ConfirmationRecord [C:1] [TYPE Class]\n# @BRIEF In-memory confirmation token model for risky operation dispatch.\n# @DATA_CONTRACT: Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]\n# @RELATION USED_BY -> [send_message]\n# @RELATION USED_BY -> [confirm_operation]\n# @RELATION USED_BY -> [cancel_operation]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: intent/dispatch/user_id are populated at confirmation request time.\n# @POST: Record tracks lifecycle state and expiry timestamp.\n# @INVARIANT: state defaults to \"pending\" and expires_at bounds confirmation validity.\nclass ConfirmationRecord(BaseModel):\n id: str\n user_id: str\n conversation_id: str\n intent: dict[str, Any]\n dispatch: dict[str, Any]\n expires_at: datetime\n state: str = \"pending\"\n created_at: datetime\n\n\n# #endregion ConfirmationRecord\n\n\n# --- In-memory stores ---\n\nCONVERSATIONS: dict[tuple[str, str], list[dict[str, Any]]] = {}\nUSER_ACTIVE_CONVERSATION: dict[str, str] = {}\nCONFIRMATIONS: dict[str, ConfirmationRecord] = {}\nASSISTANT_AUDIT: dict[str, list[dict[str, Any]]] = {}\nASSISTANT_ARCHIVE_AFTER_DAYS = 14\nASSISTANT_MESSAGE_TTL_DAYS = 90\n\n# Operations that are read-only and do not require confirmation.\n_SAFE_OPS = {\n \"show_capabilities\",\n \"get_task_status\",\n \"get_health_summary\",\n \"dataset_review_answer_context\",\n}\n\n_DATASET_REVIEW_OPS = {\n \"dataset_review_approve_mappings\",\n \"dataset_review_set_field_semantics\",\n \"dataset_review_generate_sql_preview\",\n}\n\nINTENT_PERMISSION_CHECKS: dict[str, list[tuple[str, str]]] = {\n \"get_task_status\": [(\"tasks\", \"READ\")],\n \"create_branch\": [(\"plugin:git\", \"EXECUTE\")],\n \"commit_changes\": [(\"plugin:git\", \"EXECUTE\")],\n \"deploy_dashboard\": [(\"plugin:git\", \"EXECUTE\")],\n \"execute_migration\": [\n (\"plugin:migration\", \"EXECUTE\"),\n (\"plugin:superset-migration\", \"EXECUTE\"),\n ],\n \"run_backup\": [(\"plugin:superset-backup\", \"EXECUTE\"), (\"plugin:backup\", \"EXECUTE\")],\n \"run_llm_validation\": [(\"plugin:llm_dashboard_validation\", \"EXECUTE\")],\n \"run_llm_documentation\": [(\"plugin:llm_documentation\", \"EXECUTE\")],\n \"get_health_summary\": [(\"plugin:migration\", \"READ\")],\n \"dataset_review_answer_context\": [(\"dataset:session\", \"READ\")],\n \"dataset_review_approve_mappings\": [(\"dataset:session\", \"MANAGE\")],\n \"dataset_review_set_field_semantics\": [(\"dataset:session\", \"MANAGE\")],\n \"dataset_review_generate_sql_preview\": [(\"dataset:session\", \"MANAGE\")],\n}\n\n\n# #endregion AssistantSchemas\n" }, { "contract_id": "AssistantMessageRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/assistant/_schemas.py", - "start_line": 19, - "end_line": 34, + "start_line": 16, + "end_line": 30, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "DATA_CONTRACT": "Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]", "INVARIANT": "message is always non-empty and no longer than 4000 characters.", "POST": "Request object provides message text and optional conversation binding.", @@ -11598,19 +10030,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantMessageRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Input payload for assistant message endpoint.\n# @DATA_CONTRACT: Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]\n# @RELATION: USED_BY -> [send_message]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: message length is within accepted bounds.\n# @POST: Request object provides message text and optional conversation binding.\n# @INVARIANT: message is always non-empty and no longer than 4000 characters.\nclass AssistantMessageRequest(BaseModel):\n conversation_id: Optional[str] = None\n message: str = Field(..., min_length=1, max_length=4000)\n dataset_review_session_id: Optional[str] = None\n\n\n# [/DEF:AssistantMessageRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region AssistantMessageRequest [C:1] [TYPE Class]\n# @BRIEF Input payload for assistant message endpoint.\n# @DATA_CONTRACT: Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]\n# @RELATION USED_BY -> [send_message]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: message length is within accepted bounds.\n# @POST: Request object provides message text and optional conversation binding.\n# @INVARIANT: message is always non-empty and no longer than 4000 characters.\nclass AssistantMessageRequest(BaseModel):\n conversation_id: str | None = None\n message: str = Field(..., min_length=1, max_length=4000)\n dataset_review_session_id: str | None = None\n\n\n# #endregion AssistantMessageRequest\n" }, { "contract_id": "AssistantAction", "contract_type": "Class", "file_path": "backend/src/api/routes/assistant/_schemas.py", - "start_line": 37, - "end_line": 52, + "start_line": 33, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "DATA_CONTRACT": "Input[type:str, label:str, target?:str] -> Output[AssistantAction]", "INVARIANT": "type and label are required for every UI action.", "POST": "Action can be rendered as button on frontend.", @@ -11709,19 +10141,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantAction:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: UI action descriptor returned with assistant responses.\n# @DATA_CONTRACT: Input[type:str, label:str, target?:str] -> Output[AssistantAction]\n# @RELATION: USED_BY -> [AssistantMessageResponse]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: type and label are provided by orchestration logic.\n# @POST: Action can be rendered as button on frontend.\n# @INVARIANT: type and label are required for every UI action.\nclass AssistantAction(BaseModel):\n type: str\n label: str\n target: Optional[str] = None\n\n\n# [/DEF:AssistantAction:Class]\n" + "anchor_syntax": "region", + "body": "# #region AssistantAction [C:1] [TYPE Class]\n# @BRIEF UI action descriptor returned with assistant responses.\n# @DATA_CONTRACT: Input[type:str, label:str, target?:str] -> Output[AssistantAction]\n# @RELATION USED_BY -> [AssistantMessageResponse]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: type and label are provided by orchestration logic.\n# @POST: Action can be rendered as button on frontend.\n# @INVARIANT: type and label are required for every UI action.\nclass AssistantAction(BaseModel):\n type: str\n label: str\n target: str | None = None\n\n\n# #endregion AssistantAction\n" }, { "contract_id": "AssistantMessageResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/assistant/_schemas.py", - "start_line": 55, - "end_line": 78, + "start_line": 50, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "DATA_CONTRACT": "Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]", "INVARIANT": "created_at and state are always present in endpoint responses.", "POST": "Payload may include task_id/confirmation_id/actions for UI follow-up.", @@ -11868,19 +10300,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantMessageResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Output payload contract for assistant interaction endpoints.\n# @DATA_CONTRACT: Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]\n# @RELATION: RETURNED_BY -> [send_message]\n# @RELATION: RETURNED_BY -> [confirm_operation]\n# @RELATION: RETURNED_BY -> [cancel_operation]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: Response includes deterministic state and text.\n# @POST: Payload may include task_id/confirmation_id/actions for UI follow-up.\n# @INVARIANT: created_at and state are always present in endpoint responses.\nclass AssistantMessageResponse(BaseModel):\n conversation_id: str\n response_id: str\n state: str\n text: str\n intent: Optional[Dict[str, Any]] = None\n confirmation_id: Optional[str] = None\n task_id: Optional[str] = None\n actions: List[AssistantAction] = Field(default_factory=list)\n created_at: datetime\n\n\n# [/DEF:AssistantMessageResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region AssistantMessageResponse [C:1] [TYPE Class]\n# @BRIEF Output payload contract for assistant interaction endpoints.\n# @DATA_CONTRACT: Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]\n# @RELATION RETURNED_BY -> [send_message]\n# @RELATION RETURNED_BY -> [confirm_operation]\n# @RELATION RETURNED_BY -> [cancel_operation]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: Response includes deterministic state and text.\n# @POST: Payload may include task_id/confirmation_id/actions for UI follow-up.\n# @INVARIANT: created_at and state are always present in endpoint responses.\nclass AssistantMessageResponse(BaseModel):\n conversation_id: str\n response_id: str\n state: str\n text: str\n intent: dict[str, Any] | None = None\n confirmation_id: str | None = None\n task_id: str | None = None\n actions: list[AssistantAction] = Field(default_factory=list)\n created_at: datetime\n\n\n# #endregion AssistantMessageResponse\n" }, { "contract_id": "ConfirmationRecord", "contract_type": "Class", "file_path": "backend/src/api/routes/assistant/_schemas.py", - "start_line": 81, - "end_line": 103, + "start_line": 75, + "end_line": 96, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "DATA_CONTRACT": "Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]", "INVARIANT": "state defaults to \"pending\" and expires_at bounds confirmation validity.", "POST": "Record tracks lifecycle state and expiry timestamp.", @@ -12027,30 +10459,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ConfirmationRecord:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: In-memory confirmation token model for risky operation dispatch.\n# @DATA_CONTRACT: Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]\n# @RELATION: USED_BY -> [send_message]\n# @RELATION: USED_BY -> [confirm_operation]\n# @RELATION: USED_BY -> [cancel_operation]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: intent/dispatch/user_id are populated at confirmation request time.\n# @POST: Record tracks lifecycle state and expiry timestamp.\n# @INVARIANT: state defaults to \"pending\" and expires_at bounds confirmation validity.\nclass ConfirmationRecord(BaseModel):\n id: str\n user_id: str\n conversation_id: str\n intent: Dict[str, Any]\n dispatch: Dict[str, Any]\n expires_at: datetime\n state: str = \"pending\"\n created_at: datetime\n\n\n# [/DEF:ConfirmationRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region ConfirmationRecord [C:1] [TYPE Class]\n# @BRIEF In-memory confirmation token model for risky operation dispatch.\n# @DATA_CONTRACT: Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]\n# @RELATION USED_BY -> [send_message]\n# @RELATION USED_BY -> [confirm_operation]\n# @RELATION USED_BY -> [cancel_operation]\n# @SIDE_EFFECT: None (schema declaration only).\n# @PRE: intent/dispatch/user_id are populated at confirmation request time.\n# @POST: Record tracks lifecycle state and expiry timestamp.\n# @INVARIANT: state defaults to \"pending\" and expires_at bounds confirmation validity.\nclass ConfirmationRecord(BaseModel):\n id: str\n user_id: str\n conversation_id: str\n intent: dict[str, Any]\n dispatch: dict[str, Any]\n expires_at: datetime\n state: str = \"pending\"\n created_at: datetime\n\n\n# #endregion ConfirmationRecord\n" }, { "contract_id": "CleanReleaseApi", "contract_type": "Module", "file_path": "backend/src/api/routes/clean_release.py", "start_line": 1, - "end_line": 639, + "end_line": 630, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "INVARIANT": "API never reports prepared status if preparation errors are present.", "LAYER": "API", "POST": "Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation.", "PRE": "Clean release repository and preparation service dependencies are configured for the current request scope.", "PURPOSE": "Expose clean release endpoints for candidate preparation and subsequent compliance flow.", - "SEMANTICS": [ - "api", - "clean-release", - "candidate-preparation", - "compliance" - ], "SIDE_EFFECT": "Persists candidate/compliance lifecycle state and triggers clean-release orchestration services." }, "relations": [ @@ -12078,15 +10504,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseApi:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: api, clean-release, candidate-preparation, compliance\n# @PURPOSE: Expose clean release endpoints for candidate preparation and subsequent compliance flow.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [get_clean_release_repository]\n# @RELATION: DEPENDS_ON -> [PreparationService]\n# @PRE: Clean release repository and preparation service dependencies are configured for the current request scope.\n# @POST: Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation.\n# @SIDE_EFFECT: Persists candidate/compliance lifecycle state and triggers clean-release orchestration services.\n# @INVARIANT: API never reports prepared status if preparation errors are present.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, List\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel, Field\n\nfrom ...core.logger import belief_scope, logger\nfrom ...dependencies import get_clean_release_repository, get_config_manager\nfrom ...services.clean_release.preparation_service import prepare_candidate\nfrom ...services.clean_release.repository import CleanReleaseRepository\nfrom ...services.clean_release.compliance_orchestrator import (\n CleanComplianceOrchestrator,\n)\nfrom ...services.clean_release.report_builder import ComplianceReportBuilder\nfrom ...services.clean_release.compliance_execution_service import (\n ComplianceExecutionService,\n ComplianceRunError,\n)\nfrom ...services.clean_release.dto import (\n CandidateDTO,\n ManifestDTO,\n CandidateOverviewDTO,\n ComplianceRunDTO,\n)\nfrom ...services.clean_release.enums import (\n ComplianceDecision,\n ComplianceStageName,\n ViolationCategory,\n ViolationSeverity,\n RunStatus,\n CandidateStatus,\n)\nfrom ...models.clean_release import (\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n CandidateArtifact,\n ReleaseCandidate,\n)\n\nrouter = APIRouter(prefix=\"/api/clean-release\", tags=[\"Clean Release\"])\n\n\n# [DEF:PrepareCandidateRequest:Class]\n# @PURPOSE: Request schema for candidate preparation endpoint.\nclass PrepareCandidateRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n artifacts: List[Dict[str, Any]] = Field(default_factory=list)\n sources: List[str] = Field(default_factory=list)\n operator_id: str = Field(min_length=1)\n\n\n# [/DEF:PrepareCandidateRequest:Class]\n\n\n# [DEF:StartCheckRequest:Class]\n# @PURPOSE: Request schema for clean compliance check run startup.\nclass StartCheckRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n profile: str = Field(default=\"enterprise-clean\")\n execution_mode: str = Field(default=\"tui\")\n triggered_by: str = Field(default=\"system\")\n\n\n# [/DEF:StartCheckRequest:Class]\n\n\n# [DEF:RegisterCandidateRequest:Class]\n# @PURPOSE: Request schema for candidate registration endpoint.\nclass RegisterCandidateRequest(BaseModel):\n id: str = Field(min_length=1)\n version: str = Field(min_length=1)\n source_snapshot_ref: str = Field(min_length=1)\n created_by: str = Field(min_length=1)\n\n\n# [/DEF:RegisterCandidateRequest:Class]\n\n\n# [DEF:ImportArtifactsRequest:Class]\n# @PURPOSE: Request schema for candidate artifact import endpoint.\nclass ImportArtifactsRequest(BaseModel):\n artifacts: List[Dict[str, Any]] = Field(default_factory=list)\n\n\n# [/DEF:ImportArtifactsRequest:Class]\n\n\n# [DEF:BuildManifestRequest:Class]\n# @PURPOSE: Request schema for manifest build endpoint.\nclass BuildManifestRequest(BaseModel):\n created_by: str = Field(default=\"system\")\n\n\n# [/DEF:BuildManifestRequest:Class]\n\n\n# [DEF:CreateComplianceRunRequest:Class]\n# @PURPOSE: Request schema for compliance run creation with optional manifest pinning.\nclass CreateComplianceRunRequest(BaseModel):\n requested_by: str = Field(min_length=1)\n manifest_id: str | None = None\n\n\n# [/DEF:CreateComplianceRunRequest:Class]\n\n\n# [DEF:register_candidate_v2_endpoint:Function]\n# @PURPOSE: Register a clean-release candidate for headless lifecycle.\n# @PRE: Candidate identifier is unique.\n# @POST: Candidate is persisted in DRAFT status.\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate_v2_endpoint(\n payload: RegisterCandidateRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n existing = repository.get_candidate(payload.id)\n if existing is not None:\n raise HTTPException(\n status_code=409,\n detail={\"message\": \"Candidate already exists\", \"code\": \"CANDIDATE_EXISTS\"},\n )\n\n candidate = ReleaseCandidate(\n id=payload.id,\n version=payload.version,\n source_snapshot_ref=payload.source_snapshot_ref,\n created_by=payload.created_by,\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status),\n )\n\n\n# [/DEF:register_candidate_v2_endpoint:Function]\n\n\n# [DEF:import_candidate_artifacts_v2_endpoint:Function]\n# @PURPOSE: Import candidate artifacts in headless flow.\n# @PRE: Candidate exists and artifacts array is non-empty.\n# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_candidate_artifacts_v2_endpoint(\n candidate_id: str,\n payload: ImportArtifactsRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Candidate not found\", \"code\": \"CANDIDATE_NOT_FOUND\"},\n )\n if not payload.artifacts:\n raise HTTPException(\n status_code=400,\n detail={\"message\": \"Artifacts list is required\", \"code\": \"ARTIFACTS_EMPTY\"},\n )\n\n for artifact in payload.artifacts:\n required = (\"id\", \"path\", \"sha256\", \"size\")\n for field_name in required:\n if field_name not in artifact:\n raise HTTPException(\n status_code=400,\n detail={\n \"message\": f\"Artifact missing field '{field_name}'\",\n \"code\": \"ARTIFACT_INVALID\",\n },\n )\n\n artifact_model = CandidateArtifact(\n id=str(artifact[\"id\"]),\n candidate_id=candidate_id,\n path=str(artifact[\"path\"]),\n sha256=str(artifact[\"sha256\"]),\n size=int(artifact[\"size\"]),\n detected_category=artifact.get(\"detected_category\"),\n declared_category=artifact.get(\"declared_category\"),\n source_uri=artifact.get(\"source_uri\"),\n source_host=artifact.get(\"source_host\"),\n metadata_json=artifact.get(\"metadata_json\", {}),\n )\n repository.save_artifact(artifact_model)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n return {\"status\": \"success\"}\n\n\n# [/DEF:import_candidate_artifacts_v2_endpoint:Function]\n\n\n# [DEF:build_candidate_manifest_v2_endpoint:Function]\n# @PURPOSE: Build immutable manifest snapshot for prepared candidate.\n# @PRE: Candidate exists and has imported artifacts.\n# @POST: Returns created ManifestDTO with incremented version.\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_candidate_manifest_v2_endpoint(\n candidate_id: str,\n payload: BuildManifestRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n from ...services.clean_release.manifest_service import build_manifest_snapshot\n\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=candidate_id,\n created_by=payload.created_by,\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=400,\n detail={\"message\": str(exc), \"code\": \"MANIFEST_BUILD_ERROR\"},\n )\n\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json,\n )\n\n\n# [/DEF:build_candidate_manifest_v2_endpoint:Function]\n\n\n# [DEF:get_candidate_overview_v2_endpoint:Function]\n# @PURPOSE: Return expanded candidate overview DTO for headless lifecycle visibility.\n# @PRE: Candidate exists.\n# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.\n@router.get(\"/candidates/{candidate_id}/overview\", response_model=CandidateOverviewDTO)\nasync def get_candidate_overview_v2_endpoint(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Candidate not found\", \"code\": \"CANDIDATE_NOT_FOUND\"},\n )\n\n manifests = repository.get_manifests_by_candidate(candidate_id)\n latest_manifest = (\n sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0]\n if manifests\n else None\n )\n\n runs = [\n run\n for run in repository.check_runs.values()\n if run.candidate_id == candidate_id\n ]\n latest_run = (\n sorted(\n runs,\n key=lambda run: run.requested_at\n or datetime.min.replace(tzinfo=timezone.utc),\n reverse=True,\n )[0]\n if runs\n else None\n )\n\n latest_report = None\n if latest_run is not None:\n latest_report = next(\n (r for r in repository.reports.values() if r.run_id == latest_run.id), None\n )\n\n latest_policy_snapshot = (\n repository.get_policy(latest_run.policy_snapshot_id) if latest_run else None\n )\n latest_registry_snapshot = (\n repository.get_registry(latest_run.registry_snapshot_id) if latest_run else None\n )\n\n approval_decisions = getattr(repository, \"approval_decisions\", [])\n latest_approval = (\n sorted(\n [item for item in approval_decisions if item.candidate_id == candidate_id],\n key=lambda item: item.decided_at\n or datetime.min.replace(tzinfo=timezone.utc),\n reverse=True,\n )[0]\n if approval_decisions\n and any(item.candidate_id == candidate_id for item in approval_decisions)\n else None\n )\n\n publication_records = getattr(repository, \"publication_records\", [])\n latest_publication = (\n sorted(\n [item for item in publication_records if item.candidate_id == candidate_id],\n key=lambda item: item.published_at\n or datetime.min.replace(tzinfo=timezone.utc),\n reverse=True,\n )[0]\n if publication_records\n and any(item.candidate_id == candidate_id for item in publication_records)\n else None\n )\n\n return CandidateOverviewDTO(\n candidate_id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n status=CandidateStatus(candidate.status),\n latest_manifest_id=latest_manifest.id if latest_manifest else None,\n latest_manifest_digest=latest_manifest.manifest_digest\n if latest_manifest\n else None,\n latest_run_id=latest_run.id if latest_run else None,\n latest_run_status=RunStatus(latest_run.status) if latest_run else None,\n latest_report_id=latest_report.id if latest_report else None,\n latest_report_final_status=ComplianceDecision(latest_report.final_status)\n if latest_report\n else None,\n latest_policy_snapshot_id=latest_policy_snapshot.id\n if latest_policy_snapshot\n else None,\n latest_policy_version=latest_policy_snapshot.policy_version\n if latest_policy_snapshot\n else None,\n latest_registry_snapshot_id=latest_registry_snapshot.id\n if latest_registry_snapshot\n else None,\n latest_registry_version=latest_registry_snapshot.registry_version\n if latest_registry_snapshot\n else None,\n latest_approval_decision=latest_approval.decision if latest_approval else None,\n latest_publication_id=latest_publication.id if latest_publication else None,\n latest_publication_status=latest_publication.status\n if latest_publication\n else None,\n )\n\n\n# [/DEF:get_candidate_overview_v2_endpoint:Function]\n\n\n# [DEF:prepare_candidate_endpoint:Function]\n# @PURPOSE: Prepare candidate with policy evaluation and deterministic manifest generation.\n# @PRE: Candidate and active policy exist in repository.\n# @POST: Returns preparation result including manifest reference and violations.\n@router.post(\"/candidates/prepare\")\nasync def prepare_candidate_endpoint(\n payload: PrepareCandidateRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n result = prepare_candidate(\n repository=repository,\n candidate_id=payload.candidate_id,\n artifacts=payload.artifacts,\n sources=payload.sources,\n operator_id=payload.operator_id,\n )\n legacy_status = result.get(\"status\")\n if isinstance(legacy_status, str):\n normalized_status = legacy_status.lower()\n if normalized_status == \"check_blocked\":\n normalized_status = \"blocked\"\n result[\"status\"] = normalized_status\n return result\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail={\"message\": str(exc), \"code\": \"CLEAN_PREPARATION_ERROR\"},\n )\n\n\n# [/DEF:prepare_candidate_endpoint:Function]\n\n\n# [DEF:start_check:Function]\n# @PURPOSE: Start and finalize a clean compliance check run and persist report artifacts.\n# @PRE: Active policy and candidate exist.\n# @POST: Returns accepted payload with check_run_id and started_at.\n@router.post(\"/checks\", status_code=status.HTTP_202_ACCEPTED)\nasync def start_check(\n payload: StartCheckRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.start_check\"):\n logger.reason(\"Starting clean-release compliance check run\")\n policy = repository.get_active_policy()\n if policy is None:\n raise HTTPException(\n status_code=409,\n detail={\n \"message\": \"Active policy not found\",\n \"code\": \"POLICY_NOT_FOUND\",\n },\n )\n\n candidate = repository.get_candidate(payload.candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=409,\n detail={\n \"message\": \"Candidate not found\",\n \"code\": \"CANDIDATE_NOT_FOUND\",\n },\n )\n\n manifests = repository.get_manifests_by_candidate(payload.candidate_id)\n if not manifests:\n logger.explore(\n \"No manifest found for candidate; bootstrapping legacy empty manifest for compatibility\"\n )\n from ...services.clean_release.manifest_builder import (\n build_distribution_manifest,\n )\n\n boot_manifest = build_distribution_manifest(\n manifest_id=f\"manifest-{payload.candidate_id}\",\n candidate_id=payload.candidate_id,\n policy_id=getattr(policy, \"policy_id\", None)\n or getattr(policy, \"id\", \"\"),\n generated_by=payload.triggered_by,\n artifacts=[],\n )\n repository.save_manifest(boot_manifest)\n manifests = [boot_manifest]\n latest_manifest = sorted(\n manifests, key=lambda m: m.manifest_version, reverse=True\n )[0]\n\n orchestrator = CleanComplianceOrchestrator(repository)\n run = orchestrator.start_check_run(\n candidate_id=payload.candidate_id,\n policy_id=policy.id,\n requested_by=payload.triggered_by,\n manifest_id=latest_manifest.id,\n )\n\n forced = [\n ComplianceStageRun(\n id=f\"stage-{run.id}-1\",\n run_id=run.id,\n stage_name=ComplianceStageName.DATA_PURITY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-2\",\n run_id=run.id,\n stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-3\",\n run_id=run.id,\n stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-4\",\n run_id=run.id,\n stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ]\n run = orchestrator.execute_stages(run, forced_results=forced)\n run = orchestrator.finalize_run(run)\n\n if str(run.final_status) in {\n ComplianceDecision.BLOCKED.value,\n \"CheckFinalStatus.BLOCKED\",\n \"BLOCKED\",\n }:\n logger.explore(\n \"Run ended as BLOCKED, persisting synthetic external-source violation\"\n )\n violation = ComplianceViolation(\n id=f\"viol-{run.id}\",\n run_id=run.id,\n stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,\n code=\"EXTERNAL_SOURCE_DETECTED\",\n severity=ViolationSeverity.CRITICAL.value,\n message=\"Replace with approved internal server\",\n evidence_json={\"location\": \"external.example.com\"},\n )\n repository.save_violation(violation)\n\n builder = ComplianceReportBuilder(repository)\n report = builder.build_report_payload(\n run, repository.get_violations_by_run(run.id)\n )\n builder.persist_report(report)\n logger.reflect(f\"Compliance report persisted for run_id={run.id}\")\n\n return {\n \"check_run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"status\": \"running\",\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n }\n\n\n# [/DEF:start_check:Function]\n\n\n# [DEF:get_check_status:Function]\n# @PURPOSE: Return terminal/intermediate status payload for a check run.\n# @PRE: check_run_id references an existing run.\n# @POST: Deterministic payload shape includes checks and violations arrays.\n@router.get(\"/checks/{check_run_id}\")\nasync def get_check_status(\n check_run_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.get_check_status\"):\n run = repository.get_check_run(check_run_id)\n if run is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Check run not found\", \"code\": \"CHECK_NOT_FOUND\"},\n )\n\n logger.reflect(f\"Returning check status for check_run_id={check_run_id}\")\n checks = [\n {\n \"stage_name\": stage.stage_name,\n \"status\": stage.status,\n \"decision\": stage.decision,\n \"details\": stage.details_json,\n }\n for stage in repository.stage_runs.values()\n if stage.run_id == run.id\n ]\n violations = [\n {\n \"violation_id\": violation.id,\n \"category\": violation.stage_name,\n \"code\": violation.code,\n \"message\": violation.message,\n \"evidence\": violation.evidence_json,\n }\n for violation in repository.get_violations_by_run(run.id)\n ]\n return {\n \"check_run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"final_status\": getattr(run.final_status, \"value\", run.final_status),\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"finished_at\": run.finished_at.isoformat() if run.finished_at else None,\n \"checks\": checks,\n \"violations\": violations,\n }\n\n\n# [/DEF:get_check_status:Function]\n\n\n# [DEF:get_report:Function]\n# @PURPOSE: Return persisted compliance report by report_id.\n# @PRE: report_id references an existing report.\n# @POST: Returns serialized report object.\n@router.get(\"/reports/{report_id}\")\nasync def get_report(\n report_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.get_report\"):\n report = repository.get_report(report_id)\n if report is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Report not found\", \"code\": \"REPORT_NOT_FOUND\"},\n )\n\n logger.reflect(f\"Returning compliance report report_id={report_id}\")\n return {\n \"report_id\": report.id,\n \"check_run_id\": report.run_id,\n \"candidate_id\": report.candidate_id,\n \"final_status\": getattr(report.final_status, \"value\", report.final_status),\n \"generated_at\": report.generated_at.isoformat()\n if getattr(report, \"generated_at\", None)\n else None,\n \"operator_summary\": getattr(report, \"operator_summary\", \"\"),\n \"structured_payload_ref\": getattr(report, \"structured_payload_ref\", None),\n \"violations_count\": getattr(report, \"violations_count\", 0),\n \"blocking_violations_count\": getattr(\n report, \"blocking_violations_count\", 0\n ),\n }\n\n\n# [/DEF:get_report:Function]\n# [/DEF:CleanReleaseApi:Module]\n" + "anchor_syntax": "region", + "body": "# #region CleanReleaseApi [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, api, compliance, candidate, release]\n# @BRIEF Expose clean release endpoints for candidate preparation and subsequent compliance flow.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [get_clean_release_repository]\n# @RELATION DEPENDS_ON -> [PreparationService]\n# @PRE: Clean release repository and preparation service dependencies are configured for the current request scope.\n# @POST: Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation.\n# @SIDE_EFFECT: Persists candidate/compliance lifecycle state and triggers clean-release orchestration services.\n# @INVARIANT: API never reports prepared status if preparation errors are present.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel, Field\n\nfrom ...core.logger import belief_scope, logger\nfrom ...dependencies import get_clean_release_repository\nfrom ...models.clean_release import (\n CandidateArtifact,\n ComplianceStageRun,\n ComplianceViolation,\n ReleaseCandidate,\n)\nfrom ...services.clean_release.compliance_orchestrator import (\n CleanComplianceOrchestrator,\n)\nfrom ...services.clean_release.dto import (\n CandidateDTO,\n CandidateOverviewDTO,\n ManifestDTO,\n)\nfrom ...services.clean_release.enums import (\n CandidateStatus,\n ComplianceDecision,\n ComplianceStageName,\n RunStatus,\n ViolationSeverity,\n)\nfrom ...services.clean_release.preparation_service import prepare_candidate\nfrom ...services.clean_release.report_builder import ComplianceReportBuilder\nfrom ...services.clean_release.repository import CleanReleaseRepository\n\nrouter = APIRouter(prefix=\"/api/clean-release\", tags=[\"Clean Release\"])\n\n\n# #region PrepareCandidateRequest [TYPE Class]\n# @BRIEF Request schema for candidate preparation endpoint.\nclass PrepareCandidateRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n artifacts: list[dict[str, Any]] = Field(default_factory=list)\n sources: list[str] = Field(default_factory=list)\n operator_id: str = Field(min_length=1)\n\n\n# #endregion PrepareCandidateRequest\n\n\n# #region StartCheckRequest [TYPE Class]\n# @BRIEF Request schema for clean compliance check run startup.\nclass StartCheckRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n profile: str = Field(default=\"enterprise-clean\")\n execution_mode: str = Field(default=\"tui\")\n triggered_by: str = Field(default=\"system\")\n\n\n# #endregion StartCheckRequest\n\n\n# #region RegisterCandidateRequest [TYPE Class]\n# @BRIEF Request schema for candidate registration endpoint.\nclass RegisterCandidateRequest(BaseModel):\n id: str = Field(min_length=1)\n version: str = Field(min_length=1)\n source_snapshot_ref: str = Field(min_length=1)\n created_by: str = Field(min_length=1)\n\n\n# #endregion RegisterCandidateRequest\n\n\n# #region ImportArtifactsRequest [TYPE Class]\n# @BRIEF Request schema for candidate artifact import endpoint.\nclass ImportArtifactsRequest(BaseModel):\n artifacts: list[dict[str, Any]] = Field(default_factory=list)\n\n\n# #endregion ImportArtifactsRequest\n\n\n# #region BuildManifestRequest [TYPE Class]\n# @BRIEF Request schema for manifest build endpoint.\nclass BuildManifestRequest(BaseModel):\n created_by: str = Field(default=\"system\")\n\n\n# #endregion BuildManifestRequest\n\n\n# #region CreateComplianceRunRequest [TYPE Class]\n# @BRIEF Request schema for compliance run creation with optional manifest pinning.\nclass CreateComplianceRunRequest(BaseModel):\n requested_by: str = Field(min_length=1)\n manifest_id: str | None = None\n\n\n# #endregion CreateComplianceRunRequest\n\n\n# #region register_candidate_v2_endpoint [TYPE Function]\n# @BRIEF Register a clean-release candidate for headless lifecycle.\n# @PRE: Candidate identifier is unique.\n# @POST: Candidate is persisted in DRAFT status.\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate_v2_endpoint(\n payload: RegisterCandidateRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n existing = repository.get_candidate(payload.id)\n if existing is not None:\n raise HTTPException(\n status_code=409,\n detail={\"message\": \"Candidate already exists\", \"code\": \"CANDIDATE_EXISTS\"},\n )\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\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_v2_endpoint\n\n\n# #region import_candidate_artifacts_v2_endpoint [TYPE Function]\n# @BRIEF Import candidate artifacts in headless flow.\n# @PRE: Candidate exists and artifacts array is non-empty.\n# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_candidate_artifacts_v2_endpoint(\n candidate_id: str,\n payload: ImportArtifactsRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Candidate not found\", \"code\": \"CANDIDATE_NOT_FOUND\"},\n )\n if not payload.artifacts:\n raise HTTPException(\n status_code=400,\n detail={\"message\": \"Artifacts list is required\", \"code\": \"ARTIFACTS_EMPTY\"},\n )\n\n for artifact in payload.artifacts:\n required = (\"id\", \"path\", \"sha256\", \"size\")\n for field_name in required:\n if field_name not in artifact:\n raise HTTPException(\n status_code=400,\n detail={\n \"message\": f\"Artifact missing field '{field_name}'\",\n \"code\": \"ARTIFACT_INVALID\",\n },\n )\n\n artifact_model = CandidateArtifact(\n id=str(artifact[\"id\"]),\n candidate_id=candidate_id,\n path=str(artifact[\"path\"]),\n sha256=str(artifact[\"sha256\"]),\n size=int(artifact[\"size\"]),\n detected_category=artifact.get(\"detected_category\"),\n declared_category=artifact.get(\"declared_category\"),\n source_uri=artifact.get(\"source_uri\"),\n source_host=artifact.get(\"source_host\"),\n metadata_json=artifact.get(\"metadata_json\", {}),\n )\n repository.save_artifact(artifact_model)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n return {\"status\": \"success\"}\n\n\n# #endregion import_candidate_artifacts_v2_endpoint\n\n\n# #region build_candidate_manifest_v2_endpoint [TYPE Function]\n# @BRIEF Build immutable manifest snapshot for prepared candidate.\n# @PRE: Candidate exists and has imported artifacts.\n# @POST: Returns created ManifestDTO with incremented version.\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_candidate_manifest_v2_endpoint(\n candidate_id: str,\n payload: BuildManifestRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n from ...services.clean_release.manifest_service import build_manifest_snapshot\n\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=candidate_id,\n created_by=payload.created_by,\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=400,\n detail={\"message\": str(exc), \"code\": \"MANIFEST_BUILD_ERROR\"},\n )\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_candidate_manifest_v2_endpoint\n\n\n# #region get_candidate_overview_v2_endpoint [TYPE Function]\n# @BRIEF Return expanded candidate overview DTO for headless lifecycle visibility.\n# @PRE: Candidate exists.\n# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.\n@router.get(\"/candidates/{candidate_id}/overview\", response_model=CandidateOverviewDTO)\nasync def get_candidate_overview_v2_endpoint(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Candidate not found\", \"code\": \"CANDIDATE_NOT_FOUND\"},\n )\n\n manifests = repository.get_manifests_by_candidate(candidate_id)\n latest_manifest = (\n sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0]\n if manifests\n else None\n )\n\n runs = [\n run\n for run in repository.check_runs.values()\n if run.candidate_id == candidate_id\n ]\n latest_run = (\n sorted(\n runs,\n key=lambda run: run.requested_at\n or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n if runs\n else None\n )\n\n latest_report = None\n if latest_run is not None:\n latest_report = next(\n (r for r in repository.reports.values() if r.run_id == latest_run.id), None\n )\n\n latest_policy_snapshot = (\n repository.get_policy(latest_run.policy_snapshot_id) if latest_run else None\n )\n latest_registry_snapshot = (\n repository.get_registry(latest_run.registry_snapshot_id) if latest_run else None\n )\n\n approval_decisions = getattr(repository, \"approval_decisions\", [])\n latest_approval = (\n sorted(\n [item for item in approval_decisions if item.candidate_id == candidate_id],\n key=lambda item: item.decided_at\n or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n if approval_decisions\n and any(item.candidate_id == candidate_id for item in approval_decisions)\n else None\n )\n\n publication_records = getattr(repository, \"publication_records\", [])\n latest_publication = (\n sorted(\n [item for item in publication_records if item.candidate_id == candidate_id],\n key=lambda item: item.published_at\n or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n if publication_records\n and any(item.candidate_id == candidate_id for item in publication_records)\n else None\n )\n\n return CandidateOverviewDTO(\n candidate_id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n status=CandidateStatus(candidate.status),\n latest_manifest_id=latest_manifest.id if latest_manifest else None,\n latest_manifest_digest=latest_manifest.manifest_digest\n if latest_manifest\n else None,\n latest_run_id=latest_run.id if latest_run else None,\n latest_run_status=RunStatus(latest_run.status) if latest_run else None,\n latest_report_id=latest_report.id if latest_report else None,\n latest_report_final_status=ComplianceDecision(latest_report.final_status)\n if latest_report\n else None,\n latest_policy_snapshot_id=latest_policy_snapshot.id\n if latest_policy_snapshot\n else None,\n latest_policy_version=latest_policy_snapshot.policy_version\n if latest_policy_snapshot\n else None,\n latest_registry_snapshot_id=latest_registry_snapshot.id\n if latest_registry_snapshot\n else None,\n latest_registry_version=latest_registry_snapshot.registry_version\n if latest_registry_snapshot\n else None,\n latest_approval_decision=latest_approval.decision if latest_approval else None,\n latest_publication_id=latest_publication.id if latest_publication else None,\n latest_publication_status=latest_publication.status\n if latest_publication\n else None,\n )\n\n\n# #endregion get_candidate_overview_v2_endpoint\n\n\n# #region prepare_candidate_endpoint [TYPE Function]\n# @BRIEF Prepare candidate with policy evaluation and deterministic manifest generation.\n# @PRE: Candidate and active policy exist in repository.\n# @POST: Returns preparation result including manifest reference and violations.\n@router.post(\"/candidates/prepare\")\nasync def prepare_candidate_endpoint(\n payload: PrepareCandidateRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n result = prepare_candidate(\n repository=repository,\n candidate_id=payload.candidate_id,\n artifacts=payload.artifacts,\n sources=payload.sources,\n operator_id=payload.operator_id,\n )\n legacy_status = result.get(\"status\")\n if isinstance(legacy_status, str):\n normalized_status = legacy_status.lower()\n if normalized_status == \"check_blocked\":\n normalized_status = \"blocked\"\n result[\"status\"] = normalized_status\n return result\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail={\"message\": str(exc), \"code\": \"CLEAN_PREPARATION_ERROR\"},\n )\n\n\n# #endregion prepare_candidate_endpoint\n\n\n# #region start_check [TYPE Function]\n# @BRIEF Start and finalize a clean compliance check run and persist report artifacts.\n# @PRE: Active policy and candidate exist.\n# @POST: Returns accepted payload with check_run_id and started_at.\n@router.post(\"/checks\", status_code=status.HTTP_202_ACCEPTED)\nasync def start_check(\n payload: StartCheckRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.start_check\"):\n logger.reason(\"Starting clean-release compliance check run\")\n policy = repository.get_active_policy()\n if policy is None:\n raise HTTPException(\n status_code=409,\n detail={\n \"message\": \"Active policy not found\",\n \"code\": \"POLICY_NOT_FOUND\",\n },\n )\n\n candidate = repository.get_candidate(payload.candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=409,\n detail={\n \"message\": \"Candidate not found\",\n \"code\": \"CANDIDATE_NOT_FOUND\",\n },\n )\n\n manifests = repository.get_manifests_by_candidate(payload.candidate_id)\n if not manifests:\n logger.explore(\n \"No manifest found for candidate; bootstrapping legacy empty manifest for compatibility\"\n )\n from ...services.clean_release.manifest_builder import (\n build_distribution_manifest,\n )\n\n boot_manifest = build_distribution_manifest(\n manifest_id=f\"manifest-{payload.candidate_id}\",\n candidate_id=payload.candidate_id,\n policy_id=getattr(policy, \"policy_id\", None)\n or getattr(policy, \"id\", \"\"),\n generated_by=payload.triggered_by,\n artifacts=[],\n )\n repository.save_manifest(boot_manifest)\n manifests = [boot_manifest]\n latest_manifest = sorted(\n manifests, key=lambda m: m.manifest_version, reverse=True\n )[0]\n\n orchestrator = CleanComplianceOrchestrator(repository)\n run = orchestrator.start_check_run(\n candidate_id=payload.candidate_id,\n policy_id=policy.id,\n requested_by=payload.triggered_by,\n manifest_id=latest_manifest.id,\n )\n\n forced = [\n ComplianceStageRun(\n id=f\"stage-{run.id}-1\",\n run_id=run.id,\n stage_name=ComplianceStageName.DATA_PURITY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-2\",\n run_id=run.id,\n stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-3\",\n run_id=run.id,\n stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-4\",\n run_id=run.id,\n stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ]\n run = orchestrator.execute_stages(run, forced_results=forced)\n run = orchestrator.finalize_run(run)\n\n if str(run.final_status) in {\n ComplianceDecision.BLOCKED.value,\n \"CheckFinalStatus.BLOCKED\",\n \"BLOCKED\",\n }:\n logger.explore(\n \"Run ended as BLOCKED, persisting synthetic external-source violation\"\n )\n violation = ComplianceViolation(\n id=f\"viol-{run.id}\",\n run_id=run.id,\n stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,\n code=\"EXTERNAL_SOURCE_DETECTED\",\n severity=ViolationSeverity.CRITICAL.value,\n message=\"Replace with approved internal server\",\n evidence_json={\"location\": \"external.example.com\"},\n )\n repository.save_violation(violation)\n\n builder = ComplianceReportBuilder(repository)\n report = builder.build_report_payload(\n run, repository.get_violations_by_run(run.id)\n )\n builder.persist_report(report)\n logger.reflect(f\"Compliance report persisted for run_id={run.id}\")\n\n return {\n \"check_run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"status\": \"running\",\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n }\n\n\n# #endregion start_check\n\n\n# #region get_check_status [TYPE Function]\n# @BRIEF Return terminal/intermediate status payload for a check run.\n# @PRE: check_run_id references an existing run.\n# @POST: Deterministic payload shape includes checks and violations arrays.\n@router.get(\"/checks/{check_run_id}\")\nasync def get_check_status(\n check_run_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.get_check_status\"):\n run = repository.get_check_run(check_run_id)\n if run is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Check run not found\", \"code\": \"CHECK_NOT_FOUND\"},\n )\n\n logger.reflect(f\"Returning check status for check_run_id={check_run_id}\")\n checks = [\n {\n \"stage_name\": stage.stage_name,\n \"status\": stage.status,\n \"decision\": stage.decision,\n \"details\": stage.details_json,\n }\n for stage in repository.stage_runs.values()\n if stage.run_id == run.id\n ]\n violations = [\n {\n \"violation_id\": violation.id,\n \"category\": violation.stage_name,\n \"code\": violation.code,\n \"message\": violation.message,\n \"evidence\": violation.evidence_json,\n }\n for violation in repository.get_violations_by_run(run.id)\n ]\n return {\n \"check_run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"final_status\": getattr(run.final_status, \"value\", run.final_status),\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"finished_at\": run.finished_at.isoformat() if run.finished_at else None,\n \"checks\": checks,\n \"violations\": violations,\n }\n\n\n# #endregion get_check_status\n\n\n# #region get_report [TYPE Function]\n# @BRIEF Return persisted compliance report by report_id.\n# @PRE: report_id references an existing report.\n# @POST: Returns serialized report object.\n@router.get(\"/reports/{report_id}\")\nasync def get_report(\n report_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.get_report\"):\n report = repository.get_report(report_id)\n if report is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Report not found\", \"code\": \"REPORT_NOT_FOUND\"},\n )\n\n logger.reflect(f\"Returning compliance report report_id={report_id}\")\n return {\n \"report_id\": report.id,\n \"check_run_id\": report.run_id,\n \"candidate_id\": report.candidate_id,\n \"final_status\": getattr(report.final_status, \"value\", report.final_status),\n \"generated_at\": report.generated_at.isoformat()\n if getattr(report, \"generated_at\", None)\n else None,\n \"operator_summary\": getattr(report, \"operator_summary\", \"\"),\n \"structured_payload_ref\": getattr(report, \"structured_payload_ref\", None),\n \"violations_count\": getattr(report, \"violations_count\", 0),\n \"blocking_violations_count\": getattr(\n report, \"blocking_violations_count\", 0\n ),\n }\n\n\n# #endregion get_report\n# #endregion CleanReleaseApi\n" }, { "contract_id": "PrepareCandidateRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 58, - "end_line": 67, + "start_line": 49, + "end_line": 58, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12104,15 +10530,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PrepareCandidateRequest:Class]\n# @PURPOSE: Request schema for candidate preparation endpoint.\nclass PrepareCandidateRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n artifacts: List[Dict[str, Any]] = Field(default_factory=list)\n sources: List[str] = Field(default_factory=list)\n operator_id: str = Field(min_length=1)\n\n\n# [/DEF:PrepareCandidateRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region PrepareCandidateRequest [TYPE Class]\n# @BRIEF Request schema for candidate preparation endpoint.\nclass PrepareCandidateRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n artifacts: list[dict[str, Any]] = Field(default_factory=list)\n sources: list[str] = Field(default_factory=list)\n operator_id: str = Field(min_length=1)\n\n\n# #endregion PrepareCandidateRequest\n" }, { "contract_id": "StartCheckRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 70, - "end_line": 79, + "start_line": 61, + "end_line": 70, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12130,15 +10556,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:StartCheckRequest:Class]\n# @PURPOSE: Request schema for clean compliance check run startup.\nclass StartCheckRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n profile: str = Field(default=\"enterprise-clean\")\n execution_mode: str = Field(default=\"tui\")\n triggered_by: str = Field(default=\"system\")\n\n\n# [/DEF:StartCheckRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region StartCheckRequest [TYPE Class]\n# @BRIEF Request schema for clean compliance check run startup.\nclass StartCheckRequest(BaseModel):\n candidate_id: str = Field(min_length=1)\n profile: str = Field(default=\"enterprise-clean\")\n execution_mode: str = Field(default=\"tui\")\n triggered_by: str = Field(default=\"system\")\n\n\n# #endregion StartCheckRequest\n" }, { "contract_id": "RegisterCandidateRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 82, - "end_line": 91, + "start_line": 73, + "end_line": 82, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12156,15 +10582,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RegisterCandidateRequest:Class]\n# @PURPOSE: Request schema for candidate registration endpoint.\nclass RegisterCandidateRequest(BaseModel):\n id: str = Field(min_length=1)\n version: str = Field(min_length=1)\n source_snapshot_ref: str = Field(min_length=1)\n created_by: str = Field(min_length=1)\n\n\n# [/DEF:RegisterCandidateRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region RegisterCandidateRequest [TYPE Class]\n# @BRIEF Request schema for candidate registration endpoint.\nclass RegisterCandidateRequest(BaseModel):\n id: str = Field(min_length=1)\n version: str = Field(min_length=1)\n source_snapshot_ref: str = Field(min_length=1)\n created_by: str = Field(min_length=1)\n\n\n# #endregion RegisterCandidateRequest\n" }, { "contract_id": "ImportArtifactsRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 94, - "end_line": 100, + "start_line": 85, + "end_line": 91, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12182,15 +10608,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ImportArtifactsRequest:Class]\n# @PURPOSE: Request schema for candidate artifact import endpoint.\nclass ImportArtifactsRequest(BaseModel):\n artifacts: List[Dict[str, Any]] = Field(default_factory=list)\n\n\n# [/DEF:ImportArtifactsRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region ImportArtifactsRequest [TYPE Class]\n# @BRIEF Request schema for candidate artifact import endpoint.\nclass ImportArtifactsRequest(BaseModel):\n artifacts: list[dict[str, Any]] = Field(default_factory=list)\n\n\n# #endregion ImportArtifactsRequest\n" }, { "contract_id": "BuildManifestRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 103, - "end_line": 109, + "start_line": 94, + "end_line": 100, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12208,15 +10634,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BuildManifestRequest:Class]\n# @PURPOSE: Request schema for manifest build endpoint.\nclass BuildManifestRequest(BaseModel):\n created_by: str = Field(default=\"system\")\n\n\n# [/DEF:BuildManifestRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region BuildManifestRequest [TYPE Class]\n# @BRIEF Request schema for manifest build endpoint.\nclass BuildManifestRequest(BaseModel):\n created_by: str = Field(default=\"system\")\n\n\n# #endregion BuildManifestRequest\n" }, { "contract_id": "CreateComplianceRunRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 112, - "end_line": 119, + "start_line": 103, + "end_line": 110, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12234,15 +10660,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CreateComplianceRunRequest:Class]\n# @PURPOSE: Request schema for compliance run creation with optional manifest pinning.\nclass CreateComplianceRunRequest(BaseModel):\n requested_by: str = Field(min_length=1)\n manifest_id: str | None = None\n\n\n# [/DEF:CreateComplianceRunRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region CreateComplianceRunRequest [TYPE Class]\n# @BRIEF Request schema for compliance run creation with optional manifest pinning.\nclass CreateComplianceRunRequest(BaseModel):\n requested_by: str = Field(min_length=1)\n manifest_id: str | None = None\n\n\n# #endregion CreateComplianceRunRequest\n" }, { "contract_id": "register_candidate_v2_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 122, - "end_line": 160, + "start_line": 113, + "end_line": 151, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12280,15 +10706,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:register_candidate_v2_endpoint:Function]\n# @PURPOSE: Register a clean-release candidate for headless lifecycle.\n# @PRE: Candidate identifier is unique.\n# @POST: Candidate is persisted in DRAFT status.\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate_v2_endpoint(\n payload: RegisterCandidateRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n existing = repository.get_candidate(payload.id)\n if existing is not None:\n raise HTTPException(\n status_code=409,\n detail={\"message\": \"Candidate already exists\", \"code\": \"CANDIDATE_EXISTS\"},\n )\n\n candidate = ReleaseCandidate(\n id=payload.id,\n version=payload.version,\n source_snapshot_ref=payload.source_snapshot_ref,\n created_by=payload.created_by,\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status),\n )\n\n\n# [/DEF:register_candidate_v2_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #region register_candidate_v2_endpoint [TYPE Function]\n# @BRIEF Register a clean-release candidate for headless lifecycle.\n# @PRE: Candidate identifier is unique.\n# @POST: Candidate is persisted in DRAFT status.\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate_v2_endpoint(\n payload: RegisterCandidateRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n existing = repository.get_candidate(payload.id)\n if existing is not None:\n raise HTTPException(\n status_code=409,\n detail={\"message\": \"Candidate already exists\", \"code\": \"CANDIDATE_EXISTS\"},\n )\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\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_v2_endpoint\n" }, { "contract_id": "import_candidate_artifacts_v2_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 163, - "end_line": 218, + "start_line": 154, + "end_line": 209, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12326,15 +10752,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:import_candidate_artifacts_v2_endpoint:Function]\n# @PURPOSE: Import candidate artifacts in headless flow.\n# @PRE: Candidate exists and artifacts array is non-empty.\n# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_candidate_artifacts_v2_endpoint(\n candidate_id: str,\n payload: ImportArtifactsRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Candidate not found\", \"code\": \"CANDIDATE_NOT_FOUND\"},\n )\n if not payload.artifacts:\n raise HTTPException(\n status_code=400,\n detail={\"message\": \"Artifacts list is required\", \"code\": \"ARTIFACTS_EMPTY\"},\n )\n\n for artifact in payload.artifacts:\n required = (\"id\", \"path\", \"sha256\", \"size\")\n for field_name in required:\n if field_name not in artifact:\n raise HTTPException(\n status_code=400,\n detail={\n \"message\": f\"Artifact missing field '{field_name}'\",\n \"code\": \"ARTIFACT_INVALID\",\n },\n )\n\n artifact_model = CandidateArtifact(\n id=str(artifact[\"id\"]),\n candidate_id=candidate_id,\n path=str(artifact[\"path\"]),\n sha256=str(artifact[\"sha256\"]),\n size=int(artifact[\"size\"]),\n detected_category=artifact.get(\"detected_category\"),\n declared_category=artifact.get(\"declared_category\"),\n source_uri=artifact.get(\"source_uri\"),\n source_host=artifact.get(\"source_host\"),\n metadata_json=artifact.get(\"metadata_json\", {}),\n )\n repository.save_artifact(artifact_model)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n return {\"status\": \"success\"}\n\n\n# [/DEF:import_candidate_artifacts_v2_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #region import_candidate_artifacts_v2_endpoint [TYPE Function]\n# @BRIEF Import candidate artifacts in headless flow.\n# @PRE: Candidate exists and artifacts array is non-empty.\n# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_candidate_artifacts_v2_endpoint(\n candidate_id: str,\n payload: ImportArtifactsRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Candidate not found\", \"code\": \"CANDIDATE_NOT_FOUND\"},\n )\n if not payload.artifacts:\n raise HTTPException(\n status_code=400,\n detail={\"message\": \"Artifacts list is required\", \"code\": \"ARTIFACTS_EMPTY\"},\n )\n\n for artifact in payload.artifacts:\n required = (\"id\", \"path\", \"sha256\", \"size\")\n for field_name in required:\n if field_name not in artifact:\n raise HTTPException(\n status_code=400,\n detail={\n \"message\": f\"Artifact missing field '{field_name}'\",\n \"code\": \"ARTIFACT_INVALID\",\n },\n )\n\n artifact_model = CandidateArtifact(\n id=str(artifact[\"id\"]),\n candidate_id=candidate_id,\n path=str(artifact[\"path\"]),\n sha256=str(artifact[\"sha256\"]),\n size=int(artifact[\"size\"]),\n detected_category=artifact.get(\"detected_category\"),\n declared_category=artifact.get(\"declared_category\"),\n source_uri=artifact.get(\"source_uri\"),\n source_host=artifact.get(\"source_host\"),\n metadata_json=artifact.get(\"metadata_json\", {}),\n )\n repository.save_artifact(artifact_model)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n return {\"status\": \"success\"}\n\n\n# #endregion import_candidate_artifacts_v2_endpoint\n" }, { "contract_id": "build_candidate_manifest_v2_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 221, - "end_line": 262, + "start_line": 212, + "end_line": 253, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12372,15 +10798,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_candidate_manifest_v2_endpoint:Function]\n# @PURPOSE: Build immutable manifest snapshot for prepared candidate.\n# @PRE: Candidate exists and has imported artifacts.\n# @POST: Returns created ManifestDTO with incremented version.\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_candidate_manifest_v2_endpoint(\n candidate_id: str,\n payload: BuildManifestRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n from ...services.clean_release.manifest_service import build_manifest_snapshot\n\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=candidate_id,\n created_by=payload.created_by,\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=400,\n detail={\"message\": str(exc), \"code\": \"MANIFEST_BUILD_ERROR\"},\n )\n\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json,\n )\n\n\n# [/DEF:build_candidate_manifest_v2_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #region build_candidate_manifest_v2_endpoint [TYPE Function]\n# @BRIEF Build immutable manifest snapshot for prepared candidate.\n# @PRE: Candidate exists and has imported artifacts.\n# @POST: Returns created ManifestDTO with incremented version.\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_candidate_manifest_v2_endpoint(\n candidate_id: str,\n payload: BuildManifestRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n from ...services.clean_release.manifest_service import build_manifest_snapshot\n\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=candidate_id,\n created_by=payload.created_by,\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=400,\n detail={\"message\": str(exc), \"code\": \"MANIFEST_BUILD_ERROR\"},\n )\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_candidate_manifest_v2_endpoint\n" }, { "contract_id": "get_candidate_overview_v2_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 265, - "end_line": 378, + "start_line": 256, + "end_line": 369, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12418,15 +10844,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_candidate_overview_v2_endpoint:Function]\n# @PURPOSE: Return expanded candidate overview DTO for headless lifecycle visibility.\n# @PRE: Candidate exists.\n# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.\n@router.get(\"/candidates/{candidate_id}/overview\", response_model=CandidateOverviewDTO)\nasync def get_candidate_overview_v2_endpoint(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Candidate not found\", \"code\": \"CANDIDATE_NOT_FOUND\"},\n )\n\n manifests = repository.get_manifests_by_candidate(candidate_id)\n latest_manifest = (\n sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0]\n if manifests\n else None\n )\n\n runs = [\n run\n for run in repository.check_runs.values()\n if run.candidate_id == candidate_id\n ]\n latest_run = (\n sorted(\n runs,\n key=lambda run: run.requested_at\n or datetime.min.replace(tzinfo=timezone.utc),\n reverse=True,\n )[0]\n if runs\n else None\n )\n\n latest_report = None\n if latest_run is not None:\n latest_report = next(\n (r for r in repository.reports.values() if r.run_id == latest_run.id), None\n )\n\n latest_policy_snapshot = (\n repository.get_policy(latest_run.policy_snapshot_id) if latest_run else None\n )\n latest_registry_snapshot = (\n repository.get_registry(latest_run.registry_snapshot_id) if latest_run else None\n )\n\n approval_decisions = getattr(repository, \"approval_decisions\", [])\n latest_approval = (\n sorted(\n [item for item in approval_decisions if item.candidate_id == candidate_id],\n key=lambda item: item.decided_at\n or datetime.min.replace(tzinfo=timezone.utc),\n reverse=True,\n )[0]\n if approval_decisions\n and any(item.candidate_id == candidate_id for item in approval_decisions)\n else None\n )\n\n publication_records = getattr(repository, \"publication_records\", [])\n latest_publication = (\n sorted(\n [item for item in publication_records if item.candidate_id == candidate_id],\n key=lambda item: item.published_at\n or datetime.min.replace(tzinfo=timezone.utc),\n reverse=True,\n )[0]\n if publication_records\n and any(item.candidate_id == candidate_id for item in publication_records)\n else None\n )\n\n return CandidateOverviewDTO(\n candidate_id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n status=CandidateStatus(candidate.status),\n latest_manifest_id=latest_manifest.id if latest_manifest else None,\n latest_manifest_digest=latest_manifest.manifest_digest\n if latest_manifest\n else None,\n latest_run_id=latest_run.id if latest_run else None,\n latest_run_status=RunStatus(latest_run.status) if latest_run else None,\n latest_report_id=latest_report.id if latest_report else None,\n latest_report_final_status=ComplianceDecision(latest_report.final_status)\n if latest_report\n else None,\n latest_policy_snapshot_id=latest_policy_snapshot.id\n if latest_policy_snapshot\n else None,\n latest_policy_version=latest_policy_snapshot.policy_version\n if latest_policy_snapshot\n else None,\n latest_registry_snapshot_id=latest_registry_snapshot.id\n if latest_registry_snapshot\n else None,\n latest_registry_version=latest_registry_snapshot.registry_version\n if latest_registry_snapshot\n else None,\n latest_approval_decision=latest_approval.decision if latest_approval else None,\n latest_publication_id=latest_publication.id if latest_publication else None,\n latest_publication_status=latest_publication.status\n if latest_publication\n else None,\n )\n\n\n# [/DEF:get_candidate_overview_v2_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_candidate_overview_v2_endpoint [TYPE Function]\n# @BRIEF Return expanded candidate overview DTO for headless lifecycle visibility.\n# @PRE: Candidate exists.\n# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.\n@router.get(\"/candidates/{candidate_id}/overview\", response_model=CandidateOverviewDTO)\nasync def get_candidate_overview_v2_endpoint(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Candidate not found\", \"code\": \"CANDIDATE_NOT_FOUND\"},\n )\n\n manifests = repository.get_manifests_by_candidate(candidate_id)\n latest_manifest = (\n sorted(manifests, key=lambda m: m.manifest_version, reverse=True)[0]\n if manifests\n else None\n )\n\n runs = [\n run\n for run in repository.check_runs.values()\n if run.candidate_id == candidate_id\n ]\n latest_run = (\n sorted(\n runs,\n key=lambda run: run.requested_at\n or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n if runs\n else None\n )\n\n latest_report = None\n if latest_run is not None:\n latest_report = next(\n (r for r in repository.reports.values() if r.run_id == latest_run.id), None\n )\n\n latest_policy_snapshot = (\n repository.get_policy(latest_run.policy_snapshot_id) if latest_run else None\n )\n latest_registry_snapshot = (\n repository.get_registry(latest_run.registry_snapshot_id) if latest_run else None\n )\n\n approval_decisions = getattr(repository, \"approval_decisions\", [])\n latest_approval = (\n sorted(\n [item for item in approval_decisions if item.candidate_id == candidate_id],\n key=lambda item: item.decided_at\n or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n if approval_decisions\n and any(item.candidate_id == candidate_id for item in approval_decisions)\n else None\n )\n\n publication_records = getattr(repository, \"publication_records\", [])\n latest_publication = (\n sorted(\n [item for item in publication_records if item.candidate_id == candidate_id],\n key=lambda item: item.published_at\n or datetime.min.replace(tzinfo=UTC),\n reverse=True,\n )[0]\n if publication_records\n and any(item.candidate_id == candidate_id for item in publication_records)\n else None\n )\n\n return CandidateOverviewDTO(\n candidate_id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n status=CandidateStatus(candidate.status),\n latest_manifest_id=latest_manifest.id if latest_manifest else None,\n latest_manifest_digest=latest_manifest.manifest_digest\n if latest_manifest\n else None,\n latest_run_id=latest_run.id if latest_run else None,\n latest_run_status=RunStatus(latest_run.status) if latest_run else None,\n latest_report_id=latest_report.id if latest_report else None,\n latest_report_final_status=ComplianceDecision(latest_report.final_status)\n if latest_report\n else None,\n latest_policy_snapshot_id=latest_policy_snapshot.id\n if latest_policy_snapshot\n else None,\n latest_policy_version=latest_policy_snapshot.policy_version\n if latest_policy_snapshot\n else None,\n latest_registry_snapshot_id=latest_registry_snapshot.id\n if latest_registry_snapshot\n else None,\n latest_registry_version=latest_registry_snapshot.registry_version\n if latest_registry_snapshot\n else None,\n latest_approval_decision=latest_approval.decision if latest_approval else None,\n latest_publication_id=latest_publication.id if latest_publication else None,\n latest_publication_status=latest_publication.status\n if latest_publication\n else None,\n )\n\n\n# #endregion get_candidate_overview_v2_endpoint\n" }, { "contract_id": "prepare_candidate_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 381, - "end_line": 412, + "start_line": 372, + "end_line": 403, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12464,15 +10890,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:prepare_candidate_endpoint:Function]\n# @PURPOSE: Prepare candidate with policy evaluation and deterministic manifest generation.\n# @PRE: Candidate and active policy exist in repository.\n# @POST: Returns preparation result including manifest reference and violations.\n@router.post(\"/candidates/prepare\")\nasync def prepare_candidate_endpoint(\n payload: PrepareCandidateRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n result = prepare_candidate(\n repository=repository,\n candidate_id=payload.candidate_id,\n artifacts=payload.artifacts,\n sources=payload.sources,\n operator_id=payload.operator_id,\n )\n legacy_status = result.get(\"status\")\n if isinstance(legacy_status, str):\n normalized_status = legacy_status.lower()\n if normalized_status == \"check_blocked\":\n normalized_status = \"blocked\"\n result[\"status\"] = normalized_status\n return result\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail={\"message\": str(exc), \"code\": \"CLEAN_PREPARATION_ERROR\"},\n )\n\n\n# [/DEF:prepare_candidate_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #region prepare_candidate_endpoint [TYPE Function]\n# @BRIEF Prepare candidate with policy evaluation and deterministic manifest generation.\n# @PRE: Candidate and active policy exist in repository.\n# @POST: Returns preparation result including manifest reference and violations.\n@router.post(\"/candidates/prepare\")\nasync def prepare_candidate_endpoint(\n payload: PrepareCandidateRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n result = prepare_candidate(\n repository=repository,\n candidate_id=payload.candidate_id,\n artifacts=payload.artifacts,\n sources=payload.sources,\n operator_id=payload.operator_id,\n )\n legacy_status = result.get(\"status\")\n if isinstance(legacy_status, str):\n normalized_status = legacy_status.lower()\n if normalized_status == \"check_blocked\":\n normalized_status = \"blocked\"\n result[\"status\"] = normalized_status\n return result\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail={\"message\": str(exc), \"code\": \"CLEAN_PREPARATION_ERROR\"},\n )\n\n\n# #endregion prepare_candidate_endpoint\n" }, { "contract_id": "start_check", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 415, - "end_line": 548, + "start_line": 406, + "end_line": 539, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12510,15 +10936,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:start_check:Function]\n# @PURPOSE: Start and finalize a clean compliance check run and persist report artifacts.\n# @PRE: Active policy and candidate exist.\n# @POST: Returns accepted payload with check_run_id and started_at.\n@router.post(\"/checks\", status_code=status.HTTP_202_ACCEPTED)\nasync def start_check(\n payload: StartCheckRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.start_check\"):\n logger.reason(\"Starting clean-release compliance check run\")\n policy = repository.get_active_policy()\n if policy is None:\n raise HTTPException(\n status_code=409,\n detail={\n \"message\": \"Active policy not found\",\n \"code\": \"POLICY_NOT_FOUND\",\n },\n )\n\n candidate = repository.get_candidate(payload.candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=409,\n detail={\n \"message\": \"Candidate not found\",\n \"code\": \"CANDIDATE_NOT_FOUND\",\n },\n )\n\n manifests = repository.get_manifests_by_candidate(payload.candidate_id)\n if not manifests:\n logger.explore(\n \"No manifest found for candidate; bootstrapping legacy empty manifest for compatibility\"\n )\n from ...services.clean_release.manifest_builder import (\n build_distribution_manifest,\n )\n\n boot_manifest = build_distribution_manifest(\n manifest_id=f\"manifest-{payload.candidate_id}\",\n candidate_id=payload.candidate_id,\n policy_id=getattr(policy, \"policy_id\", None)\n or getattr(policy, \"id\", \"\"),\n generated_by=payload.triggered_by,\n artifacts=[],\n )\n repository.save_manifest(boot_manifest)\n manifests = [boot_manifest]\n latest_manifest = sorted(\n manifests, key=lambda m: m.manifest_version, reverse=True\n )[0]\n\n orchestrator = CleanComplianceOrchestrator(repository)\n run = orchestrator.start_check_run(\n candidate_id=payload.candidate_id,\n policy_id=policy.id,\n requested_by=payload.triggered_by,\n manifest_id=latest_manifest.id,\n )\n\n forced = [\n ComplianceStageRun(\n id=f\"stage-{run.id}-1\",\n run_id=run.id,\n stage_name=ComplianceStageName.DATA_PURITY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-2\",\n run_id=run.id,\n stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-3\",\n run_id=run.id,\n stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-4\",\n run_id=run.id,\n stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ]\n run = orchestrator.execute_stages(run, forced_results=forced)\n run = orchestrator.finalize_run(run)\n\n if str(run.final_status) in {\n ComplianceDecision.BLOCKED.value,\n \"CheckFinalStatus.BLOCKED\",\n \"BLOCKED\",\n }:\n logger.explore(\n \"Run ended as BLOCKED, persisting synthetic external-source violation\"\n )\n violation = ComplianceViolation(\n id=f\"viol-{run.id}\",\n run_id=run.id,\n stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,\n code=\"EXTERNAL_SOURCE_DETECTED\",\n severity=ViolationSeverity.CRITICAL.value,\n message=\"Replace with approved internal server\",\n evidence_json={\"location\": \"external.example.com\"},\n )\n repository.save_violation(violation)\n\n builder = ComplianceReportBuilder(repository)\n report = builder.build_report_payload(\n run, repository.get_violations_by_run(run.id)\n )\n builder.persist_report(report)\n logger.reflect(f\"Compliance report persisted for run_id={run.id}\")\n\n return {\n \"check_run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"status\": \"running\",\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n }\n\n\n# [/DEF:start_check:Function]\n" + "anchor_syntax": "region", + "body": "# #region start_check [TYPE Function]\n# @BRIEF Start and finalize a clean compliance check run and persist report artifacts.\n# @PRE: Active policy and candidate exist.\n# @POST: Returns accepted payload with check_run_id and started_at.\n@router.post(\"/checks\", status_code=status.HTTP_202_ACCEPTED)\nasync def start_check(\n payload: StartCheckRequest,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.start_check\"):\n logger.reason(\"Starting clean-release compliance check run\")\n policy = repository.get_active_policy()\n if policy is None:\n raise HTTPException(\n status_code=409,\n detail={\n \"message\": \"Active policy not found\",\n \"code\": \"POLICY_NOT_FOUND\",\n },\n )\n\n candidate = repository.get_candidate(payload.candidate_id)\n if candidate is None:\n raise HTTPException(\n status_code=409,\n detail={\n \"message\": \"Candidate not found\",\n \"code\": \"CANDIDATE_NOT_FOUND\",\n },\n )\n\n manifests = repository.get_manifests_by_candidate(payload.candidate_id)\n if not manifests:\n logger.explore(\n \"No manifest found for candidate; bootstrapping legacy empty manifest for compatibility\"\n )\n from ...services.clean_release.manifest_builder import (\n build_distribution_manifest,\n )\n\n boot_manifest = build_distribution_manifest(\n manifest_id=f\"manifest-{payload.candidate_id}\",\n candidate_id=payload.candidate_id,\n policy_id=getattr(policy, \"policy_id\", None)\n or getattr(policy, \"id\", \"\"),\n generated_by=payload.triggered_by,\n artifacts=[],\n )\n repository.save_manifest(boot_manifest)\n manifests = [boot_manifest]\n latest_manifest = sorted(\n manifests, key=lambda m: m.manifest_version, reverse=True\n )[0]\n\n orchestrator = CleanComplianceOrchestrator(repository)\n run = orchestrator.start_check_run(\n candidate_id=payload.candidate_id,\n policy_id=policy.id,\n requested_by=payload.triggered_by,\n manifest_id=latest_manifest.id,\n )\n\n forced = [\n ComplianceStageRun(\n id=f\"stage-{run.id}-1\",\n run_id=run.id,\n stage_name=ComplianceStageName.DATA_PURITY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-2\",\n run_id=run.id,\n stage_name=ComplianceStageName.INTERNAL_SOURCES_ONLY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-3\",\n run_id=run.id,\n stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ComplianceStageRun(\n id=f\"stage-{run.id}-4\",\n run_id=run.id,\n stage_name=ComplianceStageName.MANIFEST_CONSISTENCY.value,\n status=RunStatus.SUCCEEDED.value,\n decision=ComplianceDecision.PASSED.value,\n details_json={\"message\": \"ok\"},\n ),\n ]\n run = orchestrator.execute_stages(run, forced_results=forced)\n run = orchestrator.finalize_run(run)\n\n if str(run.final_status) in {\n ComplianceDecision.BLOCKED.value,\n \"CheckFinalStatus.BLOCKED\",\n \"BLOCKED\",\n }:\n logger.explore(\n \"Run ended as BLOCKED, persisting synthetic external-source violation\"\n )\n violation = ComplianceViolation(\n id=f\"viol-{run.id}\",\n run_id=run.id,\n stage_name=ComplianceStageName.NO_EXTERNAL_ENDPOINTS.value,\n code=\"EXTERNAL_SOURCE_DETECTED\",\n severity=ViolationSeverity.CRITICAL.value,\n message=\"Replace with approved internal server\",\n evidence_json={\"location\": \"external.example.com\"},\n )\n repository.save_violation(violation)\n\n builder = ComplianceReportBuilder(repository)\n report = builder.build_report_payload(\n run, repository.get_violations_by_run(run.id)\n )\n builder.persist_report(report)\n logger.reflect(f\"Compliance report persisted for run_id={run.id}\")\n\n return {\n \"check_run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"status\": \"running\",\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n }\n\n\n# #endregion start_check\n" }, { "contract_id": "get_check_status", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 551, - "end_line": 600, + "start_line": 542, + "end_line": 591, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12556,15 +10982,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_check_status:Function]\n# @PURPOSE: Return terminal/intermediate status payload for a check run.\n# @PRE: check_run_id references an existing run.\n# @POST: Deterministic payload shape includes checks and violations arrays.\n@router.get(\"/checks/{check_run_id}\")\nasync def get_check_status(\n check_run_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.get_check_status\"):\n run = repository.get_check_run(check_run_id)\n if run is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Check run not found\", \"code\": \"CHECK_NOT_FOUND\"},\n )\n\n logger.reflect(f\"Returning check status for check_run_id={check_run_id}\")\n checks = [\n {\n \"stage_name\": stage.stage_name,\n \"status\": stage.status,\n \"decision\": stage.decision,\n \"details\": stage.details_json,\n }\n for stage in repository.stage_runs.values()\n if stage.run_id == run.id\n ]\n violations = [\n {\n \"violation_id\": violation.id,\n \"category\": violation.stage_name,\n \"code\": violation.code,\n \"message\": violation.message,\n \"evidence\": violation.evidence_json,\n }\n for violation in repository.get_violations_by_run(run.id)\n ]\n return {\n \"check_run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"final_status\": getattr(run.final_status, \"value\", run.final_status),\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"finished_at\": run.finished_at.isoformat() if run.finished_at else None,\n \"checks\": checks,\n \"violations\": violations,\n }\n\n\n# [/DEF:get_check_status:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_check_status [TYPE Function]\n# @BRIEF Return terminal/intermediate status payload for a check run.\n# @PRE: check_run_id references an existing run.\n# @POST: Deterministic payload shape includes checks and violations arrays.\n@router.get(\"/checks/{check_run_id}\")\nasync def get_check_status(\n check_run_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.get_check_status\"):\n run = repository.get_check_run(check_run_id)\n if run is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Check run not found\", \"code\": \"CHECK_NOT_FOUND\"},\n )\n\n logger.reflect(f\"Returning check status for check_run_id={check_run_id}\")\n checks = [\n {\n \"stage_name\": stage.stage_name,\n \"status\": stage.status,\n \"decision\": stage.decision,\n \"details\": stage.details_json,\n }\n for stage in repository.stage_runs.values()\n if stage.run_id == run.id\n ]\n violations = [\n {\n \"violation_id\": violation.id,\n \"category\": violation.stage_name,\n \"code\": violation.code,\n \"message\": violation.message,\n \"evidence\": violation.evidence_json,\n }\n for violation in repository.get_violations_by_run(run.id)\n ]\n return {\n \"check_run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"final_status\": getattr(run.final_status, \"value\", run.final_status),\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"finished_at\": run.finished_at.isoformat() if run.finished_at else None,\n \"checks\": checks,\n \"violations\": violations,\n }\n\n\n# #endregion get_check_status\n" }, { "contract_id": "get_report", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release.py", - "start_line": 603, - "end_line": 638, + "start_line": 594, + "end_line": 629, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12602,19 +11028,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_report:Function]\n# @PURPOSE: Return persisted compliance report by report_id.\n# @PRE: report_id references an existing report.\n# @POST: Returns serialized report object.\n@router.get(\"/reports/{report_id}\")\nasync def get_report(\n report_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.get_report\"):\n report = repository.get_report(report_id)\n if report is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Report not found\", \"code\": \"REPORT_NOT_FOUND\"},\n )\n\n logger.reflect(f\"Returning compliance report report_id={report_id}\")\n return {\n \"report_id\": report.id,\n \"check_run_id\": report.run_id,\n \"candidate_id\": report.candidate_id,\n \"final_status\": getattr(report.final_status, \"value\", report.final_status),\n \"generated_at\": report.generated_at.isoformat()\n if getattr(report, \"generated_at\", None)\n else None,\n \"operator_summary\": getattr(report, \"operator_summary\", \"\"),\n \"structured_payload_ref\": getattr(report, \"structured_payload_ref\", None),\n \"violations_count\": getattr(report, \"violations_count\", 0),\n \"blocking_violations_count\": getattr(\n report, \"blocking_violations_count\", 0\n ),\n }\n\n\n# [/DEF:get_report:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_report [TYPE Function]\n# @BRIEF Return persisted compliance report by report_id.\n# @PRE: report_id references an existing report.\n# @POST: Returns serialized report object.\n@router.get(\"/reports/{report_id}\")\nasync def get_report(\n report_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n with belief_scope(\"clean_release.get_report\"):\n report = repository.get_report(report_id)\n if report is None:\n raise HTTPException(\n status_code=404,\n detail={\"message\": \"Report not found\", \"code\": \"REPORT_NOT_FOUND\"},\n )\n\n logger.reflect(f\"Returning compliance report report_id={report_id}\")\n return {\n \"report_id\": report.id,\n \"check_run_id\": report.run_id,\n \"candidate_id\": report.candidate_id,\n \"final_status\": getattr(report.final_status, \"value\", report.final_status),\n \"generated_at\": report.generated_at.isoformat()\n if getattr(report, \"generated_at\", None)\n else None,\n \"operator_summary\": getattr(report, \"operator_summary\", \"\"),\n \"structured_payload_ref\": getattr(report, \"structured_payload_ref\", None),\n \"violations_count\": getattr(report, \"violations_count\", 0),\n \"blocking_violations_count\": getattr(\n report, \"blocking_violations_count\", 0\n ),\n }\n\n\n# #endregion get_report\n" }, { "contract_id": "CleanReleaseV2Api", "contract_type": "Module", "file_path": "backend/src/api/routes/clean_release_v2.py", "start_line": 1, - "end_line": 331, + "end_line": 320, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "LAYER": "UI (API)", "POST": "Candidate registration, approval, publication, and revocation routes are registered without behavior changes.", "PRE": "Clean release repository dependency is available for candidate lifecycle endpoints.", @@ -12642,19 +11068,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseV2Api:Module]\n# @COMPLEXITY: 4\n# @PURPOSE: Redesigned clean release API for headless candidate lifecycle.\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION: CALLS -> [approve_candidate]\n# @RELATION: CALLS -> [publish_candidate]\n# @PRE: Clean release repository dependency is available for candidate lifecycle endpoints.\n# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes.\n# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom typing import List, Dict, Any\nfrom datetime import datetime, timezone\nfrom ...services.clean_release.approval_service import (\n approve_candidate,\n reject_candidate,\n)\nfrom ...services.clean_release.publication_service import (\n publish_candidate,\n revoke_publication,\n)\nfrom ...services.clean_release.repository import CleanReleaseRepository\nfrom ...dependencies import get_clean_release_repository\nfrom ...services.clean_release.enums import CandidateStatus\nfrom ...models.clean_release import (\n ReleaseCandidate,\n CandidateArtifact,\n DistributionManifest,\n)\nfrom ...services.clean_release.dto import CandidateDTO, ManifestDTO\n\nrouter = APIRouter(prefix=\"/api/v2/clean-release\", tags=[\"Clean Release V2\"])\n\n\n# [DEF:ApprovalRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for approval request payload.\nclass ApprovalRequest(dict):\n pass\n\n\n# [/DEF:ApprovalRequest:Class]\n\n\n# [DEF:PublishRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for publication request payload.\nclass PublishRequest(dict):\n pass\n\n\n# [/DEF:PublishRequest:Class]\n\n\n# [DEF:RevokeRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for revocation request payload.\nclass RevokeRequest(dict):\n pass\n\n\n# [/DEF:RevokeRequest:Class]\n\n\n# [DEF:register_candidate:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Register a new release candidate.\n# @PRE: Payload contains required fields (id, version, source_snapshot_ref, created_by).\n# @POST: Candidate is saved in repository.\n# @RETURN: CandidateDTO\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION: DEPENDS_ON -> [clean_release_dto]\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate(\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = ReleaseCandidate(\n id=payload[\"id\"],\n version=payload[\"version\"],\n source_snapshot_ref=payload[\"source_snapshot_ref\"],\n created_by=payload[\"created_by\"],\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status),\n )\n\n\n# [/DEF:register_candidate:Function]\n\n\n# [DEF:import_artifacts:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Associate artifacts with a release candidate.\n# @PRE: Candidate exists.\n# @POST: Artifacts are processed (placeholder).\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_artifacts(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n for art_data in payload.get(\"artifacts\", []):\n artifact = CandidateArtifact(\n id=art_data[\"id\"],\n candidate_id=candidate_id,\n path=art_data[\"path\"],\n sha256=art_data[\"sha256\"],\n size=art_data[\"size\"],\n )\n # In a real repo we'd have save_artifact\n # repository.save_artifact(artifact)\n pass\n\n return {\"status\": \"success\"}\n\n\n# [/DEF:import_artifacts:Function]\n\n\n# [DEF:build_manifest:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Generate distribution manifest for a candidate.\n# @PRE: Candidate exists.\n# @POST: Manifest is created and saved.\n# @RETURN: ManifestDTO\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_manifest(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n manifest = DistributionManifest(\n id=f\"manifest-{candidate_id}\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash-123\",\n artifacts_digest=\"art-hash-123\",\n created_by=\"system\",\n created_at=datetime.now(timezone.utc),\n source_snapshot_ref=candidate.source_snapshot_ref,\n content_json={\"items\": [], \"summary\": {}},\n )\n repository.save_manifest(manifest)\n\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json,\n )\n\n\n# [/DEF:build_manifest:Function]\n\n\n# [DEF:approve_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to record candidate approval.\n# @RELATION: CALLS -> [approve_candidate]\n@router.post(\"/candidates/{candidate_id}/approve\")\nasync def approve_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# [/DEF:approve_candidate_endpoint:Function]\n\n\n# [DEF:reject_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to record candidate rejection.\n# @RELATION: CALLS -> [reject_candidate]\n@router.post(\"/candidates/{candidate_id}/reject\")\nasync def reject_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# [/DEF:reject_candidate_endpoint:Function]\n\n\n# [DEF:publish_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to publish an approved candidate.\n# @RELATION: CALLS -> [publish_candidate]\n@router.post(\"/candidates/{candidate_id}/publish\")\nasync def publish_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n published_by=str(payload[\"published_by\"]),\n target_channel=str(payload[\"target_channel\"]),\n publication_ref=payload.get(\"publication_ref\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# [/DEF:publish_candidate_endpoint:Function]\n\n\n# [DEF:revoke_publication_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to revoke a previous publication.\n# @RELATION: CALLS -> [revoke_publication]\n@router.post(\"/publications/{publication_id}/revoke\")\nasync def revoke_publication_endpoint(\n publication_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=publication_id,\n revoked_by=str(payload[\"revoked_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# [/DEF:revoke_publication_endpoint:Function]\n\n# [/DEF:CleanReleaseV2Api:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ApprovalRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 36, + "start_line": 37, "end_line": 43, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Schema for approval request payload." }, "relations": [], @@ -12669,19 +11095,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ApprovalRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for approval request payload.\nclass ApprovalRequest(dict):\n pass\n\n\n# [/DEF:ApprovalRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region ApprovalRequest [C:1] [TYPE Class]\n# @BRIEF Schema for approval request payload.\nclass ApprovalRequest(dict):\n pass\n\n\n# #endregion ApprovalRequest\n" }, { "contract_id": "PublishRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release_v2.py", "start_line": 46, - "end_line": 53, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Schema for publication request payload." }, "relations": [], @@ -12696,19 +11122,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PublishRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for publication request payload.\nclass PublishRequest(dict):\n pass\n\n\n# [/DEF:PublishRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region PublishRequest [C:1] [TYPE Class]\n# @BRIEF Schema for publication request payload.\nclass PublishRequest(dict):\n pass\n\n\n# #endregion PublishRequest\n" }, { "contract_id": "RevokeRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 56, - "end_line": 63, + "start_line": 55, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Schema for revocation request payload." }, "relations": [], @@ -12723,23 +11149,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RevokeRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Schema for revocation request payload.\nclass RevokeRequest(dict):\n pass\n\n\n# [/DEF:RevokeRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region RevokeRequest [C:1] [TYPE Class]\n# @BRIEF Schema for revocation request payload.\nclass RevokeRequest(dict):\n pass\n\n\n# #endregion RevokeRequest\n" }, { "contract_id": "register_candidate", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 66, - "end_line": 100, + "start_line": 64, + "end_line": 96, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Candidate is saved in repository.", "PRE": "Payload contains required fields (id, version, source_snapshot_ref, created_by).", - "PURPOSE": "Register a new release candidate.", - "RETURN": "CandidateDTO" + "PURPOSE": "Register a new release candidate." }, "relations": [ { @@ -12773,27 +11198,21 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:register_candidate:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Register a new release candidate.\n# @PRE: Payload contains required fields (id, version, source_snapshot_ref, created_by).\n# @POST: Candidate is saved in repository.\n# @RETURN: CandidateDTO\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION: DEPENDS_ON -> [clean_release_dto]\n@router.post(\n \"/candidates\", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED\n)\nasync def register_candidate(\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = ReleaseCandidate(\n id=payload[\"id\"],\n version=payload[\"version\"],\n source_snapshot_ref=payload[\"source_snapshot_ref\"],\n created_by=payload[\"created_by\"],\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status),\n )\n\n\n# [/DEF:register_candidate:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "import_artifacts", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 103, - "end_line": 134, + "start_line": 99, + "end_line": 129, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Artifacts are processed (placeholder).", "PRE": "Candidate exists.", "PURPOSE": "Associate artifacts with a release candidate." @@ -12826,23 +11245,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:import_artifacts:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Associate artifacts with a release candidate.\n# @PRE: Candidate exists.\n# @POST: Artifacts are processed (placeholder).\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\"/candidates/{candidate_id}/artifacts\")\nasync def import_artifacts(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n for art_data in payload.get(\"artifacts\", []):\n artifact = CandidateArtifact(\n id=art_data[\"id\"],\n candidate_id=candidate_id,\n path=art_data[\"path\"],\n sha256=art_data[\"sha256\"],\n size=art_data[\"size\"],\n )\n # In a real repo we'd have save_artifact\n # repository.save_artifact(artifact)\n pass\n\n return {\"status\": \"success\"}\n\n\n# [/DEF:import_artifacts:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "build_manifest", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 137, - "end_line": 183, + "start_line": 132, + "end_line": 176, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Manifest is created and saved.", "PRE": "Candidate exists.", - "PURPOSE": "Generate distribution manifest for a candidate.", - "RETURN": "ManifestDTO" + "PURPOSE": "Generate distribution manifest for a candidate." }, "relations": [ { @@ -12870,27 +11288,21 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:build_manifest:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Generate distribution manifest for a candidate.\n# @PRE: Candidate exists.\n# @POST: Manifest is created and saved.\n# @RETURN: ManifestDTO\n# @RELATION: DEPENDS_ON -> [CleanReleaseRepository]\n@router.post(\n \"/candidates/{candidate_id}/manifests\",\n response_model=ManifestDTO,\n status_code=status.HTTP_201_CREATED,\n)\nasync def build_manifest(\n candidate_id: str,\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n candidate = repository.get_candidate(candidate_id)\n if not candidate:\n raise HTTPException(status_code=404, detail=\"Candidate not found\")\n\n manifest = DistributionManifest(\n id=f\"manifest-{candidate_id}\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash-123\",\n artifacts_digest=\"art-hash-123\",\n created_by=\"system\",\n created_at=datetime.now(timezone.utc),\n source_snapshot_ref=candidate.source_snapshot_ref,\n content_json={\"items\": [], \"summary\": {}},\n )\n repository.save_manifest(manifest)\n\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json,\n )\n\n\n# [/DEF:build_manifest:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "approve_candidate_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 186, - "end_line": 212, + "start_line": 179, + "end_line": 204, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Endpoint to record candidate approval." }, "relations": [ @@ -12902,19 +11314,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:approve_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to record candidate approval.\n# @RELATION: CALLS -> [approve_candidate]\n@router.post(\"/candidates/{candidate_id}/approve\")\nasync def approve_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# [/DEF:approve_candidate_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "reject_candidate_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 215, - "end_line": 241, + "start_line": 207, + "end_line": 232, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Endpoint to record candidate rejection." }, "relations": [ @@ -12926,19 +11338,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:reject_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to record candidate rejection.\n# @RELATION: CALLS -> [reject_candidate]\n@router.post(\"/candidates/{candidate_id}/reject\")\nasync def reject_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n decided_by=str(payload[\"decided_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409, detail={\"message\": str(exc), \"code\": \"APPROVAL_GATE_ERROR\"}\n )\n\n return {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n\n\n# [/DEF:reject_candidate_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "publish_candidate_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 244, - "end_line": 286, + "start_line": 235, + "end_line": 276, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Endpoint to publish an approved candidate." }, "relations": [ @@ -12950,19 +11362,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:publish_candidate_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to publish an approved candidate.\n# @RELATION: CALLS -> [publish_candidate]\n@router.post(\"/candidates/{candidate_id}/publish\")\nasync def publish_candidate_endpoint(\n candidate_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=str(payload[\"report_id\"]),\n published_by=str(payload[\"published_by\"]),\n target_channel=str(payload[\"target_channel\"]),\n publication_ref=payload.get(\"publication_ref\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# [/DEF:publish_candidate_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "revoke_publication_endpoint", "contract_type": "Function", "file_path": "backend/src/api/routes/clean_release_v2.py", - "start_line": 289, - "end_line": 329, + "start_line": 279, + "end_line": 318, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Endpoint to revoke a previous publication." }, "relations": [ @@ -12974,27 +11386,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:revoke_publication_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Endpoint to revoke a previous publication.\n# @RELATION: CALLS -> [revoke_publication]\n@router.post(\"/publications/{publication_id}/revoke\")\nasync def revoke_publication_endpoint(\n publication_id: str,\n payload: Dict[str, Any],\n repository: CleanReleaseRepository = Depends(get_clean_release_repository),\n):\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=publication_id,\n revoked_by=str(payload[\"revoked_by\"]),\n comment=payload.get(\"comment\"),\n )\n except Exception as exc: # noqa: BLE001\n raise HTTPException(\n status_code=409,\n detail={\"message\": str(exc), \"code\": \"PUBLICATION_GATE_ERROR\"},\n )\n\n return {\n \"status\": \"ok\",\n \"publication\": {\n \"id\": publication.id,\n \"candidate_id\": publication.candidate_id,\n \"report_id\": publication.report_id,\n \"published_by\": publication.published_by,\n \"published_at\": publication.published_at.isoformat()\n if publication.published_at\n else None,\n \"target_channel\": publication.target_channel,\n \"publication_ref\": publication.publication_ref,\n \"status\": publication.status,\n },\n }\n\n\n# [/DEF:revoke_publication_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ConnectionsRouter", "contract_type": "Module", "file_path": "backend/src/api/routes/connections.py", "start_line": 1, - "end_line": 158, + "end_line": 141, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI (API)", - "PURPOSE": "Defines the FastAPI router for managing external database connections.", - "SEMANTICS": [ - "api", - "router", - "connections", - "database" - ] + "PURPOSE": "Defines the FastAPI router for managing external database connections." }, "relations": [ { @@ -13011,19 +11417,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ConnectionsRouter:Module]\n# @SEMANTICS: api, router, connections, database\n# @PURPOSE: Defines the FastAPI router for managing external database connections.\n# @COMPLEXITY: 3\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [get_db]\n# @RELATION: DEPENDS_ON -> [ConnectionConfig]\n\n# [SECTION: IMPORTS]\nfrom typing import List, Optional\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\nfrom ...core.database import get_db, ensure_connection_configs_table\nfrom ...models.connection import ConnectionConfig\nfrom pydantic import BaseModel\nfrom datetime import datetime\nfrom ...core.logger import logger, belief_scope\n# [/SECTION]\n\nrouter = APIRouter()\n\n\n# [DEF:_ensure_connections_schema:Function]\n# @PURPOSE: Ensures the connection_configs table exists before CRUD access.\n# @COMPLEXITY: 3\n# @PRE: db is an active SQLAlchemy session.\n# @POST: The current bind can safely query ConnectionConfig.\n# @RELATION: CALLS -> ensure_connection_configs_table\ndef _ensure_connections_schema(db: Session):\n with belief_scope(\"ConnectionsRouter.ensure_schema\"):\n ensure_connection_configs_table(db.get_bind())\n\n\n# [/DEF:_ensure_connections_schema:Function]\n\n\n# [DEF:ConnectionSchema:Class]\n# @PURPOSE: Pydantic model for connection response.\n# @COMPLEXITY: 3\n# @RELATION: BINDS_TO -> ConnectionConfig\nclass ConnectionSchema(BaseModel):\n id: str\n name: str\n type: str\n host: Optional[str] = None\n port: Optional[int] = None\n database: Optional[str] = None\n username: Optional[str] = None\n created_at: datetime\n\n class Config:\n orm_mode = True\n\n\n# [/DEF:ConnectionSchema:Class]\n\n\n# [DEF:ConnectionCreate:Class]\n# @PURPOSE: Pydantic model for creating a connection.\n# @COMPLEXITY: 3\n# @RELATION: BINDS_TO -> ConnectionConfig\nclass ConnectionCreate(BaseModel):\n name: str\n type: str\n host: Optional[str] = None\n port: Optional[int] = None\n database: Optional[str] = None\n username: Optional[str] = None\n password: Optional[str] = None\n\n\n# [/DEF:ConnectionCreate:Class]\n\n\n# [DEF:list_connections:Function]\n# @PURPOSE: Lists all saved connections.\n# @COMPLEXITY: 3\n# @PRE: Database session is active.\n# @POST: Returns list of connection configs.\n# @PARAM: db (Session) - Database session.\n# @RETURN: List[ConnectionSchema] - List of connections.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.get(\"\", response_model=List[ConnectionSchema])\nasync def list_connections(db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.list_connections\"):\n _ensure_connections_schema(db)\n connections = db.query(ConnectionConfig).all()\n return connections\n\n\n# [/DEF:list_connections:Function]\n\n\n# [DEF:create_connection:Function]\n# @PURPOSE: Creates a new connection configuration.\n# @COMPLEXITY: 3\n# @PRE: Connection name is unique.\n# @POST: Connection is saved to DB.\n# @PARAM: connection (ConnectionCreate) - Config data.\n# @PARAM: db (Session) - Database session.\n# @RETURN: ConnectionSchema - Created connection.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.post(\"\", response_model=ConnectionSchema, status_code=status.HTTP_201_CREATED)\nasync def create_connection(\n connection: ConnectionCreate, db: Session = Depends(get_db)\n):\n with belief_scope(\"ConnectionsRouter.create_connection\", f\"name={connection.name}\"):\n _ensure_connections_schema(db)\n db_connection = ConnectionConfig(**connection.dict())\n db.add(db_connection)\n db.commit()\n db.refresh(db_connection)\n logger.info(\n f\"[ConnectionsRouter.create_connection][Success] Created connection {db_connection.id}\"\n )\n return db_connection\n\n\n# [/DEF:create_connection:Function]\n\n\n# [DEF:delete_connection:Function]\n# @PURPOSE: Deletes a connection configuration.\n# @COMPLEXITY: 3\n# @PRE: Connection ID exists.\n# @POST: Connection is removed from DB.\n# @PARAM: connection_id (str) - ID to delete.\n# @PARAM: db (Session) - Database session.\n# @RETURN: None.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.delete(\"/{connection_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_connection(connection_id: str, db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.delete_connection\", f\"id={connection_id}\"):\n _ensure_connections_schema(db)\n db_connection = (\n db.query(ConnectionConfig)\n .filter(ConnectionConfig.id == connection_id)\n .first()\n )\n if not db_connection:\n logger.error(\n f\"[ConnectionsRouter.delete_connection][State] Connection {connection_id} not found\"\n )\n raise HTTPException(status_code=404, detail=\"Connection not found\")\n db.delete(db_connection)\n db.commit()\n logger.info(\n f\"[ConnectionsRouter.delete_connection][Success] Deleted connection {connection_id}\"\n )\n return\n\n\n# [/DEF:delete_connection:Function]\n\n# [/DEF:ConnectionsRouter:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_ensure_connections_schema", "contract_type": "Function", "file_path": "backend/src/api/routes/connections.py", - "start_line": 23, - "end_line": 34, + "start_line": 20, + "end_line": 30, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "The current bind can safely query ConnectionConfig.", "PRE": "db is an active SQLAlchemy session.", "PURPOSE": "Ensures the connection_configs table exists before CRUD access." @@ -13056,19 +11462,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_connections_schema:Function]\n# @PURPOSE: Ensures the connection_configs table exists before CRUD access.\n# @COMPLEXITY: 3\n# @PRE: db is an active SQLAlchemy session.\n# @POST: The current bind can safely query ConnectionConfig.\n# @RELATION: CALLS -> ensure_connection_configs_table\ndef _ensure_connections_schema(db: Session):\n with belief_scope(\"ConnectionsRouter.ensure_schema\"):\n ensure_connection_configs_table(db.get_bind())\n\n\n# [/DEF:_ensure_connections_schema:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ConnectionSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/connections.py", - "start_line": 37, - "end_line": 55, + "start_line": 33, + "end_line": 50, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Pydantic model for connection response." }, "relations": [ @@ -13080,19 +11486,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ConnectionSchema:Class]\n# @PURPOSE: Pydantic model for connection response.\n# @COMPLEXITY: 3\n# @RELATION: BINDS_TO -> ConnectionConfig\nclass ConnectionSchema(BaseModel):\n id: str\n name: str\n type: str\n host: Optional[str] = None\n port: Optional[int] = None\n database: Optional[str] = None\n username: Optional[str] = None\n created_at: datetime\n\n class Config:\n orm_mode = True\n\n\n# [/DEF:ConnectionSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ConnectionCreate", "contract_type": "Class", "file_path": "backend/src/api/routes/connections.py", - "start_line": 58, - "end_line": 72, + "start_line": 53, + "end_line": 66, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Pydantic model for creating a connection." }, "relations": [ @@ -13104,24 +11510,22 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ConnectionCreate:Class]\n# @PURPOSE: Pydantic model for creating a connection.\n# @COMPLEXITY: 3\n# @RELATION: BINDS_TO -> ConnectionConfig\nclass ConnectionCreate(BaseModel):\n name: str\n type: str\n host: Optional[str] = None\n port: Optional[int] = None\n database: Optional[str] = None\n username: Optional[str] = None\n password: Optional[str] = None\n\n\n# [/DEF:ConnectionCreate:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "list_connections", "contract_type": "Function", "file_path": "backend/src/api/routes/connections.py", - "start_line": 75, - "end_line": 92, + "start_line": 69, + "end_line": 83, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Database session.", + "COMPLEXITY": 3, "POST": "Returns list of connection configs.", "PRE": "Database session is active.", - "PURPOSE": "Lists all saved connections.", - "RETURN": "List[ConnectionSchema] - List of connections." + "PURPOSE": "Lists all saved connections." }, "relations": [ { @@ -13138,12 +11542,6 @@ } ], "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", @@ -13161,32 +11559,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:list_connections:Function]\n# @PURPOSE: Lists all saved connections.\n# @COMPLEXITY: 3\n# @PRE: Database session is active.\n# @POST: Returns list of connection configs.\n# @PARAM: db (Session) - Database session.\n# @RETURN: List[ConnectionSchema] - List of connections.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.get(\"\", response_model=List[ConnectionSchema])\nasync def list_connections(db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.list_connections\"):\n _ensure_connections_schema(db)\n connections = db.query(ConnectionConfig).all()\n return connections\n\n\n# [/DEF:list_connections:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "create_connection", "contract_type": "Function", "file_path": "backend/src/api/routes/connections.py", - "start_line": 95, - "end_line": 121, + "start_line": 86, + "end_line": 108, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Database session.", + "COMPLEXITY": 3, "POST": "Connection is saved to DB.", "PRE": "Connection name is unique.", - "PURPOSE": "Creates a new connection configuration.", - "RETURN": "ConnectionSchema - Created connection." + "PURPOSE": "Creates a new connection configuration." }, "relations": [ { @@ -13203,12 +11593,6 @@ } ], "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", @@ -13226,32 +11610,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:create_connection:Function]\n# @PURPOSE: Creates a new connection configuration.\n# @COMPLEXITY: 3\n# @PRE: Connection name is unique.\n# @POST: Connection is saved to DB.\n# @PARAM: connection (ConnectionCreate) - Config data.\n# @PARAM: db (Session) - Database session.\n# @RETURN: ConnectionSchema - Created connection.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.post(\"\", response_model=ConnectionSchema, status_code=status.HTTP_201_CREATED)\nasync def create_connection(\n connection: ConnectionCreate, db: Session = Depends(get_db)\n):\n with belief_scope(\"ConnectionsRouter.create_connection\", f\"name={connection.name}\"):\n _ensure_connections_schema(db)\n db_connection = ConnectionConfig(**connection.dict())\n db.add(db_connection)\n db.commit()\n db.refresh(db_connection)\n logger.info(\n f\"[ConnectionsRouter.create_connection][Success] Created connection {db_connection.id}\"\n )\n return db_connection\n\n\n# [/DEF:create_connection:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "delete_connection", "contract_type": "Function", "file_path": "backend/src/api/routes/connections.py", - "start_line": 124, - "end_line": 156, + "start_line": 111, + "end_line": 139, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Database session.", + "COMPLEXITY": 3, "POST": "Connection is removed from DB.", "PRE": "Connection ID exists.", - "PURPOSE": "Deletes a connection configuration.", - "RETURN": "None." + "PURPOSE": "Deletes a connection configuration." }, "relations": [ { @@ -13268,12 +11644,6 @@ } ], "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", @@ -13291,39 +11661,27 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:delete_connection:Function]\n# @PURPOSE: Deletes a connection configuration.\n# @COMPLEXITY: 3\n# @PRE: Connection ID exists.\n# @POST: Connection is removed from DB.\n# @PARAM: connection_id (str) - ID to delete.\n# @PARAM: db (Session) - Database session.\n# @RETURN: None.\n# @RELATION: CALLS -> _ensure_connections_schema\n# @RELATION: DEPENDS_ON -> ConnectionConfig\n@router.delete(\"/{connection_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_connection(connection_id: str, db: Session = Depends(get_db)):\n with belief_scope(\"ConnectionsRouter.delete_connection\", f\"id={connection_id}\"):\n _ensure_connections_schema(db)\n db_connection = (\n db.query(ConnectionConfig)\n .filter(ConnectionConfig.id == connection_id)\n .first()\n )\n if not db_connection:\n logger.error(\n f\"[ConnectionsRouter.delete_connection][State] Connection {connection_id} not found\"\n )\n raise HTTPException(status_code=404, detail=\"Connection not found\")\n db.delete(db_connection)\n db.commit()\n logger.info(\n f\"[ConnectionsRouter.delete_connection][Success] Deleted connection {connection_id}\"\n )\n return\n\n\n# [/DEF:delete_connection:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DashboardsApi", "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/__init__.py", "start_line": 1, - "end_line": 44, + "end_line": 42, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input(env_id, filters) -> Output(DashboardsResponse)", "INVARIANT": "All dashboard responses include git_status and last_task metadata", "LAYER": "API", "POST": "Dashboard responses are projected into DashboardsResponse DTO.", "PRE": "Valid environment configurations exist in ConfigManager.", "PURPOSE": "API endpoints for the Dashboard Hub - listing dashboards with Git and task status", - "SEMANTICS": [ - "api", - "dashboards", - "resources", - "hub" - ], "SIDE_EFFECT": "Performs external calls to Superset API and potentially Git providers.", "TEST_CONTRACT": "DashboardsAPI -> {" }, @@ -13388,27 +11746,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardsApi:Module]\n#\n# @COMPLEXITY: 5\n# @SEMANTICS: api, dashboards, resources, hub\n# @PURPOSE: API endpoints for the Dashboard Hub - listing dashboards with Git and task status\n# @LAYER: API\n# @RELATION: DEPENDS_ON ->[AppDependencies]\n# @RELATION: DEPENDS_ON ->[ResourceService]\n# @RELATION: DEPENDS_ON ->[SupersetClient]\n#\n# @INVARIANT: All dashboard responses include git_status and last_task metadata\n#\n# @PRE: Valid environment configurations exist in ConfigManager.\n# @POST: Dashboard responses are projected into DashboardsResponse DTO.\n# @SIDE_EFFECT: Performs external calls to Superset API and potentially Git providers.\n# @DATA_CONTRACT: Input(env_id, filters) -> Output(DashboardsResponse)\n#\n# @TEST_CONTRACT: DashboardsAPI -> {\n# required_fields: {env_id: string, page: integer, page_size: integer},\n# optional_fields: {search: string},\n# invariants: [\"Pagination must be valid\", \"Environment must exist\"]\n# }\n#\n# @TEST_FIXTURE: dashboard_list_happy -> {\n# \"env_id\": \"prod\",\n# \"expected_count\": 1,\n# \"dashboards\": [{\"id\": 1, \"title\": \"Main Revenue\"}]\n# }\n#\n# @TEST_EDGE: pagination_zero_page -> {\"env_id\": \"prod\", \"page\": 0, \"status\": 400}\n# @TEST_EDGE: pagination_oversize -> {\"env_id\": \"prod\", \"page_size\": 101, \"status\": 400}\n# @TEST_EDGE: missing_env -> {\"env_id\": \"ghost\", \"status\": 404}\n# @TEST_EDGE: empty_dashboards -> {\"env_id\": \"empty_env\", \"expected_total\": 0}\n# @TEST_EDGE: external_superset_failure -> {\"env_id\": \"bad_conn\", \"status\": 503}\n#\n# @TEST_INVARIANT: metadata_consistency -> verifies: [dashboard_list_happy, empty_dashboards]\n\nfrom ._router import router\nfrom ._schemas import *\nfrom ._listing_routes import *\nfrom ._detail_routes import *\nfrom ._action_routes import *\n\n# [/DEF:DashboardsApi:Module]\n" + "anchor_syntax": "region", + "body": "# #region DashboardsApi [C:5] [TYPE Module] [SEMANTICS dashboard, api, package, search, git, task]\n#\n# @BRIEF API endpoints for the Dashboard Hub - listing dashboards with Git and task status\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [ResourceService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n#\n# @INVARIANT: All dashboard responses include git_status and last_task metadata\n#\n# @PRE: Valid environment configurations exist in ConfigManager.\n# @POST: Dashboard responses are projected into DashboardsResponse DTO.\n# @SIDE_EFFECT: Performs external calls to Superset API and potentially Git providers.\n# @DATA_CONTRACT: Input(env_id, filters) -> Output(DashboardsResponse)\n#\n# @TEST_CONTRACT: DashboardsAPI -> {\n# required_fields: {env_id: string, page: integer, page_size: integer},\n# optional_fields: {search: string},\n# invariants: [\"Pagination must be valid\", \"Environment must exist\"]\n# }\n#\n# @TEST_FIXTURE: dashboard_list_happy -> {\n# \"env_id\": \"prod\",\n# \"expected_count\": 1,\n# \"dashboards\": [{\"id\": 1, \"title\": \"Main Revenue\"}]\n# }\n#\n# @TEST_EDGE: pagination_zero_page -> {\"env_id\": \"prod\", \"page\": 0, \"status\": 400}\n# @TEST_EDGE: pagination_oversize -> {\"env_id\": \"prod\", \"page_size\": 101, \"status\": 400}\n# @TEST_EDGE: missing_env -> {\"env_id\": \"ghost\", \"status\": 404}\n# @TEST_EDGE: empty_dashboards -> {\"env_id\": \"empty_env\", \"expected_total\": 0}\n# @TEST_EDGE: external_superset_failure -> {\"env_id\": \"bad_conn\", \"status\": 503}\n#\n# @TEST_INVARIANT: metadata_consistency -> verifies: [dashboard_list_happy, empty_dashboards]\n\nfrom ._action_routes import *\nfrom ._detail_routes import *\nfrom ._listing_routes import *\nfrom ._router import router\nfrom ._schemas import *\n\n# #endregion DashboardsApi\n" }, { "contract_id": "DashboardActionRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/_action_routes.py", "start_line": 1, - "end_line": 176, + "end_line": 167, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "API", - "PURPOSE": "Dashboard action route handlers — migrate, backup.", - "SEMANTICS": [ - "api", - "dashboards", - "actions", - "routes" - ] + "PURPOSE": "Dashboard action route handlers — migrate, backup." }, "relations": [ { @@ -13435,24 +11787,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardActionRoutes:Module]\n# @COMPLEXITY: 2\n# @SEMANTICS: api, dashboards, actions, routes\n# @PURPOSE: Dashboard action route handlers — migrate, backup.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DashboardRouter]\n# @RELATION: DEPENDS_ON -> [DashboardSchemas]\n\n# [SECTION: IMPORTS]\nfrom fastapi import Depends, HTTPException\n\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n has_permission,\n)\nfrom src.core.logger import logger, belief_scope\nfrom ._schemas import (\n MigrateRequest,\n BackupRequest,\n TaskResponse,\n)\nfrom ._router import router\n# [/SECTION]\n\n\n# [DEF:migrate_dashboards:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Trigger bulk migration of dashboards from source to target environment\n# @PRE: User has permission plugin:migration:execute\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @PRE: dashboard_ids is a non-empty list\n# @POST: Returns task_id for tracking migration progress\n# @POST: Task is created and queued for execution\n# @PARAM: request (MigrateRequest) - Migration request with source, target, and dashboard IDs\n# @RETURN: TaskResponse - Task ID for tracking\n# @RELATION: DISPATCHES ->[MigrationPlugin:execute]\n# @RELATION: CALLS ->[TaskManager]\n@router.post(\"/migrate\", response_model=TaskResponse)\nasync def migrate_dashboards(\n request: MigrateRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\n \"migrate_dashboards\",\n f\"source={request.source_env_id}, target={request.target_env_id}, count={len(request.dashboard_ids)}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[migrate_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n source_env = next(\n (e for e in environments if e.id == request.source_env_id), None\n )\n target_env = next(\n (e for e in environments if e.id == request.target_env_id), None\n )\n\n if not source_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Source environment not found: {request.source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Target environment not found: {request.target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n task_params = {\n \"source_env_id\": request.source_env_id,\n \"target_env_id\": request.target_env_id,\n \"selected_ids\": request.dashboard_ids,\n \"replace_db_config\": request.replace_db_config,\n \"db_mappings\": request.db_mappings or {},\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-migration\", params=task_params\n )\n\n logger.info(\n f\"[migrate_dashboards][Coherence:OK] Migration task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Failed to create migration task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create migration task: {str(e)}\"\n )\n\n\n# [/DEF:migrate_dashboards:Function]\n\n\n# [DEF:backup_dashboards:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Trigger bulk backup of dashboards with optional cron schedule\n# @PRE: User has permission plugin:backup:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dashboard_ids is a non-empty list\n# @POST: Returns task_id for tracking backup progress\n# @POST: Task is created and queued for execution\n# @POST: If schedule is provided, a scheduled task is created\n# @PARAM: request (BackupRequest) - Backup request with environment and dashboard IDs\n# @RETURN: TaskResponse - Task ID for tracking\n# @RELATION: DISPATCHES ->[BackupPlugin:execute]\n# @RELATION: CALLS ->[TaskManager]\n@router.post(\"/backup\", response_model=TaskResponse)\nasync def backup_dashboards(\n request: BackupRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:backup\", \"EXECUTE\")),\n):\n with belief_scope(\n \"backup_dashboards\",\n f\"env={request.env_id}, count={len(request.dashboard_ids)}, schedule={request.schedule}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[backup_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Environment not found: {request.env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n task_params = {\n \"env\": request.env_id,\n \"dashboards\": request.dashboard_ids,\n \"schedule\": request.schedule,\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-backup\", params=task_params\n )\n\n logger.info(\n f\"[backup_dashboards][Coherence:OK] Backup task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Failed to create backup task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create backup task: {str(e)}\"\n )\n\n\n# [/DEF:backup_dashboards:Function]\n# [/DEF:DashboardActionRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region DashboardActionRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, dashboard, api, backup]\n# @BRIEF Dashboard action route handlers — migrate, backup.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [DashboardRouter]\n# @RELATION DEPENDS_ON -> [DashboardSchemas]\n\nfrom fastapi import Depends, HTTPException\n\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n has_permission,\n)\n\nfrom ._router import router\nfrom ._schemas import (\n BackupRequest,\n MigrateRequest,\n TaskResponse,\n)\n\n\n# #region migrate_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger bulk migration of dashboards from source to target environment\n# @PRE: User has permission plugin:migration:execute\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @PRE: dashboard_ids is a non-empty list\n# @POST: Returns task_id for tracking migration progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [MigrationPlugin:execute]\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/migrate\", response_model=TaskResponse)\nasync def migrate_dashboards(\n request: MigrateRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\n \"migrate_dashboards\",\n f\"source={request.source_env_id}, target={request.target_env_id}, count={len(request.dashboard_ids)}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[migrate_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n source_env = next(\n (e for e in environments if e.id == request.source_env_id), None\n )\n target_env = next(\n (e for e in environments if e.id == request.target_env_id), None\n )\n\n if not source_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Source environment not found: {request.source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Target environment not found: {request.target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n task_params = {\n \"source_env_id\": request.source_env_id,\n \"target_env_id\": request.target_env_id,\n \"selected_ids\": request.dashboard_ids,\n \"replace_db_config\": request.replace_db_config,\n \"db_mappings\": request.db_mappings or {},\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-migration\", params=task_params\n )\n\n logger.info(\n f\"[migrate_dashboards][Coherence:OK] Migration task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Failed to create migration task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create migration task: {e!s}\"\n )\n\n\n# #endregion migrate_dashboards\n\n\n# #region backup_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger bulk backup of dashboards with optional cron schedule\n# @PRE: User has permission plugin:backup:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dashboard_ids is a non-empty list\n# @POST: Returns task_id for tracking backup progress\n# @POST: Task is created and queued for execution\n# @POST: If schedule is provided, a scheduled task is created\n# @RELATION DISPATCHES -> [BackupPlugin:execute]\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/backup\", response_model=TaskResponse)\nasync def backup_dashboards(\n request: BackupRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:backup\", \"EXECUTE\")),\n):\n with belief_scope(\n \"backup_dashboards\",\n f\"env={request.env_id}, count={len(request.dashboard_ids)}, schedule={request.schedule}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[backup_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Environment not found: {request.env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n task_params = {\n \"env\": request.env_id,\n \"dashboards\": request.dashboard_ids,\n \"schedule\": request.schedule,\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-backup\", params=task_params\n )\n\n logger.info(\n f\"[backup_dashboards][Coherence:OK] Backup task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Failed to create backup task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create backup task: {e!s}\"\n )\n\n\n# #endregion backup_dashboards\n# #endregion DashboardActionRoutes\n" }, { "contract_id": "migrate_dashboards", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_action_routes.py", - "start_line": 27, - "end_line": 105, + "start_line": 24, + "end_line": 99, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "request (MigrateRequest) - Migration request with source, target, and dashboard IDs", + "COMPLEXITY": 2, "POST": "Task is created and queued for execution", "PRE": "dashboard_ids is a non-empty list", - "PURPOSE": "Trigger bulk migration of dashboards from source to target environment", - "RETURN": "TaskResponse - Task ID for tracking" + "PURPOSE": "Trigger bulk migration of dashboards from source to target environment" }, "relations": [ { @@ -13469,12 +11819,6 @@ } ], "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", @@ -13493,12 +11837,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -13509,24 +11847,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:migrate_dashboards:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Trigger bulk migration of dashboards from source to target environment\n# @PRE: User has permission plugin:migration:execute\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @PRE: dashboard_ids is a non-empty list\n# @POST: Returns task_id for tracking migration progress\n# @POST: Task is created and queued for execution\n# @PARAM: request (MigrateRequest) - Migration request with source, target, and dashboard IDs\n# @RETURN: TaskResponse - Task ID for tracking\n# @RELATION: DISPATCHES ->[MigrationPlugin:execute]\n# @RELATION: CALLS ->[TaskManager]\n@router.post(\"/migrate\", response_model=TaskResponse)\nasync def migrate_dashboards(\n request: MigrateRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\n \"migrate_dashboards\",\n f\"source={request.source_env_id}, target={request.target_env_id}, count={len(request.dashboard_ids)}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[migrate_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n source_env = next(\n (e for e in environments if e.id == request.source_env_id), None\n )\n target_env = next(\n (e for e in environments if e.id == request.target_env_id), None\n )\n\n if not source_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Source environment not found: {request.source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Target environment not found: {request.target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n task_params = {\n \"source_env_id\": request.source_env_id,\n \"target_env_id\": request.target_env_id,\n \"selected_ids\": request.dashboard_ids,\n \"replace_db_config\": request.replace_db_config,\n \"db_mappings\": request.db_mappings or {},\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-migration\", params=task_params\n )\n\n logger.info(\n f\"[migrate_dashboards][Coherence:OK] Migration task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Failed to create migration task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create migration task: {str(e)}\"\n )\n\n\n# [/DEF:migrate_dashboards:Function]\n" + "anchor_syntax": "region", + "body": "# #region migrate_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger bulk migration of dashboards from source to target environment\n# @PRE: User has permission plugin:migration:execute\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @PRE: dashboard_ids is a non-empty list\n# @POST: Returns task_id for tracking migration progress\n# @POST: Task is created and queued for execution\n# @RELATION DISPATCHES -> [MigrationPlugin:execute]\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/migrate\", response_model=TaskResponse)\nasync def migrate_dashboards(\n request: MigrateRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\n \"migrate_dashboards\",\n f\"source={request.source_env_id}, target={request.target_env_id}, count={len(request.dashboard_ids)}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[migrate_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n source_env = next(\n (e for e in environments if e.id == request.source_env_id), None\n )\n target_env = next(\n (e for e in environments if e.id == request.target_env_id), None\n )\n\n if not source_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Source environment not found: {request.source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Target environment not found: {request.target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n task_params = {\n \"source_env_id\": request.source_env_id,\n \"target_env_id\": request.target_env_id,\n \"selected_ids\": request.dashboard_ids,\n \"replace_db_config\": request.replace_db_config,\n \"db_mappings\": request.db_mappings or {},\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-migration\", params=task_params\n )\n\n logger.info(\n f\"[migrate_dashboards][Coherence:OK] Migration task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[migrate_dashboards][Coherence:Failed] Failed to create migration task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create migration task: {e!s}\"\n )\n\n\n# #endregion migrate_dashboards\n" }, { "contract_id": "backup_dashboards", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_action_routes.py", - "start_line": 108, - "end_line": 175, + "start_line": 102, + "end_line": 166, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "request (BackupRequest) - Backup request with environment and dashboard IDs", + "COMPLEXITY": 2, "POST": "If schedule is provided, a scheduled task is created", "PRE": "dashboard_ids is a non-empty list", - "PURPOSE": "Trigger bulk backup of dashboards with optional cron schedule", - "RETURN": "TaskResponse - Task ID for tracking" + "PURPOSE": "Trigger bulk backup of dashboards with optional cron schedule" }, "relations": [ { @@ -13543,12 +11879,6 @@ } ], "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", @@ -13567,12 +11897,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -13583,27 +11907,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:backup_dashboards:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Trigger bulk backup of dashboards with optional cron schedule\n# @PRE: User has permission plugin:backup:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dashboard_ids is a non-empty list\n# @POST: Returns task_id for tracking backup progress\n# @POST: Task is created and queued for execution\n# @POST: If schedule is provided, a scheduled task is created\n# @PARAM: request (BackupRequest) - Backup request with environment and dashboard IDs\n# @RETURN: TaskResponse - Task ID for tracking\n# @RELATION: DISPATCHES ->[BackupPlugin:execute]\n# @RELATION: CALLS ->[TaskManager]\n@router.post(\"/backup\", response_model=TaskResponse)\nasync def backup_dashboards(\n request: BackupRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:backup\", \"EXECUTE\")),\n):\n with belief_scope(\n \"backup_dashboards\",\n f\"env={request.env_id}, count={len(request.dashboard_ids)}, schedule={request.schedule}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[backup_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Environment not found: {request.env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n task_params = {\n \"env\": request.env_id,\n \"dashboards\": request.dashboard_ids,\n \"schedule\": request.schedule,\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-backup\", params=task_params\n )\n\n logger.info(\n f\"[backup_dashboards][Coherence:OK] Backup task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Failed to create backup task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create backup task: {str(e)}\"\n )\n\n\n# [/DEF:backup_dashboards:Function]\n" + "anchor_syntax": "region", + "body": "# #region backup_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger bulk backup of dashboards with optional cron schedule\n# @PRE: User has permission plugin:backup:execute\n# @PRE: env_id is a valid environment ID\n# @PRE: dashboard_ids is a non-empty list\n# @POST: Returns task_id for tracking backup progress\n# @POST: Task is created and queued for execution\n# @POST: If schedule is provided, a scheduled task is created\n# @RELATION DISPATCHES -> [BackupPlugin:execute]\n# @RELATION CALLS -> [TaskManager]\n@router.post(\"/backup\", response_model=TaskResponse)\nasync def backup_dashboards(\n request: BackupRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:backup\", \"EXECUTE\")),\n):\n with belief_scope(\n \"backup_dashboards\",\n f\"env={request.env_id}, count={len(request.dashboard_ids)}, schedule={request.schedule}\",\n ):\n if not request.dashboard_ids:\n logger.error(\n \"[backup_dashboards][Coherence:Failed] No dashboard IDs provided\"\n )\n raise HTTPException(\n status_code=400, detail=\"At least one dashboard ID must be provided\"\n )\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Environment not found: {request.env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n task_params = {\n \"env\": request.env_id,\n \"dashboards\": request.dashboard_ids,\n \"schedule\": request.schedule,\n }\n\n task_obj = await task_manager.create_task(\n plugin_id=\"superset-backup\", params=task_params\n )\n\n logger.info(\n f\"[backup_dashboards][Coherence:OK] Backup task created: {task_obj.id} for {len(request.dashboard_ids)} dashboards\"\n )\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(\n f\"[backup_dashboards][Coherence:Failed] Failed to create backup task: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to create backup task: {e!s}\"\n )\n\n\n# #endregion backup_dashboards\n" }, { "contract_id": "DashboardDetailRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/_detail_routes.py", "start_line": 1, - "end_line": 392, + "end_line": 382, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "API", - "PURPOSE": "Dashboard detail, db-mappings, task history, thumbnail route handlers.", - "SEMANTICS": [ - "api", - "dashboards", - "detail", - "routes" - ] + "PURPOSE": "Dashboard detail, db-mappings, task history, thumbnail route handlers." }, "relations": [ { @@ -13632,24 +11950,22 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DashboardDetailRoutes:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: api, dashboards, detail, routes\n# @PURPOSE: Dashboard detail, db-mappings, task history, thumbnail route handlers.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DashboardRouter]\n# @RELATION: DEPENDS_ON -> [DashboardSchemas]\n# @RELATION: DEPENDS_ON -> [DashboardHelpers]\n# @RELATION: DEPENDS_ON -> [DashboardProjection]\n\n# [SECTION: IMPORTS]\nfrom typing import Optional, List, Dict, Any\nimport re\nfrom urllib.parse import urlparse\n\nfrom fastapi import Depends, HTTPException, Query, Response\nfrom fastapi.responses import JSONResponse\n\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n get_mapping_service,\n has_permission,\n)\nfrom src.core.async_superset_client import AsyncSupersetClient\nfrom src.core.superset_client import SupersetClient\nfrom src.core.logger import logger, belief_scope\nfrom src.core.utils.network import DashboardNotFoundError\nfrom ._helpers import (\n _resolve_dashboard_id_from_ref,\n _resolve_dashboard_id_from_ref_async,\n)\nfrom ._projection import (\n _task_matches_dashboard,\n)\nfrom ._schemas import (\n DashboardDetailResponse,\n DashboardTaskHistoryItem,\n DashboardTaskHistoryResponse,\n DatabaseMapping,\n DatabaseMappingsResponse,\n)\nfrom ._router import router\n# [/SECTION]\n\n\n# [DEF:get_database_mappings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Get database mapping suggestions between source and target environments\n# @PRE: User has permission plugin:migration:read\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @POST: Returns list of suggested database mappings with confidence scores\n# @PARAM: source_env_id (str) - Source environment ID\n# @PARAM: target_env_id (str) - Target environment ID\n# @RETURN: DatabaseMappingsResponse - List of suggested mappings\n# @RELATION: CALLS ->[MappingService:get_suggestions]\n@router.get(\"/db-mappings\", response_model=DatabaseMappingsResponse)\nasync def get_database_mappings(\n source_env_id: str,\n target_env_id: str,\n config_manager=Depends(get_config_manager),\n mapping_service=Depends(get_mapping_service),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_database_mappings\", f\"source={source_env_id}, target={target_env_id}\"\n ):\n environments = config_manager.get_environments()\n source_env = next((e for e in environments if e.id == source_env_id), None)\n target_env = next((e for e in environments if e.id == target_env_id), None)\n\n if not source_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Source environment not found: {source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Target environment not found: {target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n suggestions = await mapping_service.get_suggestions(\n source_env_id, target_env_id\n )\n\n mappings = [\n DatabaseMapping(\n source_db=s.get(\"source_db\", \"\"),\n target_db=s.get(\"target_db\", \"\"),\n source_db_uuid=s.get(\"source_db_uuid\"),\n target_db_uuid=s.get(\"target_db_uuid\"),\n confidence=s.get(\"confidence\", 0.0),\n )\n for s in suggestions\n ]\n\n logger.info(\n f\"[get_database_mappings][Coherence:OK] Returning {len(mappings)} database mapping suggestions\"\n )\n\n return DatabaseMappingsResponse(mappings=mappings)\n\n except Exception as e:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to get database mappings: {str(e)}\"\n )\n\n\n# [/DEF:get_database_mappings:Function]\n\n\n# [DEF:get_dashboard_detail:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Fetch detailed dashboard info with related charts and datasets\n# @PRE: env_id must be valid and dashboard ref (slug or id) must exist\n# @POST: Returns dashboard detail payload for overview page\n# @RELATION: CALLS ->[AsyncSupersetClient]\n@router.get(\"/{dashboard_ref}\", response_model=DashboardDetailResponse)\nasync def get_dashboard_detail(\n dashboard_ref: str,\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_detail\", f\"dashboard_ref={dashboard_ref}, env_id={env_id}\"\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n sync_client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, sync_client)\n detail = sync_client.get_dashboard_detail(dashboard_id)\n logger.info(\n f\"[get_dashboard_detail][Coherence:OK] Dashboard ref={dashboard_ref} resolved_id={dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets\"\n )\n return DashboardDetailResponse(**detail)\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboard detail: {str(e)}\"\n )\n\n\n# [/DEF:get_dashboard_detail:Function]\n\n\n# [DEF:get_dashboard_tasks_history:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Returns history of backup and LLM validation tasks for a dashboard.\n# @PRE: dashboard ref (slug or id) is valid.\n# @POST: Response contains sorted task history (newest first).\n@router.get(\"/{dashboard_ref}/tasks\", response_model=DashboardTaskHistoryResponse)\nasync def get_dashboard_tasks_history(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n limit: int = Query(20, ge=1, le=100),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_tasks_history\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, limit={limit}\",\n ):\n dashboard_id: Optional[int] = None\n client: Optional[AsyncSupersetClient] = None\n try:\n if dashboard_ref.isdigit():\n dashboard_id = int(dashboard_ref)\n elif env_id:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_tasks_history][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n client = AsyncSupersetClient(env)\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, client\n )\n else:\n logger.error(\n \"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id\"\n )\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required when dashboard reference is a slug\",\n )\n\n matching_tasks = []\n for task in task_manager.get_all_tasks():\n if _task_matches_dashboard(task, dashboard_id, env_id):\n matching_tasks.append(task)\n\n def _sort_key(task_obj: Any) -> str:\n return str(getattr(task_obj, \"started_at\", \"\") or \"\") or str(\n getattr(task_obj, \"finished_at\", \"\") or \"\"\n )\n\n matching_tasks.sort(key=_sort_key, reverse=True)\n selected = matching_tasks[:limit]\n\n items = []\n for task in selected:\n result = getattr(task, \"result\", None)\n summary = None\n validation_status = None\n if isinstance(result, dict):\n raw_validation_status = result.get(\"status\")\n if raw_validation_status is not None:\n validation_status = str(raw_validation_status)\n summary = (\n result.get(\"summary\")\n or result.get(\"status\")\n or result.get(\"message\")\n )\n params = getattr(task, \"params\", {}) or {}\n items.append(\n DashboardTaskHistoryItem(\n id=str(getattr(task, \"id\", \"\")),\n plugin_id=str(getattr(task, \"plugin_id\", \"\")),\n status=str(getattr(task, \"status\", \"\")),\n validation_status=validation_status,\n started_at=getattr(task, \"started_at\", None).isoformat()\n if getattr(task, \"started_at\", None)\n else None,\n finished_at=getattr(task, \"finished_at\", None).isoformat()\n if getattr(task, \"finished_at\", None)\n else None,\n env_id=str(params.get(\"environment_id\") or params.get(\"env\"))\n if (params.get(\"environment_id\") or params.get(\"env\"))\n else None,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}\"\n )\n return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)\n finally:\n if client is not None:\n await client.aclose()\n\n\n# [/DEF:get_dashboard_tasks_history:Function]\n\n\n# [DEF:get_dashboard_thumbnail:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Proxies Superset dashboard thumbnail with cache support.\n# @RELATION: CALLS ->[AsyncSupersetClient]\n# @PRE: env_id must exist.\n# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.\n@router.get(\"/{dashboard_ref}/thumbnail\")\nasync def get_dashboard_thumbnail(\n dashboard_ref: str,\n env_id: str,\n force: bool = Query(False),\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_thumbnail\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, force={force}\",\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)\n digest = None\n thumb_endpoint = None\n\n try:\n screenshot_payload = client.network.request(\n method=\"POST\",\n endpoint=f\"/dashboard/{dashboard_id}/cache_dashboard_screenshot/\",\n json={\"force\": force},\n )\n payload = (\n screenshot_payload.get(\"result\", screenshot_payload)\n if isinstance(screenshot_payload, dict)\n else {}\n )\n image_url = (\n payload.get(\"image_url\", \"\") if isinstance(payload, dict) else \"\"\n )\n if isinstance(image_url, str) and image_url:\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n image_url,\n )\n if matched:\n digest = matched.group(1)\n except DashboardNotFoundError:\n logger.warning(\n \"[get_dashboard_thumbnail][Fallback] cache_dashboard_screenshot endpoint unavailable, fallback to dashboard.thumbnail_url\"\n )\n\n if not digest:\n dashboard_payload = client.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}\",\n )\n dashboard_data = (\n dashboard_payload.get(\"result\", dashboard_payload)\n if isinstance(dashboard_payload, dict)\n else {}\n )\n thumbnail_url = (\n dashboard_data.get(\"thumbnail_url\", \"\")\n if isinstance(dashboard_data, dict)\n else \"\"\n )\n if isinstance(thumbnail_url, str) and thumbnail_url:\n parsed = urlparse(thumbnail_url)\n parsed_path = parsed.path or thumbnail_url\n if parsed_path.startswith(\"/api/v1/\"):\n parsed_path = parsed_path[len(\"/api/v1\") :]\n thumb_endpoint = parsed_path\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n parsed_path,\n )\n if matched:\n digest = matched.group(1)\n\n if not thumb_endpoint:\n thumb_endpoint = (\n f\"/dashboard/{dashboard_id}/thumbnail/{digest or 'latest'}/\"\n )\n\n thumb_response = client.network.request(\n method=\"GET\",\n endpoint=thumb_endpoint,\n raw_response=True,\n allow_redirects=True,\n )\n\n if thumb_response.status_code == 202:\n payload_202: Dict[str, Any] = {}\n try:\n payload_202 = thumb_response.json()\n except Exception:\n payload_202 = {\"message\": \"Thumbnail is being generated\"}\n return JSONResponse(status_code=202, content=payload_202)\n\n content_type = thumb_response.headers.get(\"Content-Type\", \"image/png\")\n return Response(content=thumb_response.content, media_type=content_type)\n except DashboardNotFoundError as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Dashboard not found for thumbnail: {e}\"\n )\n raise HTTPException(status_code=404, detail=\"Dashboard thumbnail not found\")\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Failed to fetch dashboard thumbnail: {e}\"\n )\n raise HTTPException(\n status_code=503,\n detail=f\"Failed to fetch dashboard thumbnail: {str(e)}\",\n )\n\n\n# [/DEF:get_dashboard_thumbnail:Function]\n# [/DEF:DashboardDetailRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region DashboardDetailRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, dashboard, api, transform, search, task]\n# @BRIEF Dashboard detail, db-mappings, task history, thumbnail route handlers.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [DashboardRouter]\n# @RELATION DEPENDS_ON -> [DashboardSchemas]\n# @RELATION DEPENDS_ON -> [DashboardHelpers]\n# @RELATION DEPENDS_ON -> [DashboardProjection]\n\nimport re\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nfrom fastapi import Depends, HTTPException, Query, Response\nfrom fastapi.responses import JSONResponse\n\nfrom src.core.async_superset_client import AsyncSupersetClient\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.core.utils.network import DashboardNotFoundError\nfrom src.dependencies import (\n get_config_manager,\n get_mapping_service,\n get_task_manager,\n has_permission,\n)\n\nfrom ._helpers import (\n _resolve_dashboard_id_from_ref,\n _resolve_dashboard_id_from_ref_async,\n)\nfrom ._projection import (\n _task_matches_dashboard,\n)\nfrom ._router import router\nfrom ._schemas import (\n DashboardDetailResponse,\n DashboardTaskHistoryItem,\n DashboardTaskHistoryResponse,\n DatabaseMapping,\n DatabaseMappingsResponse,\n)\n\n\n# #region get_database_mappings [C:2] [TYPE Function]\n# @BRIEF Get database mapping suggestions between source and target environments\n# @PRE: User has permission plugin:migration:read\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @POST: Returns list of suggested database mappings with confidence scores\n# @RELATION CALLS -> [MappingService:get_suggestions]\n@router.get(\"/db-mappings\", response_model=DatabaseMappingsResponse)\nasync def get_database_mappings(\n source_env_id: str,\n target_env_id: str,\n config_manager=Depends(get_config_manager),\n mapping_service=Depends(get_mapping_service),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_database_mappings\", f\"source={source_env_id}, target={target_env_id}\"\n ):\n environments = config_manager.get_environments()\n source_env = next((e for e in environments if e.id == source_env_id), None)\n target_env = next((e for e in environments if e.id == target_env_id), None)\n\n if not source_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Source environment not found: {source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Target environment not found: {target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n suggestions = await mapping_service.get_suggestions(\n source_env_id, target_env_id\n )\n\n mappings = [\n DatabaseMapping(\n source_db=s.get(\"source_db\", \"\"),\n target_db=s.get(\"target_db\", \"\"),\n source_db_uuid=s.get(\"source_db_uuid\"),\n target_db_uuid=s.get(\"target_db_uuid\"),\n confidence=s.get(\"confidence\", 0.0),\n )\n for s in suggestions\n ]\n\n logger.info(\n f\"[get_database_mappings][Coherence:OK] Returning {len(mappings)} database mapping suggestions\"\n )\n\n return DatabaseMappingsResponse(mappings=mappings)\n\n except Exception as e:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to get database mappings: {e!s}\"\n )\n\n\n# #endregion get_database_mappings\n\n\n# #region get_dashboard_detail [C:2] [TYPE Function]\n# @BRIEF Fetch detailed dashboard info with related charts and datasets\n# @PRE: env_id must be valid and dashboard ref (slug or id) must exist\n# @POST: Returns dashboard detail payload for overview page\n# @RELATION CALLS -> [AsyncSupersetClient]\n@router.get(\"/{dashboard_ref}\", response_model=DashboardDetailResponse)\nasync def get_dashboard_detail(\n dashboard_ref: str,\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_detail\", f\"dashboard_ref={dashboard_ref}, env_id={env_id}\"\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n sync_client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, sync_client)\n detail = sync_client.get_dashboard_detail(dashboard_id)\n logger.info(\n f\"[get_dashboard_detail][Coherence:OK] Dashboard ref={dashboard_ref} resolved_id={dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets\"\n )\n return DashboardDetailResponse(**detail)\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboard detail: {e!s}\"\n )\n\n\n# #endregion get_dashboard_detail\n\n\n# #region get_dashboard_tasks_history [C:2] [TYPE Function]\n# @BRIEF Returns history of backup and LLM validation tasks for a dashboard.\n# @PRE: dashboard ref (slug or id) is valid.\n# @POST: Response contains sorted task history (newest first).\n@router.get(\"/{dashboard_ref}/tasks\", response_model=DashboardTaskHistoryResponse)\nasync def get_dashboard_tasks_history(\n dashboard_ref: str,\n env_id: str | None = None,\n limit: int = Query(20, ge=1, le=100),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_tasks_history\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, limit={limit}\",\n ):\n dashboard_id: int | None = None\n client: AsyncSupersetClient | None = None\n try:\n if dashboard_ref.isdigit():\n dashboard_id = int(dashboard_ref)\n elif env_id:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_tasks_history][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n client = AsyncSupersetClient(env)\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, client\n )\n else:\n logger.error(\n \"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id\"\n )\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required when dashboard reference is a slug\",\n )\n\n matching_tasks = []\n for task in task_manager.get_all_tasks():\n if _task_matches_dashboard(task, dashboard_id, env_id):\n matching_tasks.append(task)\n\n def _sort_key(task_obj: Any) -> str:\n return str(getattr(task_obj, \"started_at\", \"\") or \"\") or str(\n getattr(task_obj, \"finished_at\", \"\") or \"\"\n )\n\n matching_tasks.sort(key=_sort_key, reverse=True)\n selected = matching_tasks[:limit]\n\n items = []\n for task in selected:\n result = getattr(task, \"result\", None)\n summary = None\n validation_status = None\n if isinstance(result, dict):\n raw_validation_status = result.get(\"status\")\n if raw_validation_status is not None:\n validation_status = str(raw_validation_status)\n summary = (\n result.get(\"summary\")\n or result.get(\"status\")\n or result.get(\"message\")\n )\n params = getattr(task, \"params\", {}) or {}\n items.append(\n DashboardTaskHistoryItem(\n id=str(getattr(task, \"id\", \"\")),\n plugin_id=str(getattr(task, \"plugin_id\", \"\")),\n status=str(getattr(task, \"status\", \"\")),\n validation_status=validation_status,\n started_at=getattr(task, \"started_at\", None).isoformat()\n if getattr(task, \"started_at\", None)\n else None,\n finished_at=getattr(task, \"finished_at\", None).isoformat()\n if getattr(task, \"finished_at\", None)\n else None,\n env_id=str(params.get(\"environment_id\") or params.get(\"env\"))\n if (params.get(\"environment_id\") or params.get(\"env\"))\n else None,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}\"\n )\n return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)\n finally:\n if client is not None:\n await client.aclose()\n\n\n# #endregion get_dashboard_tasks_history\n\n\n# #region get_dashboard_thumbnail [C:3] [TYPE Function]\n# @BRIEF Proxies Superset dashboard thumbnail with cache support.\n# @RELATION CALLS -> [AsyncSupersetClient]\n# @PRE: env_id must exist.\n# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.\n@router.get(\"/{dashboard_ref}/thumbnail\")\nasync def get_dashboard_thumbnail(\n dashboard_ref: str,\n env_id: str,\n force: bool = Query(False),\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_thumbnail\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, force={force}\",\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)\n digest = None\n thumb_endpoint = None\n\n try:\n screenshot_payload = client.network.request(\n method=\"POST\",\n endpoint=f\"/dashboard/{dashboard_id}/cache_dashboard_screenshot/\",\n json={\"force\": force},\n )\n payload = (\n screenshot_payload.get(\"result\", screenshot_payload)\n if isinstance(screenshot_payload, dict)\n else {}\n )\n image_url = (\n payload.get(\"image_url\", \"\") if isinstance(payload, dict) else \"\"\n )\n if isinstance(image_url, str) and image_url:\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n image_url,\n )\n if matched:\n digest = matched.group(1)\n except DashboardNotFoundError:\n logger.warning(\n \"[get_dashboard_thumbnail][Fallback] cache_dashboard_screenshot endpoint unavailable, fallback to dashboard.thumbnail_url\"\n )\n\n if not digest:\n dashboard_payload = client.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}\",\n )\n dashboard_data = (\n dashboard_payload.get(\"result\", dashboard_payload)\n if isinstance(dashboard_payload, dict)\n else {}\n )\n thumbnail_url = (\n dashboard_data.get(\"thumbnail_url\", \"\")\n if isinstance(dashboard_data, dict)\n else \"\"\n )\n if isinstance(thumbnail_url, str) and thumbnail_url:\n parsed = urlparse(thumbnail_url)\n parsed_path = parsed.path or thumbnail_url\n if parsed_path.startswith(\"/api/v1/\"):\n parsed_path = parsed_path[len(\"/api/v1\") :]\n thumb_endpoint = parsed_path\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n parsed_path,\n )\n if matched:\n digest = matched.group(1)\n\n if not thumb_endpoint:\n thumb_endpoint = (\n f\"/dashboard/{dashboard_id}/thumbnail/{digest or 'latest'}/\"\n )\n\n thumb_response = client.network.request(\n method=\"GET\",\n endpoint=thumb_endpoint,\n raw_response=True,\n allow_redirects=True,\n )\n\n if thumb_response.status_code == 202:\n payload_202: dict[str, Any] = {}\n try:\n payload_202 = thumb_response.json()\n except Exception:\n payload_202 = {\"message\": \"Thumbnail is being generated\"}\n return JSONResponse(status_code=202, content=payload_202)\n\n content_type = thumb_response.headers.get(\"Content-Type\", \"image/png\")\n return Response(content=thumb_response.content, media_type=content_type)\n except DashboardNotFoundError as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Dashboard not found for thumbnail: {e}\"\n )\n raise HTTPException(status_code=404, detail=\"Dashboard thumbnail not found\")\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Failed to fetch dashboard thumbnail: {e}\"\n )\n raise HTTPException(\n status_code=503,\n detail=f\"Failed to fetch dashboard thumbnail: {e!s}\",\n )\n\n\n# #endregion get_dashboard_thumbnail\n# #endregion DashboardDetailRoutes\n" }, { "contract_id": "get_database_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_detail_routes.py", - "start_line": 47, - "end_line": 114, + "start_line": 44, + "end_line": 107, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "target_env_id (str) - Target environment ID", + "COMPLEXITY": 2, "POST": "Returns list of suggested database mappings with confidence scores", "PRE": "source_env_id and target_env_id are valid environment IDs", - "PURPOSE": "Get database mapping suggestions between source and target environments", - "RETURN": "DatabaseMappingsResponse - List of suggested mappings" + "PURPOSE": "Get database mapping suggestions between source and target environments" }, "relations": [ { @@ -13660,12 +11976,6 @@ } ], "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", @@ -13684,12 +11994,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -13700,19 +12004,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_database_mappings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Get database mapping suggestions between source and target environments\n# @PRE: User has permission plugin:migration:read\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @POST: Returns list of suggested database mappings with confidence scores\n# @PARAM: source_env_id (str) - Source environment ID\n# @PARAM: target_env_id (str) - Target environment ID\n# @RETURN: DatabaseMappingsResponse - List of suggested mappings\n# @RELATION: CALLS ->[MappingService:get_suggestions]\n@router.get(\"/db-mappings\", response_model=DatabaseMappingsResponse)\nasync def get_database_mappings(\n source_env_id: str,\n target_env_id: str,\n config_manager=Depends(get_config_manager),\n mapping_service=Depends(get_mapping_service),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_database_mappings\", f\"source={source_env_id}, target={target_env_id}\"\n ):\n environments = config_manager.get_environments()\n source_env = next((e for e in environments if e.id == source_env_id), None)\n target_env = next((e for e in environments if e.id == target_env_id), None)\n\n if not source_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Source environment not found: {source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Target environment not found: {target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n suggestions = await mapping_service.get_suggestions(\n source_env_id, target_env_id\n )\n\n mappings = [\n DatabaseMapping(\n source_db=s.get(\"source_db\", \"\"),\n target_db=s.get(\"target_db\", \"\"),\n source_db_uuid=s.get(\"source_db_uuid\"),\n target_db_uuid=s.get(\"target_db_uuid\"),\n confidence=s.get(\"confidence\", 0.0),\n )\n for s in suggestions\n ]\n\n logger.info(\n f\"[get_database_mappings][Coherence:OK] Returning {len(mappings)} database mapping suggestions\"\n )\n\n return DatabaseMappingsResponse(mappings=mappings)\n\n except Exception as e:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to get database mappings: {str(e)}\"\n )\n\n\n# [/DEF:get_database_mappings:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_database_mappings [C:2] [TYPE Function]\n# @BRIEF Get database mapping suggestions between source and target environments\n# @PRE: User has permission plugin:migration:read\n# @PRE: source_env_id and target_env_id are valid environment IDs\n# @POST: Returns list of suggested database mappings with confidence scores\n# @RELATION CALLS -> [MappingService:get_suggestions]\n@router.get(\"/db-mappings\", response_model=DatabaseMappingsResponse)\nasync def get_database_mappings(\n source_env_id: str,\n target_env_id: str,\n config_manager=Depends(get_config_manager),\n mapping_service=Depends(get_mapping_service),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_database_mappings\", f\"source={source_env_id}, target={target_env_id}\"\n ):\n environments = config_manager.get_environments()\n source_env = next((e for e in environments if e.id == source_env_id), None)\n target_env = next((e for e in environments if e.id == target_env_id), None)\n\n if not source_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Source environment not found: {source_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Source environment not found\")\n if not target_env:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Target environment not found: {target_env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Target environment not found\")\n\n try:\n suggestions = await mapping_service.get_suggestions(\n source_env_id, target_env_id\n )\n\n mappings = [\n DatabaseMapping(\n source_db=s.get(\"source_db\", \"\"),\n target_db=s.get(\"target_db\", \"\"),\n source_db_uuid=s.get(\"source_db_uuid\"),\n target_db_uuid=s.get(\"target_db_uuid\"),\n confidence=s.get(\"confidence\", 0.0),\n )\n for s in suggestions\n ]\n\n logger.info(\n f\"[get_database_mappings][Coherence:OK] Returning {len(mappings)} database mapping suggestions\"\n )\n\n return DatabaseMappingsResponse(mappings=mappings)\n\n except Exception as e:\n logger.error(\n f\"[get_database_mappings][Coherence:Failed] Failed to get database mappings: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to get database mappings: {e!s}\"\n )\n\n\n# #endregion get_database_mappings\n" }, { "contract_id": "get_dashboard_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_detail_routes.py", - "start_line": 117, - "end_line": 160, + "start_line": 110, + "end_line": 152, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns dashboard detail payload for overview page", "PRE": "env_id must be valid and dashboard ref (slug or id) must exist", "PURPOSE": "Fetch detailed dashboard info with related charts and datasets" @@ -13754,19 +12058,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_dashboard_detail:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Fetch detailed dashboard info with related charts and datasets\n# @PRE: env_id must be valid and dashboard ref (slug or id) must exist\n# @POST: Returns dashboard detail payload for overview page\n# @RELATION: CALLS ->[AsyncSupersetClient]\n@router.get(\"/{dashboard_ref}\", response_model=DashboardDetailResponse)\nasync def get_dashboard_detail(\n dashboard_ref: str,\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_detail\", f\"dashboard_ref={dashboard_ref}, env_id={env_id}\"\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n sync_client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, sync_client)\n detail = sync_client.get_dashboard_detail(dashboard_id)\n logger.info(\n f\"[get_dashboard_detail][Coherence:OK] Dashboard ref={dashboard_ref} resolved_id={dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets\"\n )\n return DashboardDetailResponse(**detail)\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboard detail: {str(e)}\"\n )\n\n\n# [/DEF:get_dashboard_detail:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_dashboard_detail [C:2] [TYPE Function]\n# @BRIEF Fetch detailed dashboard info with related charts and datasets\n# @PRE: env_id must be valid and dashboard ref (slug or id) must exist\n# @POST: Returns dashboard detail payload for overview page\n# @RELATION CALLS -> [AsyncSupersetClient]\n@router.get(\"/{dashboard_ref}\", response_model=DashboardDetailResponse)\nasync def get_dashboard_detail(\n dashboard_ref: str,\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_detail\", f\"dashboard_ref={dashboard_ref}, env_id={env_id}\"\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n sync_client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, sync_client)\n detail = sync_client.get_dashboard_detail(dashboard_id)\n logger.info(\n f\"[get_dashboard_detail][Coherence:OK] Dashboard ref={dashboard_ref} resolved_id={dashboard_id}: {detail.get('chart_count', 0)} charts, {detail.get('dataset_count', 0)} datasets\"\n )\n return DashboardDetailResponse(**detail)\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_detail][Coherence:Failed] Failed to fetch dashboard detail: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboard detail: {e!s}\"\n )\n\n\n# #endregion get_dashboard_detail\n" }, { "contract_id": "get_dashboard_tasks_history", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_detail_routes.py", - "start_line": 163, - "end_line": 263, + "start_line": 155, + "end_line": 254, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Response contains sorted task history (newest first).", "PRE": "dashboard ref (slug or id) is valid.", "PURPOSE": "Returns history of backup and LLM validation tasks for a dashboard." @@ -13792,19 +12096,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_dashboard_tasks_history:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Returns history of backup and LLM validation tasks for a dashboard.\n# @PRE: dashboard ref (slug or id) is valid.\n# @POST: Response contains sorted task history (newest first).\n@router.get(\"/{dashboard_ref}/tasks\", response_model=DashboardTaskHistoryResponse)\nasync def get_dashboard_tasks_history(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n limit: int = Query(20, ge=1, le=100),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_tasks_history\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, limit={limit}\",\n ):\n dashboard_id: Optional[int] = None\n client: Optional[AsyncSupersetClient] = None\n try:\n if dashboard_ref.isdigit():\n dashboard_id = int(dashboard_ref)\n elif env_id:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_tasks_history][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n client = AsyncSupersetClient(env)\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, client\n )\n else:\n logger.error(\n \"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id\"\n )\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required when dashboard reference is a slug\",\n )\n\n matching_tasks = []\n for task in task_manager.get_all_tasks():\n if _task_matches_dashboard(task, dashboard_id, env_id):\n matching_tasks.append(task)\n\n def _sort_key(task_obj: Any) -> str:\n return str(getattr(task_obj, \"started_at\", \"\") or \"\") or str(\n getattr(task_obj, \"finished_at\", \"\") or \"\"\n )\n\n matching_tasks.sort(key=_sort_key, reverse=True)\n selected = matching_tasks[:limit]\n\n items = []\n for task in selected:\n result = getattr(task, \"result\", None)\n summary = None\n validation_status = None\n if isinstance(result, dict):\n raw_validation_status = result.get(\"status\")\n if raw_validation_status is not None:\n validation_status = str(raw_validation_status)\n summary = (\n result.get(\"summary\")\n or result.get(\"status\")\n or result.get(\"message\")\n )\n params = getattr(task, \"params\", {}) or {}\n items.append(\n DashboardTaskHistoryItem(\n id=str(getattr(task, \"id\", \"\")),\n plugin_id=str(getattr(task, \"plugin_id\", \"\")),\n status=str(getattr(task, \"status\", \"\")),\n validation_status=validation_status,\n started_at=getattr(task, \"started_at\", None).isoformat()\n if getattr(task, \"started_at\", None)\n else None,\n finished_at=getattr(task, \"finished_at\", None).isoformat()\n if getattr(task, \"finished_at\", None)\n else None,\n env_id=str(params.get(\"environment_id\") or params.get(\"env\"))\n if (params.get(\"environment_id\") or params.get(\"env\"))\n else None,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}\"\n )\n return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)\n finally:\n if client is not None:\n await client.aclose()\n\n\n# [/DEF:get_dashboard_tasks_history:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_dashboard_tasks_history [C:2] [TYPE Function]\n# @BRIEF Returns history of backup and LLM validation tasks for a dashboard.\n# @PRE: dashboard ref (slug or id) is valid.\n# @POST: Response contains sorted task history (newest first).\n@router.get(\"/{dashboard_ref}/tasks\", response_model=DashboardTaskHistoryResponse)\nasync def get_dashboard_tasks_history(\n dashboard_ref: str,\n env_id: str | None = None,\n limit: int = Query(20, ge=1, le=100),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_tasks_history\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, limit={limit}\",\n ):\n dashboard_id: int | None = None\n client: AsyncSupersetClient | None = None\n try:\n if dashboard_ref.isdigit():\n dashboard_id = int(dashboard_ref)\n elif env_id:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_tasks_history][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n client = AsyncSupersetClient(env)\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, client\n )\n else:\n logger.error(\n \"[get_dashboard_tasks_history][Coherence:Failed] Non-numeric dashboard ref requires env_id\"\n )\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required when dashboard reference is a slug\",\n )\n\n matching_tasks = []\n for task in task_manager.get_all_tasks():\n if _task_matches_dashboard(task, dashboard_id, env_id):\n matching_tasks.append(task)\n\n def _sort_key(task_obj: Any) -> str:\n return str(getattr(task_obj, \"started_at\", \"\") or \"\") or str(\n getattr(task_obj, \"finished_at\", \"\") or \"\"\n )\n\n matching_tasks.sort(key=_sort_key, reverse=True)\n selected = matching_tasks[:limit]\n\n items = []\n for task in selected:\n result = getattr(task, \"result\", None)\n summary = None\n validation_status = None\n if isinstance(result, dict):\n raw_validation_status = result.get(\"status\")\n if raw_validation_status is not None:\n validation_status = str(raw_validation_status)\n summary = (\n result.get(\"summary\")\n or result.get(\"status\")\n or result.get(\"message\")\n )\n params = getattr(task, \"params\", {}) or {}\n items.append(\n DashboardTaskHistoryItem(\n id=str(getattr(task, \"id\", \"\")),\n plugin_id=str(getattr(task, \"plugin_id\", \"\")),\n status=str(getattr(task, \"status\", \"\")),\n validation_status=validation_status,\n started_at=getattr(task, \"started_at\", None).isoformat()\n if getattr(task, \"started_at\", None)\n else None,\n finished_at=getattr(task, \"finished_at\", None).isoformat()\n if getattr(task, \"finished_at\", None)\n else None,\n env_id=str(params.get(\"environment_id\") or params.get(\"env\"))\n if (params.get(\"environment_id\") or params.get(\"env\"))\n else None,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[get_dashboard_tasks_history][Coherence:OK] Found {len(items)} tasks for dashboard_ref={dashboard_ref}, dashboard_id={dashboard_id}\"\n )\n return DashboardTaskHistoryResponse(dashboard_id=dashboard_id, items=items)\n finally:\n if client is not None:\n await client.aclose()\n\n\n# #endregion get_dashboard_tasks_history\n" }, { "contract_id": "get_dashboard_thumbnail", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_detail_routes.py", - "start_line": 266, - "end_line": 391, + "start_line": 257, + "end_line": 381, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Returns image bytes or 202 when thumbnail is being prepared by Superset.", "PRE": "env_id must exist.", "PURPOSE": "Proxies Superset dashboard thumbnail with cache support." @@ -13837,27 +12141,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_dashboard_thumbnail:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Proxies Superset dashboard thumbnail with cache support.\n# @RELATION: CALLS ->[AsyncSupersetClient]\n# @PRE: env_id must exist.\n# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.\n@router.get(\"/{dashboard_ref}/thumbnail\")\nasync def get_dashboard_thumbnail(\n dashboard_ref: str,\n env_id: str,\n force: bool = Query(False),\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_thumbnail\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, force={force}\",\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)\n digest = None\n thumb_endpoint = None\n\n try:\n screenshot_payload = client.network.request(\n method=\"POST\",\n endpoint=f\"/dashboard/{dashboard_id}/cache_dashboard_screenshot/\",\n json={\"force\": force},\n )\n payload = (\n screenshot_payload.get(\"result\", screenshot_payload)\n if isinstance(screenshot_payload, dict)\n else {}\n )\n image_url = (\n payload.get(\"image_url\", \"\") if isinstance(payload, dict) else \"\"\n )\n if isinstance(image_url, str) and image_url:\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n image_url,\n )\n if matched:\n digest = matched.group(1)\n except DashboardNotFoundError:\n logger.warning(\n \"[get_dashboard_thumbnail][Fallback] cache_dashboard_screenshot endpoint unavailable, fallback to dashboard.thumbnail_url\"\n )\n\n if not digest:\n dashboard_payload = client.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}\",\n )\n dashboard_data = (\n dashboard_payload.get(\"result\", dashboard_payload)\n if isinstance(dashboard_payload, dict)\n else {}\n )\n thumbnail_url = (\n dashboard_data.get(\"thumbnail_url\", \"\")\n if isinstance(dashboard_data, dict)\n else \"\"\n )\n if isinstance(thumbnail_url, str) and thumbnail_url:\n parsed = urlparse(thumbnail_url)\n parsed_path = parsed.path or thumbnail_url\n if parsed_path.startswith(\"/api/v1/\"):\n parsed_path = parsed_path[len(\"/api/v1\") :]\n thumb_endpoint = parsed_path\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n parsed_path,\n )\n if matched:\n digest = matched.group(1)\n\n if not thumb_endpoint:\n thumb_endpoint = (\n f\"/dashboard/{dashboard_id}/thumbnail/{digest or 'latest'}/\"\n )\n\n thumb_response = client.network.request(\n method=\"GET\",\n endpoint=thumb_endpoint,\n raw_response=True,\n allow_redirects=True,\n )\n\n if thumb_response.status_code == 202:\n payload_202: Dict[str, Any] = {}\n try:\n payload_202 = thumb_response.json()\n except Exception:\n payload_202 = {\"message\": \"Thumbnail is being generated\"}\n return JSONResponse(status_code=202, content=payload_202)\n\n content_type = thumb_response.headers.get(\"Content-Type\", \"image/png\")\n return Response(content=thumb_response.content, media_type=content_type)\n except DashboardNotFoundError as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Dashboard not found for thumbnail: {e}\"\n )\n raise HTTPException(status_code=404, detail=\"Dashboard thumbnail not found\")\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Failed to fetch dashboard thumbnail: {e}\"\n )\n raise HTTPException(\n status_code=503,\n detail=f\"Failed to fetch dashboard thumbnail: {str(e)}\",\n )\n\n\n# [/DEF:get_dashboard_thumbnail:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_dashboard_thumbnail [C:3] [TYPE Function]\n# @BRIEF Proxies Superset dashboard thumbnail with cache support.\n# @RELATION CALLS -> [AsyncSupersetClient]\n# @PRE: env_id must exist.\n# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.\n@router.get(\"/{dashboard_ref}/thumbnail\")\nasync def get_dashboard_thumbnail(\n dashboard_ref: str,\n env_id: str,\n force: bool = Query(False),\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboard_thumbnail\",\n f\"dashboard_ref={dashboard_ref}, env_id={env_id}, force={force}\",\n ):\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Environment not found: {env_id}\"\n )\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n dashboard_id = _resolve_dashboard_id_from_ref(dashboard_ref, client)\n digest = None\n thumb_endpoint = None\n\n try:\n screenshot_payload = client.network.request(\n method=\"POST\",\n endpoint=f\"/dashboard/{dashboard_id}/cache_dashboard_screenshot/\",\n json={\"force\": force},\n )\n payload = (\n screenshot_payload.get(\"result\", screenshot_payload)\n if isinstance(screenshot_payload, dict)\n else {}\n )\n image_url = (\n payload.get(\"image_url\", \"\") if isinstance(payload, dict) else \"\"\n )\n if isinstance(image_url, str) and image_url:\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n image_url,\n )\n if matched:\n digest = matched.group(1)\n except DashboardNotFoundError:\n logger.warning(\n \"[get_dashboard_thumbnail][Fallback] cache_dashboard_screenshot endpoint unavailable, fallback to dashboard.thumbnail_url\"\n )\n\n if not digest:\n dashboard_payload = client.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}\",\n )\n dashboard_data = (\n dashboard_payload.get(\"result\", dashboard_payload)\n if isinstance(dashboard_payload, dict)\n else {}\n )\n thumbnail_url = (\n dashboard_data.get(\"thumbnail_url\", \"\")\n if isinstance(dashboard_data, dict)\n else \"\"\n )\n if isinstance(thumbnail_url, str) and thumbnail_url:\n parsed = urlparse(thumbnail_url)\n parsed_path = parsed.path or thumbnail_url\n if parsed_path.startswith(\"/api/v1/\"):\n parsed_path = parsed_path[len(\"/api/v1\") :]\n thumb_endpoint = parsed_path\n matched = re.search(\n r\"/dashboard/\\d+/(?:thumbnail|screenshot)/([^/]+)/?$\",\n parsed_path,\n )\n if matched:\n digest = matched.group(1)\n\n if not thumb_endpoint:\n thumb_endpoint = (\n f\"/dashboard/{dashboard_id}/thumbnail/{digest or 'latest'}/\"\n )\n\n thumb_response = client.network.request(\n method=\"GET\",\n endpoint=thumb_endpoint,\n raw_response=True,\n allow_redirects=True,\n )\n\n if thumb_response.status_code == 202:\n payload_202: dict[str, Any] = {}\n try:\n payload_202 = thumb_response.json()\n except Exception:\n payload_202 = {\"message\": \"Thumbnail is being generated\"}\n return JSONResponse(status_code=202, content=payload_202)\n\n content_type = thumb_response.headers.get(\"Content-Type\", \"image/png\")\n return Response(content=thumb_response.content, media_type=content_type)\n except DashboardNotFoundError as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Dashboard not found for thumbnail: {e}\"\n )\n raise HTTPException(status_code=404, detail=\"Dashboard thumbnail not found\")\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboard_thumbnail][Coherence:Failed] Failed to fetch dashboard thumbnail: {e}\"\n )\n raise HTTPException(\n status_code=503,\n detail=f\"Failed to fetch dashboard thumbnail: {e!s}\",\n )\n\n\n# #endregion get_dashboard_thumbnail\n" }, { "contract_id": "DashboardHelpers", "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/_helpers.py", "start_line": 1, - "end_line": 187, + "end_line": 180, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "Infra", - "PURPOSE": "Basic helper functions for dashboard route handlers — slug resolution, filter normalization.", - "SEMANTICS": [ - "api", - "dashboards", - "helpers", - "resolution" - ] + "PURPOSE": "Basic helper functions for dashboard route handlers — slug resolution, filter normalization." }, "relations": [ { @@ -13884,19 +12182,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardHelpers:Module]\n# @COMPLEXITY: 2\n# @SEMANTICS: api, dashboards, helpers, resolution\n# @PURPOSE: Basic helper functions for dashboard route handlers — slug resolution, filter normalization.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> [SupersetClient]\n# @RELATION: DEPENDS_ON -> [AsyncSupersetClient]\n\nfrom typing import List, Optional, Dict, Any\nfrom fastapi import HTTPException\nfrom src.core.superset_client import SupersetClient\nfrom src.core.async_superset_client import AsyncSupersetClient\nfrom src.core.logger import logger\n\n\n# [DEF:_find_dashboard_id_by_slug:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard numeric ID by slug using Superset list endpoint.\n# @PRE: `dashboard_slug` is non-empty.\n# @POST: Returns dashboard ID when found, otherwise None.\ndef _find_dashboard_id_by_slug(\n client: SupersetClient,\n dashboard_slug: str,\n) -> Optional[int]:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = client.get_dashboards_page(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n\n return None\n\n\n# [/DEF:_find_dashboard_id_by_slug:Function]\n\n\n# [DEF:_resolve_dashboard_id_from_ref:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard ID from slug-first reference with numeric fallback.\n# @PRE: `dashboard_ref` is provided in route path.\n# @POST: Returns a valid dashboard ID or raises HTTPException(404).\ndef _resolve_dashboard_id_from_ref(\n dashboard_ref: str,\n client: SupersetClient,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n # Slug-first: even if ref looks numeric, try slug first.\n slug_match_id = _find_dashboard_id_by_slug(client, normalized_ref)\n if slug_match_id is not None:\n return slug_match_id\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n\n# [/DEF:_resolve_dashboard_id_from_ref:Function]\n\n\n# [DEF:_find_dashboard_id_by_slug_async:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard numeric ID by slug using async Superset list endpoint.\n# @PRE: dashboard_slug is non-empty.\n# @POST: Returns dashboard ID when found, otherwise None.\nasync def _find_dashboard_id_by_slug_async(\n client: AsyncSupersetClient,\n dashboard_slug: str,\n) -> Optional[int]:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = await client.get_dashboards_page_async(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n\n return None\n\n\n# [/DEF:_find_dashboard_id_by_slug_async:Function]\n\n\n# [DEF:_resolve_dashboard_id_from_ref_async:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard ID from slug-first reference using async Superset client.\n# @PRE: dashboard_ref is provided in route path.\n# @POST: Returns valid dashboard ID or raises HTTPException(404).\nasync def _resolve_dashboard_id_from_ref_async(\n dashboard_ref: str,\n client: AsyncSupersetClient,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n slug_match_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)\n if slug_match_id is not None:\n return slug_match_id\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n\n# [/DEF:_resolve_dashboard_id_from_ref_async:Function]\n\n\n# [DEF:_normalize_filter_values:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize query filter values to lower-cased non-empty tokens.\n# @PRE: values may be None or list of strings.\n# @POST: Returns trimmed normalized list preserving input order.\ndef _normalize_filter_values(values: Optional[List[str]]) -> List[str]:\n if not values:\n return []\n normalized: List[str] = []\n for value in values:\n token = str(value or \"\").strip().lower()\n if token:\n normalized.append(token)\n return normalized\n\n\n# [/DEF:_normalize_filter_values:Function]\n\n\n# [DEF:_dashboard_git_filter_value:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build comparable git status token for dashboards filtering.\n# @PRE: dashboard payload may contain git_status or None.\n# @POST: Returns one of ok|diff|no_repo|error|pending.\ndef _dashboard_git_filter_value(dashboard: Dict[str, Any]) -> str:\n git_status = dashboard.get(\"git_status\") or {}\n sync_status = str(git_status.get(\"sync_status\") or \"\").strip().upper()\n has_repo = git_status.get(\"has_repo\")\n if has_repo is False or sync_status == \"NO_REPO\":\n return \"no_repo\"\n if sync_status == \"DIFF\":\n return \"diff\"\n if sync_status == \"OK\":\n return \"ok\"\n if sync_status == \"ERROR\":\n return \"error\"\n return \"pending\"\n\n\n# [/DEF:_dashboard_git_filter_value:Function]\n\n\n# [/DEF:DashboardHelpers:Module]\n" + "anchor_syntax": "region", + "body": "# #region DashboardHelpers [C:2] [TYPE Module] [SEMANTICS fastapi, dashboard, api, search, filter]\n# @BRIEF Basic helper functions for dashboard route handlers — slug resolution, filter normalization.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AsyncSupersetClient]\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\n\nfrom src.core.async_superset_client import AsyncSupersetClient\nfrom src.core.superset_client import SupersetClient\n\n\n# #region _find_dashboard_id_by_slug [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard numeric ID by slug using Superset list endpoint.\n# @PRE: `dashboard_slug` is non-empty.\n# @POST: Returns dashboard ID when found, otherwise None.\ndef _find_dashboard_id_by_slug(\n client: SupersetClient,\n dashboard_slug: str,\n) -> int | None:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = client.get_dashboards_page(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n\n return None\n\n\n# #endregion _find_dashboard_id_by_slug\n\n\n# #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard ID from slug-first reference with numeric fallback.\n# @PRE: `dashboard_ref` is provided in route path.\n# @POST: Returns a valid dashboard ID or raises HTTPException(404).\ndef _resolve_dashboard_id_from_ref(\n dashboard_ref: str,\n client: SupersetClient,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n # Slug-first: even if ref looks numeric, try slug first.\n slug_match_id = _find_dashboard_id_by_slug(client, normalized_ref)\n if slug_match_id is not None:\n return slug_match_id\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n\n# #endregion _resolve_dashboard_id_from_ref\n\n\n# #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard numeric ID by slug using async Superset list endpoint.\n# @PRE: dashboard_slug is non-empty.\n# @POST: Returns dashboard ID when found, otherwise None.\nasync def _find_dashboard_id_by_slug_async(\n client: AsyncSupersetClient,\n dashboard_slug: str,\n) -> int | None:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = await client.get_dashboards_page_async(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n\n return None\n\n\n# #endregion _find_dashboard_id_by_slug_async\n\n\n# #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard ID from slug-first reference using async Superset client.\n# @PRE: dashboard_ref is provided in route path.\n# @POST: Returns valid dashboard ID or raises HTTPException(404).\nasync def _resolve_dashboard_id_from_ref_async(\n dashboard_ref: str,\n client: AsyncSupersetClient,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n slug_match_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)\n if slug_match_id is not None:\n return slug_match_id\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n\n# #endregion _resolve_dashboard_id_from_ref_async\n\n\n# #region _normalize_filter_values [C:2] [TYPE Function]\n# @BRIEF Normalize query filter values to lower-cased non-empty tokens.\n# @PRE: values may be None or list of strings.\n# @POST: Returns trimmed normalized list preserving input order.\ndef _normalize_filter_values(values: list[str] | None) -> list[str]:\n if not values:\n return []\n normalized: list[str] = []\n for value in values:\n token = str(value or \"\").strip().lower()\n if token:\n normalized.append(token)\n return normalized\n\n\n# #endregion _normalize_filter_values\n\n\n# #region _dashboard_git_filter_value [C:2] [TYPE Function]\n# @BRIEF Build comparable git status token for dashboards filtering.\n# @PRE: dashboard payload may contain git_status or None.\n# @POST: Returns one of ok|diff|no_repo|error|pending.\ndef _dashboard_git_filter_value(dashboard: dict[str, Any]) -> str:\n git_status = dashboard.get(\"git_status\") or {}\n sync_status = str(git_status.get(\"sync_status\") or \"\").strip().upper()\n has_repo = git_status.get(\"has_repo\")\n if has_repo is False or sync_status == \"NO_REPO\":\n return \"no_repo\"\n if sync_status == \"DIFF\":\n return \"diff\"\n if sync_status == \"OK\":\n return \"ok\"\n if sync_status == \"ERROR\":\n return \"error\"\n return \"pending\"\n\n\n# #endregion _dashboard_git_filter_value\n\n\n# #endregion DashboardHelpers\n" }, { "contract_id": "_find_dashboard_id_by_slug", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_helpers.py", - "start_line": 16, - "end_line": 51, + "start_line": 15, + "end_line": 49, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns dashboard ID when found, otherwise None.", "PRE": "`dashboard_slug` is non-empty.", "PURPOSE": "Resolve dashboard numeric ID by slug using Superset list endpoint." @@ -13922,19 +12220,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_find_dashboard_id_by_slug:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard numeric ID by slug using Superset list endpoint.\n# @PRE: `dashboard_slug` is non-empty.\n# @POST: Returns dashboard ID when found, otherwise None.\ndef _find_dashboard_id_by_slug(\n client: SupersetClient,\n dashboard_slug: str,\n) -> Optional[int]:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = client.get_dashboards_page(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n\n return None\n\n\n# [/DEF:_find_dashboard_id_by_slug:Function]\n" + "anchor_syntax": "region", + "body": "# #region _find_dashboard_id_by_slug [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard numeric ID by slug using Superset list endpoint.\n# @PRE: `dashboard_slug` is non-empty.\n# @POST: Returns dashboard ID when found, otherwise None.\ndef _find_dashboard_id_by_slug(\n client: SupersetClient,\n dashboard_slug: str,\n) -> int | None:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = client.get_dashboards_page(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n\n return None\n\n\n# #endregion _find_dashboard_id_by_slug\n" }, { "contract_id": "_resolve_dashboard_id_from_ref", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_helpers.py", - "start_line": 54, - "end_line": 78, + "start_line": 52, + "end_line": 75, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns a valid dashboard ID or raises HTTPException(404).", "PRE": "`dashboard_ref` is provided in route path.", "PURPOSE": "Resolve dashboard ID from slug-first reference with numeric fallback." @@ -13960,19 +12258,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_dashboard_id_from_ref:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard ID from slug-first reference with numeric fallback.\n# @PRE: `dashboard_ref` is provided in route path.\n# @POST: Returns a valid dashboard ID or raises HTTPException(404).\ndef _resolve_dashboard_id_from_ref(\n dashboard_ref: str,\n client: SupersetClient,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n # Slug-first: even if ref looks numeric, try slug first.\n slug_match_id = _find_dashboard_id_by_slug(client, normalized_ref)\n if slug_match_id is not None:\n return slug_match_id\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n\n# [/DEF:_resolve_dashboard_id_from_ref:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard ID from slug-first reference with numeric fallback.\n# @PRE: `dashboard_ref` is provided in route path.\n# @POST: Returns a valid dashboard ID or raises HTTPException(404).\ndef _resolve_dashboard_id_from_ref(\n dashboard_ref: str,\n client: SupersetClient,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n # Slug-first: even if ref looks numeric, try slug first.\n slug_match_id = _find_dashboard_id_by_slug(client, normalized_ref)\n if slug_match_id is not None:\n return slug_match_id\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n\n# #endregion _resolve_dashboard_id_from_ref\n" }, { "contract_id": "_find_dashboard_id_by_slug_async", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_helpers.py", - "start_line": 81, - "end_line": 116, + "start_line": 78, + "end_line": 112, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns dashboard ID when found, otherwise None.", "PRE": "dashboard_slug is non-empty.", "PURPOSE": "Resolve dashboard numeric ID by slug using async Superset list endpoint." @@ -13998,19 +12296,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_find_dashboard_id_by_slug_async:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard numeric ID by slug using async Superset list endpoint.\n# @PRE: dashboard_slug is non-empty.\n# @POST: Returns dashboard ID when found, otherwise None.\nasync def _find_dashboard_id_by_slug_async(\n client: AsyncSupersetClient,\n dashboard_slug: str,\n) -> Optional[int]:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = await client.get_dashboards_page_async(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n\n return None\n\n\n# [/DEF:_find_dashboard_id_by_slug_async:Function]\n" + "anchor_syntax": "region", + "body": "# #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard numeric ID by slug using async Superset list endpoint.\n# @PRE: dashboard_slug is non-empty.\n# @POST: Returns dashboard ID when found, otherwise None.\nasync def _find_dashboard_id_by_slug_async(\n client: AsyncSupersetClient,\n dashboard_slug: str,\n) -> int | None:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = await client.get_dashboards_page_async(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n\n return None\n\n\n# #endregion _find_dashboard_id_by_slug_async\n" }, { "contract_id": "_resolve_dashboard_id_from_ref_async", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_helpers.py", - "start_line": 119, - "end_line": 142, + "start_line": 115, + "end_line": 137, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns valid dashboard ID or raises HTTPException(404).", "PRE": "dashboard_ref is provided in route path.", "PURPOSE": "Resolve dashboard ID from slug-first reference using async Superset client." @@ -14036,19 +12334,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_dashboard_id_from_ref_async:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard ID from slug-first reference using async Superset client.\n# @PRE: dashboard_ref is provided in route path.\n# @POST: Returns valid dashboard ID or raises HTTPException(404).\nasync def _resolve_dashboard_id_from_ref_async(\n dashboard_ref: str,\n client: AsyncSupersetClient,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n slug_match_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)\n if slug_match_id is not None:\n return slug_match_id\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n\n# [/DEF:_resolve_dashboard_id_from_ref_async:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard ID from slug-first reference using async Superset client.\n# @PRE: dashboard_ref is provided in route path.\n# @POST: Returns valid dashboard ID or raises HTTPException(404).\nasync def _resolve_dashboard_id_from_ref_async(\n dashboard_ref: str,\n client: AsyncSupersetClient,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n slug_match_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)\n if slug_match_id is not None:\n return slug_match_id\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n raise HTTPException(status_code=404, detail=\"Dashboard not found\")\n\n\n# #endregion _resolve_dashboard_id_from_ref_async\n" }, { "contract_id": "_normalize_filter_values", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_helpers.py", - "start_line": 145, - "end_line": 161, + "start_line": 140, + "end_line": 155, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns trimmed normalized list preserving input order.", "PRE": "values may be None or list of strings.", "PURPOSE": "Normalize query filter values to lower-cased non-empty tokens." @@ -14074,19 +12372,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_normalize_filter_values:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize query filter values to lower-cased non-empty tokens.\n# @PRE: values may be None or list of strings.\n# @POST: Returns trimmed normalized list preserving input order.\ndef _normalize_filter_values(values: Optional[List[str]]) -> List[str]:\n if not values:\n return []\n normalized: List[str] = []\n for value in values:\n token = str(value or \"\").strip().lower()\n if token:\n normalized.append(token)\n return normalized\n\n\n# [/DEF:_normalize_filter_values:Function]\n" + "anchor_syntax": "region", + "body": "# #region _normalize_filter_values [C:2] [TYPE Function]\n# @BRIEF Normalize query filter values to lower-cased non-empty tokens.\n# @PRE: values may be None or list of strings.\n# @POST: Returns trimmed normalized list preserving input order.\ndef _normalize_filter_values(values: list[str] | None) -> list[str]:\n if not values:\n return []\n normalized: list[str] = []\n for value in values:\n token = str(value or \"\").strip().lower()\n if token:\n normalized.append(token)\n return normalized\n\n\n# #endregion _normalize_filter_values\n" }, { "contract_id": "_dashboard_git_filter_value", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_helpers.py", - "start_line": 164, - "end_line": 184, + "start_line": 158, + "end_line": 177, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns one of ok|diff|no_repo|error|pending.", "PRE": "dashboard payload may contain git_status or None.", "PURPOSE": "Build comparable git status token for dashboards filtering." @@ -14112,27 +12410,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_dashboard_git_filter_value:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build comparable git status token for dashboards filtering.\n# @PRE: dashboard payload may contain git_status or None.\n# @POST: Returns one of ok|diff|no_repo|error|pending.\ndef _dashboard_git_filter_value(dashboard: Dict[str, Any]) -> str:\n git_status = dashboard.get(\"git_status\") or {}\n sync_status = str(git_status.get(\"sync_status\") or \"\").strip().upper()\n has_repo = git_status.get(\"has_repo\")\n if has_repo is False or sync_status == \"NO_REPO\":\n return \"no_repo\"\n if sync_status == \"DIFF\":\n return \"diff\"\n if sync_status == \"OK\":\n return \"ok\"\n if sync_status == \"ERROR\":\n return \"error\"\n return \"pending\"\n\n\n# [/DEF:_dashboard_git_filter_value:Function]\n" + "anchor_syntax": "region", + "body": "# #region _dashboard_git_filter_value [C:2] [TYPE Function]\n# @BRIEF Build comparable git status token for dashboards filtering.\n# @PRE: dashboard payload may contain git_status or None.\n# @POST: Returns one of ok|diff|no_repo|error|pending.\ndef _dashboard_git_filter_value(dashboard: dict[str, Any]) -> str:\n git_status = dashboard.get(\"git_status\") or {}\n sync_status = str(git_status.get(\"sync_status\") or \"\").strip().upper()\n has_repo = git_status.get(\"has_repo\")\n if has_repo is False or sync_status == \"NO_REPO\":\n return \"no_repo\"\n if sync_status == \"DIFF\":\n return \"diff\"\n if sync_status == \"OK\":\n return \"ok\"\n if sync_status == \"ERROR\":\n return \"error\"\n return \"pending\"\n\n\n# #endregion _dashboard_git_filter_value\n" }, { "contract_id": "DashboardListingRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/_listing_routes.py", "start_line": 1, - "end_line": 394, + "end_line": 391, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "LAYER": "API", - "PURPOSE": "Dashboard listing route handler for Dashboard Hub.", - "SEMANTICS": [ - "api", - "dashboards", - "listing", - "routes" - ] + "PURPOSE": "Dashboard listing route handler for Dashboard Hub." }, "relations": [ { @@ -14189,24 +12481,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardListingRoutes:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: api, dashboards, listing, routes\n# @PURPOSE: Dashboard listing route handler for Dashboard Hub.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DashboardRouter]\n# @RELATION: DEPENDS_ON -> [DashboardSchemas]\n# @RELATION: DEPENDS_ON -> [DashboardHelpers]\n# @RELATION: DEPENDS_ON -> [DashboardProjection]\n\n# [SECTION: IMPORTS]\nimport os\nfrom typing import List, Optional, Dict, Any, Literal\nfrom fastapi import Depends, HTTPException, Query\nfrom sqlalchemy.orm import Session\nfrom src.dependencies import (\n get_config_manager, get_task_manager, get_resource_service,\n get_current_user, has_permission,\n)\nfrom src.core.database import get_db\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import logger, belief_scope\n\nlog = MarkerLogger(\"DashboardListing\")\nfrom src.models.auth import User\nfrom src.services.profile_service import ProfileService\nfrom ._helpers import _normalize_filter_values, _dashboard_git_filter_value\nfrom ._projection import (\n _project_dashboard_response_items, _get_profile_filter_binding,\n _resolve_profile_actor_aliases, _matches_dashboard_actor_aliases,\n)\nfrom ._schemas import EffectiveProfileFilter, DashboardsResponse\nfrom ._router import router\n# [/SECTION]\n\n\n# [DEF:get_dashboards:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Fetch list of dashboards from a specific environment with Git status and last task status\n# @PRE: env_id must be a valid environment ID\n# @PRE: page must be >= 1 if provided\n# @PRE: page_size must be between 1 and 100 if provided\n# @POST: Returns a list of dashboards with enhanced metadata and pagination info\n# @POST: Response includes pagination metadata (page, page_size, total, total_pages)\n# @POST: Response includes effective profile filter metadata for main dashboards page context\n# @PARAM: env_id (str) - The environment ID to fetch dashboards from\n# @PARAM: search (Optional[str]) - Filter by title/slug\n# @PARAM: page (Optional[int]) - Page number (default: 1)\n# @PARAM: page_size (Optional[int]) - Items per page (default: 10, max: 100)\n# @RETURN: DashboardsResponse - List of dashboards with status metadata\n# @RELATION: CALLS ->[get_dashboards_with_status]\n@router.get(\"\", response_model=DashboardsResponse)\nasync def get_dashboards(\n env_id: str,\n search: Optional[str] = None,\n page: int = 1,\n page_size: int = 10,\n page_context: Literal[\"dashboards_main\", \"other\"] = Query(\"dashboards_main\"),\n apply_profile_default: bool = Query(default=True),\n override_show_all: bool = Query(default=False),\n filter_title: Optional[List[str]] = Query(default=None),\n filter_git_status: Optional[List[str]] = Query(default=None),\n filter_llm_status: Optional[List[str]] = Query(default=None),\n filter_changed_on: Optional[List[str]] = Query(default=None),\n filter_actor: Optional[List[str]] = Query(default=None),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboards\",\n f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}, \"\n f\"page_context={page_context}, applyp={apply_profile_default}, overhide={override_show_all}\",\n ):\n if page < 1:\n log.explore(f\"Invalid page: {page}\", error=\"Page must be >= 1\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n log.explore(f\"Invalid page_size: {page_size}\", error=\"Page size must be between 1 and 100\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n log.explore(f\"Environment not found: {env_id}\", error=\"Environment not found\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n bound_username: Optional[str] = None\n can_apply_profile_filter = False\n can_apply_slug_filter = False\n effective_profile_filter = EffectiveProfileFilter(\n applied=False,\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=None,\n match_logic=None,\n )\n profile_service: Optional[ProfileService] = None\n\n try:\n profile_service_module = getattr(ProfileService, \"__module__\", \"\")\n is_mock_db = db.__class__.__module__.startswith(\"unittest.mock\")\n use_profile_service = (not is_mock_db) or profile_service_module.startswith(\n \"unittest.mock\"\n )\n if use_profile_service:\n profile_service = ProfileService(db=db, config_manager=config_manager)\n profile_preference = _get_profile_filter_binding(\n profile_service, current_user\n )\n normalized_username = (\n str(profile_preference.get(\"superset_username_normalized\") or \"\")\n .strip()\n .lower()\n )\n raw_username = (\n str(profile_preference.get(\"superset_username\") or \"\")\n .strip()\n .lower()\n )\n bound_username = normalized_username or raw_username or None\n\n can_apply_profile_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_my_dashboards\", False))\n and bool(bound_username)\n )\n can_apply_slug_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_slug_dashboards\", True))\n )\n\n profile_match_logic = None\n if can_apply_profile_filter and can_apply_slug_filter:\n profile_match_logic = \"owners_or_modified_by+slug_only\"\n elif can_apply_profile_filter:\n profile_match_logic = \"owners_or_modified_by\"\n elif can_apply_slug_filter:\n profile_match_logic = \"slug_only\"\n\n effective_profile_filter = EffectiveProfileFilter(\n applied=bool(can_apply_profile_filter or can_apply_slug_filter),\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=bound_username if can_apply_profile_filter else None,\n match_logic=profile_match_logic,\n )\n except Exception as profile_error:\n logger.explore(\n f\"Profile preference unavailable; continuing without profile-default filter\",\n error=str(profile_error),\n )\n\n try:\n all_tasks = task_manager.get_all_tasks()\n title_filters = _normalize_filter_values(filter_title)\n git_filters = _normalize_filter_values(filter_git_status)\n llm_filters = _normalize_filter_values(filter_llm_status)\n changed_on_filters = _normalize_filter_values(filter_changed_on)\n actor_filters = _normalize_filter_values(filter_actor)\n has_column_filters = any(\n (\n title_filters,\n git_filters,\n llm_filters,\n changed_on_filters,\n actor_filters,\n )\n )\n needs_full_scan = (\n has_column_filters\n or bool(can_apply_profile_filter)\n or bool(can_apply_slug_filter)\n )\n\n if not needs_full_scan:\n try:\n page_payload = (\n await resource_service.get_dashboards_page_with_status(\n env,\n all_tasks,\n page=page,\n page_size=page_size,\n search=search,\n include_git_status=False,\n require_slug=bool(can_apply_slug_filter),\n )\n )\n paginated_dashboards = page_payload[\"dashboards\"]\n total = page_payload[\"total\"]\n total_pages = page_payload[\"total_pages\"]\n except Exception as page_error:\n log.explore(f\"Page-based fetch failed; using compatibility fallback\", error=str(page_error))\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n )\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n total = len(dashboards)\n total_pages = (\n (total + page_size - 1) // page_size if total > 0 else 1\n )\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n else:\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n )\n\n if (\n can_apply_profile_filter\n and bound_username\n and profile_service is not None\n ):\n actor_aliases = _resolve_profile_actor_aliases(env, bound_username)\n if not actor_aliases:\n actor_aliases = [bound_username]\n logger.reason(\n f\"Applying profile actor filter \"\n f\"(env={env_id}, bound_username={bound_username}, actor_aliases={actor_aliases!r}, \"\n f\"dashboards_before={len(dashboards)})\"\n )\n filtered_dashboards: List[Dict[str, Any]] = []\n max_actor_samples = 15\n for index, dashboard in enumerate(dashboards):\n owners_value = dashboard.get(\"owners\")\n created_by_value = dashboard.get(\"created_by\")\n modified_by_value = dashboard.get(\"modified_by\")\n matches_actor = _matches_dashboard_actor_aliases(\n profile_service=profile_service,\n actor_aliases=actor_aliases,\n owners=owners_value,\n modified_by=modified_by_value,\n )\n if index < max_actor_samples:\n logger.reflect(\n f\"Profile actor filter sample \"\n f\"(env={env_id}, dashboard_id={dashboard.get('id')}, \"\n f\"bound_username={bound_username!r}, actor_aliases={actor_aliases!r}, \"\n f\"owners={owners_value!r}, created_by={created_by_value!r}, \"\n f\"modified_by={modified_by_value!r}, matches={matches_actor})\"\n )\n if matches_actor:\n filtered_dashboards.append(dashboard)\n\n logger.reflect(\n f\"Profile actor filter summary \"\n f\"(env={env_id}, bound_username={bound_username!r}, \"\n f\"dashboards_before={len(dashboards)}, dashboards_after={len(filtered_dashboards)})\"\n )\n dashboards = filtered_dashboards\n\n if can_apply_slug_filter:\n dashboards = [\n dashboard\n for dashboard in dashboards\n if str(dashboard.get(\"slug\") or \"\").strip()\n ]\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n def _matches_dashboard_filters(dashboard: Dict[str, Any]) -> bool:\n title_value = str(dashboard.get(\"title\") or \"\").strip().lower()\n if title_filters and title_value not in title_filters:\n return False\n\n if git_filters:\n git_value = _dashboard_git_filter_value(dashboard)\n if git_value not in git_filters:\n return False\n\n llm_value = (\n str(\n (\n (dashboard.get(\"last_task\") or {}).get(\n \"validation_status\"\n )\n )\n or \"UNKNOWN\"\n )\n .strip()\n .lower()\n )\n if llm_filters and llm_value not in llm_filters:\n return False\n\n changed_on_raw = (\n str(dashboard.get(\"last_modified\") or \"\").strip().lower()\n )\n changed_on_prefix = (\n changed_on_raw[:10]\n if len(changed_on_raw) >= 10\n else changed_on_raw\n )\n if (\n changed_on_filters\n and changed_on_raw not in changed_on_filters\n and changed_on_prefix not in changed_on_filters\n ):\n return False\n\n owners = dashboard.get(\"owners\") or []\n if isinstance(owners, list):\n actor_value = \", \".join(\n str(item).strip() for item in owners if str(item).strip()\n ).lower()\n else:\n actor_value = str(owners).strip().lower()\n if not actor_value:\n actor_value = \"-\"\n if actor_filters and actor_value not in actor_filters:\n return False\n return True\n\n if has_column_filters:\n dashboards = [\n d for d in dashboards if _matches_dashboard_filters(d)\n ]\n\n total = len(dashboards)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n\n log.reflect(f\"Returning {len(paginated_dashboards)} dashboards (page {page}/{total_pages}, total: {total}, profile_filter_applied={effective_profile_filter.applied})\")\n\n response_dashboards = _project_dashboard_response_items(\n paginated_dashboards\n )\n\n return DashboardsResponse(\n dashboards=response_dashboards,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages,\n effective_profile_filter=effective_profile_filter,\n )\n\n except HTTPException:\n raise\n except Exception as e:\n log.explore(\"Failed to fetch dashboards\", error=str(e))\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboards: {str(e)}\"\n )\n\n\n# [/DEF:get_dashboards:Function]\n# [/DEF:DashboardListingRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region DashboardListingRoutes [C:4] [TYPE Module] [SEMANTICS fastapi, dashboard, api, search]\n# @BRIEF Dashboard listing route handler for Dashboard Hub.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [DashboardRouter]\n# @RELATION DEPENDS_ON -> [DashboardSchemas]\n# @RELATION DEPENDS_ON -> [DashboardHelpers]\n# @RELATION DEPENDS_ON -> [DashboardProjection]\n\nfrom typing import Any, Literal\n\nfrom fastapi import Depends, HTTPException, Query\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_resource_service,\n get_task_manager,\n has_permission,\n)\nfrom src.models.auth import User\nfrom src.services.profile_service import ProfileService\n\nfrom ._helpers import _dashboard_git_filter_value, _normalize_filter_values\nfrom ._projection import (\n _get_profile_filter_binding,\n _matches_dashboard_actor_aliases,\n _project_dashboard_response_items,\n _resolve_profile_actor_aliases,\n)\nfrom ._router import router\nfrom ._schemas import DashboardsResponse, EffectiveProfileFilter\n\n\n# #region get_dashboards [C:3] [TYPE Function]\n# @BRIEF Fetch list of dashboards from a specific environment with Git status and last task status\n# @PRE: env_id must be a valid environment ID\n# @PRE: page must be >= 1 if provided\n# @PRE: page_size must be between 1 and 100 if provided\n# @POST: Returns a list of dashboards with enhanced metadata and pagination info\n# @POST: Response includes pagination metadata (page, page_size, total, total_pages)\n# @POST: Response includes effective profile filter metadata for main dashboards page context\n# @RELATION CALLS -> [get_dashboards_with_status]\n@router.get(\"\", response_model=DashboardsResponse)\nasync def get_dashboards(\n env_id: str,\n search: str | None = None,\n page: int = 1,\n page_size: int = 10,\n page_context: Literal[\"dashboards_main\", \"other\"] = Query(\"dashboards_main\"),\n apply_profile_default: bool = Query(default=True),\n override_show_all: bool = Query(default=False),\n filter_title: list[str] | None = Query(default=None),\n filter_git_status: list[str] | None = Query(default=None),\n filter_llm_status: list[str] | None = Query(default=None),\n filter_changed_on: list[str] | None = Query(default=None),\n filter_actor: list[str] | None = Query(default=None),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboards\",\n f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}, \"\n f\"page_context={page_context}, applyp={apply_profile_default}, overhide={override_show_all}\",\n ):\n if page < 1:\n logger.error(f\"[get_dashboards][Coherence:Failed] Invalid page: {page}\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n logger.error(f\"[get_dashboards][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dashboards][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n bound_username: str | None = None\n can_apply_profile_filter = False\n can_apply_slug_filter = False\n effective_profile_filter = EffectiveProfileFilter(\n applied=False,\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=None,\n match_logic=None,\n )\n profile_service: ProfileService | None = None\n\n try:\n profile_service_module = getattr(ProfileService, \"__module__\", \"\")\n is_mock_db = db.__class__.__module__.startswith(\"unittest.mock\")\n use_profile_service = (not is_mock_db) or profile_service_module.startswith(\n \"unittest.mock\"\n )\n if use_profile_service:\n profile_service = ProfileService(db=db, config_manager=config_manager)\n profile_preference = _get_profile_filter_binding(\n profile_service, current_user\n )\n normalized_username = (\n str(profile_preference.get(\"superset_username_normalized\") or \"\")\n .strip()\n .lower()\n )\n raw_username = (\n str(profile_preference.get(\"superset_username\") or \"\")\n .strip()\n .lower()\n )\n bound_username = normalized_username or raw_username or None\n\n can_apply_profile_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_my_dashboards\", False))\n and bool(bound_username)\n )\n can_apply_slug_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_slug_dashboards\", True))\n )\n\n profile_match_logic = None\n if can_apply_profile_filter and can_apply_slug_filter:\n profile_match_logic = \"owners_or_modified_by+slug_only\"\n elif can_apply_profile_filter:\n profile_match_logic = \"owners_or_modified_by\"\n elif can_apply_slug_filter:\n profile_match_logic = \"slug_only\"\n\n effective_profile_filter = EffectiveProfileFilter(\n applied=bool(can_apply_profile_filter or can_apply_slug_filter),\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=bound_username if can_apply_profile_filter else None,\n match_logic=profile_match_logic,\n )\n except Exception as profile_error:\n logger.explore(\n f\"Profile preference unavailable; continuing without profile-default filter: {profile_error}\",\n extra={\"src\": \"get_dashboards\"},\n )\n\n try:\n all_tasks = task_manager.get_all_tasks()\n title_filters = _normalize_filter_values(filter_title)\n git_filters = _normalize_filter_values(filter_git_status)\n llm_filters = _normalize_filter_values(filter_llm_status)\n changed_on_filters = _normalize_filter_values(filter_changed_on)\n actor_filters = _normalize_filter_values(filter_actor)\n has_column_filters = any(\n (\n title_filters,\n git_filters,\n llm_filters,\n changed_on_filters,\n actor_filters,\n )\n )\n needs_full_scan = (\n has_column_filters\n or bool(can_apply_profile_filter)\n or bool(can_apply_slug_filter)\n )\n\n if not needs_full_scan:\n try:\n page_payload = (\n await resource_service.get_dashboards_page_with_status(\n env,\n all_tasks,\n page=page,\n page_size=page_size,\n search=search,\n include_git_status=False,\n require_slug=bool(can_apply_slug_filter),\n )\n )\n paginated_dashboards = page_payload[\"dashboards\"]\n total = page_payload[\"total\"]\n total_pages = page_payload[\"total_pages\"]\n except Exception as page_error:\n logger.reason(\n f\"Page-based fetch failed; using compatibility fallback: {page_error}\",\n extra={\"src\": \"get_dashboards\"},\n )\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n )\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n total = len(dashboards)\n total_pages = (\n (total + page_size - 1) // page_size if total > 0 else 1\n )\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n else:\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n )\n\n if (\n can_apply_profile_filter\n and bound_username\n and profile_service is not None\n ):\n actor_aliases = _resolve_profile_actor_aliases(env, bound_username)\n if not actor_aliases:\n actor_aliases = [bound_username]\n logger.reason(\n \"Applying profile actor filter\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"bound_username\": bound_username, \"actor_aliases\": actor_aliases, \"dashboards_before\": len(dashboards)}},\n )\n filtered_dashboards: list[dict[str, Any]] = []\n max_actor_samples = 15\n for index, dashboard in enumerate(dashboards):\n owners_value = dashboard.get(\"owners\")\n created_by_value = dashboard.get(\"created_by\")\n modified_by_value = dashboard.get(\"modified_by\")\n matches_actor = _matches_dashboard_actor_aliases(\n profile_service=profile_service,\n actor_aliases=actor_aliases,\n owners=owners_value,\n modified_by=modified_by_value,\n )\n if index < max_actor_samples:\n logger.reflect(\n \"Profile actor filter sample\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"dashboard_id\": dashboard.get('id'), \"bound_username\": bound_username, \"actor_aliases\": actor_aliases, \"owners\": owners_value, \"created_by\": created_by_value, \"modified_by\": modified_by_value, \"matches\": matches_actor}},\n )\n if matches_actor:\n filtered_dashboards.append(dashboard)\n\n logger.reflect(\n \"Profile actor filter summary\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"bound_username\": bound_username, \"dashboards_before\": len(dashboards), \"dashboards_after\": len(filtered_dashboards)}},\n )\n dashboards = filtered_dashboards\n\n if can_apply_slug_filter:\n dashboards = [\n dashboard\n for dashboard in dashboards\n if str(dashboard.get(\"slug\") or \"\").strip()\n ]\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n def _matches_dashboard_filters(dashboard: dict[str, Any]) -> bool:\n title_value = str(dashboard.get(\"title\") or \"\").strip().lower()\n if title_filters and title_value not in title_filters:\n return False\n\n if git_filters:\n git_value = _dashboard_git_filter_value(dashboard)\n if git_value not in git_filters:\n return False\n\n llm_value = (\n str(\n (\n (dashboard.get(\"last_task\") or {}).get(\n \"validation_status\"\n )\n )\n or \"UNKNOWN\"\n )\n .strip()\n .lower()\n )\n if llm_filters and llm_value not in llm_filters:\n return False\n\n changed_on_raw = (\n str(dashboard.get(\"last_modified\") or \"\").strip().lower()\n )\n changed_on_prefix = (\n changed_on_raw[:10]\n if len(changed_on_raw) >= 10\n else changed_on_raw\n )\n if (\n changed_on_filters\n and changed_on_raw not in changed_on_filters\n and changed_on_prefix not in changed_on_filters\n ):\n return False\n\n owners = dashboard.get(\"owners\") or []\n if isinstance(owners, list):\n actor_value = \", \".join(\n str(item).strip() for item in owners if str(item).strip()\n ).lower()\n else:\n actor_value = str(owners).strip().lower()\n if not actor_value:\n actor_value = \"-\"\n if actor_filters and actor_value not in actor_filters:\n return False\n return True\n\n if has_column_filters:\n dashboards = [\n d for d in dashboards if _matches_dashboard_filters(d)\n ]\n\n total = len(dashboards)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n\n logger.info(\n f\"[get_dashboards][Coherence:OK] Returning {len(paginated_dashboards)} dashboards \"\n f\"(page {page}/{total_pages}, total: {total}, profile_filter_applied={effective_profile_filter.applied})\"\n )\n\n response_dashboards = _project_dashboard_response_items(\n paginated_dashboards\n )\n\n return DashboardsResponse(\n dashboards=response_dashboards,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages,\n effective_profile_filter=effective_profile_filter,\n )\n\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboards][Coherence:Failed] Failed to fetch dashboards: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboards: {e!s}\"\n )\n\n\n# #endregion get_dashboards\n# #endregion DashboardListingRoutes\n" }, { "contract_id": "get_dashboards", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_listing_routes.py", "start_line": 37, - "end_line": 393, + "end_line": 390, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "page_size (Optional[int]) - Items per page (default: 10, max: 100)", + "COMPLEXITY": 3, "POST": "Response includes effective profile filter metadata for main dashboards page context", "PRE": "page_size must be between 1 and 100 if provided", - "PURPOSE": "Fetch list of dashboards from a specific environment with Git status and last task status", - "RETURN": "DashboardsResponse - List of dashboards with status metadata" + "PURPOSE": "Fetch list of dashboards from a specific environment with Git status and last task status" }, "relations": [ { @@ -14217,12 +12507,6 @@ } ], "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", @@ -14240,36 +12524,23 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:get_dashboards:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Fetch list of dashboards from a specific environment with Git status and last task status\n# @PRE: env_id must be a valid environment ID\n# @PRE: page must be >= 1 if provided\n# @PRE: page_size must be between 1 and 100 if provided\n# @POST: Returns a list of dashboards with enhanced metadata and pagination info\n# @POST: Response includes pagination metadata (page, page_size, total, total_pages)\n# @POST: Response includes effective profile filter metadata for main dashboards page context\n# @PARAM: env_id (str) - The environment ID to fetch dashboards from\n# @PARAM: search (Optional[str]) - Filter by title/slug\n# @PARAM: page (Optional[int]) - Page number (default: 1)\n# @PARAM: page_size (Optional[int]) - Items per page (default: 10, max: 100)\n# @RETURN: DashboardsResponse - List of dashboards with status metadata\n# @RELATION: CALLS ->[get_dashboards_with_status]\n@router.get(\"\", response_model=DashboardsResponse)\nasync def get_dashboards(\n env_id: str,\n search: Optional[str] = None,\n page: int = 1,\n page_size: int = 10,\n page_context: Literal[\"dashboards_main\", \"other\"] = Query(\"dashboards_main\"),\n apply_profile_default: bool = Query(default=True),\n override_show_all: bool = Query(default=False),\n filter_title: Optional[List[str]] = Query(default=None),\n filter_git_status: Optional[List[str]] = Query(default=None),\n filter_llm_status: Optional[List[str]] = Query(default=None),\n filter_changed_on: Optional[List[str]] = Query(default=None),\n filter_actor: Optional[List[str]] = Query(default=None),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboards\",\n f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}, \"\n f\"page_context={page_context}, applyp={apply_profile_default}, overhide={override_show_all}\",\n ):\n if page < 1:\n log.explore(f\"Invalid page: {page}\", error=\"Page must be >= 1\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n log.explore(f\"Invalid page_size: {page_size}\", error=\"Page size must be between 1 and 100\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n log.explore(f\"Environment not found: {env_id}\", error=\"Environment not found\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n bound_username: Optional[str] = None\n can_apply_profile_filter = False\n can_apply_slug_filter = False\n effective_profile_filter = EffectiveProfileFilter(\n applied=False,\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=None,\n match_logic=None,\n )\n profile_service: Optional[ProfileService] = None\n\n try:\n profile_service_module = getattr(ProfileService, \"__module__\", \"\")\n is_mock_db = db.__class__.__module__.startswith(\"unittest.mock\")\n use_profile_service = (not is_mock_db) or profile_service_module.startswith(\n \"unittest.mock\"\n )\n if use_profile_service:\n profile_service = ProfileService(db=db, config_manager=config_manager)\n profile_preference = _get_profile_filter_binding(\n profile_service, current_user\n )\n normalized_username = (\n str(profile_preference.get(\"superset_username_normalized\") or \"\")\n .strip()\n .lower()\n )\n raw_username = (\n str(profile_preference.get(\"superset_username\") or \"\")\n .strip()\n .lower()\n )\n bound_username = normalized_username or raw_username or None\n\n can_apply_profile_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_my_dashboards\", False))\n and bool(bound_username)\n )\n can_apply_slug_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_slug_dashboards\", True))\n )\n\n profile_match_logic = None\n if can_apply_profile_filter and can_apply_slug_filter:\n profile_match_logic = \"owners_or_modified_by+slug_only\"\n elif can_apply_profile_filter:\n profile_match_logic = \"owners_or_modified_by\"\n elif can_apply_slug_filter:\n profile_match_logic = \"slug_only\"\n\n effective_profile_filter = EffectiveProfileFilter(\n applied=bool(can_apply_profile_filter or can_apply_slug_filter),\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=bound_username if can_apply_profile_filter else None,\n match_logic=profile_match_logic,\n )\n except Exception as profile_error:\n logger.explore(\n f\"Profile preference unavailable; continuing without profile-default filter\",\n error=str(profile_error),\n )\n\n try:\n all_tasks = task_manager.get_all_tasks()\n title_filters = _normalize_filter_values(filter_title)\n git_filters = _normalize_filter_values(filter_git_status)\n llm_filters = _normalize_filter_values(filter_llm_status)\n changed_on_filters = _normalize_filter_values(filter_changed_on)\n actor_filters = _normalize_filter_values(filter_actor)\n has_column_filters = any(\n (\n title_filters,\n git_filters,\n llm_filters,\n changed_on_filters,\n actor_filters,\n )\n )\n needs_full_scan = (\n has_column_filters\n or bool(can_apply_profile_filter)\n or bool(can_apply_slug_filter)\n )\n\n if not needs_full_scan:\n try:\n page_payload = (\n await resource_service.get_dashboards_page_with_status(\n env,\n all_tasks,\n page=page,\n page_size=page_size,\n search=search,\n include_git_status=False,\n require_slug=bool(can_apply_slug_filter),\n )\n )\n paginated_dashboards = page_payload[\"dashboards\"]\n total = page_payload[\"total\"]\n total_pages = page_payload[\"total_pages\"]\n except Exception as page_error:\n log.explore(f\"Page-based fetch failed; using compatibility fallback\", error=str(page_error))\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n )\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n total = len(dashboards)\n total_pages = (\n (total + page_size - 1) // page_size if total > 0 else 1\n )\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n else:\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n )\n\n if (\n can_apply_profile_filter\n and bound_username\n and profile_service is not None\n ):\n actor_aliases = _resolve_profile_actor_aliases(env, bound_username)\n if not actor_aliases:\n actor_aliases = [bound_username]\n logger.reason(\n f\"Applying profile actor filter \"\n f\"(env={env_id}, bound_username={bound_username}, actor_aliases={actor_aliases!r}, \"\n f\"dashboards_before={len(dashboards)})\"\n )\n filtered_dashboards: List[Dict[str, Any]] = []\n max_actor_samples = 15\n for index, dashboard in enumerate(dashboards):\n owners_value = dashboard.get(\"owners\")\n created_by_value = dashboard.get(\"created_by\")\n modified_by_value = dashboard.get(\"modified_by\")\n matches_actor = _matches_dashboard_actor_aliases(\n profile_service=profile_service,\n actor_aliases=actor_aliases,\n owners=owners_value,\n modified_by=modified_by_value,\n )\n if index < max_actor_samples:\n logger.reflect(\n f\"Profile actor filter sample \"\n f\"(env={env_id}, dashboard_id={dashboard.get('id')}, \"\n f\"bound_username={bound_username!r}, actor_aliases={actor_aliases!r}, \"\n f\"owners={owners_value!r}, created_by={created_by_value!r}, \"\n f\"modified_by={modified_by_value!r}, matches={matches_actor})\"\n )\n if matches_actor:\n filtered_dashboards.append(dashboard)\n\n logger.reflect(\n f\"Profile actor filter summary \"\n f\"(env={env_id}, bound_username={bound_username!r}, \"\n f\"dashboards_before={len(dashboards)}, dashboards_after={len(filtered_dashboards)})\"\n )\n dashboards = filtered_dashboards\n\n if can_apply_slug_filter:\n dashboards = [\n dashboard\n for dashboard in dashboards\n if str(dashboard.get(\"slug\") or \"\").strip()\n ]\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n def _matches_dashboard_filters(dashboard: Dict[str, Any]) -> bool:\n title_value = str(dashboard.get(\"title\") or \"\").strip().lower()\n if title_filters and title_value not in title_filters:\n return False\n\n if git_filters:\n git_value = _dashboard_git_filter_value(dashboard)\n if git_value not in git_filters:\n return False\n\n llm_value = (\n str(\n (\n (dashboard.get(\"last_task\") or {}).get(\n \"validation_status\"\n )\n )\n or \"UNKNOWN\"\n )\n .strip()\n .lower()\n )\n if llm_filters and llm_value not in llm_filters:\n return False\n\n changed_on_raw = (\n str(dashboard.get(\"last_modified\") or \"\").strip().lower()\n )\n changed_on_prefix = (\n changed_on_raw[:10]\n if len(changed_on_raw) >= 10\n else changed_on_raw\n )\n if (\n changed_on_filters\n and changed_on_raw not in changed_on_filters\n and changed_on_prefix not in changed_on_filters\n ):\n return False\n\n owners = dashboard.get(\"owners\") or []\n if isinstance(owners, list):\n actor_value = \", \".join(\n str(item).strip() for item in owners if str(item).strip()\n ).lower()\n else:\n actor_value = str(owners).strip().lower()\n if not actor_value:\n actor_value = \"-\"\n if actor_filters and actor_value not in actor_filters:\n return False\n return True\n\n if has_column_filters:\n dashboards = [\n d for d in dashboards if _matches_dashboard_filters(d)\n ]\n\n total = len(dashboards)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n\n log.reflect(f\"Returning {len(paginated_dashboards)} dashboards (page {page}/{total_pages}, total: {total}, profile_filter_applied={effective_profile_filter.applied})\")\n\n response_dashboards = _project_dashboard_response_items(\n paginated_dashboards\n )\n\n return DashboardsResponse(\n dashboards=response_dashboards,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages,\n effective_profile_filter=effective_profile_filter,\n )\n\n except HTTPException:\n raise\n except Exception as e:\n log.explore(\"Failed to fetch dashboards\", error=str(e))\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboards: {str(e)}\"\n )\n\n\n# [/DEF:get_dashboards:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_dashboards [C:3] [TYPE Function]\n# @BRIEF Fetch list of dashboards from a specific environment with Git status and last task status\n# @PRE: env_id must be a valid environment ID\n# @PRE: page must be >= 1 if provided\n# @PRE: page_size must be between 1 and 100 if provided\n# @POST: Returns a list of dashboards with enhanced metadata and pagination info\n# @POST: Response includes pagination metadata (page, page_size, total, total_pages)\n# @POST: Response includes effective profile filter metadata for main dashboards page context\n# @RELATION CALLS -> [get_dashboards_with_status]\n@router.get(\"\", response_model=DashboardsResponse)\nasync def get_dashboards(\n env_id: str,\n search: str | None = None,\n page: int = 1,\n page_size: int = 10,\n page_context: Literal[\"dashboards_main\", \"other\"] = Query(\"dashboards_main\"),\n apply_profile_default: bool = Query(default=True),\n override_show_all: bool = Query(default=False),\n filter_title: list[str] | None = Query(default=None),\n filter_git_status: list[str] | None = Query(default=None),\n filter_llm_status: list[str] | None = Query(default=None),\n filter_changed_on: list[str] | None = Query(default=None),\n filter_actor: list[str] | None = Query(default=None),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\n \"get_dashboards\",\n f\"env_id={env_id}, search={search}, page={page}, page_size={page_size}, \"\n f\"page_context={page_context}, applyp={apply_profile_default}, overhide={override_show_all}\",\n ):\n if page < 1:\n logger.error(f\"[get_dashboards][Coherence:Failed] Invalid page: {page}\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n logger.error(f\"[get_dashboards][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n logger.error(f\"[get_dashboards][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n bound_username: str | None = None\n can_apply_profile_filter = False\n can_apply_slug_filter = False\n effective_profile_filter = EffectiveProfileFilter(\n applied=False,\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=None,\n match_logic=None,\n )\n profile_service: ProfileService | None = None\n\n try:\n profile_service_module = getattr(ProfileService, \"__module__\", \"\")\n is_mock_db = db.__class__.__module__.startswith(\"unittest.mock\")\n use_profile_service = (not is_mock_db) or profile_service_module.startswith(\n \"unittest.mock\"\n )\n if use_profile_service:\n profile_service = ProfileService(db=db, config_manager=config_manager)\n profile_preference = _get_profile_filter_binding(\n profile_service, current_user\n )\n normalized_username = (\n str(profile_preference.get(\"superset_username_normalized\") or \"\")\n .strip()\n .lower()\n )\n raw_username = (\n str(profile_preference.get(\"superset_username\") or \"\")\n .strip()\n .lower()\n )\n bound_username = normalized_username or raw_username or None\n\n can_apply_profile_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_my_dashboards\", False))\n and bool(bound_username)\n )\n can_apply_slug_filter = (\n page_context == \"dashboards_main\"\n and bool(apply_profile_default)\n and not bool(override_show_all)\n and bool(profile_preference.get(\"show_only_slug_dashboards\", True))\n )\n\n profile_match_logic = None\n if can_apply_profile_filter and can_apply_slug_filter:\n profile_match_logic = \"owners_or_modified_by+slug_only\"\n elif can_apply_profile_filter:\n profile_match_logic = \"owners_or_modified_by\"\n elif can_apply_slug_filter:\n profile_match_logic = \"slug_only\"\n\n effective_profile_filter = EffectiveProfileFilter(\n applied=bool(can_apply_profile_filter or can_apply_slug_filter),\n source_page=page_context,\n override_show_all=bool(override_show_all),\n username=bound_username if can_apply_profile_filter else None,\n match_logic=profile_match_logic,\n )\n except Exception as profile_error:\n logger.explore(\n f\"Profile preference unavailable; continuing without profile-default filter: {profile_error}\",\n extra={\"src\": \"get_dashboards\"},\n )\n\n try:\n all_tasks = task_manager.get_all_tasks()\n title_filters = _normalize_filter_values(filter_title)\n git_filters = _normalize_filter_values(filter_git_status)\n llm_filters = _normalize_filter_values(filter_llm_status)\n changed_on_filters = _normalize_filter_values(filter_changed_on)\n actor_filters = _normalize_filter_values(filter_actor)\n has_column_filters = any(\n (\n title_filters,\n git_filters,\n llm_filters,\n changed_on_filters,\n actor_filters,\n )\n )\n needs_full_scan = (\n has_column_filters\n or bool(can_apply_profile_filter)\n or bool(can_apply_slug_filter)\n )\n\n if not needs_full_scan:\n try:\n page_payload = (\n await resource_service.get_dashboards_page_with_status(\n env,\n all_tasks,\n page=page,\n page_size=page_size,\n search=search,\n include_git_status=False,\n require_slug=bool(can_apply_slug_filter),\n )\n )\n paginated_dashboards = page_payload[\"dashboards\"]\n total = page_payload[\"total\"]\n total_pages = page_payload[\"total_pages\"]\n except Exception as page_error:\n logger.reason(\n f\"Page-based fetch failed; using compatibility fallback: {page_error}\",\n extra={\"src\": \"get_dashboards\"},\n )\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=False,\n )\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n total = len(dashboards)\n total_pages = (\n (total + page_size - 1) // page_size if total > 0 else 1\n )\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n else:\n if can_apply_slug_filter:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n require_slug=True,\n )\n else:\n dashboards = await resource_service.get_dashboards_with_status(\n env,\n all_tasks,\n include_git_status=bool(git_filters),\n )\n\n if (\n can_apply_profile_filter\n and bound_username\n and profile_service is not None\n ):\n actor_aliases = _resolve_profile_actor_aliases(env, bound_username)\n if not actor_aliases:\n actor_aliases = [bound_username]\n logger.reason(\n \"Applying profile actor filter\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"bound_username\": bound_username, \"actor_aliases\": actor_aliases, \"dashboards_before\": len(dashboards)}},\n )\n filtered_dashboards: list[dict[str, Any]] = []\n max_actor_samples = 15\n for index, dashboard in enumerate(dashboards):\n owners_value = dashboard.get(\"owners\")\n created_by_value = dashboard.get(\"created_by\")\n modified_by_value = dashboard.get(\"modified_by\")\n matches_actor = _matches_dashboard_actor_aliases(\n profile_service=profile_service,\n actor_aliases=actor_aliases,\n owners=owners_value,\n modified_by=modified_by_value,\n )\n if index < max_actor_samples:\n logger.reflect(\n \"Profile actor filter sample\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"dashboard_id\": dashboard.get('id'), \"bound_username\": bound_username, \"actor_aliases\": actor_aliases, \"owners\": owners_value, \"created_by\": created_by_value, \"modified_by\": modified_by_value, \"matches\": matches_actor}},\n )\n if matches_actor:\n filtered_dashboards.append(dashboard)\n\n logger.reflect(\n \"Profile actor filter summary\",\n extra={\"src\": \"get_dashboards\", \"payload\": {\"env\": env_id, \"bound_username\": bound_username, \"dashboards_before\": len(dashboards), \"dashboards_after\": len(filtered_dashboards)}},\n )\n dashboards = filtered_dashboards\n\n if can_apply_slug_filter:\n dashboards = [\n dashboard\n for dashboard in dashboards\n if str(dashboard.get(\"slug\") or \"\").strip()\n ]\n\n if search:\n search_lower = search.lower()\n dashboards = [\n d\n for d in dashboards\n if search_lower in d.get(\"title\", \"\").lower()\n or search_lower in d.get(\"slug\", \"\").lower()\n ]\n\n def _matches_dashboard_filters(dashboard: dict[str, Any]) -> bool:\n title_value = str(dashboard.get(\"title\") or \"\").strip().lower()\n if title_filters and title_value not in title_filters:\n return False\n\n if git_filters:\n git_value = _dashboard_git_filter_value(dashboard)\n if git_value not in git_filters:\n return False\n\n llm_value = (\n str(\n (\n (dashboard.get(\"last_task\") or {}).get(\n \"validation_status\"\n )\n )\n or \"UNKNOWN\"\n )\n .strip()\n .lower()\n )\n if llm_filters and llm_value not in llm_filters:\n return False\n\n changed_on_raw = (\n str(dashboard.get(\"last_modified\") or \"\").strip().lower()\n )\n changed_on_prefix = (\n changed_on_raw[:10]\n if len(changed_on_raw) >= 10\n else changed_on_raw\n )\n if (\n changed_on_filters\n and changed_on_raw not in changed_on_filters\n and changed_on_prefix not in changed_on_filters\n ):\n return False\n\n owners = dashboard.get(\"owners\") or []\n if isinstance(owners, list):\n actor_value = \", \".join(\n str(item).strip() for item in owners if str(item).strip()\n ).lower()\n else:\n actor_value = str(owners).strip().lower()\n if not actor_value:\n actor_value = \"-\"\n if actor_filters and actor_value not in actor_filters:\n return False\n return True\n\n if has_column_filters:\n dashboards = [\n d for d in dashboards if _matches_dashboard_filters(d)\n ]\n\n total = len(dashboards)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n paginated_dashboards = dashboards[start_idx:end_idx]\n\n logger.info(\n f\"[get_dashboards][Coherence:OK] Returning {len(paginated_dashboards)} dashboards \"\n f\"(page {page}/{total_pages}, total: {total}, profile_filter_applied={effective_profile_filter.applied})\"\n )\n\n response_dashboards = _project_dashboard_response_items(\n paginated_dashboards\n )\n\n return DashboardsResponse(\n dashboards=response_dashboards,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages,\n effective_profile_filter=effective_profile_filter,\n )\n\n except HTTPException:\n raise\n except Exception as e:\n logger.error(\n f\"[get_dashboards][Coherence:Failed] Failed to fetch dashboards: {e}\"\n )\n raise HTTPException(\n status_code=503, detail=f\"Failed to fetch dashboards: {e!s}\"\n )\n\n\n# #endregion get_dashboards\n" }, { "contract_id": "DashboardProjection", "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/_projection.py", "start_line": 1, - "end_line": 260, + "end_line": 249, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "Infra", - "PURPOSE": "Dashboard response projection and profile-filter helpers for Dashboard Hub routes.", - "SEMANTICS": [ - "api", - "dashboards", - "projection", - "profile", - "filtering" - ] + "PURPOSE": "Dashboard response projection and profile-filter helpers for Dashboard Hub routes." }, "relations": [ { @@ -14296,104 +12567,104 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardProjection:Module]\n# @COMPLEXITY: 2\n# @SEMANTICS: api, dashboards, projection, profile, filtering\n# @PURPOSE: Dashboard response projection and profile-filter helpers for Dashboard Hub routes.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> [SupersetClient]\n# @RELATION: DEPENDS_ON -> [ProfileService]\n\nfrom typing import Any, Dict, List, Optional\n\nfrom src.core.logger import logger, belief_scope\nfrom src.core.superset_client import SupersetClient\nfrom src.core.superset_profile_lookup import SupersetAccountLookupAdapter\nfrom src.models.auth import User\nfrom src.services.profile_service import ProfileService\n\n\n# [DEF:_normalize_actor_alias_token:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize actor alias token to comparable trim+lower text.\ndef _normalize_actor_alias_token(value: Any) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip().lower()\n return normalized if normalized else None\n\n\n# [/DEF:_normalize_actor_alias_token:Function]\n\n\n# [DEF:_normalize_owner_display_token:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Project owner payload value into stable display string for API response contracts.\ndef _normalize_owner_display_token(owner: Any) -> Optional[str]:\n if owner is None:\n return None\n if isinstance(owner, dict):\n for key in (\"username\", \"full_name\", \"first_name\", \"email\"):\n candidate = owner.get(key)\n if isinstance(candidate, str) and candidate.strip():\n return candidate.strip()\n return None\n if isinstance(owner, str):\n return owner.strip() or None\n return None\n\n\n# [/DEF:_normalize_owner_display_token:Function]\n\n\n# [DEF:_normalize_dashboard_owner_values:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize dashboard owners payload to optional list of display strings.\ndef _normalize_dashboard_owner_values(owners: Any) -> Optional[List[str]]:\n if owners is None:\n return None\n raw_items: List[Any]\n if isinstance(owners, list):\n raw_items = owners\n else:\n raw_items = [owners]\n normalized: List[str] = []\n for owner in raw_items:\n token = _normalize_owner_display_token(owner)\n if token and token not in normalized:\n normalized.append(token)\n return normalized\n\n\n# [/DEF:_normalize_dashboard_owner_values:Function]\n\n\n# [DEF:_project_dashboard_response_items:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Project dashboard payloads to response-contract-safe shape.\ndef _project_dashboard_response_items(\n dashboards: List[Dict[str, Any]],\n) -> List[Dict[str, Any]]:\n projected: List[Dict[str, Any]] = []\n for dashboard in dashboards:\n projected_dashboard = dict(dashboard)\n projected_dashboard[\"owners\"] = _normalize_dashboard_owner_values(\n projected_dashboard.get(\"owners\")\n )\n projected.append(projected_dashboard)\n return projected\n\n\n# [/DEF:_project_dashboard_response_items:Function]\n\n\n# [DEF:_get_profile_filter_binding:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard profile-filter binding through current or legacy profile service contracts.\ndef _get_profile_filter_binding(\n profile_service: Any, current_user: User\n) -> Dict[str, Any]:\n\n def _read_optional_string(value: Any) -> Optional[str]:\n return value if isinstance(value, str) else None\n\n def _read_bool(value: Any, default: bool) -> bool:\n return value if isinstance(value, bool) else default\n\n if hasattr(profile_service, \"get_dashboard_filter_binding\"):\n binding = profile_service.get_dashboard_filter_binding(current_user)\n if isinstance(binding, dict):\n return {\n \"superset_username\": _read_optional_string(\n binding.get(\"superset_username\")\n ),\n \"superset_username_normalized\": _read_optional_string(\n binding.get(\"superset_username_normalized\")\n ),\n \"show_only_my_dashboards\": _read_bool(\n binding.get(\"show_only_my_dashboards\"), False\n ),\n \"show_only_slug_dashboards\": _read_bool(\n binding.get(\"show_only_slug_dashboards\"), False\n ),\n }\n if hasattr(profile_service, \"get_my_preference\"):\n response = profile_service.get_my_preference(current_user)\n preference = getattr(response, \"preference\", None)\n return {\n \"superset_username\": _read_optional_string(\n getattr(preference, \"superset_username\", None)\n ),\n \"superset_username_normalized\": _read_optional_string(\n getattr(preference, \"superset_username_normalized\", None)\n ),\n \"show_only_my_dashboards\": _read_bool(\n getattr(preference, \"show_only_my_dashboards\", False), False\n ),\n \"show_only_slug_dashboards\": _read_bool(\n getattr(preference, \"show_only_slug_dashboards\", False), False\n ),\n }\n return {\n \"superset_username\": None,\n \"superset_username_normalized\": None,\n \"show_only_my_dashboards\": False,\n \"show_only_slug_dashboards\": False,\n }\n\n\n# [/DEF:_get_profile_filter_binding:Function]\n\n\n# [DEF:_resolve_profile_actor_aliases:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.\n# @SIDE_EFFECT: Performs at most one Superset users-lookup request.\ndef _resolve_profile_actor_aliases(env: Any, bound_username: str) -> List[str]:\n normalized_bound = _normalize_actor_alias_token(bound_username)\n if not normalized_bound:\n return []\n aliases: List[str] = [normalized_bound]\n try:\n client = SupersetClient(env)\n adapter = SupersetAccountLookupAdapter(\n network_client=client.network,\n environment_id=str(getattr(env, \"id\", \"\")),\n )\n lookup_payload = adapter.get_users_page(\n search=normalized_bound,\n page_index=0,\n page_size=20,\n sort_column=\"username\",\n sort_order=\"asc\",\n )\n lookup_items = (\n lookup_payload.get(\"items\", []) if isinstance(lookup_payload, dict) else []\n )\n matched_item: Optional[Dict[str, Any]] = None\n for item in lookup_items:\n if not isinstance(item, dict):\n continue\n if _normalize_actor_alias_token(item.get(\"username\")) == normalized_bound:\n matched_item = item\n break\n if matched_item is None:\n for item in lookup_items:\n if isinstance(item, dict):\n matched_item = item\n break\n display_alias = _normalize_actor_alias_token(\n (matched_item or {}).get(\"display_name\")\n )\n if display_alias and display_alias not in aliases:\n aliases.append(display_alias)\n logger.reflect(\n \"[REFLECT] Resolved profile actor aliases \"\n f\"(env={getattr(env, 'id', None)}, bound_username={normalized_bound!r}, \"\n f\"lookup_items={len(lookup_items)}, aliases={aliases!r})\"\n )\n except Exception as alias_error:\n logger.explore(\n \"[EXPLORE] Failed to resolve profile actor aliases via Superset users lookup \"\n f\"(env={getattr(env, 'id', None)}, bound_username={normalized_bound!r}): {alias_error}\"\n )\n return aliases\n\n\n# [/DEF:_resolve_profile_actor_aliases:Function]\n\n\n# [DEF:_matches_dashboard_actor_aliases:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Apply profile actor matching against multiple aliases (username + optional display name).\ndef _matches_dashboard_actor_aliases(\n profile_service: ProfileService,\n actor_aliases: List[str],\n owners: Optional[Any],\n modified_by: Optional[str],\n) -> bool:\n for actor_alias in actor_aliases:\n if profile_service.matches_dashboard_actor(\n bound_username=actor_alias,\n owners=owners,\n modified_by=modified_by,\n ):\n return True\n return False\n\n\n# [/DEF:_matches_dashboard_actor_aliases:Function]\n\n\n# [DEF:_task_matches_dashboard:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Checks whether task params are tied to a specific dashboard and environment.\ndef _task_matches_dashboard(\n task: Any, dashboard_id: int, env_id: Optional[str]\n) -> bool:\n plugin_id = getattr(task, \"plugin_id\", None)\n if plugin_id not in {\"superset-backup\", \"llm_dashboard_validation\"}:\n return False\n params = getattr(task, \"params\", {}) or {}\n dashboard_id_str = str(dashboard_id)\n if plugin_id == \"llm_dashboard_validation\":\n task_dashboard_id = params.get(\"dashboard_id\")\n if str(task_dashboard_id) != dashboard_id_str:\n return False\n if env_id:\n task_env = params.get(\"environment_id\")\n return str(task_env) == str(env_id)\n return True\n dashboard_ids = params.get(\"dashboard_ids\") or params.get(\"dashboards\") or []\n normalized_ids = {str(item) for item in dashboard_ids}\n if dashboard_id_str not in normalized_ids:\n return False\n if env_id:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n return str(task_env) == str(env_id)\n return True\n\n\n# [/DEF:_task_matches_dashboard:Function]\n# [/DEF:DashboardProjection:Module]\n" + "anchor_syntax": "region", + "body": "# #region DashboardProjection [C:2] [TYPE Module] [SEMANTICS dashboard, api, transform, profile, filter]\n# @BRIEF Dashboard response projection and profile-filter helpers for Dashboard Hub routes.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ProfileService]\n\nfrom typing import Any\n\nfrom src.core.logger import logger\nfrom src.core.superset_client import SupersetClient\nfrom src.core.superset_profile_lookup import SupersetAccountLookupAdapter\nfrom src.models.auth import User\nfrom src.services.profile_service import ProfileService\n\n\n# #region _normalize_actor_alias_token [C:2] [TYPE Function]\n# @BRIEF Normalize actor alias token to comparable trim+lower text.\ndef _normalize_actor_alias_token(value: Any) -> str | None:\n if value is None:\n return None\n normalized = str(value).strip().lower()\n return normalized if normalized else None\n\n\n# #endregion _normalize_actor_alias_token\n\n\n# #region _normalize_owner_display_token [C:2] [TYPE Function]\n# @BRIEF Project owner payload value into stable display string for API response contracts.\ndef _normalize_owner_display_token(owner: Any) -> str | None:\n if owner is None:\n return None\n if isinstance(owner, dict):\n for key in (\"username\", \"full_name\", \"first_name\", \"email\"):\n candidate = owner.get(key)\n if isinstance(candidate, str) and candidate.strip():\n return candidate.strip()\n return None\n if isinstance(owner, str):\n return owner.strip() or None\n return None\n\n\n# #endregion _normalize_owner_display_token\n\n\n# #region _normalize_dashboard_owner_values [C:2] [TYPE Function]\n# @BRIEF Normalize dashboard owners payload to optional list of display strings.\ndef _normalize_dashboard_owner_values(owners: Any) -> list[str] | None:\n if owners is None:\n return None\n raw_items: list[Any]\n if isinstance(owners, list):\n raw_items = owners\n else:\n raw_items = [owners]\n normalized: list[str] = []\n for owner in raw_items:\n token = _normalize_owner_display_token(owner)\n if token and token not in normalized:\n normalized.append(token)\n return normalized\n\n\n# #endregion _normalize_dashboard_owner_values\n\n\n# #region _project_dashboard_response_items [C:2] [TYPE Function]\n# @BRIEF Project dashboard payloads to response-contract-safe shape.\ndef _project_dashboard_response_items(\n dashboards: list[dict[str, Any]],\n) -> list[dict[str, Any]]:\n projected: list[dict[str, Any]] = []\n for dashboard in dashboards:\n projected_dashboard = dict(dashboard)\n projected_dashboard[\"owners\"] = _normalize_dashboard_owner_values(\n projected_dashboard.get(\"owners\")\n )\n projected.append(projected_dashboard)\n return projected\n\n\n# #endregion _project_dashboard_response_items\n\n\n# #region _get_profile_filter_binding [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard profile-filter binding through current or legacy profile service contracts.\ndef _get_profile_filter_binding(\n profile_service: Any, current_user: User\n) -> dict[str, Any]:\n\n def _read_optional_string(value: Any) -> str | None:\n return value if isinstance(value, str) else None\n\n def _read_bool(value: Any, default: bool) -> bool:\n return value if isinstance(value, bool) else default\n\n if hasattr(profile_service, \"get_dashboard_filter_binding\"):\n binding = profile_service.get_dashboard_filter_binding(current_user)\n if isinstance(binding, dict):\n return {\n \"superset_username\": _read_optional_string(\n binding.get(\"superset_username\")\n ),\n \"superset_username_normalized\": _read_optional_string(\n binding.get(\"superset_username_normalized\")\n ),\n \"show_only_my_dashboards\": _read_bool(\n binding.get(\"show_only_my_dashboards\"), False\n ),\n \"show_only_slug_dashboards\": _read_bool(\n binding.get(\"show_only_slug_dashboards\"), False\n ),\n }\n if hasattr(profile_service, \"get_my_preference\"):\n response = profile_service.get_my_preference(current_user)\n preference = getattr(response, \"preference\", None)\n return {\n \"superset_username\": _read_optional_string(\n getattr(preference, \"superset_username\", None)\n ),\n \"superset_username_normalized\": _read_optional_string(\n getattr(preference, \"superset_username_normalized\", None)\n ),\n \"show_only_my_dashboards\": _read_bool(\n getattr(preference, \"show_only_my_dashboards\", False), False\n ),\n \"show_only_slug_dashboards\": _read_bool(\n getattr(preference, \"show_only_slug_dashboards\", False), False\n ),\n }\n return {\n \"superset_username\": None,\n \"superset_username_normalized\": None,\n \"show_only_my_dashboards\": False,\n \"show_only_slug_dashboards\": False,\n }\n\n\n# #endregion _get_profile_filter_binding\n\n\n# #region _resolve_profile_actor_aliases [C:2] [TYPE Function]\n# @BRIEF Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.\n# @SIDE_EFFECT: Performs at most one Superset users-lookup request.\ndef _resolve_profile_actor_aliases(env: Any, bound_username: str) -> list[str]:\n normalized_bound = _normalize_actor_alias_token(bound_username)\n if not normalized_bound:\n return []\n aliases: list[str] = [normalized_bound]\n try:\n client = SupersetClient(env)\n adapter = SupersetAccountLookupAdapter(\n network_client=client.network,\n environment_id=str(getattr(env, \"id\", \"\")),\n )\n lookup_payload = adapter.get_users_page(\n search=normalized_bound,\n page_index=0,\n page_size=20,\n sort_column=\"username\",\n sort_order=\"asc\",\n )\n lookup_items = (\n lookup_payload.get(\"items\", []) if isinstance(lookup_payload, dict) else []\n )\n matched_item: dict[str, Any] | None = None\n for item in lookup_items:\n if not isinstance(item, dict):\n continue\n if _normalize_actor_alias_token(item.get(\"username\")) == normalized_bound:\n matched_item = item\n break\n if matched_item is None:\n for item in lookup_items:\n if isinstance(item, dict):\n matched_item = item\n break\n display_alias = _normalize_actor_alias_token(\n (matched_item or {}).get(\"display_name\")\n )\n if display_alias and display_alias not in aliases:\n aliases.append(display_alias)\n logger.reflect(\n \"Resolved profile actor aliases\",\n extra={\"src\": \"_resolve_profile_actor_aliases\", \"payload\": {\"env\": getattr(env, 'id', None), \"bound_username\": normalized_bound, \"lookup_items\": len(lookup_items), \"aliases\": aliases}},\n )\n except Exception as alias_error:\n logger.explore(\n \"Failed to resolve profile actor aliases via Superset users lookup\",\n extra={\"src\": \"_resolve_profile_actor_aliases\", \"payload\": {\"env\": getattr(env, 'id', None), \"bound_username\": normalized_bound}, \"error\": str(alias_error)},\n )\n return aliases\n\n\n# #endregion _resolve_profile_actor_aliases\n\n\n# #region _matches_dashboard_actor_aliases [C:2] [TYPE Function]\n# @BRIEF Apply profile actor matching against multiple aliases (username + optional display name).\ndef _matches_dashboard_actor_aliases(\n profile_service: ProfileService,\n actor_aliases: list[str],\n owners: Any | None,\n modified_by: str | None,\n) -> bool:\n for actor_alias in actor_aliases:\n if profile_service.matches_dashboard_actor(\n bound_username=actor_alias,\n owners=owners,\n modified_by=modified_by,\n ):\n return True\n return False\n\n\n# #endregion _matches_dashboard_actor_aliases\n\n\n# #region _task_matches_dashboard [C:2] [TYPE Function]\n# @BRIEF Checks whether task params are tied to a specific dashboard and environment.\ndef _task_matches_dashboard(\n task: Any, dashboard_id: int, env_id: str | None\n) -> bool:\n plugin_id = getattr(task, \"plugin_id\", None)\n if plugin_id not in {\"superset-backup\", \"llm_dashboard_validation\"}:\n return False\n params = getattr(task, \"params\", {}) or {}\n dashboard_id_str = str(dashboard_id)\n if plugin_id == \"llm_dashboard_validation\":\n task_dashboard_id = params.get(\"dashboard_id\")\n if str(task_dashboard_id) != dashboard_id_str:\n return False\n if env_id:\n task_env = params.get(\"environment_id\")\n return str(task_env) == str(env_id)\n return True\n dashboard_ids = params.get(\"dashboard_ids\") or params.get(\"dashboards\") or []\n normalized_ids = {str(item) for item in dashboard_ids}\n if dashboard_id_str not in normalized_ids:\n return False\n if env_id:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n return str(task_env) == str(env_id)\n return True\n\n\n# #endregion _task_matches_dashboard\n# #endregion DashboardProjection\n" }, { "contract_id": "_normalize_actor_alias_token", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_projection.py", - "start_line": 18, - "end_line": 28, + "start_line": 16, + "end_line": 25, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Normalize actor alias token to comparable trim+lower text." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_normalize_actor_alias_token:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize actor alias token to comparable trim+lower text.\ndef _normalize_actor_alias_token(value: Any) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip().lower()\n return normalized if normalized else None\n\n\n# [/DEF:_normalize_actor_alias_token:Function]\n" + "anchor_syntax": "region", + "body": "# #region _normalize_actor_alias_token [C:2] [TYPE Function]\n# @BRIEF Normalize actor alias token to comparable trim+lower text.\ndef _normalize_actor_alias_token(value: Any) -> str | None:\n if value is None:\n return None\n normalized = str(value).strip().lower()\n return normalized if normalized else None\n\n\n# #endregion _normalize_actor_alias_token\n" }, { "contract_id": "_normalize_owner_display_token", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_projection.py", - "start_line": 31, - "end_line": 48, + "start_line": 28, + "end_line": 44, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Project owner payload value into stable display string for API response contracts." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_normalize_owner_display_token:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Project owner payload value into stable display string for API response contracts.\ndef _normalize_owner_display_token(owner: Any) -> Optional[str]:\n if owner is None:\n return None\n if isinstance(owner, dict):\n for key in (\"username\", \"full_name\", \"first_name\", \"email\"):\n candidate = owner.get(key)\n if isinstance(candidate, str) and candidate.strip():\n return candidate.strip()\n return None\n if isinstance(owner, str):\n return owner.strip() or None\n return None\n\n\n# [/DEF:_normalize_owner_display_token:Function]\n" + "anchor_syntax": "region", + "body": "# #region _normalize_owner_display_token [C:2] [TYPE Function]\n# @BRIEF Project owner payload value into stable display string for API response contracts.\ndef _normalize_owner_display_token(owner: Any) -> str | None:\n if owner is None:\n return None\n if isinstance(owner, dict):\n for key in (\"username\", \"full_name\", \"first_name\", \"email\"):\n candidate = owner.get(key)\n if isinstance(candidate, str) and candidate.strip():\n return candidate.strip()\n return None\n if isinstance(owner, str):\n return owner.strip() or None\n return None\n\n\n# #endregion _normalize_owner_display_token\n" }, { "contract_id": "_normalize_dashboard_owner_values", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_projection.py", - "start_line": 51, - "end_line": 70, + "start_line": 47, + "end_line": 65, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Normalize dashboard owners payload to optional list of display strings." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_normalize_dashboard_owner_values:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize dashboard owners payload to optional list of display strings.\ndef _normalize_dashboard_owner_values(owners: Any) -> Optional[List[str]]:\n if owners is None:\n return None\n raw_items: List[Any]\n if isinstance(owners, list):\n raw_items = owners\n else:\n raw_items = [owners]\n normalized: List[str] = []\n for owner in raw_items:\n token = _normalize_owner_display_token(owner)\n if token and token not in normalized:\n normalized.append(token)\n return normalized\n\n\n# [/DEF:_normalize_dashboard_owner_values:Function]\n" + "anchor_syntax": "region", + "body": "# #region _normalize_dashboard_owner_values [C:2] [TYPE Function]\n# @BRIEF Normalize dashboard owners payload to optional list of display strings.\ndef _normalize_dashboard_owner_values(owners: Any) -> list[str] | None:\n if owners is None:\n return None\n raw_items: list[Any]\n if isinstance(owners, list):\n raw_items = owners\n else:\n raw_items = [owners]\n normalized: list[str] = []\n for owner in raw_items:\n token = _normalize_owner_display_token(owner)\n if token and token not in normalized:\n normalized.append(token)\n return normalized\n\n\n# #endregion _normalize_dashboard_owner_values\n" }, { "contract_id": "_project_dashboard_response_items", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_projection.py", - "start_line": 73, - "end_line": 89, + "start_line": 68, + "end_line": 83, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Project dashboard payloads to response-contract-safe shape." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_project_dashboard_response_items:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Project dashboard payloads to response-contract-safe shape.\ndef _project_dashboard_response_items(\n dashboards: List[Dict[str, Any]],\n) -> List[Dict[str, Any]]:\n projected: List[Dict[str, Any]] = []\n for dashboard in dashboards:\n projected_dashboard = dict(dashboard)\n projected_dashboard[\"owners\"] = _normalize_dashboard_owner_values(\n projected_dashboard.get(\"owners\")\n )\n projected.append(projected_dashboard)\n return projected\n\n\n# [/DEF:_project_dashboard_response_items:Function]\n" + "anchor_syntax": "region", + "body": "# #region _project_dashboard_response_items [C:2] [TYPE Function]\n# @BRIEF Project dashboard payloads to response-contract-safe shape.\ndef _project_dashboard_response_items(\n dashboards: list[dict[str, Any]],\n) -> list[dict[str, Any]]:\n projected: list[dict[str, Any]] = []\n for dashboard in dashboards:\n projected_dashboard = dict(dashboard)\n projected_dashboard[\"owners\"] = _normalize_dashboard_owner_values(\n projected_dashboard.get(\"owners\")\n )\n projected.append(projected_dashboard)\n return projected\n\n\n# #endregion _project_dashboard_response_items\n" }, { "contract_id": "_get_profile_filter_binding", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_projection.py", - "start_line": 92, - "end_line": 147, + "start_line": 86, + "end_line": 140, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Resolve dashboard profile-filter binding through current or legacy profile service contracts." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_get_profile_filter_binding:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard profile-filter binding through current or legacy profile service contracts.\ndef _get_profile_filter_binding(\n profile_service: Any, current_user: User\n) -> Dict[str, Any]:\n\n def _read_optional_string(value: Any) -> Optional[str]:\n return value if isinstance(value, str) else None\n\n def _read_bool(value: Any, default: bool) -> bool:\n return value if isinstance(value, bool) else default\n\n if hasattr(profile_service, \"get_dashboard_filter_binding\"):\n binding = profile_service.get_dashboard_filter_binding(current_user)\n if isinstance(binding, dict):\n return {\n \"superset_username\": _read_optional_string(\n binding.get(\"superset_username\")\n ),\n \"superset_username_normalized\": _read_optional_string(\n binding.get(\"superset_username_normalized\")\n ),\n \"show_only_my_dashboards\": _read_bool(\n binding.get(\"show_only_my_dashboards\"), False\n ),\n \"show_only_slug_dashboards\": _read_bool(\n binding.get(\"show_only_slug_dashboards\"), False\n ),\n }\n if hasattr(profile_service, \"get_my_preference\"):\n response = profile_service.get_my_preference(current_user)\n preference = getattr(response, \"preference\", None)\n return {\n \"superset_username\": _read_optional_string(\n getattr(preference, \"superset_username\", None)\n ),\n \"superset_username_normalized\": _read_optional_string(\n getattr(preference, \"superset_username_normalized\", None)\n ),\n \"show_only_my_dashboards\": _read_bool(\n getattr(preference, \"show_only_my_dashboards\", False), False\n ),\n \"show_only_slug_dashboards\": _read_bool(\n getattr(preference, \"show_only_slug_dashboards\", False), False\n ),\n }\n return {\n \"superset_username\": None,\n \"superset_username_normalized\": None,\n \"show_only_my_dashboards\": False,\n \"show_only_slug_dashboards\": False,\n }\n\n\n# [/DEF:_get_profile_filter_binding:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_profile_filter_binding [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard profile-filter binding through current or legacy profile service contracts.\ndef _get_profile_filter_binding(\n profile_service: Any, current_user: User\n) -> dict[str, Any]:\n\n def _read_optional_string(value: Any) -> str | None:\n return value if isinstance(value, str) else None\n\n def _read_bool(value: Any, default: bool) -> bool:\n return value if isinstance(value, bool) else default\n\n if hasattr(profile_service, \"get_dashboard_filter_binding\"):\n binding = profile_service.get_dashboard_filter_binding(current_user)\n if isinstance(binding, dict):\n return {\n \"superset_username\": _read_optional_string(\n binding.get(\"superset_username\")\n ),\n \"superset_username_normalized\": _read_optional_string(\n binding.get(\"superset_username_normalized\")\n ),\n \"show_only_my_dashboards\": _read_bool(\n binding.get(\"show_only_my_dashboards\"), False\n ),\n \"show_only_slug_dashboards\": _read_bool(\n binding.get(\"show_only_slug_dashboards\"), False\n ),\n }\n if hasattr(profile_service, \"get_my_preference\"):\n response = profile_service.get_my_preference(current_user)\n preference = getattr(response, \"preference\", None)\n return {\n \"superset_username\": _read_optional_string(\n getattr(preference, \"superset_username\", None)\n ),\n \"superset_username_normalized\": _read_optional_string(\n getattr(preference, \"superset_username_normalized\", None)\n ),\n \"show_only_my_dashboards\": _read_bool(\n getattr(preference, \"show_only_my_dashboards\", False), False\n ),\n \"show_only_slug_dashboards\": _read_bool(\n getattr(preference, \"show_only_slug_dashboards\", False), False\n ),\n }\n return {\n \"superset_username\": None,\n \"superset_username_normalized\": None,\n \"show_only_my_dashboards\": False,\n \"show_only_slug_dashboards\": False,\n }\n\n\n# #endregion _get_profile_filter_binding\n" }, { "contract_id": "_resolve_profile_actor_aliases", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_projection.py", - "start_line": 150, - "end_line": 205, + "start_line": 143, + "end_line": 196, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.", "SIDE_EFFECT": "Performs at most one Superset users-lookup request." }, @@ -14409,53 +12680,53 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_profile_actor_aliases:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.\n# @SIDE_EFFECT: Performs at most one Superset users-lookup request.\ndef _resolve_profile_actor_aliases(env: Any, bound_username: str) -> List[str]:\n normalized_bound = _normalize_actor_alias_token(bound_username)\n if not normalized_bound:\n return []\n aliases: List[str] = [normalized_bound]\n try:\n client = SupersetClient(env)\n adapter = SupersetAccountLookupAdapter(\n network_client=client.network,\n environment_id=str(getattr(env, \"id\", \"\")),\n )\n lookup_payload = adapter.get_users_page(\n search=normalized_bound,\n page_index=0,\n page_size=20,\n sort_column=\"username\",\n sort_order=\"asc\",\n )\n lookup_items = (\n lookup_payload.get(\"items\", []) if isinstance(lookup_payload, dict) else []\n )\n matched_item: Optional[Dict[str, Any]] = None\n for item in lookup_items:\n if not isinstance(item, dict):\n continue\n if _normalize_actor_alias_token(item.get(\"username\")) == normalized_bound:\n matched_item = item\n break\n if matched_item is None:\n for item in lookup_items:\n if isinstance(item, dict):\n matched_item = item\n break\n display_alias = _normalize_actor_alias_token(\n (matched_item or {}).get(\"display_name\")\n )\n if display_alias and display_alias not in aliases:\n aliases.append(display_alias)\n logger.reflect(\n \"[REFLECT] Resolved profile actor aliases \"\n f\"(env={getattr(env, 'id', None)}, bound_username={normalized_bound!r}, \"\n f\"lookup_items={len(lookup_items)}, aliases={aliases!r})\"\n )\n except Exception as alias_error:\n logger.explore(\n \"[EXPLORE] Failed to resolve profile actor aliases via Superset users lookup \"\n f\"(env={getattr(env, 'id', None)}, bound_username={normalized_bound!r}): {alias_error}\"\n )\n return aliases\n\n\n# [/DEF:_resolve_profile_actor_aliases:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_profile_actor_aliases [C:2] [TYPE Function]\n# @BRIEF Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.\n# @SIDE_EFFECT: Performs at most one Superset users-lookup request.\ndef _resolve_profile_actor_aliases(env: Any, bound_username: str) -> list[str]:\n normalized_bound = _normalize_actor_alias_token(bound_username)\n if not normalized_bound:\n return []\n aliases: list[str] = [normalized_bound]\n try:\n client = SupersetClient(env)\n adapter = SupersetAccountLookupAdapter(\n network_client=client.network,\n environment_id=str(getattr(env, \"id\", \"\")),\n )\n lookup_payload = adapter.get_users_page(\n search=normalized_bound,\n page_index=0,\n page_size=20,\n sort_column=\"username\",\n sort_order=\"asc\",\n )\n lookup_items = (\n lookup_payload.get(\"items\", []) if isinstance(lookup_payload, dict) else []\n )\n matched_item: dict[str, Any] | None = None\n for item in lookup_items:\n if not isinstance(item, dict):\n continue\n if _normalize_actor_alias_token(item.get(\"username\")) == normalized_bound:\n matched_item = item\n break\n if matched_item is None:\n for item in lookup_items:\n if isinstance(item, dict):\n matched_item = item\n break\n display_alias = _normalize_actor_alias_token(\n (matched_item or {}).get(\"display_name\")\n )\n if display_alias and display_alias not in aliases:\n aliases.append(display_alias)\n logger.reflect(\n \"Resolved profile actor aliases\",\n extra={\"src\": \"_resolve_profile_actor_aliases\", \"payload\": {\"env\": getattr(env, 'id', None), \"bound_username\": normalized_bound, \"lookup_items\": len(lookup_items), \"aliases\": aliases}},\n )\n except Exception as alias_error:\n logger.explore(\n \"Failed to resolve profile actor aliases via Superset users lookup\",\n extra={\"src\": \"_resolve_profile_actor_aliases\", \"payload\": {\"env\": getattr(env, 'id', None), \"bound_username\": normalized_bound}, \"error\": str(alias_error)},\n )\n return aliases\n\n\n# #endregion _resolve_profile_actor_aliases\n" }, { "contract_id": "_matches_dashboard_actor_aliases", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_projection.py", - "start_line": 208, - "end_line": 227, + "start_line": 199, + "end_line": 217, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Apply profile actor matching against multiple aliases (username + optional display name)." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_matches_dashboard_actor_aliases:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Apply profile actor matching against multiple aliases (username + optional display name).\ndef _matches_dashboard_actor_aliases(\n profile_service: ProfileService,\n actor_aliases: List[str],\n owners: Optional[Any],\n modified_by: Optional[str],\n) -> bool:\n for actor_alias in actor_aliases:\n if profile_service.matches_dashboard_actor(\n bound_username=actor_alias,\n owners=owners,\n modified_by=modified_by,\n ):\n return True\n return False\n\n\n# [/DEF:_matches_dashboard_actor_aliases:Function]\n" + "anchor_syntax": "region", + "body": "# #region _matches_dashboard_actor_aliases [C:2] [TYPE Function]\n# @BRIEF Apply profile actor matching against multiple aliases (username + optional display name).\ndef _matches_dashboard_actor_aliases(\n profile_service: ProfileService,\n actor_aliases: list[str],\n owners: Any | None,\n modified_by: str | None,\n) -> bool:\n for actor_alias in actor_aliases:\n if profile_service.matches_dashboard_actor(\n bound_username=actor_alias,\n owners=owners,\n modified_by=modified_by,\n ):\n return True\n return False\n\n\n# #endregion _matches_dashboard_actor_aliases\n" }, { "contract_id": "_task_matches_dashboard", "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_projection.py", - "start_line": 230, - "end_line": 259, + "start_line": 220, + "end_line": 248, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Checks whether task params are tied to a specific dashboard and environment." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_task_matches_dashboard:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Checks whether task params are tied to a specific dashboard and environment.\ndef _task_matches_dashboard(\n task: Any, dashboard_id: int, env_id: Optional[str]\n) -> bool:\n plugin_id = getattr(task, \"plugin_id\", None)\n if plugin_id not in {\"superset-backup\", \"llm_dashboard_validation\"}:\n return False\n params = getattr(task, \"params\", {}) or {}\n dashboard_id_str = str(dashboard_id)\n if plugin_id == \"llm_dashboard_validation\":\n task_dashboard_id = params.get(\"dashboard_id\")\n if str(task_dashboard_id) != dashboard_id_str:\n return False\n if env_id:\n task_env = params.get(\"environment_id\")\n return str(task_env) == str(env_id)\n return True\n dashboard_ids = params.get(\"dashboard_ids\") or params.get(\"dashboards\") or []\n normalized_ids = {str(item) for item in dashboard_ids}\n if dashboard_id_str not in normalized_ids:\n return False\n if env_id:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n return str(task_env) == str(env_id)\n return True\n\n\n# [/DEF:_task_matches_dashboard:Function]\n" + "anchor_syntax": "region", + "body": "# #region _task_matches_dashboard [C:2] [TYPE Function]\n# @BRIEF Checks whether task params are tied to a specific dashboard and environment.\ndef _task_matches_dashboard(\n task: Any, dashboard_id: int, env_id: str | None\n) -> bool:\n plugin_id = getattr(task, \"plugin_id\", None)\n if plugin_id not in {\"superset-backup\", \"llm_dashboard_validation\"}:\n return False\n params = getattr(task, \"params\", {}) or {}\n dashboard_id_str = str(dashboard_id)\n if plugin_id == \"llm_dashboard_validation\":\n task_dashboard_id = params.get(\"dashboard_id\")\n if str(task_dashboard_id) != dashboard_id_str:\n return False\n if env_id:\n task_env = params.get(\"environment_id\")\n return str(task_env) == str(env_id)\n return True\n dashboard_ids = params.get(\"dashboard_ids\") or params.get(\"dashboards\") or []\n normalized_ids = {str(item) for item in dashboard_ids}\n if dashboard_id_str not in normalized_ids:\n return False\n if env_id:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n return str(task_env) == str(env_id)\n return True\n\n\n# #endregion _task_matches_dashboard\n" }, { "contract_id": "DashboardsRouter", "contract_type": "Block", "file_path": "backend/src/api/routes/dashboards/_router.py", "start_line": 1, - "end_line": 8, + "end_line": 7, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Single APIRouter instance for all dashboard endpoints." }, "relations": [ @@ -14486,27 +12757,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardsRouter:Block]\n# @COMPLEXITY: 1\n# @PURPOSE: Single APIRouter instance for all dashboard endpoints.\n# @RELATION: BINDS_TO -> [DashboardsApi]\nfrom fastapi import APIRouter\n\nrouter = APIRouter(prefix=\"/api/dashboards\", tags=[\"Dashboards\"])\n# [/DEF:DashboardsRouter:Block]\n" + "anchor_syntax": "region", + "body": "# #region DashboardsRouter [C:1] [TYPE Block] [SEMANTICS dashboard, api, router]\n# @BRIEF Single APIRouter instance for all dashboard endpoints.\n# @RELATION BINDS_TO -> [DashboardsApi]\nfrom fastapi import APIRouter\n\nrouter = APIRouter(prefix=\"/api/dashboards\", tags=[\"Dashboards\"])\n# #endregion DashboardsRouter\n" }, { "contract_id": "DashboardSchemas", "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/_schemas.py", "start_line": 1, - "end_line": 232, + "end_line": 214, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "LAYER": "Infra", - "PURPOSE": "DTO classes for the Dashboard Hub API.", - "SEMANTICS": [ - "api", - "dashboards", - "schemas", - "dto" - ] + "PURPOSE": "DTO classes for the Dashboard Hub API." }, "relations": [ { @@ -14536,19 +12801,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardSchemas:Module]\n# @COMPLEXITY: 1\n# @SEMANTICS: api, dashboards, schemas, dto\n# @PURPOSE: DTO classes for the Dashboard Hub API.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> [Pydantic]\n\nfrom typing import List, Optional, Dict, Any, Literal\nfrom pydantic import BaseModel, Field\n\n\n# [DEF:GitStatus:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for dashboard Git synchronization status.\nclass GitStatus(BaseModel):\n branch: Optional[str] = None\n sync_status: Optional[str] = Field(None, pattern=\"^OK|DIFF|NO_REPO|ERROR$\")\n has_repo: Optional[bool] = None\n has_changes_for_commit: Optional[bool] = None\n\n\n# [/DEF:GitStatus:DataClass]\n\n\n# [DEF:LastTask:DataClass]\n# @COMPLEXITY: 2\n# @PURPOSE: DTO for the most recent background task associated with a dashboard.\nclass LastTask(BaseModel):\n task_id: Optional[str] = None\n status: Optional[str] = Field(\n None,\n pattern=\"^PENDING|RUNNING|SUCCESS|FAILED|ERROR|AWAITING_INPUT|WAITING_INPUT|AWAITING_MAPPING$\",\n )\n validation_status: Optional[str] = Field(None, pattern=\"^PASS|FAIL|WARN|UNKNOWN$\")\n\n\n# [/DEF:LastTask:DataClass]\n\n\n# [DEF:DashboardItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO representing a single dashboard with projected metadata.\nclass DashboardItem(BaseModel):\n id: int\n title: str\n slug: Optional[str] = None\n url: Optional[str] = None\n last_modified: Optional[str] = None\n created_by: Optional[str] = None\n modified_by: Optional[str] = None\n owners: Optional[List[str]] = None\n git_status: Optional[GitStatus] = None\n last_task: Optional[LastTask] = None\n\n\n# [/DEF:DashboardItem:DataClass]\n\n\n# [DEF:EffectiveProfileFilter:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Metadata about applied profile filters for UI context.\nclass EffectiveProfileFilter(BaseModel):\n applied: bool\n source_page: Literal[\"dashboards_main\", \"other\"] = \"dashboards_main\"\n override_show_all: bool = False\n username: Optional[str] = None\n match_logic: Optional[\n Literal[\"owners_or_modified_by\", \"slug_only\", \"owners_or_modified_by+slug_only\"]\n ] = None\n\n\n# [/DEF:EffectiveProfileFilter:DataClass]\n\n\n# [DEF:DashboardsResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Envelope DTO for paginated dashboards list.\nclass DashboardsResponse(BaseModel):\n dashboards: List[DashboardItem]\n total: int\n page: int\n page_size: int\n total_pages: int\n effective_profile_filter: Optional[EffectiveProfileFilter] = None\n\n\n# [/DEF:DashboardsResponse:DataClass]\n\n\n# [DEF:DashboardChartItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for a chart linked to a dashboard.\nclass DashboardChartItem(BaseModel):\n id: int\n title: str\n viz_type: Optional[str] = None\n dataset_id: Optional[int] = None\n last_modified: Optional[str] = None\n overview: Optional[str] = None\n\n\n# [/DEF:DashboardChartItem:DataClass]\n\n\n# [DEF:DashboardDatasetItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for a dataset associated with a dashboard.\nclass DashboardDatasetItem(BaseModel):\n id: int\n table_name: str\n schema: Optional[str] = None\n database: str\n last_modified: Optional[str] = None\n overview: Optional[str] = None\n\n\n# [/DEF:DashboardDatasetItem:DataClass]\n\n\n# [DEF:DashboardDetailResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Detailed dashboard metadata including children.\nclass DashboardDetailResponse(BaseModel):\n id: int\n title: str\n slug: Optional[str] = None\n url: Optional[str] = None\n description: Optional[str] = None\n last_modified: Optional[str] = None\n published: Optional[bool] = None\n charts: List[DashboardChartItem]\n datasets: List[DashboardDatasetItem]\n chart_count: int\n dataset_count: int\n\n\n# [/DEF:DashboardDetailResponse:DataClass]\n\n\n# [DEF:DashboardTaskHistoryItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Individual history record entry.\nclass DashboardTaskHistoryItem(BaseModel):\n id: str\n plugin_id: str\n status: str\n validation_status: Optional[str] = None\n started_at: Optional[str] = None\n finished_at: Optional[str] = None\n env_id: Optional[str] = None\n summary: Optional[str] = None\n\n\n# [/DEF:DashboardTaskHistoryItem:DataClass]\n\n\n# [DEF:DashboardTaskHistoryResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Collection DTO for task history.\nclass DashboardTaskHistoryResponse(BaseModel):\n dashboard_id: int\n items: List[DashboardTaskHistoryItem]\n\n\n# [/DEF:DashboardTaskHistoryResponse:DataClass]\n\n\n# [DEF:DatabaseMapping:DataClass]\n# @COMPLEXITY: 2\n# @PURPOSE: DTO for cross-environment database ID mapping.\nclass DatabaseMapping(BaseModel):\n source_db: str\n target_db: str\n source_db_uuid: Optional[str] = None\n target_db_uuid: Optional[str] = None\n confidence: float\n\n\n# [/DEF:DatabaseMapping:DataClass]\n\n\n# [DEF:DatabaseMappingsResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Wrapper for database mappings.\nclass DatabaseMappingsResponse(BaseModel):\n mappings: List[DatabaseMapping]\n\n\n# [/DEF:DatabaseMappingsResponse:DataClass]\n\n\n# [DEF:MigrateRequest:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for dashboard migration requests.\nclass MigrateRequest(BaseModel):\n source_env_id: str = Field(..., description=\"Source environment ID\")\n target_env_id: str = Field(..., description=\"Target environment ID\")\n dashboard_ids: List[int] = Field(\n ..., description=\"List of dashboard IDs to migrate\"\n )\n db_mappings: Optional[Dict[str, str]] = Field(\n None, description=\"Database mappings for migration\"\n )\n replace_db_config: bool = Field(False, description=\"Replace database configuration\")\n\n\n# [/DEF:MigrateRequest:DataClass]\n\n\n# [DEF:TaskResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for async task ID return.\nclass TaskResponse(BaseModel):\n task_id: str\n\n\n# [/DEF:TaskResponse:DataClass]\n\n\n# [DEF:BackupRequest:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for dashboard backup requests.\nclass BackupRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dashboard_ids: List[int] = Field(..., description=\"List of dashboard IDs to backup\")\n schedule: Optional[str] = Field(\n None, description=\"Cron schedule for recurring backups (e.g., '0 0 * * *')\"\n )\n\n\n# [/DEF:BackupRequest:DataClass]\n# [/DEF:DashboardSchemas:Module]\n" + "anchor_syntax": "region", + "body": "# #region DashboardSchemas [C:1] [TYPE Module] [SEMANTICS pydantic, dashboard, api, dto, git-status]\n# @BRIEF DTO classes for the Dashboard Hub API.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [Pydantic]\n\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\n\n\n# #region GitStatus [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard Git synchronization status.\nclass GitStatus(BaseModel):\n branch: str | None = None\n sync_status: str | None = Field(None, pattern=\"^OK|DIFF|NO_REPO|ERROR$\")\n has_repo: bool | None = None\n has_changes_for_commit: bool | None = None\n\n\n# #endregion GitStatus\n\n\n# #region LastTask [C:2] [TYPE DataClass]\n# @BRIEF DTO for the most recent background task associated with a dashboard.\nclass LastTask(BaseModel):\n task_id: str | None = None\n status: str | None = Field(\n None,\n pattern=\"^PENDING|RUNNING|SUCCESS|FAILED|ERROR|AWAITING_INPUT|WAITING_INPUT|AWAITING_MAPPING$\",\n )\n validation_status: str | None = Field(None, pattern=\"^PASS|FAIL|WARN|UNKNOWN$\")\n\n\n# #endregion LastTask\n\n\n# #region DashboardItem [C:1] [TYPE DataClass]\n# @BRIEF DTO representing a single dashboard with projected metadata.\nclass DashboardItem(BaseModel):\n id: int\n title: str\n slug: str | None = None\n url: str | None = None\n last_modified: str | None = None\n created_by: str | None = None\n modified_by: str | None = None\n owners: list[str] | None = None\n git_status: GitStatus | None = None\n last_task: LastTask | None = None\n\n\n# #endregion DashboardItem\n\n\n# #region EffectiveProfileFilter [C:1] [TYPE DataClass]\n# @BRIEF Metadata about applied profile filters for UI context.\nclass EffectiveProfileFilter(BaseModel):\n applied: bool\n source_page: Literal[\"dashboards_main\", \"other\"] = \"dashboards_main\"\n override_show_all: bool = False\n username: str | None = None\n match_logic: Literal[\"owners_or_modified_by\", \"slug_only\", \"owners_or_modified_by+slug_only\"] | None = None\n\n\n# #endregion EffectiveProfileFilter\n\n\n# #region DashboardsResponse [C:1] [TYPE DataClass]\n# @BRIEF Envelope DTO for paginated dashboards list.\nclass DashboardsResponse(BaseModel):\n dashboards: list[DashboardItem]\n total: int\n page: int\n page_size: int\n total_pages: int\n effective_profile_filter: EffectiveProfileFilter | None = None\n\n\n# #endregion DashboardsResponse\n\n\n# #region DashboardChartItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a chart linked to a dashboard.\nclass DashboardChartItem(BaseModel):\n id: int\n title: str\n viz_type: str | None = None\n dataset_id: int | None = None\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardChartItem\n\n\n# #region DashboardDatasetItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dataset associated with a dashboard.\nclass DashboardDatasetItem(BaseModel):\n id: int\n table_name: str\n schema: str | None = None\n database: str\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardDatasetItem\n\n\n# #region DashboardDetailResponse [C:1] [TYPE DataClass]\n# @BRIEF Detailed dashboard metadata including children.\nclass DashboardDetailResponse(BaseModel):\n id: int\n title: str\n slug: str | None = None\n url: str | None = None\n description: str | None = None\n last_modified: str | None = None\n published: bool | None = None\n charts: list[DashboardChartItem]\n datasets: list[DashboardDatasetItem]\n chart_count: int\n dataset_count: int\n\n\n# #endregion DashboardDetailResponse\n\n\n# #region DashboardTaskHistoryItem [C:1] [TYPE DataClass]\n# @BRIEF Individual history record entry.\nclass DashboardTaskHistoryItem(BaseModel):\n id: str\n plugin_id: str\n status: str\n validation_status: str | None = None\n started_at: str | None = None\n finished_at: str | None = None\n env_id: str | None = None\n summary: str | None = None\n\n\n# #endregion DashboardTaskHistoryItem\n\n\n# #region DashboardTaskHistoryResponse [C:1] [TYPE DataClass]\n# @BRIEF Collection DTO for task history.\nclass DashboardTaskHistoryResponse(BaseModel):\n dashboard_id: int\n items: list[DashboardTaskHistoryItem]\n\n\n# #endregion DashboardTaskHistoryResponse\n\n\n# #region DatabaseMapping [C:2] [TYPE DataClass]\n# @BRIEF DTO for cross-environment database ID mapping.\nclass DatabaseMapping(BaseModel):\n source_db: str\n target_db: str\n source_db_uuid: str | None = None\n target_db_uuid: str | None = None\n confidence: float\n\n\n# #endregion DatabaseMapping\n\n\n# #region DatabaseMappingsResponse [C:1] [TYPE DataClass]\n# @BRIEF Wrapper for database mappings.\nclass DatabaseMappingsResponse(BaseModel):\n mappings: list[DatabaseMapping]\n\n\n# #endregion DatabaseMappingsResponse\n\n\n# #region MigrateRequest [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard migration requests.\nclass MigrateRequest(BaseModel):\n source_env_id: str = Field(..., description=\"Source environment ID\")\n target_env_id: str = Field(..., description=\"Target environment ID\")\n dashboard_ids: list[int] = Field(\n ..., description=\"List of dashboard IDs to migrate\"\n )\n db_mappings: dict[str, str] | None = Field(\n None, description=\"Database mappings for migration\"\n )\n replace_db_config: bool = Field(False, description=\"Replace database configuration\")\n\n\n# #endregion MigrateRequest\n\n\n# #region TaskResponse [C:1] [TYPE DataClass]\n# @BRIEF DTO for async task ID return.\nclass TaskResponse(BaseModel):\n task_id: str\n\n\n# #endregion TaskResponse\n\n\n# #region BackupRequest [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard backup requests.\nclass BackupRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dashboard_ids: list[int] = Field(..., description=\"List of dashboard IDs to backup\")\n schedule: str | None = Field(\n None, description=\"Cron schedule for recurring backups (e.g., '0 0 * * *')\"\n )\n\n\n# #endregion BackupRequest\n# #endregion DashboardSchemas\n" }, { "contract_id": "GitStatus", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 12, - "end_line": 22, + "start_line": 11, + "end_line": 20, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for dashboard Git synchronization status." }, "relations": [], @@ -14607,19 +12872,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitStatus:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for dashboard Git synchronization status.\nclass GitStatus(BaseModel):\n branch: Optional[str] = None\n sync_status: Optional[str] = Field(None, pattern=\"^OK|DIFF|NO_REPO|ERROR$\")\n has_repo: Optional[bool] = None\n has_changes_for_commit: Optional[bool] = None\n\n\n# [/DEF:GitStatus:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region GitStatus [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard Git synchronization status.\nclass GitStatus(BaseModel):\n branch: str | None = None\n sync_status: str | None = Field(None, pattern=\"^OK|DIFF|NO_REPO|ERROR$\")\n has_repo: bool | None = None\n has_changes_for_commit: bool | None = None\n\n\n# #endregion GitStatus\n" }, { "contract_id": "DashboardItem", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 40, - "end_line": 56, + "start_line": 37, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO representing a single dashboard with projected metadata." }, "relations": [], @@ -14678,19 +12943,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO representing a single dashboard with projected metadata.\nclass DashboardItem(BaseModel):\n id: int\n title: str\n slug: Optional[str] = None\n url: Optional[str] = None\n last_modified: Optional[str] = None\n created_by: Optional[str] = None\n modified_by: Optional[str] = None\n owners: Optional[List[str]] = None\n git_status: Optional[GitStatus] = None\n last_task: Optional[LastTask] = None\n\n\n# [/DEF:DashboardItem:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DashboardItem [C:1] [TYPE DataClass]\n# @BRIEF DTO representing a single dashboard with projected metadata.\nclass DashboardItem(BaseModel):\n id: int\n title: str\n slug: str | None = None\n url: str | None = None\n last_modified: str | None = None\n created_by: str | None = None\n modified_by: str | None = None\n owners: list[str] | None = None\n git_status: GitStatus | None = None\n last_task: LastTask | None = None\n\n\n# #endregion DashboardItem\n" }, { "contract_id": "EffectiveProfileFilter", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 59, - "end_line": 72, + "start_line": 55, + "end_line": 65, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Metadata about applied profile filters for UI context." }, "relations": [], @@ -14749,19 +13014,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:EffectiveProfileFilter:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Metadata about applied profile filters for UI context.\nclass EffectiveProfileFilter(BaseModel):\n applied: bool\n source_page: Literal[\"dashboards_main\", \"other\"] = \"dashboards_main\"\n override_show_all: bool = False\n username: Optional[str] = None\n match_logic: Optional[\n Literal[\"owners_or_modified_by\", \"slug_only\", \"owners_or_modified_by+slug_only\"]\n ] = None\n\n\n# [/DEF:EffectiveProfileFilter:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region EffectiveProfileFilter [C:1] [TYPE DataClass]\n# @BRIEF Metadata about applied profile filters for UI context.\nclass EffectiveProfileFilter(BaseModel):\n applied: bool\n source_page: Literal[\"dashboards_main\", \"other\"] = \"dashboards_main\"\n override_show_all: bool = False\n username: str | None = None\n match_logic: Literal[\"owners_or_modified_by\", \"slug_only\", \"owners_or_modified_by+slug_only\"] | None = None\n\n\n# #endregion EffectiveProfileFilter\n" }, { "contract_id": "DashboardsResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 75, - "end_line": 87, + "start_line": 68, + "end_line": 79, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Envelope DTO for paginated dashboards list." }, "relations": [], @@ -14820,19 +13085,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardsResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Envelope DTO for paginated dashboards list.\nclass DashboardsResponse(BaseModel):\n dashboards: List[DashboardItem]\n total: int\n page: int\n page_size: int\n total_pages: int\n effective_profile_filter: Optional[EffectiveProfileFilter] = None\n\n\n# [/DEF:DashboardsResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DashboardsResponse [C:1] [TYPE DataClass]\n# @BRIEF Envelope DTO for paginated dashboards list.\nclass DashboardsResponse(BaseModel):\n dashboards: list[DashboardItem]\n total: int\n page: int\n page_size: int\n total_pages: int\n effective_profile_filter: EffectiveProfileFilter | None = None\n\n\n# #endregion DashboardsResponse\n" }, { "contract_id": "DashboardChartItem", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 90, - "end_line": 102, + "start_line": 82, + "end_line": 93, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for a chart linked to a dashboard." }, "relations": [], @@ -14891,19 +13156,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardChartItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for a chart linked to a dashboard.\nclass DashboardChartItem(BaseModel):\n id: int\n title: str\n viz_type: Optional[str] = None\n dataset_id: Optional[int] = None\n last_modified: Optional[str] = None\n overview: Optional[str] = None\n\n\n# [/DEF:DashboardChartItem:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DashboardChartItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a chart linked to a dashboard.\nclass DashboardChartItem(BaseModel):\n id: int\n title: str\n viz_type: str | None = None\n dataset_id: int | None = None\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardChartItem\n" }, { "contract_id": "DashboardDatasetItem", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 105, - "end_line": 117, + "start_line": 96, + "end_line": 107, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for a dataset associated with a dashboard." }, "relations": [], @@ -14962,19 +13227,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardDatasetItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for a dataset associated with a dashboard.\nclass DashboardDatasetItem(BaseModel):\n id: int\n table_name: str\n schema: Optional[str] = None\n database: str\n last_modified: Optional[str] = None\n overview: Optional[str] = None\n\n\n# [/DEF:DashboardDatasetItem:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DashboardDatasetItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dataset associated with a dashboard.\nclass DashboardDatasetItem(BaseModel):\n id: int\n table_name: str\n schema: str | None = None\n database: str\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardDatasetItem\n" }, { "contract_id": "DashboardDetailResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 120, - "end_line": 137, + "start_line": 110, + "end_line": 126, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Detailed dashboard metadata including children." }, "relations": [], @@ -15033,19 +13298,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardDetailResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Detailed dashboard metadata including children.\nclass DashboardDetailResponse(BaseModel):\n id: int\n title: str\n slug: Optional[str] = None\n url: Optional[str] = None\n description: Optional[str] = None\n last_modified: Optional[str] = None\n published: Optional[bool] = None\n charts: List[DashboardChartItem]\n datasets: List[DashboardDatasetItem]\n chart_count: int\n dataset_count: int\n\n\n# [/DEF:DashboardDetailResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DashboardDetailResponse [C:1] [TYPE DataClass]\n# @BRIEF Detailed dashboard metadata including children.\nclass DashboardDetailResponse(BaseModel):\n id: int\n title: str\n slug: str | None = None\n url: str | None = None\n description: str | None = None\n last_modified: str | None = None\n published: bool | None = None\n charts: list[DashboardChartItem]\n datasets: list[DashboardDatasetItem]\n chart_count: int\n dataset_count: int\n\n\n# #endregion DashboardDetailResponse\n" }, { "contract_id": "DashboardTaskHistoryItem", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 140, - "end_line": 154, + "start_line": 129, + "end_line": 142, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Individual history record entry." }, "relations": [], @@ -15104,19 +13369,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardTaskHistoryItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Individual history record entry.\nclass DashboardTaskHistoryItem(BaseModel):\n id: str\n plugin_id: str\n status: str\n validation_status: Optional[str] = None\n started_at: Optional[str] = None\n finished_at: Optional[str] = None\n env_id: Optional[str] = None\n summary: Optional[str] = None\n\n\n# [/DEF:DashboardTaskHistoryItem:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DashboardTaskHistoryItem [C:1] [TYPE DataClass]\n# @BRIEF Individual history record entry.\nclass DashboardTaskHistoryItem(BaseModel):\n id: str\n plugin_id: str\n status: str\n validation_status: str | None = None\n started_at: str | None = None\n finished_at: str | None = None\n env_id: str | None = None\n summary: str | None = None\n\n\n# #endregion DashboardTaskHistoryItem\n" }, { "contract_id": "DashboardTaskHistoryResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 157, - "end_line": 165, + "start_line": 145, + "end_line": 152, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Collection DTO for task history." }, "relations": [], @@ -15175,19 +13440,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardTaskHistoryResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Collection DTO for task history.\nclass DashboardTaskHistoryResponse(BaseModel):\n dashboard_id: int\n items: List[DashboardTaskHistoryItem]\n\n\n# [/DEF:DashboardTaskHistoryResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DashboardTaskHistoryResponse [C:1] [TYPE DataClass]\n# @BRIEF Collection DTO for task history.\nclass DashboardTaskHistoryResponse(BaseModel):\n dashboard_id: int\n items: list[DashboardTaskHistoryItem]\n\n\n# #endregion DashboardTaskHistoryResponse\n" }, { "contract_id": "DatabaseMapping", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 168, - "end_line": 179, + "start_line": 155, + "end_line": 165, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "DTO for cross-environment database ID mapping." }, "relations": [], @@ -15246,19 +13511,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatabaseMapping:DataClass]\n# @COMPLEXITY: 2\n# @PURPOSE: DTO for cross-environment database ID mapping.\nclass DatabaseMapping(BaseModel):\n source_db: str\n target_db: str\n source_db_uuid: Optional[str] = None\n target_db_uuid: Optional[str] = None\n confidence: float\n\n\n# [/DEF:DatabaseMapping:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DatabaseMapping [C:2] [TYPE DataClass]\n# @BRIEF DTO for cross-environment database ID mapping.\nclass DatabaseMapping(BaseModel):\n source_db: str\n target_db: str\n source_db_uuid: str | None = None\n target_db_uuid: str | None = None\n confidence: float\n\n\n# #endregion DatabaseMapping\n" }, { "contract_id": "DatabaseMappingsResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 182, - "end_line": 189, + "start_line": 168, + "end_line": 174, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Wrapper for database mappings." }, "relations": [], @@ -15317,19 +13582,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatabaseMappingsResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Wrapper for database mappings.\nclass DatabaseMappingsResponse(BaseModel):\n mappings: List[DatabaseMapping]\n\n\n# [/DEF:DatabaseMappingsResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DatabaseMappingsResponse [C:1] [TYPE DataClass]\n# @BRIEF Wrapper for database mappings.\nclass DatabaseMappingsResponse(BaseModel):\n mappings: list[DatabaseMapping]\n\n\n# #endregion DatabaseMappingsResponse\n" }, { "contract_id": "MigrateRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 192, - "end_line": 207, + "start_line": 177, + "end_line": 191, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for dashboard migration requests." }, "relations": [], @@ -15388,19 +13653,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrateRequest:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for dashboard migration requests.\nclass MigrateRequest(BaseModel):\n source_env_id: str = Field(..., description=\"Source environment ID\")\n target_env_id: str = Field(..., description=\"Target environment ID\")\n dashboard_ids: List[int] = Field(\n ..., description=\"List of dashboard IDs to migrate\"\n )\n db_mappings: Optional[Dict[str, str]] = Field(\n None, description=\"Database mappings for migration\"\n )\n replace_db_config: bool = Field(False, description=\"Replace database configuration\")\n\n\n# [/DEF:MigrateRequest:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region MigrateRequest [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard migration requests.\nclass MigrateRequest(BaseModel):\n source_env_id: str = Field(..., description=\"Source environment ID\")\n target_env_id: str = Field(..., description=\"Target environment ID\")\n dashboard_ids: list[int] = Field(\n ..., description=\"List of dashboard IDs to migrate\"\n )\n db_mappings: dict[str, str] | None = Field(\n None, description=\"Database mappings for migration\"\n )\n replace_db_config: bool = Field(False, description=\"Replace database configuration\")\n\n\n# #endregion MigrateRequest\n" }, { "contract_id": "BackupRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 220, - "end_line": 231, + "start_line": 203, + "end_line": 213, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for dashboard backup requests." }, "relations": [], @@ -15459,37 +13724,37 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BackupRequest:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for dashboard backup requests.\nclass BackupRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dashboard_ids: List[int] = Field(..., description=\"List of dashboard IDs to backup\")\n schedule: Optional[str] = Field(\n None, description=\"Cron schedule for recurring backups (e.g., '0 0 * * *')\"\n )\n\n\n# [/DEF:BackupRequest:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region BackupRequest [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard backup requests.\nclass BackupRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dashboard_ids: list[int] = Field(..., description=\"List of dashboard IDs to backup\")\n schedule: str | None = Field(\n None, description=\"Cron schedule for recurring backups (e.g., '0 0 * * *')\"\n )\n\n\n# #endregion BackupRequest\n" }, { "contract_id": "DatasetReviewDependencies", "contract_type": "Module", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", "start_line": 1, - "end_line": 809, + "end_line": 732, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "API", "PURPOSE": "Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewDependencies:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints.\n# @LAYER: API\n\nfrom __future__ import annotations\n\nimport json\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, Union, cast\n\nfrom fastapi import APIRouter, Depends, Header, HTTPException, Query, Response, status\nfrom pydantic import BaseModel, Field\nfrom sqlalchemy.orm import Session\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_task_manager,\n has_permission,\n)\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n AnswerKind,\n ApprovalState,\n ArtifactFormat,\n CandidateStatus,\n ClarificationSession,\n DatasetReviewSession,\n ExecutionMapping,\n FieldProvenance,\n MappingMethod,\n PreviewStatus,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SemanticCandidate,\n SemanticFieldEntry,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n ClarificationAnswerDto,\n ClarificationQuestionDto,\n ClarificationSessionDto,\n CompiledPreviewDto,\n DatasetRunContextDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionDetail,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationAnswerCommand,\n ClarificationEngine,\n ClarificationQuestionPayload,\n ClarificationStateResult,\n)\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n LaunchDatasetCommand,\n PreparePreviewCommand,\n StartSessionCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\n\nlog = MarkerLogger(\"DatasetReviewDeps\")\n\n\n# [DEF:StartSessionRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for starting one dataset review session.\nclass StartSessionRequest(BaseModel):\n source_kind: str = Field(..., pattern=\"^(superset_link|dataset_selection)$\")\n source_input: str = Field(..., min_length=1)\n environment_id: str = Field(..., min_length=1)\n\n\n# [/DEF:StartSessionRequest:Class]\n\n\n# [DEF:UpdateSessionRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for lifecycle state updates on an existing session.\nclass UpdateSessionRequest(BaseModel):\n status: SessionStatus\n note: Optional[str] = None\n\n\n# [/DEF:UpdateSessionRequest:Class]\n\n\n# [DEF:SessionCollectionResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Paginated session collection response.\nclass SessionCollectionResponse(BaseModel):\n items: List[SessionSummary]\n total: int\n page: int\n page_size: int\n has_next: bool\n\n\n# [/DEF:SessionCollectionResponse:Class]\n\n\n# [DEF:ExportArtifactResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Inline export response for documentation or validation outputs.\nclass ExportArtifactResponse(BaseModel):\n artifact_id: str\n session_id: str\n artifact_type: str\n format: str\n storage_ref: str\n created_by_user_id: str\n created_at: Optional[str] = None\n content: Dict[str, Any]\n\n\n# [/DEF:ExportArtifactResponse:Class]\n\n\n# [DEF:FieldSemanticUpdateRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for field-level semantic candidate acceptance or manual override.\nclass FieldSemanticUpdateRequest(BaseModel):\n candidate_id: Optional[str] = None\n verbose_name: Optional[str] = None\n description: Optional[str] = None\n display_format: Optional[str] = None\n lock_field: bool = False\n resolution_note: Optional[str] = None\n\n\n# [/DEF:FieldSemanticUpdateRequest:Class]\n\n\n# [DEF:FeedbackRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for thumbs up/down feedback.\nclass FeedbackRequest(BaseModel):\n feedback: str = Field(..., pattern=\"^(up|down)$\")\n\n\n# [/DEF:FeedbackRequest:Class]\n\n\n# [DEF:ClarificationAnswerRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for submitting one clarification answer.\nclass ClarificationAnswerRequest(BaseModel):\n question_id: str = Field(..., min_length=1)\n answer_kind: AnswerKind\n answer_value: Optional[str] = None\n\n\n# [/DEF:ClarificationAnswerRequest:Class]\n\n\n# [DEF:ClarificationSessionSummaryResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Summary DTO for current clarification session state.\nclass ClarificationSessionSummaryResponse(BaseModel):\n clarification_session_id: str\n session_id: str\n status: str\n current_question_id: Optional[str] = None\n resolved_count: int\n remaining_count: int\n summary_delta: Optional[str] = None\n\n\n# [/DEF:ClarificationSessionSummaryResponse:Class]\n\n\n# [DEF:ClarificationStateResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO for current clarification state and active question payload.\nclass ClarificationStateResponse(BaseModel):\n clarification_session: Optional[ClarificationSessionSummaryResponse] = None\n current_question: Optional[ClarificationQuestionDto] = None\n\n\n# [/DEF:ClarificationStateResponse:Class]\n\n\n# [DEF:ClarificationAnswerResultResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO for one clarification answer mutation result.\nclass ClarificationAnswerResultResponse(BaseModel):\n clarification_state: ClarificationStateResponse\n session: SessionSummary\n changed_findings: List[ValidationFindingDto]\n\n\n# [/DEF:ClarificationAnswerResultResponse:Class]\n\n\n# [DEF:FeedbackResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal response DTO for persisted AI feedback actions.\nclass FeedbackResponse(BaseModel):\n target_id: str\n feedback: str\n\n\n# [/DEF:FeedbackResponse:Class]\n\n\n# [DEF:ApproveMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Optional request DTO for explicit mapping approval audit notes.\nclass ApproveMappingRequest(BaseModel):\n approval_note: Optional[str] = None\n\n\n# [/DEF:ApproveMappingRequest:Class]\n\n\n# [DEF:BatchApproveSemanticItemRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for one batch semantic-approval item.\nclass BatchApproveSemanticItemRequest(BaseModel):\n field_id: str = Field(..., min_length=1)\n candidate_id: str = Field(..., min_length=1)\n lock_field: bool = False\n\n\n# [/DEF:BatchApproveSemanticItemRequest:Class]\n\n\n# [DEF:BatchApproveSemanticRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for explicit batch semantic approvals.\nclass BatchApproveSemanticRequest(BaseModel):\n items: List[BatchApproveSemanticItemRequest] = Field(..., min_length=1)\n\n\n# [/DEF:BatchApproveSemanticRequest:Class]\n\n\n# [DEF:BatchApproveMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for explicit batch mapping approvals.\nclass BatchApproveMappingRequest(BaseModel):\n mapping_ids: List[str] = Field(..., min_length=1)\n approval_note: Optional[str] = None\n\n\n# [/DEF:BatchApproveMappingRequest:Class]\n\n\n# [DEF:PreviewEnqueueResultResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Async preview trigger response exposing only enqueue state.\nclass PreviewEnqueueResultResponse(BaseModel):\n session_id: str\n session_version: Optional[int] = None\n preview_status: str\n task_id: Optional[str] = None\n\n\n# [/DEF:PreviewEnqueueResultResponse:Class]\n\n\n# [DEF:MappingCollectionResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Wrapper for execution mapping list responses.\nclass MappingCollectionResponse(BaseModel):\n items: List[ExecutionMappingDto]\n\n\n# [/DEF:MappingCollectionResponse:Class]\n\n\n# [DEF:UpdateExecutionMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for one manual execution-mapping override update.\nclass UpdateExecutionMappingRequest(BaseModel):\n effective_value: Optional[Any] = None\n mapping_method: Optional[str] = Field(default=None, pattern=\"^(manual_override|direct_match|heuristic_match|semantic_match)$\")\n transformation_note: Optional[str] = None\n\n\n# [/DEF:UpdateExecutionMappingRequest:Class]\n\n\n# [DEF:LaunchDatasetResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Launch result exposing audited run context and SQL Lab redirect target.\nclass LaunchDatasetResponse(BaseModel):\n run_context: DatasetRunContextDto\n redirect_url: str\n\n\n# [/DEF:LaunchDatasetResponse:Class]\n\n\n# --- Dependency Injection ---\n\n# [DEF:_require_auto_review_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard US1 dataset review endpoints behind the configured feature flag.\ndef _require_auto_review_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_auto_review_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_auto_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset auto review feature is disabled\")\n return True\n\n\n# [/DEF:_require_auto_review_flag:Function]\n\n\n# [DEF:_require_clarification_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard clarification-specific US2 endpoints behind the configured feature flag.\ndef _require_clarification_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_clarification_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_clarification:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset clarification feature is disabled\")\n return True\n\n\n# [/DEF:_require_clarification_flag:Function]\n\n\n# [DEF:_require_execution_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard US3 execution endpoints behind the configured feature flag.\ndef _require_execution_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_execution_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_execution:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset execution feature is disabled\")\n return True\n\n\n# [/DEF:_require_execution_flag:Function]\n\n\n# [DEF:_get_repository:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build repository dependency.\ndef _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:\n return DatasetReviewSessionRepository(db)\n\n\n# [/DEF:_get_repository:Function]\n\n\n# [DEF:_get_orchestrator:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build orchestrator dependency.\ndef _get_orchestrator(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n) -> DatasetReviewOrchestrator:\n return DatasetReviewOrchestrator(repository=repository, config_manager=config_manager, task_manager=task_manager)\n\n\n# [/DEF:_get_orchestrator:Function]\n\n\n# [DEF:_get_clarification_engine:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build clarification engine dependency.\ndef _get_clarification_engine(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n) -> ClarificationEngine:\n return ClarificationEngine(repository=repository)\n\n\n# [/DEF:_get_clarification_engine:Function]\n\n\n# --- Serialization Helpers ---\n\n# [DEF:_serialize_session_summary:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map session aggregate into stable API summary DTO.\ndef _serialize_session_summary(session: DatasetReviewSession) -> SessionSummary:\n summary = SessionSummary.model_validate(session, from_attributes=True)\n summary.session_version = summary.version\n return summary\n\n\n# [/DEF:_serialize_session_summary:Function]\n\n\n# [DEF:_serialize_session_detail:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map session aggregate into stable API detail DTO.\ndef _serialize_session_detail(session: DatasetReviewSession) -> SessionDetail:\n detail = SessionDetail.model_validate(session, from_attributes=True)\n detail.session_version = detail.version\n return detail\n\n\n# [/DEF:_serialize_session_detail:Function]\n\n\n# [DEF:_require_session_version_header:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Read the optimistic-lock session version header.\ndef _require_session_version_header(\n session_version: int = Header(..., alias=\"X-Session-Version\", ge=0),\n) -> int:\n return session_version\n\n\n# [/DEF:_require_session_version_header:Function]\n\n\n# [DEF:_build_session_version_conflict_http_exception:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Normalize optimistic-lock conflict errors into HTTP 409 responses.\ndef _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVersionConflictError) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\"error_code\": \"session_version_conflict\", \"message\": str(exc), \"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version},\n )\n\n\n# [/DEF:_build_session_version_conflict_http_exception:Function]\n\n\n# [DEF:_enforce_session_version:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses.\ndef _enforce_session_version(repository, session, expected_version):\n with belief_scope(\"_enforce_session_version\"):\n try:\n repository.require_session_version(session, expected_version)\n except DatasetReviewSessionVersionConflictError as exc:\n log.explore(\"Dataset review optimistic-lock conflict detected\", payload={\"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version}, error=\"Optimistic lock conflict\")\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# [/DEF:_enforce_session_version:Function]\n\n\n# [DEF:_get_owned_session_or_404:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resolve one session for current user or collaborator scope, returning 404 when inaccessible.\ndef _get_owned_session_or_404(repository, session_id, current_user):\n with belief_scope(\"_get_owned_session_or_404\"):\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Session not found\")\n return session\n\n\n# [/DEF:_get_owned_session_or_404:Function]\n\n\n# [DEF:_require_owner_mutation_scope:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Enforce owner-only mutation scope.\ndef _require_owner_mutation_scope(session, current_user):\n with belief_scope(\"_require_owner_mutation_scope\"):\n if session.user_id != current_user.id:\n raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"Only the owner can mutate dataset review state\")\n return session\n\n\n# [/DEF:_require_owner_mutation_scope:Function]\n\n\n# [DEF:_prepare_owned_session_mutation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resolve owner-scoped mutation session and enforce optimistic-lock version.\ndef _prepare_owned_session_mutation(repository, session_id, current_user, expected_version):\n with belief_scope(\"_prepare_owned_session_mutation\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n _require_owner_mutation_scope(session, current_user)\n return _enforce_session_version(repository, session, expected_version)\n\n\n# [/DEF:_prepare_owned_session_mutation:Function]\n\n\n# [DEF:_commit_owned_session_mutation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Centralize session version bumping and commit semantics.\ndef _commit_owned_session_mutation(repository, session, *, refresh_targets=None):\n with belief_scope(\"_commit_owned_session_mutation\"):\n try:\n repository.commit_session_mutation(session, refresh_targets=refresh_targets)\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# [/DEF:_commit_owned_session_mutation:Function]\n\n\n# [DEF:_record_session_event:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Persist one explicit audit event for an owned mutation endpoint.\ndef _record_session_event(repository, session, current_user, *, event_type, event_summary, event_details=None):\n repository.event_logger.log_for_session(session, actor_user_id=current_user.id, event_type=event_type, event_summary=event_summary, event_details=event_details or {})\n\n\n# [/DEF:_record_session_event:Function]\n\n\n# [DEF:_serialize_semantic_field:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one semantic field into stable DTO.\ndef _serialize_semantic_field(field):\n payload = SemanticFieldEntryDto.model_validate(field, from_attributes=True)\n session_ref = getattr(field, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_semantic_field:Function]\n\n\n# [DEF:_serialize_execution_mapping:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one execution mapping into stable DTO.\ndef _serialize_execution_mapping(mapping):\n payload = ExecutionMappingDto.model_validate(mapping, from_attributes=True)\n session_ref = getattr(mapping, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_execution_mapping:Function]\n\n\n# [DEF:_serialize_preview:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one preview into stable DTO.\ndef _serialize_preview(preview, *, session_version_fallback=None):\n payload = CompiledPreviewDto.model_validate(preview, from_attributes=True)\n session_ref = getattr(preview, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n if version_value is None:\n version_value = session_version_fallback\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_preview:Function]\n\n\n# [DEF:_serialize_run_context:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one run context into stable DTO.\ndef _serialize_run_context(run_context):\n payload = DatasetRunContextDto.model_validate(run_context, from_attributes=True)\n session_ref = getattr(run_context, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_run_context:Function]\n\n\n# [DEF:_serialize_clarification_question_payload:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert clarification engine payload into API DTO.\ndef _serialize_clarification_question_payload(payload):\n if payload is None:\n return None\n return ClarificationQuestionDto.model_validate({\n \"question_id\": payload.question_id, \"clarification_session_id\": payload.clarification_session_id,\n \"topic_ref\": payload.topic_ref, \"question_text\": payload.question_text,\n \"why_it_matters\": payload.why_it_matters, \"current_guess\": payload.current_guess,\n \"priority\": payload.priority, \"state\": payload.state, \"options\": payload.options,\n \"answer\": None, \"created_at\": datetime.utcnow(), \"updated_at\": datetime.utcnow(),\n })\n\n\n# [/DEF:_serialize_clarification_question_payload:Function]\n\n\n# [DEF:_serialize_clarification_state:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert clarification engine state into API response.\ndef _serialize_clarification_state(state):\n return ClarificationStateResponse(\n clarification_session=ClarificationSessionSummaryResponse(\n clarification_session_id=state.clarification_session.clarification_session_id,\n session_id=state.clarification_session.session_id, status=state.clarification_session.status.value,\n current_question_id=state.clarification_session.current_question_id,\n resolved_count=state.clarification_session.resolved_count,\n remaining_count=state.clarification_session.remaining_count,\n summary_delta=state.clarification_session.summary_delta,\n ),\n current_question=_serialize_clarification_question_payload(state.current_question),\n )\n\n\n# [/DEF:_serialize_clarification_state:Function]\n\n\n# [DEF:_serialize_empty_clarification_state:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Return empty clarification payload.\ndef _serialize_empty_clarification_state():\n return ClarificationStateResponse(clarification_session=None, current_question=None)\n\n\n# [/DEF:_serialize_empty_clarification_state:Function]\n\n\n# [DEF:_get_latest_clarification_session_or_404:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolve the latest clarification aggregate or raise.\ndef _get_latest_clarification_session_or_404(session):\n if not session.clarification_sessions:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Clarification session not found\")\n return sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)[0]\n\n\n# [/DEF:_get_latest_clarification_session_or_404:Function]\n\n\n# [DEF:_get_owned_mapping_or_404:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve one execution mapping inside one owned session.\ndef _get_owned_mapping_or_404(session, mapping_id):\n for mapping in session.execution_mappings:\n if mapping.mapping_id == mapping_id:\n return mapping\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Execution mapping not found\")\n\n\n# [/DEF:_get_owned_mapping_or_404:Function]\n\n\n# [DEF:_get_owned_field_or_404:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve a semantic field inside one owned session.\ndef _get_owned_field_or_404(session, field_id):\n for field in session.semantic_fields:\n if field.field_id == field_id:\n return field\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic field not found\")\n\n\n# [/DEF:_get_owned_field_or_404:Function]\n\n\n# [DEF:_map_candidate_provenance:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Translate accepted semantic candidate type into stable field provenance.\ndef _map_candidate_provenance(candidate):\n if str(candidate.match_type.value) == \"exact\":\n return FieldProvenance.DICTIONARY_EXACT\n if str(candidate.match_type.value) == \"reference\":\n return FieldProvenance.REFERENCE_IMPORTED\n if str(candidate.match_type.value) == \"generated\":\n return FieldProvenance.AI_GENERATED\n return FieldProvenance.FUZZY_INFERRED\n\n\n# [/DEF:_map_candidate_provenance:Function]\n\n\n# [DEF:_resolve_candidate_source_version:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolve the semantic source version for one accepted candidate.\ndef _resolve_candidate_source_version(field, source_id):\n if not source_id:\n return None\n session = getattr(field, \"session\", None)\n if session is None:\n return None\n for source in getattr(session, \"semantic_sources\", []) or []:\n if source.source_id == source_id:\n return source.source_version\n return None\n\n\n# [/DEF:_resolve_candidate_source_version:Function]\n\n\n# [DEF:_update_semantic_field_state:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Apply field-level semantic manual override or candidate acceptance.\n# @POST: Manual overrides always set manual provenance plus lock.\ndef _update_semantic_field_state(field, request, changed_by):\n has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format])\n selected_candidate = None\n if request.candidate_id:\n selected_candidate = next((c for c in field.candidates if c.candidate_id == request.candidate_id), None)\n if selected_candidate is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic candidate not found\")\n\n if has_manual_override:\n field.verbose_name = request.verbose_name\n field.description = request.description\n field.display_format = request.display_format\n field.provenance = FieldProvenance.MANUAL_OVERRIDE\n field.source_id = None\n field.source_version = None\n field.confidence_rank = None\n field.is_locked = True\n field.has_conflict = False\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.SUPERSEDED\n return field\n\n if selected_candidate is not None:\n field.verbose_name = selected_candidate.proposed_verbose_name\n field.description = selected_candidate.proposed_description\n field.display_format = selected_candidate.proposed_display_format\n field.provenance = _map_candidate_provenance(selected_candidate)\n field.source_id = selected_candidate.source_id\n field.source_version = _resolve_candidate_source_version(field, selected_candidate.source_id)\n field.confidence_rank = selected_candidate.candidate_rank\n field.is_locked = bool(request.lock_field or field.is_locked)\n field.has_conflict = len(field.candidates) > 1\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.ACCEPTED if c.candidate_id == selected_candidate.candidate_id else CandidateStatus.SUPERSEDED\n return field\n\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Provide candidate_id or at least one manual override field\")\n\n\n# [/DEF:_update_semantic_field_state:Function]\n\n\n# [DEF:_build_sql_lab_redirect_url:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build SQL Lab redirect URL.\ndef _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref):\n base_url = str(environment_url or \"\").rstrip(\"/\")\n session_ref = str(sql_lab_session_ref or \"\").strip()\n if not base_url:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"Superset environment URL is not configured\")\n if not session_ref:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"SQL Lab session reference is missing\")\n return f\"{base_url}/superset/sqllab?queryId={session_ref}\"\n\n\n# [/DEF:_build_sql_lab_redirect_url:Function]\n\n\n# [DEF:_build_documentation_export:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Produce session documentation export content.\ndef _build_documentation_export(session, export_format):\n profile = session.profile\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Dataset Review: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Environment: {session.environment_id}\", f\"- Readiness: {session.readiness_state.value}\", f\"- Recommended action: {session.recommended_action.value}\", \"\", \"## Business Summary\", profile.business_summary if profile else \"No profile summary available.\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- [{f.severity.value}] {f.title}: {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session\": _serialize_session_summary(session).model_dump(mode=\"json\"), \"profile\": profile and {\"dataset_name\": profile.dataset_name, \"business_summary\": profile.business_summary, \"confidence_state\": profile.confidence_state.value, \"dataset_type\": profile.dataset_type}, \"findings\": [{\"code\": f.code, \"severity\": f.severity.value, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.json\", \"content\": content}\n\n\n# [/DEF:_build_documentation_export:Function]\n\n\n# [DEF:_build_validation_export:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Produce validation-focused export content.\ndef _build_validation_export(session, export_format):\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Validation Report: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Readiness: {session.readiness_state.value}\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- `{f.code}` [{f.severity.value}] {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session_id\": session.session_id, \"dataset_ref\": session.dataset_ref, \"readiness_state\": session.readiness_state.value, \"findings\": [{\"finding_id\": f.finding_id, \"area\": f.area.value, \"severity\": f.severity.value, \"code\": f.code, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.json\", \"content\": content}\n\n\n# [/DEF:_build_validation_export:Function]\n\n\n# [/DEF:DatasetReviewDependencies:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewDependencies [C:2] [TYPE Module] [SEMANTICS dataset, review, dependency, di, serialize]\n# @BRIEF Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints.\n# @LAYER: API\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom fastapi import Depends, Header, HTTPException, status\nfrom pydantic import BaseModel, Field\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n)\nfrom src.models.dataset_review import (\n AnswerKind,\n ArtifactFormat,\n CandidateStatus,\n DatasetReviewSession,\n FieldProvenance,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n ClarificationQuestionDto,\n CompiledPreviewDto,\n DatasetRunContextDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionDetail,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationEngine,\n)\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\n\n\n# #region StartSessionRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for starting one dataset review session.\nclass StartSessionRequest(BaseModel):\n source_kind: str = Field(..., pattern=\"^(superset_link|dataset_selection)$\")\n source_input: str = Field(..., min_length=1)\n environment_id: str = Field(..., min_length=1)\n\n\n# #endregion StartSessionRequest\n\n\n# #region UpdateSessionRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for lifecycle state updates on an existing session.\nclass UpdateSessionRequest(BaseModel):\n status: SessionStatus\n note: str | None = None\n\n\n# #endregion UpdateSessionRequest\n\n\n# #region SessionCollectionResponse [C:1] [TYPE Class]\n# @BRIEF Paginated session collection response.\nclass SessionCollectionResponse(BaseModel):\n items: list[SessionSummary]\n total: int\n page: int\n page_size: int\n has_next: bool\n\n\n# #endregion SessionCollectionResponse\n\n\n# #region ExportArtifactResponse [C:1] [TYPE Class]\n# @BRIEF Inline export response for documentation or validation outputs.\nclass ExportArtifactResponse(BaseModel):\n artifact_id: str\n session_id: str\n artifact_type: str\n format: str\n storage_ref: str\n created_by_user_id: str\n created_at: str | None = None\n content: dict[str, Any]\n\n\n# #endregion ExportArtifactResponse\n\n\n# #region FieldSemanticUpdateRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for field-level semantic candidate acceptance or manual override.\nclass FieldSemanticUpdateRequest(BaseModel):\n candidate_id: str | None = None\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n lock_field: bool = False\n resolution_note: str | None = None\n\n\n# #endregion FieldSemanticUpdateRequest\n\n\n# #region FeedbackRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for thumbs up/down feedback.\nclass FeedbackRequest(BaseModel):\n feedback: str = Field(..., pattern=\"^(up|down)$\")\n\n\n# #endregion FeedbackRequest\n\n\n# #region ClarificationAnswerRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for submitting one clarification answer.\nclass ClarificationAnswerRequest(BaseModel):\n question_id: str = Field(..., min_length=1)\n answer_kind: AnswerKind\n answer_value: str | None = None\n\n\n# #endregion ClarificationAnswerRequest\n\n\n# #region ClarificationSessionSummaryResponse [C:1] [TYPE Class]\n# @BRIEF Summary DTO for current clarification session state.\nclass ClarificationSessionSummaryResponse(BaseModel):\n clarification_session_id: str\n session_id: str\n status: str\n current_question_id: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n\n\n# #endregion ClarificationSessionSummaryResponse\n\n\n# #region ClarificationStateResponse [C:1] [TYPE Class]\n# @BRIEF Response DTO for current clarification state and active question payload.\nclass ClarificationStateResponse(BaseModel):\n clarification_session: ClarificationSessionSummaryResponse | None = None\n current_question: ClarificationQuestionDto | None = None\n\n\n# #endregion ClarificationStateResponse\n\n\n# #region ClarificationAnswerResultResponse [C:1] [TYPE Class]\n# @BRIEF Response DTO for one clarification answer mutation result.\nclass ClarificationAnswerResultResponse(BaseModel):\n clarification_state: ClarificationStateResponse\n session: SessionSummary\n changed_findings: list[ValidationFindingDto]\n\n\n# #endregion ClarificationAnswerResultResponse\n\n\n# #region FeedbackResponse [C:1] [TYPE Class]\n# @BRIEF Minimal response DTO for persisted AI feedback actions.\nclass FeedbackResponse(BaseModel):\n target_id: str\n feedback: str\n\n\n# #endregion FeedbackResponse\n\n\n# #region ApproveMappingRequest [C:1] [TYPE Class]\n# @BRIEF Optional request DTO for explicit mapping approval audit notes.\nclass ApproveMappingRequest(BaseModel):\n approval_note: str | None = None\n\n\n# #endregion ApproveMappingRequest\n\n\n# #region BatchApproveSemanticItemRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for one batch semantic-approval item.\nclass BatchApproveSemanticItemRequest(BaseModel):\n field_id: str = Field(..., min_length=1)\n candidate_id: str = Field(..., min_length=1)\n lock_field: bool = False\n\n\n# #endregion BatchApproveSemanticItemRequest\n\n\n# #region BatchApproveSemanticRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for explicit batch semantic approvals.\nclass BatchApproveSemanticRequest(BaseModel):\n items: list[BatchApproveSemanticItemRequest] = Field(..., min_length=1)\n\n\n# #endregion BatchApproveSemanticRequest\n\n\n# #region BatchApproveMappingRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for explicit batch mapping approvals.\nclass BatchApproveMappingRequest(BaseModel):\n mapping_ids: list[str] = Field(..., min_length=1)\n approval_note: str | None = None\n\n\n# #endregion BatchApproveMappingRequest\n\n\n# #region PreviewEnqueueResultResponse [C:1] [TYPE Class]\n# @BRIEF Async preview trigger response exposing only enqueue state.\nclass PreviewEnqueueResultResponse(BaseModel):\n session_id: str\n session_version: int | None = None\n preview_status: str\n task_id: str | None = None\n\n\n# #endregion PreviewEnqueueResultResponse\n\n\n# #region MappingCollectionResponse [C:1] [TYPE Class]\n# @BRIEF Wrapper for execution mapping list responses.\nclass MappingCollectionResponse(BaseModel):\n items: list[ExecutionMappingDto]\n\n\n# #endregion MappingCollectionResponse\n\n\n# #region UpdateExecutionMappingRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for one manual execution-mapping override update.\nclass UpdateExecutionMappingRequest(BaseModel):\n effective_value: Any | None = None\n mapping_method: str | None = Field(default=None, pattern=\"^(manual_override|direct_match|heuristic_match|semantic_match)$\")\n transformation_note: str | None = None\n\n\n# #endregion UpdateExecutionMappingRequest\n\n\n# #region LaunchDatasetResponse [C:1] [TYPE Class]\n# @BRIEF Launch result exposing audited run context and SQL Lab redirect target.\nclass LaunchDatasetResponse(BaseModel):\n run_context: DatasetRunContextDto\n redirect_url: str\n\n\n# #endregion LaunchDatasetResponse\n\n\n# --- Dependency Injection ---\n\n# #region _require_auto_review_flag [C:2] [TYPE Function]\n# @BRIEF Guard US1 dataset review endpoints behind the configured feature flag.\ndef _require_auto_review_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_auto_review_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_auto_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset auto review feature is disabled\")\n return True\n\n\n# #endregion _require_auto_review_flag\n\n\n# #region _require_clarification_flag [C:2] [TYPE Function]\n# @BRIEF Guard clarification-specific US2 endpoints behind the configured feature flag.\ndef _require_clarification_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_clarification_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_clarification:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset clarification feature is disabled\")\n return True\n\n\n# #endregion _require_clarification_flag\n\n\n# #region _require_execution_flag [C:2] [TYPE Function]\n# @BRIEF Guard US3 execution endpoints behind the configured feature flag.\ndef _require_execution_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_execution_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_execution:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset execution feature is disabled\")\n return True\n\n\n# #endregion _require_execution_flag\n\n\n# #region _get_repository [C:1] [TYPE Function]\n# @BRIEF Build repository dependency.\ndef _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:\n return DatasetReviewSessionRepository(db)\n\n\n# #endregion _get_repository\n\n\n# #region _get_orchestrator [C:1] [TYPE Function]\n# @BRIEF Build orchestrator dependency.\ndef _get_orchestrator(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n) -> DatasetReviewOrchestrator:\n return DatasetReviewOrchestrator(repository=repository, config_manager=config_manager, task_manager=task_manager)\n\n\n# #endregion _get_orchestrator\n\n\n# #region _get_clarification_engine [C:1] [TYPE Function]\n# @BRIEF Build clarification engine dependency.\ndef _get_clarification_engine(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n) -> ClarificationEngine:\n return ClarificationEngine(repository=repository)\n\n\n# #endregion _get_clarification_engine\n\n\n# --- Serialization Helpers ---\n\n# #region _serialize_session_summary [C:1] [TYPE Function]\n# @BRIEF Map session aggregate into stable API summary DTO.\ndef _serialize_session_summary(session: DatasetReviewSession) -> SessionSummary:\n summary = SessionSummary.model_validate(session, from_attributes=True)\n summary.session_version = summary.version\n return summary\n\n\n# #endregion _serialize_session_summary\n\n\n# #region _serialize_session_detail [C:1] [TYPE Function]\n# @BRIEF Map session aggregate into stable API detail DTO.\ndef _serialize_session_detail(session: DatasetReviewSession) -> SessionDetail:\n detail = SessionDetail.model_validate(session, from_attributes=True)\n detail.session_version = detail.version\n return detail\n\n\n# #endregion _serialize_session_detail\n\n\n# #region _require_session_version_header [C:1] [TYPE Function]\n# @BRIEF Read the optimistic-lock session version header.\ndef _require_session_version_header(\n session_version: int = Header(..., alias=\"X-Session-Version\", ge=0),\n) -> int:\n return session_version\n\n\n# #endregion _require_session_version_header\n\n\n# #region _build_session_version_conflict_http_exception [C:1] [TYPE Function]\n# @BRIEF Normalize optimistic-lock conflict errors into HTTP 409 responses.\ndef _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVersionConflictError) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\"error_code\": \"session_version_conflict\", \"message\": str(exc), \"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version},\n )\n\n\n# #endregion _build_session_version_conflict_http_exception\n\n\n# #region _enforce_session_version [C:3] [TYPE Function]\n# @BRIEF Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses.\ndef _enforce_session_version(repository, session, expected_version):\n with belief_scope(\"_enforce_session_version\"):\n try:\n repository.require_session_version(session, expected_version)\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\"Dataset review optimistic-lock conflict detected\", extra={\"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version})\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# #endregion _enforce_session_version\n\n\n# #region _get_owned_session_or_404 [C:3] [TYPE Function]\n# @BRIEF Resolve one session for current user or collaborator scope, returning 404 when inaccessible.\ndef _get_owned_session_or_404(repository, session_id, current_user):\n with belief_scope(\"_get_owned_session_or_404\"):\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Session not found\")\n return session\n\n\n# #endregion _get_owned_session_or_404\n\n\n# #region _require_owner_mutation_scope [C:2] [TYPE Function]\n# @BRIEF Enforce owner-only mutation scope.\ndef _require_owner_mutation_scope(session, current_user):\n with belief_scope(\"_require_owner_mutation_scope\"):\n if session.user_id != current_user.id:\n raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"Only the owner can mutate dataset review state\")\n return session\n\n\n# #endregion _require_owner_mutation_scope\n\n\n# #region _prepare_owned_session_mutation [C:3] [TYPE Function]\n# @BRIEF Resolve owner-scoped mutation session and enforce optimistic-lock version.\ndef _prepare_owned_session_mutation(repository, session_id, current_user, expected_version):\n with belief_scope(\"_prepare_owned_session_mutation\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n _require_owner_mutation_scope(session, current_user)\n return _enforce_session_version(repository, session, expected_version)\n\n\n# #endregion _prepare_owned_session_mutation\n\n\n# #region _commit_owned_session_mutation [C:3] [TYPE Function]\n# @BRIEF Centralize session version bumping and commit semantics.\ndef _commit_owned_session_mutation(repository, session, *, refresh_targets=None):\n with belief_scope(\"_commit_owned_session_mutation\"):\n try:\n repository.commit_session_mutation(session, refresh_targets=refresh_targets)\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# #endregion _commit_owned_session_mutation\n\n\n# #region _record_session_event [C:1] [TYPE Function]\n# @BRIEF Persist one explicit audit event for an owned mutation endpoint.\ndef _record_session_event(repository, session, current_user, *, event_type, event_summary, event_details=None):\n repository.event_logger.log_for_session(session, actor_user_id=current_user.id, event_type=event_type, event_summary=event_summary, event_details=event_details or {})\n\n\n# #endregion _record_session_event\n\n\n# #region _serialize_semantic_field [C:1] [TYPE Function]\n# @BRIEF Map one semantic field into stable DTO.\ndef _serialize_semantic_field(field):\n payload = SemanticFieldEntryDto.model_validate(field, from_attributes=True)\n session_ref = getattr(field, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# #endregion _serialize_semantic_field\n\n\n# #region _serialize_execution_mapping [C:1] [TYPE Function]\n# @BRIEF Map one execution mapping into stable DTO.\ndef _serialize_execution_mapping(mapping):\n payload = ExecutionMappingDto.model_validate(mapping, from_attributes=True)\n session_ref = getattr(mapping, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# #endregion _serialize_execution_mapping\n\n\n# #region _serialize_preview [C:1] [TYPE Function]\n# @BRIEF Map one preview into stable DTO.\ndef _serialize_preview(preview, *, session_version_fallback=None):\n payload = CompiledPreviewDto.model_validate(preview, from_attributes=True)\n session_ref = getattr(preview, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n if version_value is None:\n version_value = session_version_fallback\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# #endregion _serialize_preview\n\n\n# #region _serialize_run_context [C:1] [TYPE Function]\n# @BRIEF Map one run context into stable DTO.\ndef _serialize_run_context(run_context):\n payload = DatasetRunContextDto.model_validate(run_context, from_attributes=True)\n session_ref = getattr(run_context, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# #endregion _serialize_run_context\n\n\n# #region _serialize_clarification_question_payload [C:1] [TYPE Function]\n# @BRIEF Convert clarification engine payload into API DTO.\ndef _serialize_clarification_question_payload(payload):\n if payload is None:\n return None\n return ClarificationQuestionDto.model_validate({\n \"question_id\": payload.question_id, \"clarification_session_id\": payload.clarification_session_id,\n \"topic_ref\": payload.topic_ref, \"question_text\": payload.question_text,\n \"why_it_matters\": payload.why_it_matters, \"current_guess\": payload.current_guess,\n \"priority\": payload.priority, \"state\": payload.state, \"options\": payload.options,\n \"answer\": None, \"created_at\": datetime.utcnow(), \"updated_at\": datetime.utcnow(),\n })\n\n\n# #endregion _serialize_clarification_question_payload\n\n\n# #region _serialize_clarification_state [C:1] [TYPE Function]\n# @BRIEF Convert clarification engine state into API response.\ndef _serialize_clarification_state(state):\n return ClarificationStateResponse(\n clarification_session=ClarificationSessionSummaryResponse(\n clarification_session_id=state.clarification_session.clarification_session_id,\n session_id=state.clarification_session.session_id, status=state.clarification_session.status.value,\n current_question_id=state.clarification_session.current_question_id,\n resolved_count=state.clarification_session.resolved_count,\n remaining_count=state.clarification_session.remaining_count,\n summary_delta=state.clarification_session.summary_delta,\n ),\n current_question=_serialize_clarification_question_payload(state.current_question),\n )\n\n\n# #endregion _serialize_clarification_state\n\n\n# #region _serialize_empty_clarification_state [C:1] [TYPE Function]\n# @BRIEF Return empty clarification payload.\ndef _serialize_empty_clarification_state():\n return ClarificationStateResponse(clarification_session=None, current_question=None)\n\n\n# #endregion _serialize_empty_clarification_state\n\n\n# #region _get_latest_clarification_session_or_404 [C:1] [TYPE Function]\n# @BRIEF Resolve the latest clarification aggregate or raise.\ndef _get_latest_clarification_session_or_404(session):\n if not session.clarification_sessions:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Clarification session not found\")\n return sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)[0]\n\n\n# #endregion _get_latest_clarification_session_or_404\n\n\n# #region _get_owned_mapping_or_404 [C:2] [TYPE Function]\n# @BRIEF Resolve one execution mapping inside one owned session.\ndef _get_owned_mapping_or_404(session, mapping_id):\n for mapping in session.execution_mappings:\n if mapping.mapping_id == mapping_id:\n return mapping\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Execution mapping not found\")\n\n\n# #endregion _get_owned_mapping_or_404\n\n\n# #region _get_owned_field_or_404 [C:2] [TYPE Function]\n# @BRIEF Resolve a semantic field inside one owned session.\ndef _get_owned_field_or_404(session, field_id):\n for field in session.semantic_fields:\n if field.field_id == field_id:\n return field\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic field not found\")\n\n\n# #endregion _get_owned_field_or_404\n\n\n# #region _map_candidate_provenance [C:1] [TYPE Function]\n# @BRIEF Translate accepted semantic candidate type into stable field provenance.\ndef _map_candidate_provenance(candidate):\n if str(candidate.match_type.value) == \"exact\":\n return FieldProvenance.DICTIONARY_EXACT\n if str(candidate.match_type.value) == \"reference\":\n return FieldProvenance.REFERENCE_IMPORTED\n if str(candidate.match_type.value) == \"generated\":\n return FieldProvenance.AI_GENERATED\n return FieldProvenance.FUZZY_INFERRED\n\n\n# #endregion _map_candidate_provenance\n\n\n# #region _resolve_candidate_source_version [C:1] [TYPE Function]\n# @BRIEF Resolve the semantic source version for one accepted candidate.\ndef _resolve_candidate_source_version(field, source_id):\n if not source_id:\n return None\n session = getattr(field, \"session\", None)\n if session is None:\n return None\n for source in getattr(session, \"semantic_sources\", []) or []:\n if source.source_id == source_id:\n return source.source_version\n return None\n\n\n# #endregion _resolve_candidate_source_version\n\n\n# #region _update_semantic_field_state [C:3] [TYPE Function]\n# @BRIEF Apply field-level semantic manual override or candidate acceptance.\n# @POST: Manual overrides always set manual provenance plus lock.\ndef _update_semantic_field_state(field, request, changed_by):\n has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format])\n selected_candidate = None\n if request.candidate_id:\n selected_candidate = next((c for c in field.candidates if c.candidate_id == request.candidate_id), None)\n if selected_candidate is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic candidate not found\")\n\n if has_manual_override:\n field.verbose_name = request.verbose_name\n field.description = request.description\n field.display_format = request.display_format\n field.provenance = FieldProvenance.MANUAL_OVERRIDE\n field.source_id = None\n field.source_version = None\n field.confidence_rank = None\n field.is_locked = True\n field.has_conflict = False\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.SUPERSEDED\n return field\n\n if selected_candidate is not None:\n field.verbose_name = selected_candidate.proposed_verbose_name\n field.description = selected_candidate.proposed_description\n field.display_format = selected_candidate.proposed_display_format\n field.provenance = _map_candidate_provenance(selected_candidate)\n field.source_id = selected_candidate.source_id\n field.source_version = _resolve_candidate_source_version(field, selected_candidate.source_id)\n field.confidence_rank = selected_candidate.candidate_rank\n field.is_locked = bool(request.lock_field or field.is_locked)\n field.has_conflict = len(field.candidates) > 1\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.ACCEPTED if c.candidate_id == selected_candidate.candidate_id else CandidateStatus.SUPERSEDED\n return field\n\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Provide candidate_id or at least one manual override field\")\n\n\n# #endregion _update_semantic_field_state\n\n\n# #region _build_sql_lab_redirect_url [C:1] [TYPE Function]\n# @BRIEF Build SQL Lab redirect URL.\ndef _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref):\n base_url = str(environment_url or \"\").rstrip(\"/\")\n session_ref = str(sql_lab_session_ref or \"\").strip()\n if not base_url:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"Superset environment URL is not configured\")\n if not session_ref:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"SQL Lab session reference is missing\")\n return f\"{base_url}/superset/sqllab?queryId={session_ref}\"\n\n\n# #endregion _build_sql_lab_redirect_url\n\n\n# #region _build_documentation_export [C:2] [TYPE Function]\n# @BRIEF Produce session documentation export content.\ndef _build_documentation_export(session, export_format):\n profile = session.profile\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Dataset Review: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Environment: {session.environment_id}\", f\"- Readiness: {session.readiness_state.value}\", f\"- Recommended action: {session.recommended_action.value}\", \"\", \"## Business Summary\", profile.business_summary if profile else \"No profile summary available.\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- [{f.severity.value}] {f.title}: {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session\": _serialize_session_summary(session).model_dump(mode=\"json\"), \"profile\": profile and {\"dataset_name\": profile.dataset_name, \"business_summary\": profile.business_summary, \"confidence_state\": profile.confidence_state.value, \"dataset_type\": profile.dataset_type}, \"findings\": [{\"code\": f.code, \"severity\": f.severity.value, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.json\", \"content\": content}\n\n\n# #endregion _build_documentation_export\n\n\n# #region _build_validation_export [C:2] [TYPE Function]\n# @BRIEF Produce validation-focused export content.\ndef _build_validation_export(session, export_format):\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Validation Report: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Readiness: {session.readiness_state.value}\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- `{f.code}` [{f.severity.value}] {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session_id\": session.session_id, \"dataset_ref\": session.dataset_ref, \"readiness_state\": session.readiness_state.value, \"findings\": [{\"finding_id\": f.finding_id, \"area\": f.area.value, \"severity\": f.severity.value, \"code\": f.code, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.json\", \"content\": content}\n\n\n# #endregion _build_validation_export\n\n\n# #endregion DatasetReviewDependencies\n" }, { "contract_id": "StartSessionRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 76, - "end_line": 85, + "start_line": 50, + "end_line": 58, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for starting one dataset review session." }, "relations": [], @@ -15504,19 +13769,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:StartSessionRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for starting one dataset review session.\nclass StartSessionRequest(BaseModel):\n source_kind: str = Field(..., pattern=\"^(superset_link|dataset_selection)$\")\n source_input: str = Field(..., min_length=1)\n environment_id: str = Field(..., min_length=1)\n\n\n# [/DEF:StartSessionRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region StartSessionRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for starting one dataset review session.\nclass StartSessionRequest(BaseModel):\n source_kind: str = Field(..., pattern=\"^(superset_link|dataset_selection)$\")\n source_input: str = Field(..., min_length=1)\n environment_id: str = Field(..., min_length=1)\n\n\n# #endregion StartSessionRequest\n" }, { "contract_id": "UpdateSessionRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 88, - "end_line": 96, + "start_line": 61, + "end_line": 68, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for lifecycle state updates on an existing session." }, "relations": [], @@ -15531,19 +13796,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:UpdateSessionRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for lifecycle state updates on an existing session.\nclass UpdateSessionRequest(BaseModel):\n status: SessionStatus\n note: Optional[str] = None\n\n\n# [/DEF:UpdateSessionRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region UpdateSessionRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for lifecycle state updates on an existing session.\nclass UpdateSessionRequest(BaseModel):\n status: SessionStatus\n note: str | None = None\n\n\n# #endregion UpdateSessionRequest\n" }, { "contract_id": "SessionCollectionResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 99, - "end_line": 110, + "start_line": 71, + "end_line": 81, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Paginated session collection response." }, "relations": [], @@ -15558,19 +13823,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionCollectionResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Paginated session collection response.\nclass SessionCollectionResponse(BaseModel):\n items: List[SessionSummary]\n total: int\n page: int\n page_size: int\n has_next: bool\n\n\n# [/DEF:SessionCollectionResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionCollectionResponse [C:1] [TYPE Class]\n# @BRIEF Paginated session collection response.\nclass SessionCollectionResponse(BaseModel):\n items: list[SessionSummary]\n total: int\n page: int\n page_size: int\n has_next: bool\n\n\n# #endregion SessionCollectionResponse\n" }, { "contract_id": "ExportArtifactResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 113, - "end_line": 127, + "start_line": 84, + "end_line": 97, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Inline export response for documentation or validation outputs." }, "relations": [], @@ -15585,19 +13850,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ExportArtifactResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Inline export response for documentation or validation outputs.\nclass ExportArtifactResponse(BaseModel):\n artifact_id: str\n session_id: str\n artifact_type: str\n format: str\n storage_ref: str\n created_by_user_id: str\n created_at: Optional[str] = None\n content: Dict[str, Any]\n\n\n# [/DEF:ExportArtifactResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region ExportArtifactResponse [C:1] [TYPE Class]\n# @BRIEF Inline export response for documentation or validation outputs.\nclass ExportArtifactResponse(BaseModel):\n artifact_id: str\n session_id: str\n artifact_type: str\n format: str\n storage_ref: str\n created_by_user_id: str\n created_at: str | None = None\n content: dict[str, Any]\n\n\n# #endregion ExportArtifactResponse\n" }, { "contract_id": "FieldSemanticUpdateRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 130, - "end_line": 142, + "start_line": 100, + "end_line": 111, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for field-level semantic candidate acceptance or manual override." }, "relations": [], @@ -15612,19 +13877,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FieldSemanticUpdateRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for field-level semantic candidate acceptance or manual override.\nclass FieldSemanticUpdateRequest(BaseModel):\n candidate_id: Optional[str] = None\n verbose_name: Optional[str] = None\n description: Optional[str] = None\n display_format: Optional[str] = None\n lock_field: bool = False\n resolution_note: Optional[str] = None\n\n\n# [/DEF:FieldSemanticUpdateRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region FieldSemanticUpdateRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for field-level semantic candidate acceptance or manual override.\nclass FieldSemanticUpdateRequest(BaseModel):\n candidate_id: str | None = None\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n lock_field: bool = False\n resolution_note: str | None = None\n\n\n# #endregion FieldSemanticUpdateRequest\n" }, { "contract_id": "FeedbackRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 145, - "end_line": 152, + "start_line": 114, + "end_line": 120, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for thumbs up/down feedback." }, "relations": [], @@ -15639,19 +13904,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FeedbackRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for thumbs up/down feedback.\nclass FeedbackRequest(BaseModel):\n feedback: str = Field(..., pattern=\"^(up|down)$\")\n\n\n# [/DEF:FeedbackRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region FeedbackRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for thumbs up/down feedback.\nclass FeedbackRequest(BaseModel):\n feedback: str = Field(..., pattern=\"^(up|down)$\")\n\n\n# #endregion FeedbackRequest\n" }, { "contract_id": "ClarificationAnswerRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 155, - "end_line": 164, + "start_line": 123, + "end_line": 131, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for submitting one clarification answer." }, "relations": [], @@ -15666,19 +13931,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationAnswerRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for submitting one clarification answer.\nclass ClarificationAnswerRequest(BaseModel):\n question_id: str = Field(..., min_length=1)\n answer_kind: AnswerKind\n answer_value: Optional[str] = None\n\n\n# [/DEF:ClarificationAnswerRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationAnswerRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for submitting one clarification answer.\nclass ClarificationAnswerRequest(BaseModel):\n question_id: str = Field(..., min_length=1)\n answer_kind: AnswerKind\n answer_value: str | None = None\n\n\n# #endregion ClarificationAnswerRequest\n" }, { "contract_id": "ClarificationSessionSummaryResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 167, - "end_line": 180, + "start_line": 134, + "end_line": 146, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Summary DTO for current clarification session state." }, "relations": [], @@ -15693,19 +13958,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationSessionSummaryResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Summary DTO for current clarification session state.\nclass ClarificationSessionSummaryResponse(BaseModel):\n clarification_session_id: str\n session_id: str\n status: str\n current_question_id: Optional[str] = None\n resolved_count: int\n remaining_count: int\n summary_delta: Optional[str] = None\n\n\n# [/DEF:ClarificationSessionSummaryResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationSessionSummaryResponse [C:1] [TYPE Class]\n# @BRIEF Summary DTO for current clarification session state.\nclass ClarificationSessionSummaryResponse(BaseModel):\n clarification_session_id: str\n session_id: str\n status: str\n current_question_id: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n\n\n# #endregion ClarificationSessionSummaryResponse\n" }, { "contract_id": "ClarificationStateResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 183, - "end_line": 191, + "start_line": 149, + "end_line": 156, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Response DTO for current clarification state and active question payload." }, "relations": [], @@ -15720,19 +13985,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationStateResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO for current clarification state and active question payload.\nclass ClarificationStateResponse(BaseModel):\n clarification_session: Optional[ClarificationSessionSummaryResponse] = None\n current_question: Optional[ClarificationQuestionDto] = None\n\n\n# [/DEF:ClarificationStateResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationStateResponse [C:1] [TYPE Class]\n# @BRIEF Response DTO for current clarification state and active question payload.\nclass ClarificationStateResponse(BaseModel):\n clarification_session: ClarificationSessionSummaryResponse | None = None\n current_question: ClarificationQuestionDto | None = None\n\n\n# #endregion ClarificationStateResponse\n" }, { "contract_id": "ClarificationAnswerResultResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 194, - "end_line": 203, + "start_line": 159, + "end_line": 167, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Response DTO for one clarification answer mutation result." }, "relations": [], @@ -15747,19 +14012,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationAnswerResultResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO for one clarification answer mutation result.\nclass ClarificationAnswerResultResponse(BaseModel):\n clarification_state: ClarificationStateResponse\n session: SessionSummary\n changed_findings: List[ValidationFindingDto]\n\n\n# [/DEF:ClarificationAnswerResultResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationAnswerResultResponse [C:1] [TYPE Class]\n# @BRIEF Response DTO for one clarification answer mutation result.\nclass ClarificationAnswerResultResponse(BaseModel):\n clarification_state: ClarificationStateResponse\n session: SessionSummary\n changed_findings: list[ValidationFindingDto]\n\n\n# #endregion ClarificationAnswerResultResponse\n" }, { "contract_id": "FeedbackResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 206, - "end_line": 214, + "start_line": 170, + "end_line": 177, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Minimal response DTO for persisted AI feedback actions." }, "relations": [], @@ -15774,19 +14039,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FeedbackResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal response DTO for persisted AI feedback actions.\nclass FeedbackResponse(BaseModel):\n target_id: str\n feedback: str\n\n\n# [/DEF:FeedbackResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region FeedbackResponse [C:1] [TYPE Class]\n# @BRIEF Minimal response DTO for persisted AI feedback actions.\nclass FeedbackResponse(BaseModel):\n target_id: str\n feedback: str\n\n\n# #endregion FeedbackResponse\n" }, { "contract_id": "ApproveMappingRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 217, - "end_line": 224, + "start_line": 180, + "end_line": 186, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Optional request DTO for explicit mapping approval audit notes." }, "relations": [], @@ -15801,19 +14066,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ApproveMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Optional request DTO for explicit mapping approval audit notes.\nclass ApproveMappingRequest(BaseModel):\n approval_note: Optional[str] = None\n\n\n# [/DEF:ApproveMappingRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region ApproveMappingRequest [C:1] [TYPE Class]\n# @BRIEF Optional request DTO for explicit mapping approval audit notes.\nclass ApproveMappingRequest(BaseModel):\n approval_note: str | None = None\n\n\n# #endregion ApproveMappingRequest\n" }, { "contract_id": "BatchApproveSemanticItemRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 227, - "end_line": 236, + "start_line": 189, + "end_line": 197, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for one batch semantic-approval item." }, "relations": [], @@ -15828,19 +14093,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BatchApproveSemanticItemRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for one batch semantic-approval item.\nclass BatchApproveSemanticItemRequest(BaseModel):\n field_id: str = Field(..., min_length=1)\n candidate_id: str = Field(..., min_length=1)\n lock_field: bool = False\n\n\n# [/DEF:BatchApproveSemanticItemRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region BatchApproveSemanticItemRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for one batch semantic-approval item.\nclass BatchApproveSemanticItemRequest(BaseModel):\n field_id: str = Field(..., min_length=1)\n candidate_id: str = Field(..., min_length=1)\n lock_field: bool = False\n\n\n# #endregion BatchApproveSemanticItemRequest\n" }, { "contract_id": "BatchApproveSemanticRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 239, - "end_line": 246, + "start_line": 200, + "end_line": 206, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for explicit batch semantic approvals." }, "relations": [], @@ -15855,19 +14120,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BatchApproveSemanticRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for explicit batch semantic approvals.\nclass BatchApproveSemanticRequest(BaseModel):\n items: List[BatchApproveSemanticItemRequest] = Field(..., min_length=1)\n\n\n# [/DEF:BatchApproveSemanticRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region BatchApproveSemanticRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for explicit batch semantic approvals.\nclass BatchApproveSemanticRequest(BaseModel):\n items: list[BatchApproveSemanticItemRequest] = Field(..., min_length=1)\n\n\n# #endregion BatchApproveSemanticRequest\n" }, { "contract_id": "BatchApproveMappingRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 249, - "end_line": 257, + "start_line": 209, + "end_line": 216, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for explicit batch mapping approvals." }, "relations": [], @@ -15882,19 +14147,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BatchApproveMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for explicit batch mapping approvals.\nclass BatchApproveMappingRequest(BaseModel):\n mapping_ids: List[str] = Field(..., min_length=1)\n approval_note: Optional[str] = None\n\n\n# [/DEF:BatchApproveMappingRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region BatchApproveMappingRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for explicit batch mapping approvals.\nclass BatchApproveMappingRequest(BaseModel):\n mapping_ids: list[str] = Field(..., min_length=1)\n approval_note: str | None = None\n\n\n# #endregion BatchApproveMappingRequest\n" }, { "contract_id": "PreviewEnqueueResultResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 260, - "end_line": 270, + "start_line": 219, + "end_line": 228, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Async preview trigger response exposing only enqueue state." }, "relations": [], @@ -15909,19 +14174,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PreviewEnqueueResultResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Async preview trigger response exposing only enqueue state.\nclass PreviewEnqueueResultResponse(BaseModel):\n session_id: str\n session_version: Optional[int] = None\n preview_status: str\n task_id: Optional[str] = None\n\n\n# [/DEF:PreviewEnqueueResultResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region PreviewEnqueueResultResponse [C:1] [TYPE Class]\n# @BRIEF Async preview trigger response exposing only enqueue state.\nclass PreviewEnqueueResultResponse(BaseModel):\n session_id: str\n session_version: int | None = None\n preview_status: str\n task_id: str | None = None\n\n\n# #endregion PreviewEnqueueResultResponse\n" }, { "contract_id": "MappingCollectionResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 273, - "end_line": 280, + "start_line": 231, + "end_line": 237, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Wrapper for execution mapping list responses." }, "relations": [], @@ -15936,19 +14201,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MappingCollectionResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Wrapper for execution mapping list responses.\nclass MappingCollectionResponse(BaseModel):\n items: List[ExecutionMappingDto]\n\n\n# [/DEF:MappingCollectionResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region MappingCollectionResponse [C:1] [TYPE Class]\n# @BRIEF Wrapper for execution mapping list responses.\nclass MappingCollectionResponse(BaseModel):\n items: list[ExecutionMappingDto]\n\n\n# #endregion MappingCollectionResponse\n" }, { "contract_id": "UpdateExecutionMappingRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 283, - "end_line": 292, + "start_line": 240, + "end_line": 248, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for one manual execution-mapping override update." }, "relations": [], @@ -15963,19 +14228,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:UpdateExecutionMappingRequest:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Request DTO for one manual execution-mapping override update.\nclass UpdateExecutionMappingRequest(BaseModel):\n effective_value: Optional[Any] = None\n mapping_method: Optional[str] = Field(default=None, pattern=\"^(manual_override|direct_match|heuristic_match|semantic_match)$\")\n transformation_note: Optional[str] = None\n\n\n# [/DEF:UpdateExecutionMappingRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region UpdateExecutionMappingRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for one manual execution-mapping override update.\nclass UpdateExecutionMappingRequest(BaseModel):\n effective_value: Any | None = None\n mapping_method: str | None = Field(default=None, pattern=\"^(manual_override|direct_match|heuristic_match|semantic_match)$\")\n transformation_note: str | None = None\n\n\n# #endregion UpdateExecutionMappingRequest\n" }, { "contract_id": "LaunchDatasetResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 295, - "end_line": 303, + "start_line": 251, + "end_line": 258, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Launch result exposing audited run context and SQL Lab redirect target." }, "relations": [], @@ -15990,70 +14255,70 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LaunchDatasetResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Launch result exposing audited run context and SQL Lab redirect target.\nclass LaunchDatasetResponse(BaseModel):\n run_context: DatasetRunContextDto\n redirect_url: str\n\n\n# [/DEF:LaunchDatasetResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region LaunchDatasetResponse [C:1] [TYPE Class]\n# @BRIEF Launch result exposing audited run context and SQL Lab redirect target.\nclass LaunchDatasetResponse(BaseModel):\n run_context: DatasetRunContextDto\n redirect_url: str\n\n\n# #endregion LaunchDatasetResponse\n" }, { "contract_id": "_require_auto_review_flag", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 308, - "end_line": 321, + "start_line": 263, + "end_line": 275, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Guard US1 dataset review endpoints behind the configured feature flag." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_require_auto_review_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard US1 dataset review endpoints behind the configured feature flag.\ndef _require_auto_review_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_auto_review_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_auto_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset auto review feature is disabled\")\n return True\n\n\n# [/DEF:_require_auto_review_flag:Function]\n" + "anchor_syntax": "region", + "body": "# #region _require_auto_review_flag [C:2] [TYPE Function]\n# @BRIEF Guard US1 dataset review endpoints behind the configured feature flag.\ndef _require_auto_review_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_auto_review_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_auto_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset auto review feature is disabled\")\n return True\n\n\n# #endregion _require_auto_review_flag\n" }, { "contract_id": "_require_clarification_flag", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 324, - "end_line": 337, + "start_line": 278, + "end_line": 290, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Guard clarification-specific US2 endpoints behind the configured feature flag." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_require_clarification_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard clarification-specific US2 endpoints behind the configured feature flag.\ndef _require_clarification_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_clarification_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_clarification:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset clarification feature is disabled\")\n return True\n\n\n# [/DEF:_require_clarification_flag:Function]\n" + "anchor_syntax": "region", + "body": "# #region _require_clarification_flag [C:2] [TYPE Function]\n# @BRIEF Guard clarification-specific US2 endpoints behind the configured feature flag.\ndef _require_clarification_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_clarification_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_clarification:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset clarification feature is disabled\")\n return True\n\n\n# #endregion _require_clarification_flag\n" }, { "contract_id": "_require_execution_flag", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 340, - "end_line": 353, + "start_line": 293, + "end_line": 305, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Guard US3 execution endpoints behind the configured feature flag." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_require_execution_flag:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Guard US3 execution endpoints behind the configured feature flag.\ndef _require_execution_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_execution_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_execution:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset execution feature is disabled\")\n return True\n\n\n# [/DEF:_require_execution_flag:Function]\n" + "anchor_syntax": "region", + "body": "# #region _require_execution_flag [C:2] [TYPE Function]\n# @BRIEF Guard US3 execution endpoints behind the configured feature flag.\ndef _require_execution_flag(config_manager=Depends(get_config_manager)) -> bool:\n with belief_scope(\"dataset_review.require_execution_flag\"):\n settings = config_manager.get_config().settings\n if not settings.features.dataset_review:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset review feature is disabled\")\n if not settings.ff_dataset_execution:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Dataset execution feature is disabled\")\n return True\n\n\n# #endregion _require_execution_flag\n" }, { "contract_id": "_get_repository", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 356, - "end_line": 363, + "start_line": 308, + "end_line": 314, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Build repository dependency." }, "relations": [], @@ -16068,19 +14333,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_repository:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build repository dependency.\ndef _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:\n return DatasetReviewSessionRepository(db)\n\n\n# [/DEF:_get_repository:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_repository [C:1] [TYPE Function]\n# @BRIEF Build repository dependency.\ndef _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:\n return DatasetReviewSessionRepository(db)\n\n\n# #endregion _get_repository\n" }, { "contract_id": "_get_orchestrator", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 366, - "end_line": 377, + "start_line": 317, + "end_line": 327, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Build orchestrator dependency." }, "relations": [], @@ -16095,19 +14360,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_orchestrator:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build orchestrator dependency.\ndef _get_orchestrator(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n) -> DatasetReviewOrchestrator:\n return DatasetReviewOrchestrator(repository=repository, config_manager=config_manager, task_manager=task_manager)\n\n\n# [/DEF:_get_orchestrator:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_orchestrator [C:1] [TYPE Function]\n# @BRIEF Build orchestrator dependency.\ndef _get_orchestrator(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n) -> DatasetReviewOrchestrator:\n return DatasetReviewOrchestrator(repository=repository, config_manager=config_manager, task_manager=task_manager)\n\n\n# #endregion _get_orchestrator\n" }, { "contract_id": "_get_clarification_engine", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 380, - "end_line": 389, + "start_line": 330, + "end_line": 338, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Build clarification engine dependency." }, "relations": [], @@ -16122,19 +14387,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_clarification_engine:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build clarification engine dependency.\ndef _get_clarification_engine(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n) -> ClarificationEngine:\n return ClarificationEngine(repository=repository)\n\n\n# [/DEF:_get_clarification_engine:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_clarification_engine [C:1] [TYPE Function]\n# @BRIEF Build clarification engine dependency.\ndef _get_clarification_engine(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n) -> ClarificationEngine:\n return ClarificationEngine(repository=repository)\n\n\n# #endregion _get_clarification_engine\n" }, { "contract_id": "_serialize_session_summary", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 394, - "end_line": 403, + "start_line": 343, + "end_line": 351, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Map session aggregate into stable API summary DTO." }, "relations": [], @@ -16149,19 +14414,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_session_summary:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map session aggregate into stable API summary DTO.\ndef _serialize_session_summary(session: DatasetReviewSession) -> SessionSummary:\n summary = SessionSummary.model_validate(session, from_attributes=True)\n summary.session_version = summary.version\n return summary\n\n\n# [/DEF:_serialize_session_summary:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_session_summary [C:1] [TYPE Function]\n# @BRIEF Map session aggregate into stable API summary DTO.\ndef _serialize_session_summary(session: DatasetReviewSession) -> SessionSummary:\n summary = SessionSummary.model_validate(session, from_attributes=True)\n summary.session_version = summary.version\n return summary\n\n\n# #endregion _serialize_session_summary\n" }, { "contract_id": "_serialize_session_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 406, - "end_line": 415, + "start_line": 354, + "end_line": 362, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Map session aggregate into stable API detail DTO." }, "relations": [], @@ -16176,19 +14441,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_session_detail:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map session aggregate into stable API detail DTO.\ndef _serialize_session_detail(session: DatasetReviewSession) -> SessionDetail:\n detail = SessionDetail.model_validate(session, from_attributes=True)\n detail.session_version = detail.version\n return detail\n\n\n# [/DEF:_serialize_session_detail:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_session_detail [C:1] [TYPE Function]\n# @BRIEF Map session aggregate into stable API detail DTO.\ndef _serialize_session_detail(session: DatasetReviewSession) -> SessionDetail:\n detail = SessionDetail.model_validate(session, from_attributes=True)\n detail.session_version = detail.version\n return detail\n\n\n# #endregion _serialize_session_detail\n" }, { "contract_id": "_require_session_version_header", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 418, - "end_line": 427, + "start_line": 365, + "end_line": 373, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Read the optimistic-lock session version header." }, "relations": [], @@ -16203,19 +14468,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_require_session_version_header:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Read the optimistic-lock session version header.\ndef _require_session_version_header(\n session_version: int = Header(..., alias=\"X-Session-Version\", ge=0),\n) -> int:\n return session_version\n\n\n# [/DEF:_require_session_version_header:Function]\n" + "anchor_syntax": "region", + "body": "# #region _require_session_version_header [C:1] [TYPE Function]\n# @BRIEF Read the optimistic-lock session version header.\ndef _require_session_version_header(\n session_version: int = Header(..., alias=\"X-Session-Version\", ge=0),\n) -> int:\n return session_version\n\n\n# #endregion _require_session_version_header\n" }, { "contract_id": "_build_session_version_conflict_http_exception", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 430, - "end_line": 440, + "start_line": 376, + "end_line": 385, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Normalize optimistic-lock conflict errors into HTTP 409 responses." }, "relations": [], @@ -16230,19 +14495,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_build_session_version_conflict_http_exception:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Normalize optimistic-lock conflict errors into HTTP 409 responses.\ndef _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVersionConflictError) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\"error_code\": \"session_version_conflict\", \"message\": str(exc), \"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version},\n )\n\n\n# [/DEF:_build_session_version_conflict_http_exception:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_session_version_conflict_http_exception [C:1] [TYPE Function]\n# @BRIEF Normalize optimistic-lock conflict errors into HTTP 409 responses.\ndef _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVersionConflictError) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\"error_code\": \"session_version_conflict\", \"message\": str(exc), \"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version},\n )\n\n\n# #endregion _build_session_version_conflict_http_exception\n" }, { "contract_id": "_enforce_session_version", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 443, - "end_line": 456, + "start_line": 388, + "end_line": 400, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses." }, "relations": [], @@ -16257,19 +14522,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_enforce_session_version:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses.\ndef _enforce_session_version(repository, session, expected_version):\n with belief_scope(\"_enforce_session_version\"):\n try:\n repository.require_session_version(session, expected_version)\n except DatasetReviewSessionVersionConflictError as exc:\n log.explore(\"Dataset review optimistic-lock conflict detected\", payload={\"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version}, error=\"Optimistic lock conflict\")\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# [/DEF:_enforce_session_version:Function]\n" + "anchor_syntax": "region", + "body": "# #region _enforce_session_version [C:3] [TYPE Function]\n# @BRIEF Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses.\ndef _enforce_session_version(repository, session, expected_version):\n with belief_scope(\"_enforce_session_version\"):\n try:\n repository.require_session_version(session, expected_version)\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\"Dataset review optimistic-lock conflict detected\", extra={\"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version})\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# #endregion _enforce_session_version\n" }, { "contract_id": "_get_owned_session_or_404", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 459, - "end_line": 470, + "start_line": 403, + "end_line": 413, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Resolve one session for current user or collaborator scope, returning 404 when inaccessible." }, "relations": [], @@ -16284,36 +14549,36 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_owned_session_or_404:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resolve one session for current user or collaborator scope, returning 404 when inaccessible.\ndef _get_owned_session_or_404(repository, session_id, current_user):\n with belief_scope(\"_get_owned_session_or_404\"):\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Session not found\")\n return session\n\n\n# [/DEF:_get_owned_session_or_404:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_owned_session_or_404 [C:3] [TYPE Function]\n# @BRIEF Resolve one session for current user or collaborator scope, returning 404 when inaccessible.\ndef _get_owned_session_or_404(repository, session_id, current_user):\n with belief_scope(\"_get_owned_session_or_404\"):\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Session not found\")\n return session\n\n\n# #endregion _get_owned_session_or_404\n" }, { "contract_id": "_require_owner_mutation_scope", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 473, - "end_line": 483, + "start_line": 416, + "end_line": 425, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Enforce owner-only mutation scope." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_require_owner_mutation_scope:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Enforce owner-only mutation scope.\ndef _require_owner_mutation_scope(session, current_user):\n with belief_scope(\"_require_owner_mutation_scope\"):\n if session.user_id != current_user.id:\n raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"Only the owner can mutate dataset review state\")\n return session\n\n\n# [/DEF:_require_owner_mutation_scope:Function]\n" + "anchor_syntax": "region", + "body": "# #region _require_owner_mutation_scope [C:2] [TYPE Function]\n# @BRIEF Enforce owner-only mutation scope.\ndef _require_owner_mutation_scope(session, current_user):\n with belief_scope(\"_require_owner_mutation_scope\"):\n if session.user_id != current_user.id:\n raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"Only the owner can mutate dataset review state\")\n return session\n\n\n# #endregion _require_owner_mutation_scope\n" }, { "contract_id": "_prepare_owned_session_mutation", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 486, - "end_line": 496, + "start_line": 428, + "end_line": 437, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Resolve owner-scoped mutation session and enforce optimistic-lock version." }, "relations": [], @@ -16328,19 +14593,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_prepare_owned_session_mutation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resolve owner-scoped mutation session and enforce optimistic-lock version.\ndef _prepare_owned_session_mutation(repository, session_id, current_user, expected_version):\n with belief_scope(\"_prepare_owned_session_mutation\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n _require_owner_mutation_scope(session, current_user)\n return _enforce_session_version(repository, session, expected_version)\n\n\n# [/DEF:_prepare_owned_session_mutation:Function]\n" + "anchor_syntax": "region", + "body": "# #region _prepare_owned_session_mutation [C:3] [TYPE Function]\n# @BRIEF Resolve owner-scoped mutation session and enforce optimistic-lock version.\ndef _prepare_owned_session_mutation(repository, session_id, current_user, expected_version):\n with belief_scope(\"_prepare_owned_session_mutation\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n _require_owner_mutation_scope(session, current_user)\n return _enforce_session_version(repository, session, expected_version)\n\n\n# #endregion _prepare_owned_session_mutation\n" }, { "contract_id": "_commit_owned_session_mutation", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 499, - "end_line": 511, + "start_line": 440, + "end_line": 451, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Centralize session version bumping and commit semantics." }, "relations": [], @@ -16355,19 +14620,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_commit_owned_session_mutation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Centralize session version bumping and commit semantics.\ndef _commit_owned_session_mutation(repository, session, *, refresh_targets=None):\n with belief_scope(\"_commit_owned_session_mutation\"):\n try:\n repository.commit_session_mutation(session, refresh_targets=refresh_targets)\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# [/DEF:_commit_owned_session_mutation:Function]\n" + "anchor_syntax": "region", + "body": "# #region _commit_owned_session_mutation [C:3] [TYPE Function]\n# @BRIEF Centralize session version bumping and commit semantics.\ndef _commit_owned_session_mutation(repository, session, *, refresh_targets=None):\n with belief_scope(\"_commit_owned_session_mutation\"):\n try:\n repository.commit_session_mutation(session, refresh_targets=refresh_targets)\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# #endregion _commit_owned_session_mutation\n" }, { "contract_id": "_record_session_event", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 514, - "end_line": 521, + "start_line": 454, + "end_line": 460, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Persist one explicit audit event for an owned mutation endpoint." }, "relations": [], @@ -16382,19 +14647,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_record_session_event:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Persist one explicit audit event for an owned mutation endpoint.\ndef _record_session_event(repository, session, current_user, *, event_type, event_summary, event_details=None):\n repository.event_logger.log_for_session(session, actor_user_id=current_user.id, event_type=event_type, event_summary=event_summary, event_details=event_details or {})\n\n\n# [/DEF:_record_session_event:Function]\n" + "anchor_syntax": "region", + "body": "# #region _record_session_event [C:1] [TYPE Function]\n# @BRIEF Persist one explicit audit event for an owned mutation endpoint.\ndef _record_session_event(repository, session, current_user, *, event_type, event_summary, event_details=None):\n repository.event_logger.log_for_session(session, actor_user_id=current_user.id, event_type=event_type, event_summary=event_summary, event_details=event_details or {})\n\n\n# #endregion _record_session_event\n" }, { "contract_id": "_serialize_semantic_field", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 524, - "end_line": 535, + "start_line": 463, + "end_line": 473, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Map one semantic field into stable DTO." }, "relations": [], @@ -16409,19 +14674,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_semantic_field:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one semantic field into stable DTO.\ndef _serialize_semantic_field(field):\n payload = SemanticFieldEntryDto.model_validate(field, from_attributes=True)\n session_ref = getattr(field, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_semantic_field:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_semantic_field [C:1] [TYPE Function]\n# @BRIEF Map one semantic field into stable DTO.\ndef _serialize_semantic_field(field):\n payload = SemanticFieldEntryDto.model_validate(field, from_attributes=True)\n session_ref = getattr(field, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# #endregion _serialize_semantic_field\n" }, { "contract_id": "_serialize_execution_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 538, - "end_line": 549, + "start_line": 476, + "end_line": 486, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Map one execution mapping into stable DTO." }, "relations": [], @@ -16436,19 +14701,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_execution_mapping:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one execution mapping into stable DTO.\ndef _serialize_execution_mapping(mapping):\n payload = ExecutionMappingDto.model_validate(mapping, from_attributes=True)\n session_ref = getattr(mapping, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_execution_mapping:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_execution_mapping [C:1] [TYPE Function]\n# @BRIEF Map one execution mapping into stable DTO.\ndef _serialize_execution_mapping(mapping):\n payload = ExecutionMappingDto.model_validate(mapping, from_attributes=True)\n session_ref = getattr(mapping, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# #endregion _serialize_execution_mapping\n" }, { "contract_id": "_serialize_preview", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 552, - "end_line": 565, + "start_line": 489, + "end_line": 501, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Map one preview into stable DTO." }, "relations": [], @@ -16463,19 +14728,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_preview:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one preview into stable DTO.\ndef _serialize_preview(preview, *, session_version_fallback=None):\n payload = CompiledPreviewDto.model_validate(preview, from_attributes=True)\n session_ref = getattr(preview, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n if version_value is None:\n version_value = session_version_fallback\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_preview:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_preview [C:1] [TYPE Function]\n# @BRIEF Map one preview into stable DTO.\ndef _serialize_preview(preview, *, session_version_fallback=None):\n payload = CompiledPreviewDto.model_validate(preview, from_attributes=True)\n session_ref = getattr(preview, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n if version_value is None:\n version_value = session_version_fallback\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# #endregion _serialize_preview\n" }, { "contract_id": "_serialize_run_context", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 568, - "end_line": 579, + "start_line": 504, + "end_line": 514, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Map one run context into stable DTO." }, "relations": [], @@ -16490,19 +14755,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_run_context:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Map one run context into stable DTO.\ndef _serialize_run_context(run_context):\n payload = DatasetRunContextDto.model_validate(run_context, from_attributes=True)\n session_ref = getattr(run_context, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# [/DEF:_serialize_run_context:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_run_context [C:1] [TYPE Function]\n# @BRIEF Map one run context into stable DTO.\ndef _serialize_run_context(run_context):\n payload = DatasetRunContextDto.model_validate(run_context, from_attributes=True)\n session_ref = getattr(run_context, \"session\", None)\n version_value = getattr(session_ref, \"version\", None)\n payload.session_version = int(version_value or 0) if version_value is not None else None\n return payload\n\n\n# #endregion _serialize_run_context\n" }, { "contract_id": "_serialize_clarification_question_payload", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 582, - "end_line": 597, + "start_line": 517, + "end_line": 531, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Convert clarification engine payload into API DTO." }, "relations": [], @@ -16517,19 +14782,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_clarification_question_payload:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert clarification engine payload into API DTO.\ndef _serialize_clarification_question_payload(payload):\n if payload is None:\n return None\n return ClarificationQuestionDto.model_validate({\n \"question_id\": payload.question_id, \"clarification_session_id\": payload.clarification_session_id,\n \"topic_ref\": payload.topic_ref, \"question_text\": payload.question_text,\n \"why_it_matters\": payload.why_it_matters, \"current_guess\": payload.current_guess,\n \"priority\": payload.priority, \"state\": payload.state, \"options\": payload.options,\n \"answer\": None, \"created_at\": datetime.utcnow(), \"updated_at\": datetime.utcnow(),\n })\n\n\n# [/DEF:_serialize_clarification_question_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_clarification_question_payload [C:1] [TYPE Function]\n# @BRIEF Convert clarification engine payload into API DTO.\ndef _serialize_clarification_question_payload(payload):\n if payload is None:\n return None\n return ClarificationQuestionDto.model_validate({\n \"question_id\": payload.question_id, \"clarification_session_id\": payload.clarification_session_id,\n \"topic_ref\": payload.topic_ref, \"question_text\": payload.question_text,\n \"why_it_matters\": payload.why_it_matters, \"current_guess\": payload.current_guess,\n \"priority\": payload.priority, \"state\": payload.state, \"options\": payload.options,\n \"answer\": None, \"created_at\": datetime.utcnow(), \"updated_at\": datetime.utcnow(),\n })\n\n\n# #endregion _serialize_clarification_question_payload\n" }, { "contract_id": "_serialize_clarification_state", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 600, - "end_line": 617, + "start_line": 534, + "end_line": 550, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Convert clarification engine state into API response." }, "relations": [], @@ -16544,19 +14809,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_clarification_state:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert clarification engine state into API response.\ndef _serialize_clarification_state(state):\n return ClarificationStateResponse(\n clarification_session=ClarificationSessionSummaryResponse(\n clarification_session_id=state.clarification_session.clarification_session_id,\n session_id=state.clarification_session.session_id, status=state.clarification_session.status.value,\n current_question_id=state.clarification_session.current_question_id,\n resolved_count=state.clarification_session.resolved_count,\n remaining_count=state.clarification_session.remaining_count,\n summary_delta=state.clarification_session.summary_delta,\n ),\n current_question=_serialize_clarification_question_payload(state.current_question),\n )\n\n\n# [/DEF:_serialize_clarification_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_clarification_state [C:1] [TYPE Function]\n# @BRIEF Convert clarification engine state into API response.\ndef _serialize_clarification_state(state):\n return ClarificationStateResponse(\n clarification_session=ClarificationSessionSummaryResponse(\n clarification_session_id=state.clarification_session.clarification_session_id,\n session_id=state.clarification_session.session_id, status=state.clarification_session.status.value,\n current_question_id=state.clarification_session.current_question_id,\n resolved_count=state.clarification_session.resolved_count,\n remaining_count=state.clarification_session.remaining_count,\n summary_delta=state.clarification_session.summary_delta,\n ),\n current_question=_serialize_clarification_question_payload(state.current_question),\n )\n\n\n# #endregion _serialize_clarification_state\n" }, { "contract_id": "_serialize_empty_clarification_state", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 620, - "end_line": 627, + "start_line": 553, + "end_line": 559, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Return empty clarification payload." }, "relations": [], @@ -16571,19 +14836,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_serialize_empty_clarification_state:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Return empty clarification payload.\ndef _serialize_empty_clarification_state():\n return ClarificationStateResponse(clarification_session=None, current_question=None)\n\n\n# [/DEF:_serialize_empty_clarification_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region _serialize_empty_clarification_state [C:1] [TYPE Function]\n# @BRIEF Return empty clarification payload.\ndef _serialize_empty_clarification_state():\n return ClarificationStateResponse(clarification_session=None, current_question=None)\n\n\n# #endregion _serialize_empty_clarification_state\n" }, { "contract_id": "_get_latest_clarification_session_or_404", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 630, - "end_line": 639, + "start_line": 562, + "end_line": 570, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Resolve the latest clarification aggregate or raise." }, "relations": [], @@ -16598,53 +14863,53 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_latest_clarification_session_or_404:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolve the latest clarification aggregate or raise.\ndef _get_latest_clarification_session_or_404(session):\n if not session.clarification_sessions:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Clarification session not found\")\n return sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)[0]\n\n\n# [/DEF:_get_latest_clarification_session_or_404:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_latest_clarification_session_or_404 [C:1] [TYPE Function]\n# @BRIEF Resolve the latest clarification aggregate or raise.\ndef _get_latest_clarification_session_or_404(session):\n if not session.clarification_sessions:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Clarification session not found\")\n return sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)[0]\n\n\n# #endregion _get_latest_clarification_session_or_404\n" }, { "contract_id": "_get_owned_mapping_or_404", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 642, - "end_line": 652, + "start_line": 573, + "end_line": 582, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Resolve one execution mapping inside one owned session." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_get_owned_mapping_or_404:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve one execution mapping inside one owned session.\ndef _get_owned_mapping_or_404(session, mapping_id):\n for mapping in session.execution_mappings:\n if mapping.mapping_id == mapping_id:\n return mapping\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Execution mapping not found\")\n\n\n# [/DEF:_get_owned_mapping_or_404:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_owned_mapping_or_404 [C:2] [TYPE Function]\n# @BRIEF Resolve one execution mapping inside one owned session.\ndef _get_owned_mapping_or_404(session, mapping_id):\n for mapping in session.execution_mappings:\n if mapping.mapping_id == mapping_id:\n return mapping\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Execution mapping not found\")\n\n\n# #endregion _get_owned_mapping_or_404\n" }, { "contract_id": "_get_owned_field_or_404", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 655, - "end_line": 665, + "start_line": 585, + "end_line": 594, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Resolve a semantic field inside one owned session." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_get_owned_field_or_404:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve a semantic field inside one owned session.\ndef _get_owned_field_or_404(session, field_id):\n for field in session.semantic_fields:\n if field.field_id == field_id:\n return field\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic field not found\")\n\n\n# [/DEF:_get_owned_field_or_404:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_owned_field_or_404 [C:2] [TYPE Function]\n# @BRIEF Resolve a semantic field inside one owned session.\ndef _get_owned_field_or_404(session, field_id):\n for field in session.semantic_fields:\n if field.field_id == field_id:\n return field\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic field not found\")\n\n\n# #endregion _get_owned_field_or_404\n" }, { "contract_id": "_map_candidate_provenance", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 668, - "end_line": 681, + "start_line": 597, + "end_line": 609, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Translate accepted semantic candidate type into stable field provenance." }, "relations": [], @@ -16659,19 +14924,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_map_candidate_provenance:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Translate accepted semantic candidate type into stable field provenance.\ndef _map_candidate_provenance(candidate):\n if str(candidate.match_type.value) == \"exact\":\n return FieldProvenance.DICTIONARY_EXACT\n if str(candidate.match_type.value) == \"reference\":\n return FieldProvenance.REFERENCE_IMPORTED\n if str(candidate.match_type.value) == \"generated\":\n return FieldProvenance.AI_GENERATED\n return FieldProvenance.FUZZY_INFERRED\n\n\n# [/DEF:_map_candidate_provenance:Function]\n" + "anchor_syntax": "region", + "body": "# #region _map_candidate_provenance [C:1] [TYPE Function]\n# @BRIEF Translate accepted semantic candidate type into stable field provenance.\ndef _map_candidate_provenance(candidate):\n if str(candidate.match_type.value) == \"exact\":\n return FieldProvenance.DICTIONARY_EXACT\n if str(candidate.match_type.value) == \"reference\":\n return FieldProvenance.REFERENCE_IMPORTED\n if str(candidate.match_type.value) == \"generated\":\n return FieldProvenance.AI_GENERATED\n return FieldProvenance.FUZZY_INFERRED\n\n\n# #endregion _map_candidate_provenance\n" }, { "contract_id": "_resolve_candidate_source_version", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 684, - "end_line": 699, + "start_line": 612, + "end_line": 626, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Resolve the semantic source version for one accepted candidate." }, "relations": [], @@ -16686,19 +14951,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_candidate_source_version:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolve the semantic source version for one accepted candidate.\ndef _resolve_candidate_source_version(field, source_id):\n if not source_id:\n return None\n session = getattr(field, \"session\", None)\n if session is None:\n return None\n for source in getattr(session, \"semantic_sources\", []) or []:\n if source.source_id == source_id:\n return source.source_version\n return None\n\n\n# [/DEF:_resolve_candidate_source_version:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_candidate_source_version [C:1] [TYPE Function]\n# @BRIEF Resolve the semantic source version for one accepted candidate.\ndef _resolve_candidate_source_version(field, source_id):\n if not source_id:\n return None\n session = getattr(field, \"session\", None)\n if session is None:\n return None\n for source in getattr(session, \"semantic_sources\", []) or []:\n if source.source_id == source_id:\n return source.source_version\n return None\n\n\n# #endregion _resolve_candidate_source_version\n" }, { "contract_id": "_update_semantic_field_state", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 702, - "end_line": 749, + "start_line": 629, + "end_line": 675, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Manual overrides always set manual provenance plus lock.", "PURPOSE": "Apply field-level semantic manual override or candidate acceptance." }, @@ -16723,19 +14988,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_update_semantic_field_state:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Apply field-level semantic manual override or candidate acceptance.\n# @POST: Manual overrides always set manual provenance plus lock.\ndef _update_semantic_field_state(field, request, changed_by):\n has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format])\n selected_candidate = None\n if request.candidate_id:\n selected_candidate = next((c for c in field.candidates if c.candidate_id == request.candidate_id), None)\n if selected_candidate is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic candidate not found\")\n\n if has_manual_override:\n field.verbose_name = request.verbose_name\n field.description = request.description\n field.display_format = request.display_format\n field.provenance = FieldProvenance.MANUAL_OVERRIDE\n field.source_id = None\n field.source_version = None\n field.confidence_rank = None\n field.is_locked = True\n field.has_conflict = False\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.SUPERSEDED\n return field\n\n if selected_candidate is not None:\n field.verbose_name = selected_candidate.proposed_verbose_name\n field.description = selected_candidate.proposed_description\n field.display_format = selected_candidate.proposed_display_format\n field.provenance = _map_candidate_provenance(selected_candidate)\n field.source_id = selected_candidate.source_id\n field.source_version = _resolve_candidate_source_version(field, selected_candidate.source_id)\n field.confidence_rank = selected_candidate.candidate_rank\n field.is_locked = bool(request.lock_field or field.is_locked)\n field.has_conflict = len(field.candidates) > 1\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.ACCEPTED if c.candidate_id == selected_candidate.candidate_id else CandidateStatus.SUPERSEDED\n return field\n\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Provide candidate_id or at least one manual override field\")\n\n\n# [/DEF:_update_semantic_field_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region _update_semantic_field_state [C:3] [TYPE Function]\n# @BRIEF Apply field-level semantic manual override or candidate acceptance.\n# @POST: Manual overrides always set manual provenance plus lock.\ndef _update_semantic_field_state(field, request, changed_by):\n has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format])\n selected_candidate = None\n if request.candidate_id:\n selected_candidate = next((c for c in field.candidates if c.candidate_id == request.candidate_id), None)\n if selected_candidate is None:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Semantic candidate not found\")\n\n if has_manual_override:\n field.verbose_name = request.verbose_name\n field.description = request.description\n field.display_format = request.display_format\n field.provenance = FieldProvenance.MANUAL_OVERRIDE\n field.source_id = None\n field.source_version = None\n field.confidence_rank = None\n field.is_locked = True\n field.has_conflict = False\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.SUPERSEDED\n return field\n\n if selected_candidate is not None:\n field.verbose_name = selected_candidate.proposed_verbose_name\n field.description = selected_candidate.proposed_description\n field.display_format = selected_candidate.proposed_display_format\n field.provenance = _map_candidate_provenance(selected_candidate)\n field.source_id = selected_candidate.source_id\n field.source_version = _resolve_candidate_source_version(field, selected_candidate.source_id)\n field.confidence_rank = selected_candidate.candidate_rank\n field.is_locked = bool(request.lock_field or field.is_locked)\n field.has_conflict = len(field.candidates) > 1\n field.needs_review = False\n field.last_changed_by = changed_by\n for c in field.candidates:\n c.status = CandidateStatus.ACCEPTED if c.candidate_id == selected_candidate.candidate_id else CandidateStatus.SUPERSEDED\n return field\n\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Provide candidate_id or at least one manual override field\")\n\n\n# #endregion _update_semantic_field_state\n" }, { "contract_id": "_build_sql_lab_redirect_url", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 752, - "end_line": 765, + "start_line": 678, + "end_line": 690, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Build SQL Lab redirect URL." }, "relations": [], @@ -16750,53 +15015,53 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_build_sql_lab_redirect_url:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build SQL Lab redirect URL.\ndef _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref):\n base_url = str(environment_url or \"\").rstrip(\"/\")\n session_ref = str(sql_lab_session_ref or \"\").strip()\n if not base_url:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"Superset environment URL is not configured\")\n if not session_ref:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"SQL Lab session reference is missing\")\n return f\"{base_url}/superset/sqllab?queryId={session_ref}\"\n\n\n# [/DEF:_build_sql_lab_redirect_url:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_sql_lab_redirect_url [C:1] [TYPE Function]\n# @BRIEF Build SQL Lab redirect URL.\ndef _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref):\n base_url = str(environment_url or \"\").rstrip(\"/\")\n session_ref = str(sql_lab_session_ref or \"\").strip()\n if not base_url:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"Superset environment URL is not configured\")\n if not session_ref:\n raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=\"SQL Lab session reference is missing\")\n return f\"{base_url}/superset/sqllab?queryId={session_ref}\"\n\n\n# #endregion _build_sql_lab_redirect_url\n" }, { "contract_id": "_build_documentation_export", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 768, - "end_line": 786, + "start_line": 693, + "end_line": 710, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Produce session documentation export content." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_build_documentation_export:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Produce session documentation export content.\ndef _build_documentation_export(session, export_format):\n profile = session.profile\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Dataset Review: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Environment: {session.environment_id}\", f\"- Readiness: {session.readiness_state.value}\", f\"- Recommended action: {session.recommended_action.value}\", \"\", \"## Business Summary\", profile.business_summary if profile else \"No profile summary available.\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- [{f.severity.value}] {f.title}: {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session\": _serialize_session_summary(session).model_dump(mode=\"json\"), \"profile\": profile and {\"dataset_name\": profile.dataset_name, \"business_summary\": profile.business_summary, \"confidence_state\": profile.confidence_state.value, \"dataset_type\": profile.dataset_type}, \"findings\": [{\"code\": f.code, \"severity\": f.severity.value, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.json\", \"content\": content}\n\n\n# [/DEF:_build_documentation_export:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_documentation_export [C:2] [TYPE Function]\n# @BRIEF Produce session documentation export content.\ndef _build_documentation_export(session, export_format):\n profile = session.profile\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Dataset Review: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Environment: {session.environment_id}\", f\"- Readiness: {session.readiness_state.value}\", f\"- Recommended action: {session.recommended_action.value}\", \"\", \"## Business Summary\", profile.business_summary if profile else \"No profile summary available.\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- [{f.severity.value}] {f.title}: {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session\": _serialize_session_summary(session).model_dump(mode=\"json\"), \"profile\": profile and {\"dataset_name\": profile.dataset_name, \"business_summary\": profile.business_summary, \"confidence_state\": profile.confidence_state.value, \"dataset_type\": profile.dataset_type}, \"findings\": [{\"code\": f.code, \"severity\": f.severity.value, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/documentation.json\", \"content\": content}\n\n\n# #endregion _build_documentation_export\n" }, { "contract_id": "_build_validation_export", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_dependencies.py", - "start_line": 789, - "end_line": 806, + "start_line": 713, + "end_line": 729, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Produce validation-focused export content." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_build_validation_export:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Produce validation-focused export content.\ndef _build_validation_export(session, export_format):\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Validation Report: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Readiness: {session.readiness_state.value}\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- `{f.code}` [{f.severity.value}] {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session_id\": session.session_id, \"dataset_ref\": session.dataset_ref, \"readiness_state\": session.readiness_state.value, \"findings\": [{\"finding_id\": f.finding_id, \"area\": f.area.value, \"severity\": f.severity.value, \"code\": f.code, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.json\", \"content\": content}\n\n\n# [/DEF:_build_validation_export:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_validation_export [C:2] [TYPE Function]\n# @BRIEF Produce validation-focused export content.\ndef _build_validation_export(session, export_format):\n findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))\n if export_format == ArtifactFormat.MARKDOWN:\n lines = [f\"# Validation Report: {session.dataset_ref}\", \"\", f\"- Session ID: {session.session_id}\", f\"- Readiness: {session.readiness_state.value}\", \"\", \"## Findings\"]\n if findings:\n for f in findings:\n lines.append(f\"- `{f.code}` [{f.severity.value}] {f.message}\")\n else:\n lines.append(\"- No findings recorded.\")\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.md\", \"content\": {\"markdown\": \"\\n\".join(lines)}}\n content = {\"session_id\": session.session_id, \"dataset_ref\": session.dataset_ref, \"readiness_state\": session.readiness_state.value, \"findings\": [{\"finding_id\": f.finding_id, \"area\": f.area.value, \"severity\": f.severity.value, \"code\": f.code, \"title\": f.title, \"message\": f.message, \"resolution_state\": f.resolution_state.value} for f in findings]}\n return {\"storage_ref\": f\"inline://dataset-review/{session.session_id}/validation.json\", \"content\": content}\n\n\n# #endregion _build_validation_export\n" }, { "contract_id": "DatasetReviewRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", "start_line": 1, - "end_line": 1123, + "end_line": 1099, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Persist thumbs up/down feedback for clarification question/answer content." }, "relations": [], @@ -16811,19 +15076,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewRoutes:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.\n\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any, List, Optional, Union, cast\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, Response, status\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n ApprovalState,\n ArtifactFormat,\n FieldProvenance,\n MappingMethod,\n PreviewStatus,\n ReadinessState,\n RecommendedAction,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n ClarificationAnswerDto,\n CompiledPreviewDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationAnswerCommand,\n ClarificationStateResult,\n)\nfrom src.services.dataset_review.orchestrator import (\n LaunchDatasetCommand,\n PreparePreviewCommand,\n StartSessionCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionVersionConflictError,\n)\nfrom src.api.routes.dataset_review_pkg._dependencies import (\n BatchApproveMappingRequest,\n BatchApproveSemanticRequest,\n ClarificationAnswerRequest,\n ClarificationAnswerResultResponse,\n ClarificationStateResponse,\n ExportArtifactResponse,\n FeedbackRequest,\n FeedbackResponse,\n FieldSemanticUpdateRequest,\n LaunchDatasetResponse,\n MappingCollectionResponse,\n PreviewEnqueueResultResponse,\n SessionCollectionResponse,\n StartSessionRequest,\n UpdateExecutionMappingRequest,\n UpdateSessionRequest,\n _build_documentation_export,\n _build_sql_lab_redirect_url,\n _build_validation_export,\n _commit_owned_session_mutation,\n _get_clarification_engine,\n _get_latest_clarification_session_or_404,\n _get_owned_field_or_404,\n _get_owned_mapping_or_404,\n _get_owned_session_or_404,\n _get_orchestrator,\n _get_repository,\n _prepare_owned_session_mutation,\n _record_session_event,\n _require_auto_review_flag,\n _require_clarification_flag,\n _require_execution_flag,\n _require_session_version_header,\n _serialize_clarification_state,\n _serialize_empty_clarification_state,\n _serialize_execution_mapping,\n _serialize_preview,\n _serialize_run_context,\n _serialize_semantic_field,\n _serialize_session_detail,\n _serialize_session_summary,\n _update_semantic_field_state,\n _build_session_version_conflict_http_exception,\n)\n\nlog = MarkerLogger(\"DatasetReviewRoutes\")\n\nrouter = APIRouter(prefix=\"/api/dataset-orchestration\", tags=[\"Dataset Orchestration\"])\n\n\n# [DEF:list_sessions:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: List resumable dataset review sessions for the current user.\n@router.get(\n \"/sessions\",\n response_model=SessionCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_sessions(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_sessions\"):\n log.reason(\n \"Listing dataset review sessions\",\n payload={\"user_id\": current_user.id, \"page\": page, \"page_size\": page_size},\n )\n sessions = repository.list_sessions_for_user(current_user.id)\n start = (page - 1) * page_size\n end = start + page_size\n items = [_serialize_session_summary(s) for s in sessions[start:end]]\n log.reflect(\n \"Session page assembled\",\n payload={\n \"user_id\": current_user.id,\n \"returned\": len(items),\n \"total\": len(sessions),\n },\n )\n return SessionCollectionResponse(\n items=items,\n total=len(sessions),\n page=page,\n page_size=page_size,\n has_next=end < len(sessions),\n )\n\n\n# [/DEF:list_sessions:Function]\n\n\n# [DEF:start_session:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Start a new dataset review session from a Superset link or dataset selection.\n@router.post(\n \"/sessions\",\n response_model=SessionSummary,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def start_session(\n request: StartSessionRequest,\n orchestrator=Depends(_get_orchestrator),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"start_session\"):\n log.reason(\n \"Starting dataset review session\",\n payload={\n \"user_id\": current_user.id,\n \"environment_id\": request.environment_id,\n },\n )\n try:\n result = orchestrator.start_session(\n StartSessionCommand(\n user=current_user,\n environment_id=request.environment_id,\n source_kind=request.source_kind,\n source_input=request.source_input,\n )\n )\n except ValueError as exc:\n log.explore(\"Session start rejected\", error=\"Session start rejected by orchestrator\", payload={\"user_id\": current_user.id, \"error\": str(exc)})\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail == \"Environment not found\"\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n log.reflect(\n \"Session started\", payload={\"session_id\": result.session.session_id}\n )\n return _serialize_session_summary(result.session)\n\n\n# [/DEF:start_session:Function]\n\n\n# [DEF:get_session_detail:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Return the full accessible dataset review session aggregate.\n@router.get(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_session_detail(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.get_session_detail\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return _serialize_session_detail(session)\n\n\n# [/DEF:get_session_detail:Function]\n\n\n# [DEF:update_session:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Update resumable lifecycle status for an owned session.\n@router.patch(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_session(\n session_id: str,\n request: UpdateSessionRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"update_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n session_record = cast(Any, session)\n session_record.status = request.status\n if request.status == SessionStatus.PAUSED:\n session_record.recommended_action = RecommendedAction.RESUME_SESSION\n elif request.status in {\n SessionStatus.ARCHIVED,\n SessionStatus.CANCELLED,\n SessionStatus.COMPLETED,\n }:\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_status_updated\",\n event_summary=\"Dataset review session lifecycle updated\",\n event_details={\n \"status\": session_record.status.value,\n \"version\": session_record.version,\n },\n )\n return _serialize_session_summary(session)\n\n\n# [/DEF:update_session:Function]\n\n\n# [DEF:delete_session:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Archive or hard-delete a session owned by the current user.\n@router.delete(\n \"/sessions/{session_id}\",\n status_code=status.HTTP_204_NO_CONTENT,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def delete_session(\n session_id: str,\n hard_delete: bool = Query(False),\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"delete_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n if hard_delete:\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_deleted\",\n event_summary=\"Session hard-deleted\",\n event_details={\"hard_delete\": True},\n )\n repository.db.delete(session)\n repository.db.commit()\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n session_record = cast(Any, session)\n session_record.status = SessionStatus.ARCHIVED\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_archived\",\n event_summary=\"Session archived\",\n event_details={\"hard_delete\": False, \"version\": session_record.version},\n )\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n\n\n# [/DEF:delete_session:Function]\n\n\n# [DEF:export_documentation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Export documentation output for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/documentation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_documentation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_documentation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_documentation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"documentation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"documentation\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# [/DEF:export_documentation:Function]\n\n\n# [DEF:export_validation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Export validation findings for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/validation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_validation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_validation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_validation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"validation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"validation_report\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# [/DEF:export_validation:Function]\n\n\n# [DEF:get_clarification_state:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Return the current clarification session summary and active question payload.\n@router.get(\n \"/sessions/{session_id}/clarification\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_clarification_state(\n session_id: str,\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"get_clarification_state\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n if not session.clarification_sessions:\n return _serialize_empty_clarification_state()\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# [/DEF:get_clarification_state:Function]\n\n\n# [DEF:resume_clarification:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resume clarification mode on the highest-priority unresolved question.\n@router.post(\n \"/sessions/{session_id}/clarification/resume\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def resume_clarification(\n session_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"resume_clarification\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# [/DEF:resume_clarification:Function]\n\n\n# [DEF:record_clarification_answer:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist one clarification answer before advancing the active pointer.\n@router.post(\n \"/sessions/{session_id}/clarification/answers\",\n response_model=ClarificationAnswerResultResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_answer(\n session_id: str,\n request: ClarificationAnswerRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_answer\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = clarification_engine.record_answer(\n ClarificationAnswerCommand(\n session=session,\n question_id=request.question_id,\n answer_kind=request.answer_kind,\n answer_value=request.answer_value,\n user=current_user,\n )\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)\n ) from exc\n return ClarificationAnswerResultResponse(\n clarification_state=_serialize_clarification_state(result),\n session=_serialize_session_summary(result.session),\n changed_findings=[\n ValidationFindingDto.model_validate(f, from_attributes=True)\n for f in result.changed_findings\n ],\n )\n\n\n# [/DEF:record_clarification_answer:Function]\n\n\n# [DEF:update_field_semantic:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Apply one field-level semantic candidate decision or manual override.\n@router.patch(\n \"/sessions/{session_id}/fields/{field_id}/semantic\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_field_semantic(\n session_id: str,\n field_id: str,\n request: FieldSemanticUpdateRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n _update_semantic_field_state(field, request, changed_by=\"user\")\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_updated\",\n event_summary=\"Semantic field decision persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# [/DEF:update_field_semantic:Function]\n\n\n# [DEF:lock_field_semantic:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Lock one semantic field against later automatic overwrite.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/lock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def lock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.lock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = True\n field.last_changed_by = \"user\"\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_locked\",\n event_summary=\"Semantic field lock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# [/DEF:lock_field_semantic:Function]\n\n\n# [DEF:unlock_field_semantic:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Unlock one semantic field so later automated candidate application may replace it.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/unlock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def unlock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.unlock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = False\n field.last_changed_by = \"user\"\n if field.provenance == FieldProvenance.MANUAL_OVERRIDE:\n field.provenance = FieldProvenance.UNRESOLVED\n field.needs_review = True\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_unlocked\",\n event_summary=\"Semantic field unlock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# [/DEF:unlock_field_semantic:Function]\n\n\n# [DEF:approve_batch_semantic_fields:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Approve multiple semantic candidate decisions in one batch.\n@router.post(\n \"/sessions/{session_id}/fields/semantic/approve-batch\",\n response_model=List[SemanticFieldEntryDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_semantic_fields(\n session_id: str,\n request: BatchApproveSemanticRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_semantic_fields\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for item in request.items:\n field = _get_owned_field_or_404(session, item.field_id)\n _update_semantic_field_state(\n field,\n FieldSemanticUpdateRequest(\n candidate_id=item.candidate_id, lock_field=item.lock_field\n ),\n changed_by=\"user\",\n )\n updated.append(field)\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_fields_batch_approved\",\n event_summary=\"Batch semantic approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_semantic_field(f) for f in updated]\n\n\n# [/DEF:approve_batch_semantic_fields:Function]\n\n\n# [DEF:list_execution_mappings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Return the current mapping-review set for one accessible session.\n@router.get(\n \"/sessions/{session_id}/mappings\",\n response_model=MappingCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_execution_mappings(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_execution_mappings\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return MappingCollectionResponse(\n items=[_serialize_execution_mapping(m) for m in session.execution_mappings]\n )\n\n\n# [/DEF:list_execution_mappings:Function]\n\n\n# [DEF:update_execution_mapping:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist one owner-authorized execution-mapping effective value override.\n@router.patch(\n \"/sessions/{session_id}/mappings/{mapping_id}\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: UpdateExecutionMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n if request.effective_value is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"effective_value is required\",\n )\n mapping.effective_value = request.effective_value\n mapping.mapping_method = MappingMethod(\n request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value\n )\n mapping.transformation_note = request.transformation_note\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n session.last_activity_at = datetime.utcnow()\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n if session.readiness_state in {\n ReadinessState.MAPPING_REVIEW_NEEDED,\n ReadinessState.COMPILED_PREVIEW_READY,\n ReadinessState.RUN_READY,\n ReadinessState.RUN_IN_PROGRESS,\n }:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n for preview in session.previews:\n if preview.preview_status == PreviewStatus.READY:\n preview.preview_status = PreviewStatus.STALE\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_updated\",\n event_summary=\"Mapping override persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# [/DEF:update_execution_mapping:Function]\n\n\n# [DEF:approve_execution_mapping:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Explicitly approve a warning-sensitive mapping transformation.\n@router.post(\n \"/sessions/{session_id}/mappings/{mapping_id}/approve\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: ApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_approved\",\n event_summary=\"Mapping approval persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# [/DEF:approve_execution_mapping:Function]\n\n\n# [DEF:approve_batch_execution_mappings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Approve multiple warning-sensitive execution mappings in one batch.\n@router.post(\n \"/sessions/{session_id}/mappings/approve-batch\",\n response_model=List[ExecutionMappingDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_execution_mappings(\n session_id: str,\n request: BatchApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_execution_mappings\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for mid in list(dict.fromkeys(request.mapping_ids)):\n mapping = _get_owned_mapping_or_404(session, mid)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n updated.append(mapping)\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mappings_batch_approved\",\n event_summary=\"Batch mapping approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_execution_mapping(m) for m in updated]\n\n\n# [/DEF:approve_batch_execution_mappings:Function]\n\n\n# [DEF:trigger_preview_generation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Trigger Superset-side preview compilation for the current owned execution context.\n@router.post(\n \"/sessions/{session_id}/preview\",\n response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def trigger_preview_generation(\n session_id: str,\n response: Response,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.trigger_preview_generation\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Preview blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n if result.preview.preview_status == PreviewStatus.PENDING:\n response.status_code = status.HTTP_202_ACCEPTED\n return PreviewEnqueueResultResponse(\n session_id=result.session.session_id,\n session_version=int(getattr(result.session, \"version\", 0) or 0),\n preview_status=result.preview.preview_status.value,\n task_id=None,\n )\n response.status_code = status.HTTP_200_OK\n return _serialize_preview(\n result.preview,\n session_version_fallback=int(getattr(result.session, \"version\", 0) or 0),\n )\n\n\n# [/DEF:trigger_preview_generation:Function]\n\n\n# [DEF:launch_dataset:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Execute the current owned session launch handoff and return audited SQL Lab run context.\n@router.post(\n \"/sessions/{session_id}/launch\",\n response_model=LaunchDatasetResponse,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:execution:launch\", \"EXECUTE\")),\n ],\n)\nasync def launch_dataset(\n session_id: str,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n config_manager=Depends(get_config_manager),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.launch_dataset\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.launch_dataset(\n LaunchDatasetCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Launch blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n environment = config_manager.get_environment(result.session.environment_id)\n env_url = getattr(environment, \"url\", \"\") if environment is not None else \"\"\n return LaunchDatasetResponse(\n run_context=_serialize_run_context(result.run_context),\n redirect_url=_build_sql_lab_redirect_url(\n environment_url=env_url,\n sql_lab_session_ref=result.run_context.sql_lab_session_ref,\n ),\n )\n\n\n# [/DEF:launch_dataset:Function]\n\n\n# [DEF:record_field_feedback:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist thumbs up/down feedback for AI-assisted semantic field content.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_field_feedback(\n session_id: str,\n field_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_field_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(target_id=field.field_id, feedback=request.feedback)\n\n\n# [/DEF:record_field_feedback:Function]\n\n\n# [DEF:record_clarification_feedback:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.\n@router.post(\n \"/sessions/{session_id}/clarification/questions/{question_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_feedback(\n session_id: str,\n question_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = next((q for q in cs.questions if q.question_id == question_id), None)\n if question is None:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"Clarification question not found\",\n )\n if question.answer is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Clarification answer not found\",\n )\n question.answer.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"clarification_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"question_id\": question.question_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(\n target_id=question.question_id, feedback=request.feedback\n )\n\n\n# [/DEF:record_clarification_feedback:Function]\n\n\n# [/DEF:DatasetReviewRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, dataset, review, feedback, route]\n# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.\n\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any, Union, cast\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, Response, status\n\nfrom src.api.routes.dataset_review_pkg._dependencies import (\n BatchApproveMappingRequest,\n BatchApproveSemanticRequest,\n ClarificationAnswerRequest,\n ClarificationAnswerResultResponse,\n ClarificationStateResponse,\n ExportArtifactResponse,\n FeedbackRequest,\n FeedbackResponse,\n FieldSemanticUpdateRequest,\n LaunchDatasetResponse,\n MappingCollectionResponse,\n PreviewEnqueueResultResponse,\n SessionCollectionResponse,\n StartSessionRequest,\n UpdateExecutionMappingRequest,\n UpdateSessionRequest,\n _build_documentation_export,\n _build_session_version_conflict_http_exception,\n _build_sql_lab_redirect_url,\n _build_validation_export,\n _commit_owned_session_mutation,\n _get_clarification_engine,\n _get_latest_clarification_session_or_404,\n _get_orchestrator,\n _get_owned_field_or_404,\n _get_owned_mapping_or_404,\n _get_owned_session_or_404,\n _get_repository,\n _prepare_owned_session_mutation,\n _record_session_event,\n _require_auto_review_flag,\n _require_clarification_flag,\n _require_execution_flag,\n _require_session_version_header,\n _serialize_clarification_state,\n _serialize_empty_clarification_state,\n _serialize_execution_mapping,\n _serialize_preview,\n _serialize_run_context,\n _serialize_semantic_field,\n _serialize_session_detail,\n _serialize_session_summary,\n _update_semantic_field_state,\n)\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n ApprovalState,\n ArtifactFormat,\n FieldProvenance,\n MappingMethod,\n PreviewStatus,\n ReadinessState,\n RecommendedAction,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n CompiledPreviewDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationAnswerCommand,\n ClarificationStateResult,\n)\nfrom src.services.dataset_review.orchestrator import (\n LaunchDatasetCommand,\n PreparePreviewCommand,\n StartSessionCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionVersionConflictError,\n)\n\nrouter = APIRouter(prefix=\"/api/dataset-orchestration\", tags=[\"Dataset Orchestration\"])\n\n\n# #region list_sessions [C:3] [TYPE Function]\n# @BRIEF List resumable dataset review sessions for the current user.\n@router.get(\n \"/sessions\",\n response_model=SessionCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_sessions(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_sessions\"):\n logger.reason(\n \"Listing dataset review sessions\",\n extra={\"user_id\": current_user.id, \"page\": page, \"page_size\": page_size},\n )\n sessions = repository.list_sessions_for_user(current_user.id)\n start = (page - 1) * page_size\n end = start + page_size\n items = [_serialize_session_summary(s) for s in sessions[start:end]]\n logger.reflect(\n \"Session page assembled\",\n extra={\n \"user_id\": current_user.id,\n \"returned\": len(items),\n \"total\": len(sessions),\n },\n )\n return SessionCollectionResponse(\n items=items,\n total=len(sessions),\n page=page,\n page_size=page_size,\n has_next=end < len(sessions),\n )\n\n\n# #endregion list_sessions\n\n\n# #region start_session [C:4] [TYPE Function]\n# @BRIEF Start a new dataset review session from a Superset link or dataset selection.\n@router.post(\n \"/sessions\",\n response_model=SessionSummary,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def start_session(\n request: StartSessionRequest,\n orchestrator=Depends(_get_orchestrator),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"start_session\"):\n logger.reason(\n \"Starting dataset review session\",\n extra={\n \"user_id\": current_user.id,\n \"environment_id\": request.environment_id,\n },\n )\n try:\n result = orchestrator.start_session(\n StartSessionCommand(\n user=current_user,\n environment_id=request.environment_id,\n source_kind=request.source_kind,\n source_input=request.source_input,\n )\n )\n except ValueError as exc:\n logger.explore(\n \"Session start rejected\",\n extra={\"user_id\": current_user.id, \"error\": str(exc)},\n )\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail == \"Environment not found\"\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n logger.reflect(\n \"Session started\", extra={\"session_id\": result.session.session_id}\n )\n return _serialize_session_summary(result.session)\n\n\n# #endregion start_session\n\n\n# #region get_session_detail [C:3] [TYPE Function]\n# @BRIEF Return the full accessible dataset review session aggregate.\n@router.get(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_session_detail(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.get_session_detail\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return _serialize_session_detail(session)\n\n\n# #endregion get_session_detail\n\n\n# #region update_session [C:3] [TYPE Function]\n# @BRIEF Update resumable lifecycle status for an owned session.\n@router.patch(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_session(\n session_id: str,\n request: UpdateSessionRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"update_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n session_record = cast(Any, session)\n session_record.status = request.status\n if request.status == SessionStatus.PAUSED:\n session_record.recommended_action = RecommendedAction.RESUME_SESSION\n elif request.status in {\n SessionStatus.ARCHIVED,\n SessionStatus.CANCELLED,\n SessionStatus.COMPLETED,\n }:\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_status_updated\",\n event_summary=\"Dataset review session lifecycle updated\",\n event_details={\n \"status\": session_record.status.value,\n \"version\": session_record.version,\n },\n )\n return _serialize_session_summary(session)\n\n\n# #endregion update_session\n\n\n# #region delete_session [C:3] [TYPE Function]\n# @BRIEF Archive or hard-delete a session owned by the current user.\n@router.delete(\n \"/sessions/{session_id}\",\n status_code=status.HTTP_204_NO_CONTENT,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def delete_session(\n session_id: str,\n hard_delete: bool = Query(False),\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"delete_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n if hard_delete:\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_deleted\",\n event_summary=\"Session hard-deleted\",\n event_details={\"hard_delete\": True},\n )\n repository.db.delete(session)\n repository.db.commit()\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n session_record = cast(Any, session)\n session_record.status = SessionStatus.ARCHIVED\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_archived\",\n event_summary=\"Session archived\",\n event_details={\"hard_delete\": False, \"version\": session_record.version},\n )\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n\n\n# #endregion delete_session\n\n\n# #region export_documentation [C:3] [TYPE Function]\n# @BRIEF Export documentation output for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/documentation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_documentation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_documentation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_documentation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"documentation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"documentation\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# #endregion export_documentation\n\n\n# #region export_validation [C:3] [TYPE Function]\n# @BRIEF Export validation findings for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/validation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_validation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_validation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_validation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"validation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"validation_report\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# #endregion export_validation\n\n\n# #region get_clarification_state [C:3] [TYPE Function]\n# @BRIEF Return the current clarification session summary and active question payload.\n@router.get(\n \"/sessions/{session_id}/clarification\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_clarification_state(\n session_id: str,\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"get_clarification_state\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n if not session.clarification_sessions:\n return _serialize_empty_clarification_state()\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# #endregion get_clarification_state\n\n\n# #region resume_clarification [C:3] [TYPE Function]\n# @BRIEF Resume clarification mode on the highest-priority unresolved question.\n@router.post(\n \"/sessions/{session_id}/clarification/resume\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def resume_clarification(\n session_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"resume_clarification\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# #endregion resume_clarification\n\n\n# #region record_clarification_answer [C:3] [TYPE Function]\n# @BRIEF Persist one clarification answer before advancing the active pointer.\n@router.post(\n \"/sessions/{session_id}/clarification/answers\",\n response_model=ClarificationAnswerResultResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_answer(\n session_id: str,\n request: ClarificationAnswerRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_answer\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = clarification_engine.record_answer(\n ClarificationAnswerCommand(\n session=session,\n question_id=request.question_id,\n answer_kind=request.answer_kind,\n answer_value=request.answer_value,\n user=current_user,\n )\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)\n ) from exc\n return ClarificationAnswerResultResponse(\n clarification_state=_serialize_clarification_state(result),\n session=_serialize_session_summary(result.session),\n changed_findings=[\n ValidationFindingDto.model_validate(f, from_attributes=True)\n for f in result.changed_findings\n ],\n )\n\n\n# #endregion record_clarification_answer\n\n\n# #region update_field_semantic [C:3] [TYPE Function]\n# @BRIEF Apply one field-level semantic candidate decision or manual override.\n@router.patch(\n \"/sessions/{session_id}/fields/{field_id}/semantic\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_field_semantic(\n session_id: str,\n field_id: str,\n request: FieldSemanticUpdateRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n _update_semantic_field_state(field, request, changed_by=\"user\")\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_updated\",\n event_summary=\"Semantic field decision persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion update_field_semantic\n\n\n# #region lock_field_semantic [C:3] [TYPE Function]\n# @BRIEF Lock one semantic field against later automatic overwrite.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/lock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def lock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.lock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = True\n field.last_changed_by = \"user\"\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_locked\",\n event_summary=\"Semantic field lock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion lock_field_semantic\n\n\n# #region unlock_field_semantic [C:3] [TYPE Function]\n# @BRIEF Unlock one semantic field so later automated candidate application may replace it.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/unlock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def unlock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.unlock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = False\n field.last_changed_by = \"user\"\n if field.provenance == FieldProvenance.MANUAL_OVERRIDE:\n field.provenance = FieldProvenance.UNRESOLVED\n field.needs_review = True\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_unlocked\",\n event_summary=\"Semantic field unlock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion unlock_field_semantic\n\n\n# #region approve_batch_semantic_fields [C:3] [TYPE Function]\n# @BRIEF Approve multiple semantic candidate decisions in one batch.\n@router.post(\n \"/sessions/{session_id}/fields/semantic/approve-batch\",\n response_model=list[SemanticFieldEntryDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_semantic_fields(\n session_id: str,\n request: BatchApproveSemanticRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_semantic_fields\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for item in request.items:\n field = _get_owned_field_or_404(session, item.field_id)\n _update_semantic_field_state(\n field,\n FieldSemanticUpdateRequest(\n candidate_id=item.candidate_id, lock_field=item.lock_field\n ),\n changed_by=\"user\",\n )\n updated.append(field)\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_fields_batch_approved\",\n event_summary=\"Batch semantic approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_semantic_field(f) for f in updated]\n\n\n# #endregion approve_batch_semantic_fields\n\n\n# #region list_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Return the current mapping-review set for one accessible session.\n@router.get(\n \"/sessions/{session_id}/mappings\",\n response_model=MappingCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_execution_mappings(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_execution_mappings\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return MappingCollectionResponse(\n items=[_serialize_execution_mapping(m) for m in session.execution_mappings]\n )\n\n\n# #endregion list_execution_mappings\n\n\n# #region update_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Persist one owner-authorized execution-mapping effective value override.\n@router.patch(\n \"/sessions/{session_id}/mappings/{mapping_id}\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: UpdateExecutionMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n if request.effective_value is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"effective_value is required\",\n )\n mapping.effective_value = request.effective_value\n mapping.mapping_method = MappingMethod(\n request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value\n )\n mapping.transformation_note = request.transformation_note\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n session.last_activity_at = datetime.utcnow()\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n if session.readiness_state in {\n ReadinessState.MAPPING_REVIEW_NEEDED,\n ReadinessState.COMPILED_PREVIEW_READY,\n ReadinessState.RUN_READY,\n ReadinessState.RUN_IN_PROGRESS,\n }:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n for preview in session.previews:\n if preview.preview_status == PreviewStatus.READY:\n preview.preview_status = PreviewStatus.STALE\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_updated\",\n event_summary=\"Mapping override persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion update_execution_mapping\n\n\n# #region approve_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Explicitly approve a warning-sensitive mapping transformation.\n@router.post(\n \"/sessions/{session_id}/mappings/{mapping_id}/approve\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: ApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_approved\",\n event_summary=\"Mapping approval persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion approve_execution_mapping\n\n\n# #region approve_batch_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Approve multiple warning-sensitive execution mappings in one batch.\n@router.post(\n \"/sessions/{session_id}/mappings/approve-batch\",\n response_model=list[ExecutionMappingDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_execution_mappings(\n session_id: str,\n request: BatchApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_execution_mappings\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for mid in list(dict.fromkeys(request.mapping_ids)):\n mapping = _get_owned_mapping_or_404(session, mid)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n updated.append(mapping)\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mappings_batch_approved\",\n event_summary=\"Batch mapping approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_execution_mapping(m) for m in updated]\n\n\n# #endregion approve_batch_execution_mappings\n\n\n# #region trigger_preview_generation [C:3] [TYPE Function]\n# @BRIEF Trigger Superset-side preview compilation for the current owned execution context.\n@router.post(\n \"/sessions/{session_id}/preview\",\n response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def trigger_preview_generation(\n session_id: str,\n response: Response,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.trigger_preview_generation\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Preview blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n if result.preview.preview_status == PreviewStatus.PENDING:\n response.status_code = status.HTTP_202_ACCEPTED\n return PreviewEnqueueResultResponse(\n session_id=result.session.session_id,\n session_version=int(getattr(result.session, \"version\", 0) or 0),\n preview_status=result.preview.preview_status.value,\n task_id=None,\n )\n response.status_code = status.HTTP_200_OK\n return _serialize_preview(\n result.preview,\n session_version_fallback=int(getattr(result.session, \"version\", 0) or 0),\n )\n\n\n# #endregion trigger_preview_generation\n\n\n# #region launch_dataset [C:3] [TYPE Function]\n# @BRIEF Execute the current owned session launch handoff and return audited SQL Lab run context.\n@router.post(\n \"/sessions/{session_id}/launch\",\n response_model=LaunchDatasetResponse,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:execution:launch\", \"EXECUTE\")),\n ],\n)\nasync def launch_dataset(\n session_id: str,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n config_manager=Depends(get_config_manager),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.launch_dataset\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.launch_dataset(\n LaunchDatasetCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Launch blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n environment = config_manager.get_environment(result.session.environment_id)\n env_url = getattr(environment, \"url\", \"\") if environment is not None else \"\"\n return LaunchDatasetResponse(\n run_context=_serialize_run_context(result.run_context),\n redirect_url=_build_sql_lab_redirect_url(\n environment_url=env_url,\n sql_lab_session_ref=result.run_context.sql_lab_session_ref,\n ),\n )\n\n\n# #endregion launch_dataset\n\n\n# #region record_field_feedback [C:3] [TYPE Function]\n# @BRIEF Persist thumbs up/down feedback for AI-assisted semantic field content.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_field_feedback(\n session_id: str,\n field_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_field_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(target_id=field.field_id, feedback=request.feedback)\n\n\n# #endregion record_field_feedback\n\n\n# #region record_clarification_feedback [C:3] [TYPE Function]\n# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.\n@router.post(\n \"/sessions/{session_id}/clarification/questions/{question_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_feedback(\n session_id: str,\n question_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = next((q for q in cs.questions if q.question_id == question_id), None)\n if question is None:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"Clarification question not found\",\n )\n if question.answer is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Clarification answer not found\",\n )\n question.answer.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"clarification_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"question_id\": question.question_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(\n target_id=question.question_id, feedback=request.feedback\n )\n\n\n# #endregion record_clarification_feedback\n\n\n# #endregion DatasetReviewRoutes\n" }, { "contract_id": "list_sessions", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 98, - "end_line": 141, + "start_line": 93, + "end_line": 135, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "List resumable dataset review sessions for the current user." }, "relations": [], @@ -16838,19 +15103,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:list_sessions:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: List resumable dataset review sessions for the current user.\n@router.get(\n \"/sessions\",\n response_model=SessionCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_sessions(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_sessions\"):\n log.reason(\n \"Listing dataset review sessions\",\n payload={\"user_id\": current_user.id, \"page\": page, \"page_size\": page_size},\n )\n sessions = repository.list_sessions_for_user(current_user.id)\n start = (page - 1) * page_size\n end = start + page_size\n items = [_serialize_session_summary(s) for s in sessions[start:end]]\n log.reflect(\n \"Session page assembled\",\n payload={\n \"user_id\": current_user.id,\n \"returned\": len(items),\n \"total\": len(sessions),\n },\n )\n return SessionCollectionResponse(\n items=items,\n total=len(sessions),\n page=page,\n page_size=page_size,\n has_next=end < len(sessions),\n )\n\n\n# [/DEF:list_sessions:Function]\n" + "anchor_syntax": "region", + "body": "# #region list_sessions [C:3] [TYPE Function]\n# @BRIEF List resumable dataset review sessions for the current user.\n@router.get(\n \"/sessions\",\n response_model=SessionCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_sessions(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_sessions\"):\n logger.reason(\n \"Listing dataset review sessions\",\n extra={\"user_id\": current_user.id, \"page\": page, \"page_size\": page_size},\n )\n sessions = repository.list_sessions_for_user(current_user.id)\n start = (page - 1) * page_size\n end = start + page_size\n items = [_serialize_session_summary(s) for s in sessions[start:end]]\n logger.reflect(\n \"Session page assembled\",\n extra={\n \"user_id\": current_user.id,\n \"returned\": len(items),\n \"total\": len(sessions),\n },\n )\n return SessionCollectionResponse(\n items=items,\n total=len(sessions),\n page=page,\n page_size=page_size,\n has_next=end < len(sessions),\n )\n\n\n# #endregion list_sessions\n" }, { "contract_id": "start_session", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 144, - "end_line": 193, + "start_line": 138, + "end_line": 189, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "PURPOSE": "Start a new dataset review session from a Superset link or dataset selection." }, "relations": [], @@ -16892,19 +15157,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:start_session:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Start a new dataset review session from a Superset link or dataset selection.\n@router.post(\n \"/sessions\",\n response_model=SessionSummary,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def start_session(\n request: StartSessionRequest,\n orchestrator=Depends(_get_orchestrator),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"start_session\"):\n log.reason(\n \"Starting dataset review session\",\n payload={\n \"user_id\": current_user.id,\n \"environment_id\": request.environment_id,\n },\n )\n try:\n result = orchestrator.start_session(\n StartSessionCommand(\n user=current_user,\n environment_id=request.environment_id,\n source_kind=request.source_kind,\n source_input=request.source_input,\n )\n )\n except ValueError as exc:\n log.explore(\"Session start rejected\", error=\"Session start rejected by orchestrator\", payload={\"user_id\": current_user.id, \"error\": str(exc)})\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail == \"Environment not found\"\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n log.reflect(\n \"Session started\", payload={\"session_id\": result.session.session_id}\n )\n return _serialize_session_summary(result.session)\n\n\n# [/DEF:start_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region start_session [C:4] [TYPE Function]\n# @BRIEF Start a new dataset review session from a Superset link or dataset selection.\n@router.post(\n \"/sessions\",\n response_model=SessionSummary,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def start_session(\n request: StartSessionRequest,\n orchestrator=Depends(_get_orchestrator),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"start_session\"):\n logger.reason(\n \"Starting dataset review session\",\n extra={\n \"user_id\": current_user.id,\n \"environment_id\": request.environment_id,\n },\n )\n try:\n result = orchestrator.start_session(\n StartSessionCommand(\n user=current_user,\n environment_id=request.environment_id,\n source_kind=request.source_kind,\n source_input=request.source_input,\n )\n )\n except ValueError as exc:\n logger.explore(\n \"Session start rejected\",\n extra={\"user_id\": current_user.id, \"error\": str(exc)},\n )\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail == \"Environment not found\"\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n logger.reflect(\n \"Session started\", extra={\"session_id\": result.session.session_id}\n )\n return _serialize_session_summary(result.session)\n\n\n# #endregion start_session\n" }, { "contract_id": "get_session_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 196, - "end_line": 217, + "start_line": 192, + "end_line": 212, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Return the full accessible dataset review session aggregate." }, "relations": [], @@ -16919,19 +15184,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_session_detail:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Return the full accessible dataset review session aggregate.\n@router.get(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_session_detail(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.get_session_detail\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return _serialize_session_detail(session)\n\n\n# [/DEF:get_session_detail:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_session_detail [C:3] [TYPE Function]\n# @BRIEF Return the full accessible dataset review session aggregate.\n@router.get(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_session_detail(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.get_session_detail\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return _serialize_session_detail(session)\n\n\n# #endregion get_session_detail\n" }, { "contract_id": "update_session", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 220, - "end_line": 267, + "start_line": 215, + "end_line": 261, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Update resumable lifecycle status for an owned session." }, "relations": [], @@ -16946,19 +15211,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_session:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Update resumable lifecycle status for an owned session.\n@router.patch(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_session(\n session_id: str,\n request: UpdateSessionRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"update_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n session_record = cast(Any, session)\n session_record.status = request.status\n if request.status == SessionStatus.PAUSED:\n session_record.recommended_action = RecommendedAction.RESUME_SESSION\n elif request.status in {\n SessionStatus.ARCHIVED,\n SessionStatus.CANCELLED,\n SessionStatus.COMPLETED,\n }:\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_status_updated\",\n event_summary=\"Dataset review session lifecycle updated\",\n event_details={\n \"status\": session_record.status.value,\n \"version\": session_record.version,\n },\n )\n return _serialize_session_summary(session)\n\n\n# [/DEF:update_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region update_session [C:3] [TYPE Function]\n# @BRIEF Update resumable lifecycle status for an owned session.\n@router.patch(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_session(\n session_id: str,\n request: UpdateSessionRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"update_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n session_record = cast(Any, session)\n session_record.status = request.status\n if request.status == SessionStatus.PAUSED:\n session_record.recommended_action = RecommendedAction.RESUME_SESSION\n elif request.status in {\n SessionStatus.ARCHIVED,\n SessionStatus.CANCELLED,\n SessionStatus.COMPLETED,\n }:\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_status_updated\",\n event_summary=\"Dataset review session lifecycle updated\",\n event_details={\n \"status\": session_record.status.value,\n \"version\": session_record.version,\n },\n )\n return _serialize_session_summary(session)\n\n\n# #endregion update_session\n" }, { "contract_id": "delete_session", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 270, - "end_line": 319, + "start_line": 264, + "end_line": 312, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Archive or hard-delete a session owned by the current user." }, "relations": [], @@ -16973,19 +15238,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:delete_session:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Archive or hard-delete a session owned by the current user.\n@router.delete(\n \"/sessions/{session_id}\",\n status_code=status.HTTP_204_NO_CONTENT,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def delete_session(\n session_id: str,\n hard_delete: bool = Query(False),\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"delete_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n if hard_delete:\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_deleted\",\n event_summary=\"Session hard-deleted\",\n event_details={\"hard_delete\": True},\n )\n repository.db.delete(session)\n repository.db.commit()\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n session_record = cast(Any, session)\n session_record.status = SessionStatus.ARCHIVED\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_archived\",\n event_summary=\"Session archived\",\n event_details={\"hard_delete\": False, \"version\": session_record.version},\n )\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n\n\n# [/DEF:delete_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region delete_session [C:3] [TYPE Function]\n# @BRIEF Archive or hard-delete a session owned by the current user.\n@router.delete(\n \"/sessions/{session_id}\",\n status_code=status.HTTP_204_NO_CONTENT,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def delete_session(\n session_id: str,\n hard_delete: bool = Query(False),\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"delete_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n if hard_delete:\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_deleted\",\n event_summary=\"Session hard-deleted\",\n event_details={\"hard_delete\": True},\n )\n repository.db.delete(session)\n repository.db.commit()\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n session_record = cast(Any, session)\n session_record.status = SessionStatus.ARCHIVED\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_archived\",\n event_summary=\"Session archived\",\n event_details={\"hard_delete\": False, \"version\": session_record.version},\n )\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n\n\n# #endregion delete_session\n" }, { "contract_id": "export_documentation", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 322, - "end_line": 358, + "start_line": 315, + "end_line": 350, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Export documentation output for the current session." }, "relations": [], @@ -17000,19 +15265,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:export_documentation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Export documentation output for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/documentation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_documentation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_documentation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_documentation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"documentation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"documentation\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# [/DEF:export_documentation:Function]\n" + "anchor_syntax": "region", + "body": "# #region export_documentation [C:3] [TYPE Function]\n# @BRIEF Export documentation output for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/documentation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_documentation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_documentation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_documentation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"documentation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"documentation\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# #endregion export_documentation\n" }, { "contract_id": "export_validation", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 361, - "end_line": 397, + "start_line": 353, + "end_line": 388, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Export validation findings for the current session." }, "relations": [], @@ -17027,19 +15292,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:export_validation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Export validation findings for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/validation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_validation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_validation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_validation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"validation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"validation_report\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# [/DEF:export_validation:Function]\n" + "anchor_syntax": "region", + "body": "# #region export_validation [C:3] [TYPE Function]\n# @BRIEF Export validation findings for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/validation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_validation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_validation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_validation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"validation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"validation_report\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# #endregion export_validation\n" }, { "contract_id": "get_clarification_state", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 400, - "end_line": 434, + "start_line": 391, + "end_line": 424, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Return the current clarification session summary and active question payload." }, "relations": [], @@ -17054,19 +15319,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_clarification_state:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Return the current clarification session summary and active question payload.\n@router.get(\n \"/sessions/{session_id}/clarification\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_clarification_state(\n session_id: str,\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"get_clarification_state\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n if not session.clarification_sessions:\n return _serialize_empty_clarification_state()\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# [/DEF:get_clarification_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_clarification_state [C:3] [TYPE Function]\n# @BRIEF Return the current clarification session summary and active question payload.\n@router.get(\n \"/sessions/{session_id}/clarification\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_clarification_state(\n session_id: str,\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"get_clarification_state\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n if not session.clarification_sessions:\n return _serialize_empty_clarification_state()\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# #endregion get_clarification_state\n" }, { "contract_id": "resume_clarification", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 437, - "end_line": 472, + "start_line": 427, + "end_line": 461, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Resume clarification mode on the highest-priority unresolved question." }, "relations": [], @@ -17081,19 +15346,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:resume_clarification:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resume clarification mode on the highest-priority unresolved question.\n@router.post(\n \"/sessions/{session_id}/clarification/resume\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def resume_clarification(\n session_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"resume_clarification\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# [/DEF:resume_clarification:Function]\n" + "anchor_syntax": "region", + "body": "# #region resume_clarification [C:3] [TYPE Function]\n# @BRIEF Resume clarification mode on the highest-priority unresolved question.\n@router.post(\n \"/sessions/{session_id}/clarification/resume\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def resume_clarification(\n session_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"resume_clarification\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# #endregion resume_clarification\n" }, { "contract_id": "record_clarification_answer", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 475, - "end_line": 523, + "start_line": 464, + "end_line": 511, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Persist one clarification answer before advancing the active pointer." }, "relations": [], @@ -17108,19 +15373,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:record_clarification_answer:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist one clarification answer before advancing the active pointer.\n@router.post(\n \"/sessions/{session_id}/clarification/answers\",\n response_model=ClarificationAnswerResultResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_answer(\n session_id: str,\n request: ClarificationAnswerRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_answer\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = clarification_engine.record_answer(\n ClarificationAnswerCommand(\n session=session,\n question_id=request.question_id,\n answer_kind=request.answer_kind,\n answer_value=request.answer_value,\n user=current_user,\n )\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)\n ) from exc\n return ClarificationAnswerResultResponse(\n clarification_state=_serialize_clarification_state(result),\n session=_serialize_session_summary(result.session),\n changed_findings=[\n ValidationFindingDto.model_validate(f, from_attributes=True)\n for f in result.changed_findings\n ],\n )\n\n\n# [/DEF:record_clarification_answer:Function]\n" + "anchor_syntax": "region", + "body": "# #region record_clarification_answer [C:3] [TYPE Function]\n# @BRIEF Persist one clarification answer before advancing the active pointer.\n@router.post(\n \"/sessions/{session_id}/clarification/answers\",\n response_model=ClarificationAnswerResultResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_answer(\n session_id: str,\n request: ClarificationAnswerRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_answer\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = clarification_engine.record_answer(\n ClarificationAnswerCommand(\n session=session,\n question_id=request.question_id,\n answer_kind=request.answer_kind,\n answer_value=request.answer_value,\n user=current_user,\n )\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)\n ) from exc\n return ClarificationAnswerResultResponse(\n clarification_state=_serialize_clarification_state(result),\n session=_serialize_session_summary(result.session),\n changed_findings=[\n ValidationFindingDto.model_validate(f, from_attributes=True)\n for f in result.changed_findings\n ],\n )\n\n\n# #endregion record_clarification_answer\n" }, { "contract_id": "update_field_semantic", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 526, - "end_line": 564, + "start_line": 514, + "end_line": 551, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Apply one field-level semantic candidate decision or manual override." }, "relations": [], @@ -17135,19 +15400,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_field_semantic:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Apply one field-level semantic candidate decision or manual override.\n@router.patch(\n \"/sessions/{session_id}/fields/{field_id}/semantic\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_field_semantic(\n session_id: str,\n field_id: str,\n request: FieldSemanticUpdateRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n _update_semantic_field_state(field, request, changed_by=\"user\")\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_updated\",\n event_summary=\"Semantic field decision persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# [/DEF:update_field_semantic:Function]\n" + "anchor_syntax": "region", + "body": "# #region update_field_semantic [C:3] [TYPE Function]\n# @BRIEF Apply one field-level semantic candidate decision or manual override.\n@router.patch(\n \"/sessions/{session_id}/fields/{field_id}/semantic\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_field_semantic(\n session_id: str,\n field_id: str,\n request: FieldSemanticUpdateRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n _update_semantic_field_state(field, request, changed_by=\"user\")\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_updated\",\n event_summary=\"Semantic field decision persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion update_field_semantic\n" }, { "contract_id": "lock_field_semantic", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 567, - "end_line": 605, + "start_line": 554, + "end_line": 591, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Lock one semantic field against later automatic overwrite." }, "relations": [], @@ -17162,19 +15427,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:lock_field_semantic:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Lock one semantic field against later automatic overwrite.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/lock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def lock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.lock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = True\n field.last_changed_by = \"user\"\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_locked\",\n event_summary=\"Semantic field lock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# [/DEF:lock_field_semantic:Function]\n" + "anchor_syntax": "region", + "body": "# #region lock_field_semantic [C:3] [TYPE Function]\n# @BRIEF Lock one semantic field against later automatic overwrite.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/lock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def lock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.lock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = True\n field.last_changed_by = \"user\"\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_locked\",\n event_summary=\"Semantic field lock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion lock_field_semantic\n" }, { "contract_id": "unlock_field_semantic", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 608, - "end_line": 649, + "start_line": 594, + "end_line": 634, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Unlock one semantic field so later automated candidate application may replace it." }, "relations": [], @@ -17189,19 +15454,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:unlock_field_semantic:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Unlock one semantic field so later automated candidate application may replace it.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/unlock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def unlock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.unlock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = False\n field.last_changed_by = \"user\"\n if field.provenance == FieldProvenance.MANUAL_OVERRIDE:\n field.provenance = FieldProvenance.UNRESOLVED\n field.needs_review = True\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_unlocked\",\n event_summary=\"Semantic field unlock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# [/DEF:unlock_field_semantic:Function]\n" + "anchor_syntax": "region", + "body": "# #region unlock_field_semantic [C:3] [TYPE Function]\n# @BRIEF Unlock one semantic field so later automated candidate application may replace it.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/unlock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def unlock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.unlock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = False\n field.last_changed_by = \"user\"\n if field.provenance == FieldProvenance.MANUAL_OVERRIDE:\n field.provenance = FieldProvenance.UNRESOLVED\n field.needs_review = True\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_unlocked\",\n event_summary=\"Semantic field unlock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion unlock_field_semantic\n" }, { "contract_id": "approve_batch_semantic_fields", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 652, - "end_line": 700, + "start_line": 637, + "end_line": 684, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Approve multiple semantic candidate decisions in one batch." }, "relations": [], @@ -17216,19 +15481,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:approve_batch_semantic_fields:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Approve multiple semantic candidate decisions in one batch.\n@router.post(\n \"/sessions/{session_id}/fields/semantic/approve-batch\",\n response_model=List[SemanticFieldEntryDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_semantic_fields(\n session_id: str,\n request: BatchApproveSemanticRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_semantic_fields\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for item in request.items:\n field = _get_owned_field_or_404(session, item.field_id)\n _update_semantic_field_state(\n field,\n FieldSemanticUpdateRequest(\n candidate_id=item.candidate_id, lock_field=item.lock_field\n ),\n changed_by=\"user\",\n )\n updated.append(field)\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_fields_batch_approved\",\n event_summary=\"Batch semantic approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_semantic_field(f) for f in updated]\n\n\n# [/DEF:approve_batch_semantic_fields:Function]\n" + "anchor_syntax": "region", + "body": "# #region approve_batch_semantic_fields [C:3] [TYPE Function]\n# @BRIEF Approve multiple semantic candidate decisions in one batch.\n@router.post(\n \"/sessions/{session_id}/fields/semantic/approve-batch\",\n response_model=list[SemanticFieldEntryDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_semantic_fields(\n session_id: str,\n request: BatchApproveSemanticRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_semantic_fields\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for item in request.items:\n field = _get_owned_field_or_404(session, item.field_id)\n _update_semantic_field_state(\n field,\n FieldSemanticUpdateRequest(\n candidate_id=item.candidate_id, lock_field=item.lock_field\n ),\n changed_by=\"user\",\n )\n updated.append(field)\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_fields_batch_approved\",\n event_summary=\"Batch semantic approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_semantic_field(f) for f in updated]\n\n\n# #endregion approve_batch_semantic_fields\n" }, { "contract_id": "list_execution_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 703, - "end_line": 727, + "start_line": 687, + "end_line": 710, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Return the current mapping-review set for one accessible session." }, "relations": [], @@ -17243,19 +15508,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:list_execution_mappings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Return the current mapping-review set for one accessible session.\n@router.get(\n \"/sessions/{session_id}/mappings\",\n response_model=MappingCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_execution_mappings(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_execution_mappings\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return MappingCollectionResponse(\n items=[_serialize_execution_mapping(m) for m in session.execution_mappings]\n )\n\n\n# [/DEF:list_execution_mappings:Function]\n" + "anchor_syntax": "region", + "body": "# #region list_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Return the current mapping-review set for one accessible session.\n@router.get(\n \"/sessions/{session_id}/mappings\",\n response_model=MappingCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_execution_mappings(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_execution_mappings\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return MappingCollectionResponse(\n items=[_serialize_execution_mapping(m) for m in session.execution_mappings]\n )\n\n\n# #endregion list_execution_mappings\n" }, { "contract_id": "update_execution_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 730, - "end_line": 793, + "start_line": 713, + "end_line": 775, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Persist one owner-authorized execution-mapping effective value override." }, "relations": [], @@ -17270,19 +15535,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_execution_mapping:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist one owner-authorized execution-mapping effective value override.\n@router.patch(\n \"/sessions/{session_id}/mappings/{mapping_id}\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: UpdateExecutionMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n if request.effective_value is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"effective_value is required\",\n )\n mapping.effective_value = request.effective_value\n mapping.mapping_method = MappingMethod(\n request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value\n )\n mapping.transformation_note = request.transformation_note\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n session.last_activity_at = datetime.utcnow()\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n if session.readiness_state in {\n ReadinessState.MAPPING_REVIEW_NEEDED,\n ReadinessState.COMPILED_PREVIEW_READY,\n ReadinessState.RUN_READY,\n ReadinessState.RUN_IN_PROGRESS,\n }:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n for preview in session.previews:\n if preview.preview_status == PreviewStatus.READY:\n preview.preview_status = PreviewStatus.STALE\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_updated\",\n event_summary=\"Mapping override persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# [/DEF:update_execution_mapping:Function]\n" + "anchor_syntax": "region", + "body": "# #region update_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Persist one owner-authorized execution-mapping effective value override.\n@router.patch(\n \"/sessions/{session_id}/mappings/{mapping_id}\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: UpdateExecutionMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n if request.effective_value is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"effective_value is required\",\n )\n mapping.effective_value = request.effective_value\n mapping.mapping_method = MappingMethod(\n request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value\n )\n mapping.transformation_note = request.transformation_note\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n session.last_activity_at = datetime.utcnow()\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n if session.readiness_state in {\n ReadinessState.MAPPING_REVIEW_NEEDED,\n ReadinessState.COMPILED_PREVIEW_READY,\n ReadinessState.RUN_READY,\n ReadinessState.RUN_IN_PROGRESS,\n }:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n for preview in session.previews:\n if preview.preview_status == PreviewStatus.READY:\n preview.preview_status = PreviewStatus.STALE\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_updated\",\n event_summary=\"Mapping override persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion update_execution_mapping\n" }, { "contract_id": "approve_execution_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 796, - "end_line": 842, + "start_line": 778, + "end_line": 823, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Explicitly approve a warning-sensitive mapping transformation." }, "relations": [], @@ -17297,19 +15562,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:approve_execution_mapping:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Explicitly approve a warning-sensitive mapping transformation.\n@router.post(\n \"/sessions/{session_id}/mappings/{mapping_id}/approve\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: ApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_approved\",\n event_summary=\"Mapping approval persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# [/DEF:approve_execution_mapping:Function]\n" + "anchor_syntax": "region", + "body": "# #region approve_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Explicitly approve a warning-sensitive mapping transformation.\n@router.post(\n \"/sessions/{session_id}/mappings/{mapping_id}/approve\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: ApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_approved\",\n event_summary=\"Mapping approval persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion approve_execution_mapping\n" }, { "contract_id": "approve_batch_execution_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 845, - "end_line": 895, + "start_line": 826, + "end_line": 875, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Approve multiple warning-sensitive execution mappings in one batch." }, "relations": [], @@ -17324,19 +15589,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:approve_batch_execution_mappings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Approve multiple warning-sensitive execution mappings in one batch.\n@router.post(\n \"/sessions/{session_id}/mappings/approve-batch\",\n response_model=List[ExecutionMappingDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_execution_mappings(\n session_id: str,\n request: BatchApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_execution_mappings\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for mid in list(dict.fromkeys(request.mapping_ids)):\n mapping = _get_owned_mapping_or_404(session, mid)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n updated.append(mapping)\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mappings_batch_approved\",\n event_summary=\"Batch mapping approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_execution_mapping(m) for m in updated]\n\n\n# [/DEF:approve_batch_execution_mappings:Function]\n" + "anchor_syntax": "region", + "body": "# #region approve_batch_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Approve multiple warning-sensitive execution mappings in one batch.\n@router.post(\n \"/sessions/{session_id}/mappings/approve-batch\",\n response_model=list[ExecutionMappingDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_execution_mappings(\n session_id: str,\n request: BatchApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_execution_mappings\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for mid in list(dict.fromkeys(request.mapping_ids)):\n mapping = _get_owned_mapping_or_404(session, mid)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n updated.append(mapping)\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mappings_batch_approved\",\n event_summary=\"Batch mapping approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_execution_mapping(m) for m in updated]\n\n\n# #endregion approve_batch_execution_mappings\n" }, { "contract_id": "trigger_preview_generation", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 898, - "end_line": 957, + "start_line": 878, + "end_line": 936, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Trigger Superset-side preview compilation for the current owned execution context." }, "relations": [], @@ -17351,19 +15616,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:trigger_preview_generation:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Trigger Superset-side preview compilation for the current owned execution context.\n@router.post(\n \"/sessions/{session_id}/preview\",\n response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def trigger_preview_generation(\n session_id: str,\n response: Response,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.trigger_preview_generation\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Preview blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n if result.preview.preview_status == PreviewStatus.PENDING:\n response.status_code = status.HTTP_202_ACCEPTED\n return PreviewEnqueueResultResponse(\n session_id=result.session.session_id,\n session_version=int(getattr(result.session, \"version\", 0) or 0),\n preview_status=result.preview.preview_status.value,\n task_id=None,\n )\n response.status_code = status.HTTP_200_OK\n return _serialize_preview(\n result.preview,\n session_version_fallback=int(getattr(result.session, \"version\", 0) or 0),\n )\n\n\n# [/DEF:trigger_preview_generation:Function]\n" + "anchor_syntax": "region", + "body": "# #region trigger_preview_generation [C:3] [TYPE Function]\n# @BRIEF Trigger Superset-side preview compilation for the current owned execution context.\n@router.post(\n \"/sessions/{session_id}/preview\",\n response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def trigger_preview_generation(\n session_id: str,\n response: Response,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.trigger_preview_generation\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Preview blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n if result.preview.preview_status == PreviewStatus.PENDING:\n response.status_code = status.HTTP_202_ACCEPTED\n return PreviewEnqueueResultResponse(\n session_id=result.session.session_id,\n session_version=int(getattr(result.session, \"version\", 0) or 0),\n preview_status=result.preview.preview_status.value,\n task_id=None,\n )\n response.status_code = status.HTTP_200_OK\n return _serialize_preview(\n result.preview,\n session_version_fallback=int(getattr(result.session, \"version\", 0) or 0),\n )\n\n\n# #endregion trigger_preview_generation\n" }, { "contract_id": "launch_dataset", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 960, - "end_line": 1016, + "start_line": 939, + "end_line": 994, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Execute the current owned session launch handoff and return audited SQL Lab run context." }, "relations": [], @@ -17378,19 +15643,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:launch_dataset:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Execute the current owned session launch handoff and return audited SQL Lab run context.\n@router.post(\n \"/sessions/{session_id}/launch\",\n response_model=LaunchDatasetResponse,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:execution:launch\", \"EXECUTE\")),\n ],\n)\nasync def launch_dataset(\n session_id: str,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n config_manager=Depends(get_config_manager),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.launch_dataset\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.launch_dataset(\n LaunchDatasetCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Launch blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n environment = config_manager.get_environment(result.session.environment_id)\n env_url = getattr(environment, \"url\", \"\") if environment is not None else \"\"\n return LaunchDatasetResponse(\n run_context=_serialize_run_context(result.run_context),\n redirect_url=_build_sql_lab_redirect_url(\n environment_url=env_url,\n sql_lab_session_ref=result.run_context.sql_lab_session_ref,\n ),\n )\n\n\n# [/DEF:launch_dataset:Function]\n" + "anchor_syntax": "region", + "body": "# #region launch_dataset [C:3] [TYPE Function]\n# @BRIEF Execute the current owned session launch handoff and return audited SQL Lab run context.\n@router.post(\n \"/sessions/{session_id}/launch\",\n response_model=LaunchDatasetResponse,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:execution:launch\", \"EXECUTE\")),\n ],\n)\nasync def launch_dataset(\n session_id: str,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n config_manager=Depends(get_config_manager),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.launch_dataset\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.launch_dataset(\n LaunchDatasetCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Launch blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n environment = config_manager.get_environment(result.session.environment_id)\n env_url = getattr(environment, \"url\", \"\") if environment is not None else \"\"\n return LaunchDatasetResponse(\n run_context=_serialize_run_context(result.run_context),\n redirect_url=_build_sql_lab_redirect_url(\n environment_url=env_url,\n sql_lab_session_ref=result.run_context.sql_lab_session_ref,\n ),\n )\n\n\n# #endregion launch_dataset\n" }, { "contract_id": "record_field_feedback", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 1019, - "end_line": 1061, + "start_line": 997, + "end_line": 1038, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Persist thumbs up/down feedback for AI-assisted semantic field content." }, "relations": [], @@ -17405,19 +15670,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:record_field_feedback:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist thumbs up/down feedback for AI-assisted semantic field content.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_field_feedback(\n session_id: str,\n field_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_field_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(target_id=field.field_id, feedback=request.feedback)\n\n\n# [/DEF:record_field_feedback:Function]\n" + "anchor_syntax": "region", + "body": "# #region record_field_feedback [C:3] [TYPE Function]\n# @BRIEF Persist thumbs up/down feedback for AI-assisted semantic field content.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_field_feedback(\n session_id: str,\n field_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_field_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(target_id=field.field_id, feedback=request.feedback)\n\n\n# #endregion record_field_feedback\n" }, { "contract_id": "record_clarification_feedback", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 1064, - "end_line": 1120, + "start_line": 1041, + "end_line": 1096, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Persist thumbs up/down feedback for clarification question/answer content." }, "relations": [], @@ -17432,28 +15697,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:record_clarification_feedback:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.\n@router.post(\n \"/sessions/{session_id}/clarification/questions/{question_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_feedback(\n session_id: str,\n question_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = next((q for q in cs.questions if q.question_id == question_id), None)\n if question is None:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"Clarification question not found\",\n )\n if question.answer is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Clarification answer not found\",\n )\n question.answer.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"clarification_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"question_id\": question.question_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(\n target_id=question.question_id, feedback=request.feedback\n )\n\n\n# [/DEF:record_clarification_feedback:Function]\n" + "anchor_syntax": "region", + "body": "# #region record_clarification_feedback [C:3] [TYPE Function]\n# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.\n@router.post(\n \"/sessions/{session_id}/clarification/questions/{question_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_feedback(\n session_id: str,\n question_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = next((q for q in cs.questions if q.question_id == question_id), None)\n if question is None:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"Clarification question not found\",\n )\n if question.answer is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Clarification answer not found\",\n )\n question.answer.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"clarification_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"question_id\": question.question_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(\n target_id=question.question_id, feedback=request.feedback\n )\n\n\n# #endregion record_clarification_feedback\n" }, { "contract_id": "DatasetsApi", "contract_type": "Module", "file_path": "backend/src/api/routes/datasets.py", "start_line": 1, - "end_line": 426, + "end_line": 393, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "All dataset responses include last_task metadata", "LAYER": "API", - "PURPOSE": "API endpoints for the Dataset Hub - listing datasets with mapping progress", - "SEMANTICS": [ - "api", - "datasets", - "resources", - "hub" - ] + "PURPOSE": "API endpoints for the Dataset Hub - listing datasets with mapping progress" }, "relations": [ { @@ -17486,19 +15745,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetsApi:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: api, datasets, resources, hub\n# @PURPOSE: 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# [SECTION: IMPORTS]\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom typing import List, Optional\nfrom pydantic import BaseModel, Field\nfrom ...dependencies import get_config_manager, get_task_manager, get_resource_service, has_permission\nfrom ...core.logger import logger, belief_scope\nfrom ...core.superset_client import SupersetClient\n# [/SECTION]\n\nrouter = APIRouter(prefix=\"/api/datasets\", tags=[\"Datasets\"])\n\n# [DEF:MappedFields:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for dataset mapping progress statistics\nclass MappedFields(BaseModel):\n total: int\n mapped: int\n# [/DEF:MappedFields:DataClass]\n\n# [DEF:LastTask:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for the most recent task associated with a dataset\nclass LastTask(BaseModel):\n task_id: Optional[str] = None\n status: Optional[str] = Field(None, pattern=\"^RUNNING|SUCCESS|ERROR|WAITING_INPUT$\")\n# [/DEF:LastTask:DataClass]\n\n# [DEF:DatasetItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: 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: Optional[MappedFields] = None\n last_task: Optional[LastTask] = None\n\n class Config:\n allow_population_by_field_name = True\n# [/DEF:DatasetItem:DataClass]\n\n# [DEF:LinkedDashboard:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for a dashboard linked to a dataset\nclass LinkedDashboard(BaseModel):\n id: int\n title: str\n slug: Optional[str] = None\n# [/DEF:LinkedDashboard:DataClass]\n\n# [DEF:DatasetColumn:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for a single dataset column's metadata\nclass DatasetColumn(BaseModel):\n id: int\n name: str\n type: Optional[str] = None\n is_dttm: bool = False\n is_active: bool = True\n description: Optional[str] = None\n# [/DEF:DatasetColumn:DataClass]\n\n# [DEF:DatasetDetailResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Detailed DTO for a dataset including columns and links\nclass DatasetDetailResponse(BaseModel):\n id: int\n table_name: Optional[str] = None\n schema_name: Optional[str] = Field(None, alias=\"schema\")\n database: str\n description: Optional[str] = None\n columns: List[DatasetColumn]\n column_count: int\n sql: Optional[str] = None\n linked_dashboards: List[LinkedDashboard]\n linked_dashboard_count: int\n is_sqllab_view: bool = False\n created_on: Optional[str] = None\n changed_on: Optional[str] = None\n\n class Config:\n allow_population_by_field_name = True\n# [/DEF:DatasetDetailResponse:DataClass]\n\n# [DEF:DatasetsResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:DatasetsResponse:DataClass]\n\n# [DEF:TaskResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO containing a task ID for tracking\nclass TaskResponse(BaseModel):\n task_id: str\n# [/DEF:TaskResponse:DataClass]\n\n# [DEF:get_dataset_ids:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: env_id (str) - The environment ID to fetch datasets from\n# @PARAM: search (Optional[str]) - Filter by table name\n# @RETURN: List[int] - List of dataset IDs\n# @RELATION: CALLS ->[get_datasets_with_status]\n@router.get(\"/ids\")\nasync def get_dataset_ids(\n env_id: str,\n search: Optional[str] = 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: {str(e)}\")\n# [/DEF:get_dataset_ids:Function]\n\n# [DEF:get_datasets:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: env_id (str) - The environment ID to fetch datasets from\n# @PARAM: search (Optional[str]) - Filter by table name\n# @PARAM: page (Optional[int]) - Page number (default: 1)\n# @PARAM: page_size (Optional[int]) - Items per page (default: 10, max: 100)\n# @RETURN: DatasetsResponse - List of datasets with status metadata\n# @RELATION: CALLS ->[get_datasets_with_status]\n@router.get(\"\", response_model=DatasetsResponse)\nasync def get_datasets(\n env_id: str,\n search: Optional[str] = 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: {str(e)}\")\n# [/DEF:get_datasets:Function]\n\n# [DEF:MapColumnsRequest:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: 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: Optional[str] = Field(None, description=\"Connection ID for PostgreSQL source\")\n file_data: Optional[str] = Field(None, description=\"File path or data for XLSX source\")\n# [/DEF:MapColumnsRequest:DataClass]\n\n# [DEF:map_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: request (MapColumnsRequest) - Mapping request with environment and dataset IDs\n# @RETURN: TaskResponse - Task ID for tracking\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: {str(e)}\")\n# [/DEF:map_columns:Function]\n\n# [DEF:GenerateDocsRequest:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: 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: Optional[dict] = Field(None, description=\"Additional options for documentation generation\")\n# [/DEF:GenerateDocsRequest:DataClass]\n\n# [DEF:generate_docs:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: request (GenerateDocsRequest) - Documentation generation request\n# @RETURN: TaskResponse - Task ID for tracking\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: {str(e)}\")\n# [/DEF:generate_docs:Function]\n\n# [DEF:get_dataset_detail:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: env_id (str) - The environment ID\n# @PARAM: dataset_id (int) - The dataset ID\n# @RETURN: DatasetDetailResponse - Detailed dataset information\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: {str(e)}\")\n# [/DEF:get_dataset_detail:Function]\n\n# [/DEF:DatasetsApi:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "MappedFields", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 24, - "end_line": 30, + "start_line": 21, + "end_line": 26, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for dataset mapping progress statistics" }, "relations": [], @@ -17557,19 +15816,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MappedFields:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for dataset mapping progress statistics\nclass MappedFields(BaseModel):\n total: int\n mapped: int\n# [/DEF:MappedFields:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "LastTask", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 32, - "end_line": 38, + "start_line": 28, + "end_line": 33, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for the most recent task associated with a dataset" }, "relations": [], @@ -17628,19 +15887,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LastTask:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for the most recent task associated with a dataset\nclass LastTask(BaseModel):\n task_id: Optional[str] = None\n status: Optional[str] = Field(None, pattern=\"^RUNNING|SUCCESS|ERROR|WAITING_INPUT$\")\n# [/DEF:LastTask:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DatasetItem", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 40, - "end_line": 53, + "start_line": 35, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Summary DTO for a dataset in the hub listing" }, "relations": [], @@ -17699,19 +15958,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetItem:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: 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: Optional[MappedFields] = None\n last_task: Optional[LastTask] = None\n\n class Config:\n allow_population_by_field_name = True\n# [/DEF:DatasetItem:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DatasetItem [C:1] [TYPE DataClass]\n# @BRIEF Summary DTO for a dataset in the hub listing\nclass DatasetItem(BaseModel):\n id: int\n table_name: str\n schema_name: str = Field(..., alias=\"schema\")\n database: str\n mapped_fields: MappedFields | None = None\n last_task: LastTask | None = None\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetItem\n" }, { "contract_id": "LinkedDashboard", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 55, - "end_line": 62, + "start_line": 49, + "end_line": 55, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for a dashboard linked to a dataset" }, "relations": [], @@ -17770,19 +16029,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LinkedDashboard:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for a dashboard linked to a dataset\nclass LinkedDashboard(BaseModel):\n id: int\n title: str\n slug: Optional[str] = None\n# [/DEF:LinkedDashboard:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DatasetColumn", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 64, - "end_line": 74, + "start_line": 57, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "DTO for a single dataset column's metadata" }, "relations": [], @@ -17841,19 +16100,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetColumn:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: DTO for a single dataset column's metadata\nclass DatasetColumn(BaseModel):\n id: int\n name: str\n type: Optional[str] = None\n is_dttm: bool = False\n is_active: bool = True\n description: Optional[str] = None\n# [/DEF:DatasetColumn:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DatasetDetailResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 76, - "end_line": 96, + "start_line": 68, + "end_line": 87, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Detailed DTO for a dataset including columns and links" }, "relations": [], @@ -17912,19 +16171,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetDetailResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Detailed DTO for a dataset including columns and links\nclass DatasetDetailResponse(BaseModel):\n id: int\n table_name: Optional[str] = None\n schema_name: Optional[str] = Field(None, alias=\"schema\")\n database: str\n description: Optional[str] = None\n columns: List[DatasetColumn]\n column_count: int\n sql: Optional[str] = None\n linked_dashboards: List[LinkedDashboard]\n linked_dashboard_count: int\n is_sqllab_view: bool = False\n created_on: Optional[str] = None\n changed_on: Optional[str] = None\n\n class Config:\n allow_population_by_field_name = True\n# [/DEF:DatasetDetailResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DatasetsResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 98, - "end_line": 107, + "start_line": 89, + "end_line": 97, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Paginated response DTO for dataset listings" }, "relations": [], @@ -17983,19 +16242,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetsResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:DatasetsResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "TaskResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 109, - "end_line": 114, + "start_line": 99, + "end_line": 103, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Response DTO containing a task ID for tracking" }, "relations": [], @@ -18054,24 +16313,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskResponse:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Response DTO containing a task ID for tracking\nclass TaskResponse(BaseModel):\n task_id: str\n# [/DEF:TaskResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_dataset_ids", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 116, - "end_line": 166, + "start_line": 105, + "end_line": 151, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "search (Optional[str]) - Filter by table name", + "COMPLEXITY": 3, "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)", - "RETURN": "List[int] - List of dataset IDs" + "PURPOSE": "Fetch list of all dataset IDs from a specific environment (without pagination)" }, "relations": [ { @@ -18082,12 +16339,6 @@ } ], "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", @@ -18105,32 +16356,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:get_dataset_ids:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: env_id (str) - The environment ID to fetch datasets from\n# @PARAM: search (Optional[str]) - Filter by table name\n# @RETURN: List[int] - List of dataset IDs\n# @RELATION: CALLS ->[get_datasets_with_status]\n@router.get(\"/ids\")\nasync def get_dataset_ids(\n env_id: str,\n search: Optional[str] = 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: {str(e)}\")\n# [/DEF:get_dataset_ids:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "get_datasets", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 168, - "end_line": 246, + "start_line": 153, + "end_line": 225, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "page_size (Optional[int]) - Items per page (default: 10, max: 100)", + "COMPLEXITY": 3, "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", - "RETURN": "DatasetsResponse - List of datasets with status metadata" + "PURPOSE": "Fetch list of datasets from a specific environment with mapping progress" }, "relations": [ { @@ -18141,12 +16384,6 @@ } ], "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", @@ -18164,27 +16401,21 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:get_datasets:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: env_id (str) - The environment ID to fetch datasets from\n# @PARAM: search (Optional[str]) - Filter by table name\n# @PARAM: page (Optional[int]) - Page number (default: 1)\n# @PARAM: page_size (Optional[int]) - Items per page (default: 10, max: 100)\n# @RETURN: DatasetsResponse - List of datasets with status metadata\n# @RELATION: CALLS ->[get_datasets_with_status]\n@router.get(\"\", response_model=DatasetsResponse)\nasync def get_datasets(\n env_id: str,\n search: Optional[str] = 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: {str(e)}\")\n# [/DEF:get_datasets:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "MapColumnsRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 248, - "end_line": 257, + "start_line": 227, + "end_line": 235, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for initiating column mapping" }, "relations": [], @@ -18243,24 +16474,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MapColumnsRequest:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: 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: Optional[str] = Field(None, description=\"Connection ID for PostgreSQL source\")\n file_data: Optional[str] = Field(None, description=\"File path or data for XLSX source\")\n# [/DEF:MapColumnsRequest:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "map_columns", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 259, - "end_line": 319, + "start_line": 237, + "end_line": 294, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "request (MapColumnsRequest) - Mapping request with environment and dataset IDs", + "COMPLEXITY": 3, "POST": "Task is created and queued for execution", "PRE": "dataset_ids is a non-empty list", - "PURPOSE": "Trigger bulk column mapping for datasets", - "RETURN": "TaskResponse - Task ID for tracking" + "PURPOSE": "Trigger bulk column mapping for datasets" }, "relations": [ { @@ -18277,12 +16506,6 @@ } ], "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", @@ -18300,27 +16523,21 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:map_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: request (MapColumnsRequest) - Mapping request with environment and dataset IDs\n# @RETURN: TaskResponse - Task ID for tracking\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: {str(e)}\")\n# [/DEF:map_columns:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GenerateDocsRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 321, - "end_line": 329, + "start_line": 296, + "end_line": 303, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Request DTO for initiating documentation generation" }, "relations": [], @@ -18379,24 +16596,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GenerateDocsRequest:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: 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: Optional[dict] = Field(None, description=\"Additional options for documentation generation\")\n# [/DEF:GenerateDocsRequest:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "generate_docs", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 331, - "end_line": 385, + "start_line": 305, + "end_line": 356, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "request (GenerateDocsRequest) - Documentation generation request", + "COMPLEXITY": 3, "POST": "Task is created and queued for execution", "PRE": "dataset_ids is a non-empty list", - "PURPOSE": "Trigger bulk documentation generation for datasets", - "RETURN": "TaskResponse - Task ID for tracking" + "PURPOSE": "Trigger bulk documentation generation for datasets" }, "relations": [ { @@ -18413,12 +16628,6 @@ } ], "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", @@ -18436,32 +16645,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:generate_docs:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: request (GenerateDocsRequest) - Documentation generation request\n# @RETURN: TaskResponse - Task ID for tracking\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: {str(e)}\")\n# [/DEF:generate_docs:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "get_dataset_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 387, - "end_line": 424, + "start_line": 358, + "end_line": 391, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "dataset_id (int) - The dataset ID", + "COMPLEXITY": 3, "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", - "RETURN": "DatasetDetailResponse - Detailed dataset information" + "PURPOSE": "Get detailed dataset information including columns and linked dashboards" }, "relations": [ { @@ -18472,12 +16673,6 @@ } ], "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", @@ -18495,36 +16690,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:get_dataset_detail:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: env_id (str) - The environment ID\n# @PARAM: dataset_id (int) - The dataset ID\n# @RETURN: DatasetDetailResponse - Detailed dataset information\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: {str(e)}\")\n# [/DEF:get_dataset_detail:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "EnvironmentsApi", "contract_type": "Module", "file_path": "backend/src/api/routes/environments.py", "start_line": 1, - "end_line": 159, + "end_line": 148, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Environment IDs must exist in the configuration.", "LAYER": "API", - "PURPOSE": "API endpoints for listing environments and their databases.", - "SEMANTICS": [ - "api", - "environments", - "superset", - "databases" - ] + "PURPOSE": "API endpoints for listing environments and their databases." }, "relations": [ { @@ -18551,15 +16734,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:EnvironmentsApi:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: api, environments, superset, databases\n# @PURPOSE: 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# [SECTION: IMPORTS]\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom typing import List, Optional\nfrom ...dependencies import get_config_manager, get_scheduler_service, has_permission\nfrom ...core.superset_client import SupersetClient\nfrom pydantic import BaseModel, Field\nfrom ...core.logger import belief_scope\n# [/SECTION]\n\nrouter = APIRouter(prefix=\"/api/environments\", tags=[\"Environments\"])\n\n\n# [DEF:_normalize_superset_env_url:Function]\n# @PURPOSE: 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# [/DEF:_normalize_superset_env_url:Function]\n\n# [DEF:ScheduleSchema: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# [/DEF:ScheduleSchema:DataClass]\n\n# [DEF:EnvironmentResponse: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: Optional[ScheduleSchema] = None\n# [/DEF:EnvironmentResponse:DataClass]\n\n# [DEF:DatabaseResponse:DataClass]\nclass DatabaseResponse(BaseModel):\n uuid: str\n database_name: str\n engine: Optional[str]\n# [/DEF:DatabaseResponse:DataClass]\n\n# [DEF:get_environments:Function]\n# @PURPOSE: List all configured environments.\n# @LAYER: API\n# @SEMANTICS: list, environments, config\n# @PRE: config_manager is injected via Depends.\n# @POST: Returns a list of EnvironmentResponse objects.\n# @RETURN: List[EnvironmentResponse]\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# [/DEF:get_environments:Function]\n\n# [DEF:update_environment_schedule:Function]\n# @PURPOSE: Update backup schedule for an environment.\n# @LAYER: API\n# @SEMANTICS: update, schedule, backup, environment\n# @PRE: Environment id exists, schedule is valid ScheduleSchema.\n# @POST: Backup schedule updated and scheduler reloaded.\n# @PARAM: id (str) - The environment ID.\n# @PARAM: schedule (ScheduleSchema) - The new schedule.\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# [/DEF:update_environment_schedule:Function]\n\n# [DEF:get_environment_databases:Function]\n# @PURPOSE: Fetch the list of databases from a specific environment.\n# @LAYER: API\n# @SEMANTICS: fetch, databases, superset, environment\n# @PRE: Environment id exists.\n# @POST: Returns a list of database summaries from the environment.\n# @PARAM: id (str) - The environment ID.\n# @RETURN: List[Dict] - List of databases.\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: {str(e)}\")\n# [/DEF:get_environment_databases:Function]\n\n# [/DEF:EnvironmentsApi:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_normalize_superset_env_url", "contract_type": "Function", "file_path": "backend/src/api/routes/environments.py", - "start_line": 24, - "end_line": 33, + "start_line": 21, + "end_line": 30, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -18597,71 +16780,64 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_normalize_superset_env_url:Function]\n# @PURPOSE: 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# [/DEF:_normalize_superset_env_url:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ScheduleSchema", "contract_type": "DataClass", "file_path": "backend/src/api/routes/environments.py", - "start_line": 35, - "end_line": 39, + "start_line": 32, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ScheduleSchema: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# [/DEF:ScheduleSchema:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "EnvironmentResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/environments.py", - "start_line": 41, - "end_line": 49, + "start_line": 38, + "end_line": 46, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:EnvironmentResponse: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: Optional[ScheduleSchema] = None\n# [/DEF:EnvironmentResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DatabaseResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/environments.py", - "start_line": 51, - "end_line": 56, + "start_line": 48, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DatabaseResponse:DataClass]\nclass DatabaseResponse(BaseModel):\n uuid: str\n database_name: str\n engine: Optional[str]\n# [/DEF:DatabaseResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region DatabaseResponse [TYPE DataClass]\nclass DatabaseResponse(BaseModel):\n uuid: str\n database_name: str\n engine: str | None\n# #endregion DatabaseResponse\n" }, { "contract_id": "update_environment_schedule", "contract_type": "Function", "file_path": "backend/src/api/routes/environments.py", - "start_line": 97, - "end_line": 129, + "start_line": 92, + "end_line": 121, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "API", - "PARAM": "schedule (ScheduleSchema) - The new schedule.", "POST": "Backup schedule updated and scheduler reloaded.", "PRE": "Environment id exists, schedule is valid ScheduleSchema.", - "PURPOSE": "Update backup schedule for an environment.", - "SEMANTICS": [ - "update", - "schedule", - "backup", - "environment" - ] + "PURPOSE": "Update backup schedule for an environment." }, "relations": [], "schema_warnings": [ @@ -18687,12 +16863,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -18719,55 +16889,24 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "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" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_environment_schedule:Function]\n# @PURPOSE: Update backup schedule for an environment.\n# @LAYER: API\n# @SEMANTICS: update, schedule, backup, environment\n# @PRE: Environment id exists, schedule is valid ScheduleSchema.\n# @POST: Backup schedule updated and scheduler reloaded.\n# @PARAM: id (str) - The environment ID.\n# @PARAM: schedule (ScheduleSchema) - The new schedule.\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# [/DEF:update_environment_schedule:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_environment_databases", "contract_type": "Function", "file_path": "backend/src/api/routes/environments.py", - "start_line": 131, - "end_line": 157, + "start_line": 123, + "end_line": 146, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "API", - "PARAM": "id (str) - The environment ID.", "POST": "Returns a list of database summaries from the environment.", "PRE": "Environment id exists.", - "PURPOSE": "Fetch the list of databases from a specific environment.", - "RETURN": "List[Dict] - List of databases.", - "SEMANTICS": [ - "fetch", - "databases", - "superset", - "environment" - ] + "PURPOSE": "Fetch the list of databases from a specific environment." }, "relations": [], "schema_warnings": [ @@ -18793,12 +16932,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -18825,57 +16958,23 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "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" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_environment_databases:Function]\n# @PURPOSE: Fetch the list of databases from a specific environment.\n# @LAYER: API\n# @SEMANTICS: fetch, databases, superset, environment\n# @PRE: Environment id exists.\n# @POST: Returns a list of database summaries from the environment.\n# @PARAM: id (str) - The environment ID.\n# @RETURN: List[Dict] - List of databases.\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: {str(e)}\")\n# [/DEF:get_environment_databases:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "GitPackage", "contract_type": "Module", "file_path": "backend/src/api/routes/git/__init__.py", "start_line": 1, - "end_line": 44, + "end_line": 42, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "API", - "PURPOSE": "Package root for decomposed git routes. Re-exports all public symbols from submodules.", - "SEMANTICS": [ - "git", - "package", - "re-exports" - ] + "PURPOSE": "Package root for decomposed git routes. Re-exports all public symbols from submodules." }, "relations": [ { @@ -18905,136 +17004,125 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitPackage:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Package root for decomposed git routes. Re-exports all public symbols from submodules.\n# @LAYER: API\n# @SEMANTICS: git, package, re-exports\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 ._router import router\nfrom ._deps import MAX_REPOSITORY_STATUS_BATCH\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 get_git_configs, create_git_config, update_git_config, delete_git_config, test_git_config # noqa: E501, F401\n\n# -- Gitea routes --\nfrom ._gitea_routes import list_gitea_repositories, create_gitea_repository, delete_gitea_repository, create_remote_repository # noqa: E501, F401\n\n# -- Repo routes (core) --\nfrom ._repo_routes import init_repository, get_repository_binding, delete_repository, get_branches, create_branch, checkout_branch # noqa: E501, F401\nfrom ._helpers import _resolve_dashboard_id_from_ref, _resolve_dashboard_id_from_ref_async, _resolve_repo_key_from_ref # noqa: E501, F401 — re-exported for test monkeypatch compatibility\n\n# -- Repo operations routes (commit, push, pull, status, diff, history, generate-message) --\nfrom ._repo_operations_routes import commit_changes, push_changes, pull_changes, get_repository_status, get_repository_status_batch, get_repository_diff, get_history, generate_commit_message # noqa: E501, F401\n\n# -- Repo lifecycle routes (sync, promote, deploy) --\nfrom ._repo_lifecycle_routes import sync_dashboard, promote_dashboard, deploy_dashboard # noqa: E501, F401\n\n# -- Merge routes --\nfrom ._merge_routes import get_merge_status, get_merge_conflicts, resolve_merge_conflicts, abort_merge, continue_merge # noqa: E501, F401\n\n# -- Environment routes --\nfrom ._environment_routes import get_environments # noqa: E501, F401\n\n# [/DEF:GitPackage:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GitConfigRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_config_routes.py", "start_line": 1, - "end_line": 159, + "end_line": 149, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "API", - "PURPOSE": "FastAPI endpoints for Git server configuration CRUD and connection testing.", - "SEMANTICS": [ - "git", - "config", - "routes", - "crud" - ] + "PURPOSE": "FastAPI endpoints for Git server configuration CRUD and connection testing." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:GitConfigRoutes:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: FastAPI endpoints for Git server configuration CRUD and connection testing.\n# @LAYER: API\n# @SEMANTICS: git, config, routes, crud\n\nfrom typing import List\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import has_permission\nfrom src.models.git import GitServerConfig\n\nfrom src.api.routes.git_schemas import (\n GitServerConfigCreate,\n GitServerConfigSchema,\n GitServerConfigUpdate,\n)\n\nfrom ._router import router\nfrom ._helpers import _get_git_config_or_404\nfrom ._deps import get_git_service\n\n\n# [DEF:get_git_configs:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List all configured Git servers.\n@router.get(\"/config\", response_model=List[GitServerConfigSchema])\nasync def get_git_configs(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"get_git_configs\"):\n configs = db.query(GitServerConfig).all()\n result = []\n for config in configs:\n schema = GitServerConfigSchema.from_orm(config)\n schema.pat = \"********\"\n result.append(schema)\n return result\n# [/DEF:get_git_configs:Function]\n\n\n# [DEF:create_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Register a new Git server configuration.\n@router.post(\"/config\", response_model=GitServerConfigSchema)\nasync def create_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_git_config\"):\n config_dict = config.dict(exclude={\"config_id\"})\n db_config = GitServerConfig(**config_dict)\n db.add(db_config)\n db.commit()\n db.refresh(db_config)\n return db_config\n# [/DEF:create_git_config:Function]\n\n\n# [DEF:update_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Update an existing Git server configuration.\n@router.put(\"/config/{config_id}\", response_model=GitServerConfigSchema)\nasync def update_git_config(\n config_id: str,\n config_update: GitServerConfigUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n update_data = config_update.dict(exclude_unset=True)\n if update_data.get(\"pat\") == \"********\":\n update_data.pop(\"pat\")\n\n for key, value in update_data.items():\n setattr(db_config, key, value)\n\n db.commit()\n db.refresh(db_config)\n\n result_schema = GitServerConfigSchema.from_orm(db_config)\n result_schema.pat = \"********\"\n return result_schema\n# [/DEF:update_git_config:Function]\n\n\n# [DEF:delete_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Remove a Git server configuration.\n@router.delete(\"/config/{config_id}\")\nasync def delete_git_config(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n db.delete(db_config)\n db.commit()\n return {\"status\": \"success\", \"message\": \"Configuration deleted\"}\n# [/DEF:delete_git_config:Function]\n\n\n# [DEF:test_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Validate connection to a Git server using provided credentials.\n@router.post(\"/config/test\")\nasync def test_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"test_git_config\"):\n _git_service = get_git_service()\n pat_to_use = config.pat\n if pat_to_use == \"********\":\n if config.config_id:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == config.config_id)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n else:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.url == config.url)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n\n success = await _git_service.test_connection(\n config.provider, config.url, pat_to_use\n )\n if success:\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n else:\n raise HTTPException(status_code=400, detail=\"Connection failed\")\n# [/DEF:test_git_config:Function]\n# [/DEF:GitConfigRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitConfigRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, api, search, connection]\n# @BRIEF FastAPI endpoints for Git server configuration CRUD and connection testing.\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.git_schemas import (\n GitServerConfigCreate,\n GitServerConfigSchema,\n GitServerConfigUpdate,\n)\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import has_permission\nfrom src.models.git import GitServerConfig\n\nfrom ._deps import get_git_service\nfrom ._router import router\n\n\n# #region get_git_configs [C:2] [TYPE Function]\n# @BRIEF List all configured Git servers.\n@router.get(\"/config\", response_model=list[GitServerConfigSchema])\nasync def get_git_configs(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"get_git_configs\"):\n configs = db.query(GitServerConfig).all()\n result = []\n for config in configs:\n schema = GitServerConfigSchema.from_orm(config)\n schema.pat = \"********\"\n result.append(schema)\n return result\n# #endregion get_git_configs\n\n\n# #region create_git_config [C:2] [TYPE Function]\n# @BRIEF Register a new Git server configuration.\n@router.post(\"/config\", response_model=GitServerConfigSchema)\nasync def create_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_git_config\"):\n config_dict = config.dict(exclude={\"config_id\"})\n db_config = GitServerConfig(**config_dict)\n db.add(db_config)\n db.commit()\n db.refresh(db_config)\n return db_config\n# #endregion create_git_config\n\n\n# #region update_git_config [C:2] [TYPE Function]\n# @BRIEF Update an existing Git server configuration.\n@router.put(\"/config/{config_id}\", response_model=GitServerConfigSchema)\nasync def update_git_config(\n config_id: str,\n config_update: GitServerConfigUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n update_data = config_update.dict(exclude_unset=True)\n if update_data.get(\"pat\") == \"********\":\n update_data.pop(\"pat\")\n\n for key, value in update_data.items():\n setattr(db_config, key, value)\n\n db.commit()\n db.refresh(db_config)\n\n result_schema = GitServerConfigSchema.from_orm(db_config)\n result_schema.pat = \"********\"\n return result_schema\n# #endregion update_git_config\n\n\n# #region delete_git_config [C:2] [TYPE Function]\n# @BRIEF Remove a Git server configuration.\n@router.delete(\"/config/{config_id}\")\nasync def delete_git_config(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n db.delete(db_config)\n db.commit()\n return {\"status\": \"success\", \"message\": \"Configuration deleted\"}\n# #endregion delete_git_config\n\n\n# #region test_git_config [C:2] [TYPE Function]\n# @BRIEF Validate connection to a Git server using provided credentials.\n@router.post(\"/config/test\")\nasync def test_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"test_git_config\"):\n _git_service = get_git_service()\n pat_to_use = config.pat\n if pat_to_use == \"********\":\n if config.config_id:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == config.config_id)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n else:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.url == config.url)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n\n success = await _git_service.test_connection(\n config.provider, config.url, pat_to_use\n )\n if success:\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n else:\n raise HTTPException(status_code=400, detail=\"Connection failed\")\n# #endregion test_git_config\n# #endregion GitConfigRoutes\n" }, { "contract_id": "get_git_configs", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_config_routes.py", - "start_line": 28, - "end_line": 44, + "start_line": 23, + "end_line": 38, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "List all configured Git servers." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_git_configs:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List all configured Git servers.\n@router.get(\"/config\", response_model=List[GitServerConfigSchema])\nasync def get_git_configs(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"get_git_configs\"):\n configs = db.query(GitServerConfig).all()\n result = []\n for config in configs:\n schema = GitServerConfigSchema.from_orm(config)\n schema.pat = \"********\"\n result.append(schema)\n return result\n# [/DEF:get_git_configs:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_git_configs [C:2] [TYPE Function]\n# @BRIEF List all configured Git servers.\n@router.get(\"/config\", response_model=list[GitServerConfigSchema])\nasync def get_git_configs(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"get_git_configs\"):\n configs = db.query(GitServerConfig).all()\n result = []\n for config in configs:\n schema = GitServerConfigSchema.from_orm(config)\n schema.pat = \"********\"\n result.append(schema)\n return result\n# #endregion get_git_configs\n" }, { "contract_id": "create_git_config", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_config_routes.py", - "start_line": 47, - "end_line": 63, + "start_line": 41, + "end_line": 56, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Register a new Git server configuration." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:create_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Register a new Git server configuration.\n@router.post(\"/config\", response_model=GitServerConfigSchema)\nasync def create_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_git_config\"):\n config_dict = config.dict(exclude={\"config_id\"})\n db_config = GitServerConfig(**config_dict)\n db.add(db_config)\n db.commit()\n db.refresh(db_config)\n return db_config\n# [/DEF:create_git_config:Function]\n" + "anchor_syntax": "region", + "body": "# #region create_git_config [C:2] [TYPE Function]\n# @BRIEF Register a new Git server configuration.\n@router.post(\"/config\", response_model=GitServerConfigSchema)\nasync def create_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_git_config\"):\n config_dict = config.dict(exclude={\"config_id\"})\n db_config = GitServerConfig(**config_dict)\n db.add(db_config)\n db.commit()\n db.refresh(db_config)\n return db_config\n# #endregion create_git_config\n" }, { "contract_id": "update_git_config", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_config_routes.py", - "start_line": 66, - "end_line": 96, + "start_line": 59, + "end_line": 88, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Update an existing Git server configuration." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:update_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Update an existing Git server configuration.\n@router.put(\"/config/{config_id}\", response_model=GitServerConfigSchema)\nasync def update_git_config(\n config_id: str,\n config_update: GitServerConfigUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n update_data = config_update.dict(exclude_unset=True)\n if update_data.get(\"pat\") == \"********\":\n update_data.pop(\"pat\")\n\n for key, value in update_data.items():\n setattr(db_config, key, value)\n\n db.commit()\n db.refresh(db_config)\n\n result_schema = GitServerConfigSchema.from_orm(db_config)\n result_schema.pat = \"********\"\n return result_schema\n# [/DEF:update_git_config:Function]\n" + "anchor_syntax": "region", + "body": "# #region update_git_config [C:2] [TYPE Function]\n# @BRIEF Update an existing Git server configuration.\n@router.put(\"/config/{config_id}\", response_model=GitServerConfigSchema)\nasync def update_git_config(\n config_id: str,\n config_update: GitServerConfigUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n update_data = config_update.dict(exclude_unset=True)\n if update_data.get(\"pat\") == \"********\":\n update_data.pop(\"pat\")\n\n for key, value in update_data.items():\n setattr(db_config, key, value)\n\n db.commit()\n db.refresh(db_config)\n\n result_schema = GitServerConfigSchema.from_orm(db_config)\n result_schema.pat = \"********\"\n return result_schema\n# #endregion update_git_config\n" }, { "contract_id": "delete_git_config", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_config_routes.py", - "start_line": 99, - "end_line": 118, + "start_line": 91, + "end_line": 109, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Remove a Git server configuration." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:delete_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Remove a Git server configuration.\n@router.delete(\"/config/{config_id}\")\nasync def delete_git_config(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n db.delete(db_config)\n db.commit()\n return {\"status\": \"success\", \"message\": \"Configuration deleted\"}\n# [/DEF:delete_git_config:Function]\n" + "anchor_syntax": "region", + "body": "# #region delete_git_config [C:2] [TYPE Function]\n# @BRIEF Remove a Git server configuration.\n@router.delete(\"/config/{config_id}\")\nasync def delete_git_config(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_git_config\"):\n db_config = (\n db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n )\n if not db_config:\n raise HTTPException(status_code=404, detail=\"Configuration not found\")\n\n db.delete(db_config)\n db.commit()\n return {\"status\": \"success\", \"message\": \"Configuration deleted\"}\n# #endregion delete_git_config\n" }, { "contract_id": "test_git_config", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_config_routes.py", - "start_line": 121, - "end_line": 158, + "start_line": 112, + "end_line": 148, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Validate connection to a Git server using provided credentials." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_git_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Validate connection to a Git server using provided credentials.\n@router.post(\"/config/test\")\nasync def test_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"test_git_config\"):\n _git_service = get_git_service()\n pat_to_use = config.pat\n if pat_to_use == \"********\":\n if config.config_id:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == config.config_id)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n else:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.url == config.url)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n\n success = await _git_service.test_connection(\n config.provider, config.url, pat_to_use\n )\n if success:\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n else:\n raise HTTPException(status_code=400, detail=\"Connection failed\")\n# [/DEF:test_git_config:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_git_config [C:2] [TYPE Function]\n# @BRIEF Validate connection to a Git server using provided credentials.\n@router.post(\"/config/test\")\nasync def test_git_config(\n config: GitServerConfigCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n with belief_scope(\"test_git_config\"):\n _git_service = get_git_service()\n pat_to_use = config.pat\n if pat_to_use == \"********\":\n if config.config_id:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == config.config_id)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n else:\n db_config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.url == config.url)\n .first()\n )\n if db_config:\n pat_to_use = db_config.pat\n\n success = await _git_service.test_connection(\n config.provider, config.url, pat_to_use\n )\n if success:\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n else:\n raise HTTPException(status_code=400, detail=\"Connection failed\")\n# #endregion test_git_config\n" }, { "contract_id": "GitDeps", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_deps.py", "start_line": 1, - "end_line": 24, + "end_line": 22, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "INVARIANT": "get_git_service() resolves from sys.modules at call time so test monkeypatching", "LAYER": "API", - "PURPOSE": "Shared dependency wiring for monkeypatch-safe git_service access and constants.", - "SEMANTICS": [ - "git", - "deps", - "service-locator" - ] + "PURPOSE": "Shared dependency wiring for monkeypatch-safe git_service access and constants." }, "relations": [], "schema_warnings": [ @@ -19057,142 +17145,125 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitDeps:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: Shared dependency wiring for monkeypatch-safe git_service access and constants.\n# @LAYER: API\n# @SEMANTICS: git, deps, service-locator\n# @INVARIANT: get_git_service() resolves from sys.modules at call time so test monkeypatching\n# of git_routes.git_service (the __init__ attribute) takes effect across all submodules.\n\nimport sys\n\n# Batch limit for repository status queries\nMAX_REPOSITORY_STATUS_BATCH: int = 50\n\n\ndef get_git_service():\n \"\"\"Resolve git_service from the git package root at call time.\n\n This indirection ensures that test monkeypatching via\n ``monkeypatch.setattr(git_routes, 'git_service', mock)``\n is visible to every submodule function, because each call\n re-resolves ``sys.modules['src.api.routes.git'].git_service``.\n \"\"\"\n return sys.modules[\"src.api.routes.git\"].git_service\n# [/DEF:GitDeps:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitDeps [C:1] [TYPE Module] [SEMANTICS git, dependency, service, provider]\n# @BRIEF Shared dependency wiring for monkeypatch-safe git_service access and constants.\n# @LAYER: API\n# @INVARIANT: get_git_service() resolves from sys.modules at call time so test monkeypatching\n# of git_routes.git_service (the __init__ attribute) takes effect across all submodules.\n\nimport sys\n\n# Batch limit for repository status queries\nMAX_REPOSITORY_STATUS_BATCH: int = 50\n\n\ndef get_git_service():\n \"\"\"Resolve git_service from the git package root at call time.\n\n This indirection ensures that test monkeypatching via\n ``monkeypatch.setattr(git_routes, 'git_service', mock)``\n is visible to every submodule function, because each call\n re-resolves ``sys.modules['src.api.routes.git'].git_service``.\n \"\"\"\n return sys.modules[\"src.api.routes.git\"].git_service\n# #endregion GitDeps\n" }, { "contract_id": "GitEnvironmentRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_environment_routes.py", "start_line": 1, - "end_line": 36, + "end_line": 31, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "API", - "PURPOSE": "FastAPI endpoint for listing deployment environments.", - "SEMANTICS": [ - "git", - "environments", - "routes" - ] + "PURPOSE": "FastAPI endpoint for listing deployment environments." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:GitEnvironmentRoutes:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: FastAPI endpoint for listing deployment environments.\n# @LAYER: API\n# @SEMANTICS: git, environments, routes\n\nfrom typing import List\n\nfrom fastapi import Depends\n\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_config_manager, has_permission\n\nfrom src.api.routes.git_schemas import DeploymentEnvironmentSchema\n\nfrom ._router import router\n\n\n# [DEF:get_environments:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List all deployment environments.\n@router.get(\"/environments\", response_model=List[DeploymentEnvironmentSchema])\nasync def get_environments(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"environments\", \"READ\")),\n):\n with belief_scope(\"get_environments\"):\n envs = config_manager.get_environments()\n return [\n DeploymentEnvironmentSchema(\n id=e.id, name=e.name, superset_url=e.url, is_active=True\n )\n for e in envs\n ]\n# [/DEF:get_environments:Function]\n# [/DEF:GitEnvironmentRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitEnvironmentRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, environment, api]\n# @BRIEF FastAPI endpoint for listing deployment environments.\n# @LAYER: API\n\n\nfrom fastapi import Depends\n\nfrom src.api.routes.git_schemas import DeploymentEnvironmentSchema\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_config_manager, has_permission\n\nfrom ._router import router\n\n\n# #region get_environments [C:2] [TYPE Function]\n# @BRIEF List all deployment environments.\n@router.get(\"/environments\", response_model=list[DeploymentEnvironmentSchema])\nasync def get_environments(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"environments\", \"READ\")),\n):\n with belief_scope(\"get_environments\"):\n envs = config_manager.get_environments()\n return [\n DeploymentEnvironmentSchema(\n id=e.id, name=e.name, superset_url=e.url, is_active=True\n )\n for e in envs\n ]\n# #endregion get_environments\n# #endregion GitEnvironmentRoutes\n" }, { "contract_id": "GitGiteaRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_gitea_routes.py", "start_line": 1, - "end_line": 188, + "end_line": 180, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "API", - "PURPOSE": "FastAPI endpoints for Gitea-specific repository operations.", - "SEMANTICS": [ - "git", - "gitea", - "routes", - "repositories" - ] + "PURPOSE": "FastAPI endpoints for Gitea-specific repository operations." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:GitGiteaRoutes:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: FastAPI endpoints for Gitea-specific repository operations.\n# @LAYER: API\n# @SEMANTICS: git, gitea, routes, repositories\n\nfrom typing import List\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import has_permission\nfrom src.models.git import GitProvider\n\nfrom src.api.routes.git_schemas import (\n GiteaRepoCreateRequest,\n GiteaRepoSchema,\n RemoteRepoCreateRequest,\n RemoteRepoSchema,\n)\n\nfrom ._router import router\nfrom ._helpers import _get_git_config_or_404\nfrom ._deps import get_git_service\n\n\n# [DEF:list_gitea_repositories:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List repositories in Gitea for a saved Gitea config.\n@router.get(\"/config/{config_id}/gitea/repos\", response_model=List[GiteaRepoSchema])\nasync def list_gitea_repositories(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n _gs = get_git_service()\n with belief_scope(\"list_gitea_repositories\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repos = await _gs.list_gitea_repositories(config.url, config.pat)\n return [\n GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n for repo in repos\n ]\n# [/DEF:list_gitea_repositories:Function]\n\n\n# [DEF:create_gitea_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create a repository in Gitea for a saved Gitea config.\n@router.post(\"/config/{config_id}/gitea/repos\", response_model=GiteaRepoSchema)\nasync def create_gitea_repository(\n config_id: str,\n request: GiteaRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n return GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# [/DEF:create_gitea_repository:Function]\n\n\n# [DEF:delete_gitea_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Delete repository in Gitea for a saved Gitea config.\n@router.delete(\"/config/{config_id}/gitea/repos/{owner}/{repo_name}\")\nasync def delete_gitea_repository(\n config_id: str,\n owner: str,\n repo_name: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"delete_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n await _gs.delete_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n owner=owner,\n repo_name=repo_name,\n )\n return {\"status\": \"success\", \"message\": \"Repository deleted\"}\n# [/DEF:delete_gitea_repository:Function]\n\n\n# [DEF:create_remote_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create repository on remote Git server using selected provider config.\n@router.post(\"/config/{config_id}/repositories\", response_model=RemoteRepoSchema)\nasync def create_remote_repository(\n config_id: str,\n request: RemoteRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_remote_repository\"):\n config = _get_git_config_or_404(db, config_id)\n\n if config.provider == GitProvider.GITEA:\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITHUB:\n repo = await _gs.create_github_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITLAB:\n repo = await _gs.create_gitlab_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} is not supported\",\n )\n\n return RemoteRepoSchema(\n provider=config.provider,\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", repo.get(\"name\", \"\")),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# [/DEF:create_remote_repository:Function]\n# [/DEF:GitGiteaRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitGiteaRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, gitea, api]\n# @BRIEF FastAPI endpoints for Gitea-specific repository operations.\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.git_schemas import (\n GiteaRepoCreateRequest,\n GiteaRepoSchema,\n RemoteRepoCreateRequest,\n RemoteRepoSchema,\n)\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import has_permission\nfrom src.models.git import GitProvider\n\nfrom ._deps import get_git_service\nfrom ._helpers import _get_git_config_or_404\nfrom ._router import router\n\n\n# #region list_gitea_repositories [C:2] [TYPE Function]\n# @BRIEF List repositories in Gitea for a saved Gitea config.\n@router.get(\"/config/{config_id}/gitea/repos\", response_model=list[GiteaRepoSchema])\nasync def list_gitea_repositories(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n _gs = get_git_service()\n with belief_scope(\"list_gitea_repositories\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repos = await _gs.list_gitea_repositories(config.url, config.pat)\n return [\n GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n for repo in repos\n ]\n# #endregion list_gitea_repositories\n\n\n# #region create_gitea_repository [C:2] [TYPE Function]\n# @BRIEF Create a repository in Gitea for a saved Gitea config.\n@router.post(\"/config/{config_id}/gitea/repos\", response_model=GiteaRepoSchema)\nasync def create_gitea_repository(\n config_id: str,\n request: GiteaRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n return GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# #endregion create_gitea_repository\n\n\n# #region delete_gitea_repository [C:2] [TYPE Function]\n# @BRIEF Delete repository in Gitea for a saved Gitea config.\n@router.delete(\"/config/{config_id}/gitea/repos/{owner}/{repo_name}\")\nasync def delete_gitea_repository(\n config_id: str,\n owner: str,\n repo_name: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"delete_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n await _gs.delete_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n owner=owner,\n repo_name=repo_name,\n )\n return {\"status\": \"success\", \"message\": \"Repository deleted\"}\n# #endregion delete_gitea_repository\n\n\n# #region create_remote_repository [C:2] [TYPE Function]\n# @BRIEF Create repository on remote Git server using selected provider config.\n@router.post(\"/config/{config_id}/repositories\", response_model=RemoteRepoSchema)\nasync def create_remote_repository(\n config_id: str,\n request: RemoteRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_remote_repository\"):\n config = _get_git_config_or_404(db, config_id)\n\n if config.provider == GitProvider.GITEA:\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITHUB:\n repo = await _gs.create_github_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITLAB:\n repo = await _gs.create_gitlab_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} is not supported\",\n )\n\n return RemoteRepoSchema(\n provider=config.provider,\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", repo.get(\"name\", \"\")),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# #endregion create_remote_repository\n# #endregion GitGiteaRoutes\n" }, { "contract_id": "list_gitea_repositories", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_gitea_routes.py", - "start_line": 29, - "end_line": 58, + "start_line": 25, + "end_line": 53, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "List repositories in Gitea for a saved Gitea config." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:list_gitea_repositories:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List repositories in Gitea for a saved Gitea config.\n@router.get(\"/config/{config_id}/gitea/repos\", response_model=List[GiteaRepoSchema])\nasync def list_gitea_repositories(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n _gs = get_git_service()\n with belief_scope(\"list_gitea_repositories\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repos = await _gs.list_gitea_repositories(config.url, config.pat)\n return [\n GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n for repo in repos\n ]\n# [/DEF:list_gitea_repositories:Function]\n" + "anchor_syntax": "region", + "body": "# #region list_gitea_repositories [C:2] [TYPE Function]\n# @BRIEF List repositories in Gitea for a saved Gitea config.\n@router.get(\"/config/{config_id}/gitea/repos\", response_model=list[GiteaRepoSchema])\nasync def list_gitea_repositories(\n config_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"git_config\", \"READ\")),\n):\n _gs = get_git_service()\n with belief_scope(\"list_gitea_repositories\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repos = await _gs.list_gitea_repositories(config.url, config.pat)\n return [\n GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n for repo in repos\n ]\n# #endregion list_gitea_repositories\n" }, { "contract_id": "create_gitea_repository", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_gitea_routes.py", - "start_line": 61, - "end_line": 96, + "start_line": 56, + "end_line": 90, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Create a repository in Gitea for a saved Gitea config." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:create_gitea_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create a repository in Gitea for a saved Gitea config.\n@router.post(\"/config/{config_id}/gitea/repos\", response_model=GiteaRepoSchema)\nasync def create_gitea_repository(\n config_id: str,\n request: GiteaRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n return GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# [/DEF:create_gitea_repository:Function]\n" + "anchor_syntax": "region", + "body": "# #region create_gitea_repository [C:2] [TYPE Function]\n# @BRIEF Create a repository in Gitea for a saved Gitea config.\n@router.post(\"/config/{config_id}/gitea/repos\", response_model=GiteaRepoSchema)\nasync def create_gitea_repository(\n config_id: str,\n request: GiteaRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n return GiteaRepoSchema(\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", \"\"),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# #endregion create_gitea_repository\n" }, { "contract_id": "delete_gitea_repository", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_gitea_routes.py", - "start_line": 99, - "end_line": 124, + "start_line": 93, + "end_line": 117, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Delete repository in Gitea for a saved Gitea config." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:delete_gitea_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Delete repository in Gitea for a saved Gitea config.\n@router.delete(\"/config/{config_id}/gitea/repos/{owner}/{repo_name}\")\nasync def delete_gitea_repository(\n config_id: str,\n owner: str,\n repo_name: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"delete_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n await _gs.delete_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n owner=owner,\n repo_name=repo_name,\n )\n return {\"status\": \"success\", \"message\": \"Repository deleted\"}\n# [/DEF:delete_gitea_repository:Function]\n" + "anchor_syntax": "region", + "body": "# #region delete_gitea_repository [C:2] [TYPE Function]\n# @BRIEF Delete repository in Gitea for a saved Gitea config.\n@router.delete(\"/config/{config_id}/gitea/repos/{owner}/{repo_name}\")\nasync def delete_gitea_repository(\n config_id: str,\n owner: str,\n repo_name: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"delete_gitea_repository\"):\n config = _get_git_config_or_404(db, config_id)\n if config.provider != GitProvider.GITEA:\n raise HTTPException(\n status_code=400, detail=\"This endpoint supports GITEA provider only\"\n )\n await _gs.delete_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n owner=owner,\n repo_name=repo_name,\n )\n return {\"status\": \"success\", \"message\": \"Repository deleted\"}\n# #endregion delete_gitea_repository\n" }, { "contract_id": "create_remote_repository", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_gitea_routes.py", - "start_line": 127, - "end_line": 187, + "start_line": 120, + "end_line": 179, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Create repository on remote Git server using selected provider config." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:create_remote_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create repository on remote Git server using selected provider config.\n@router.post(\"/config/{config_id}/repositories\", response_model=RemoteRepoSchema)\nasync def create_remote_repository(\n config_id: str,\n request: RemoteRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_remote_repository\"):\n config = _get_git_config_or_404(db, config_id)\n\n if config.provider == GitProvider.GITEA:\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITHUB:\n repo = await _gs.create_github_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITLAB:\n repo = await _gs.create_gitlab_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} is not supported\",\n )\n\n return RemoteRepoSchema(\n provider=config.provider,\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", repo.get(\"name\", \"\")),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# [/DEF:create_remote_repository:Function]\n" + "anchor_syntax": "region", + "body": "# #region create_remote_repository [C:2] [TYPE Function]\n# @BRIEF Create repository on remote Git server using selected provider config.\n@router.post(\"/config/{config_id}/repositories\", response_model=RemoteRepoSchema)\nasync def create_remote_repository(\n config_id: str,\n request: RemoteRepoCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"create_remote_repository\"):\n config = _get_git_config_or_404(db, config_id)\n\n if config.provider == GitProvider.GITEA:\n repo = await _gs.create_gitea_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITHUB:\n repo = await _gs.create_github_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n elif config.provider == GitProvider.GITLAB:\n repo = await _gs.create_gitlab_repository(\n server_url=config.url,\n pat=config.pat,\n name=request.name,\n private=request.private,\n description=request.description,\n auto_init=request.auto_init,\n default_branch=request.default_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} is not supported\",\n )\n\n return RemoteRepoSchema(\n provider=config.provider,\n name=repo.get(\"name\", \"\"),\n full_name=repo.get(\"full_name\", repo.get(\"name\", \"\")),\n private=bool(repo.get(\"private\", False)),\n clone_url=repo.get(\"clone_url\"),\n html_url=repo.get(\"html_url\"),\n ssh_url=repo.get(\"ssh_url\"),\n default_branch=repo.get(\"default_branch\"),\n )\n# #endregion create_remote_repository\n" }, { "contract_id": "GitHelpers", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_helpers.py", "start_line": 1, - "end_line": 343, + "end_line": 330, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "API", - "PURPOSE": "Shared helper functions for Git route modules.", - "SEMANTICS": [ - "git", - "helpers", - "resolution", - "identity" - ] + "PURPOSE": "Shared helper functions for Git route modules." }, "relations": [ { @@ -19270,19 +17341,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitHelpers:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Shared helper functions for Git route modules.\n# @LAYER: API\n# @SEMANTICS: git, helpers, resolution, identity\n# @RELATION USES -> [GitDeps]\n# @RELATION USES -> [SupersetClient]\n# @RELATION USES -> [UserDashboardPreference]\n\nimport os\nfrom typing import Optional\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"GitHelpers\")\nfrom src.core.superset_client import SupersetClient\nfrom src.dependencies import get_config_manager\nfrom src.models.auth import User\nfrom src.models.git import GitServerConfig\nfrom src.models.profile import UserDashboardPreference\n\nfrom ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH\n\n\n# [DEF:_build_no_repo_status_payload:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build a consistent status payload for dashboards without initialized repositories.\n# @POST: Returns a stable payload compatible with frontend repository status parsing.\ndef _build_no_repo_status_payload() -> dict:\n return {\n \"is_dirty\": False,\n \"untracked_files\": [],\n \"modified_files\": [],\n \"staged_files\": [],\n \"current_branch\": None,\n \"upstream_branch\": None,\n \"has_upstream\": False,\n \"ahead_count\": 0,\n \"behind_count\": 0,\n \"is_diverged\": False,\n \"sync_state\": \"NO_REPO\",\n \"sync_status\": \"NO_REPO\",\n \"has_repo\": False,\n }\n# [/DEF:_build_no_repo_status_payload:Function]\n\n\n# [DEF:_handle_unexpected_git_route_error:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert unexpected route-level exceptions to stable 500 API responses.\n# @PRE: `error` is a non-HTTPException instance.\n# @POST: Raises HTTPException(500) with route-specific context.\ndef _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:\n log.explore(f\"{route_name} failed: {error}\", error=str(error))\n raise HTTPException(status_code=500, detail=f\"{route_name} failed: {str(error)}\")\n# [/DEF:_handle_unexpected_git_route_error:Function]\n\n\n# [DEF:_resolve_repository_status:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve repository status for one dashboard with graceful NO_REPO semantics.\n# @PRE: `dashboard_id` is a valid integer.\n# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent.\ndef _resolve_repository_status(dashboard_id: int) -> dict:\n git_service = get_git_service()\n repo_path = git_service._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n log.reason(\n f\"Repository is not initialized for dashboard {dashboard_id}\"\n )\n return _build_no_repo_status_payload()\n\n try:\n return git_service.get_status(dashboard_id)\n except HTTPException as e:\n if e.status_code == 404:\n log.reason(\n f\"Repository is not initialized for dashboard {dashboard_id}\"\n )\n return _build_no_repo_status_payload()\n raise\n# [/DEF:_resolve_repository_status:Function]\n\n\n# [DEF:_get_git_config_or_404:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve GitServerConfig by id or raise 404.\ndef _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig:\n config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n if not config:\n raise HTTPException(status_code=404, detail=\"Git configuration not found\")\n return config\n# [/DEF:_get_git_config_or_404:Function]\n\n\n# [DEF:_find_dashboard_id_by_slug:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard numeric ID by slug in a specific environment.\ndef _find_dashboard_id_by_slug(\n client: SupersetClient,\n dashboard_slug: str,\n) -> Optional[int]:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = client.get_dashboards_page(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n return None\n# [/DEF:_find_dashboard_id_by_slug:Function]\n\n\n# [DEF:_resolve_dashboard_id_from_ref:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard ID from slug-or-id reference for Git routes.\ndef _resolve_dashboard_id_from_ref(\n dashboard_ref: str,\n config_manager,\n env_id: Optional[str] = None,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=400, detail=\"dashboard_ref is required\")\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n if not env_id:\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required for slug-based Git operations\",\n )\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n dashboard_id = _find_dashboard_id_by_slug(SupersetClient(env), normalized_ref)\n if dashboard_id is None:\n raise HTTPException(\n status_code=404, detail=f\"Dashboard slug '{normalized_ref}' not found\"\n )\n return dashboard_id\n# [/DEF:_resolve_dashboard_id_from_ref:Function]\n\n\n# [DEF:_find_dashboard_id_by_slug_async:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard numeric ID by slug asynchronously for hot-path Git routes.\nasync def _find_dashboard_id_by_slug_async(\n client: \"AsyncSupersetClient\",\n dashboard_slug: str,\n) -> Optional[int]:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = await client.get_dashboards_page_async(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n return None\n# [/DEF:_find_dashboard_id_by_slug_async:Function]\n\n\n# [DEF:_resolve_dashboard_id_from_ref_async:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve dashboard ID asynchronously from slug-or-id reference for hot Git routes.\nasync def _resolve_dashboard_id_from_ref_async(\n dashboard_ref: str,\n config_manager,\n env_id: Optional[str] = None,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=400, detail=\"dashboard_ref is required\")\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n if not env_id:\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required for slug-based Git operations\",\n )\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n from src.core.async_superset_client import AsyncSupersetClient\n\n client = AsyncSupersetClient(env)\n try:\n dashboard_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)\n if dashboard_id is None:\n raise HTTPException(\n status_code=404, detail=f\"Dashboard slug '{normalized_ref}' not found\"\n )\n return dashboard_id\n finally:\n await client.aclose()\n# [/DEF:_resolve_dashboard_id_from_ref_async:Function]\n\n\n# [DEF:_resolve_repo_key_from_ref:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve repository folder key with slug-first strategy and deterministic fallback.\ndef _resolve_repo_key_from_ref(\n dashboard_ref: str,\n dashboard_id: int,\n config_manager,\n env_id: Optional[str] = None,\n) -> str:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if normalized_ref and not normalized_ref.isdigit():\n return normalized_ref\n\n if env_id:\n try:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if env:\n payload = SupersetClient(env).get_dashboard(dashboard_id)\n dashboard_data = (\n payload.get(\"result\", payload) if isinstance(payload, dict) else {}\n )\n dashboard_slug = dashboard_data.get(\"slug\")\n if dashboard_slug:\n return str(dashboard_slug)\n except Exception:\n pass\n\n return f\"dashboard-{dashboard_id}\"\n# [/DEF:_resolve_repo_key_from_ref:Function]\n\n\n# [DEF:_sanitize_optional_identity_value:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Normalize optional identity value into trimmed string or None.\ndef _sanitize_optional_identity_value(value: Optional[str]) -> Optional[str]:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n# [/DEF:_sanitize_optional_identity_value:Function]\n\n\n# [DEF:_resolve_current_user_git_identity:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve configured Git username/email from current user's profile preferences.\ndef _resolve_current_user_git_identity(\n db: Session,\n current_user: Optional[User],\n) -> Optional[tuple[str, str]]:\n if db is None or not hasattr(db, \"query\"):\n return None\n\n user_id = _sanitize_optional_identity_value(getattr(current_user, \"id\", None))\n if not user_id:\n return None\n\n try:\n preference = (\n db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n except Exception as resolve_error:\n log.explore(f\"Failed to load profile preference for user {user_id}: {resolve_error}\", error=str(resolve_error))\n return None\n\n if not preference:\n return None\n\n git_username = _sanitize_optional_identity_value(\n getattr(preference, \"git_username\", None)\n )\n git_email = _sanitize_optional_identity_value(\n getattr(preference, \"git_email\", None)\n )\n if not git_username or not git_email:\n return None\n return git_username, git_email\n# [/DEF:_resolve_current_user_git_identity:Function]\n\n\n# [DEF:_apply_git_identity_from_profile:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Apply user-scoped Git identity to repository-local config before write/pull operations.\ndef _apply_git_identity_from_profile(\n dashboard_id: int,\n db: Session,\n current_user: Optional[User],\n) -> None:\n identity = _resolve_current_user_git_identity(db, current_user)\n if not identity:\n return\n\n git_service = get_git_service()\n configure_identity = getattr(git_service, \"configure_identity\", None)\n if not callable(configure_identity):\n return\n\n git_username, git_email = identity\n configure_identity(dashboard_id, git_username, git_email)\n# [/DEF:_apply_git_identity_from_profile:Function]\n# [/DEF:GitHelpers:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitHelpers [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]\n# @BRIEF Shared helper functions for Git route modules.\n# @LAYER: API\n# @RELATION USES -> [GitDeps]\n# @RELATION USES -> [SupersetClient]\n# @RELATION USES -> [UserDashboardPreference]\n\nimport os\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import logger\nfrom src.core.superset_client import SupersetClient\nfrom src.models.auth import User\nfrom src.models.git import GitServerConfig\nfrom src.models.profile import UserDashboardPreference\n\nfrom ._deps import get_git_service\n\n\n# #region _build_no_repo_status_payload [C:1] [TYPE Function]\n# @BRIEF Build a consistent status payload for dashboards without initialized repositories.\n# @POST: Returns a stable payload compatible with frontend repository status parsing.\ndef _build_no_repo_status_payload() -> dict:\n return {\n \"is_dirty\": False,\n \"untracked_files\": [],\n \"modified_files\": [],\n \"staged_files\": [],\n \"current_branch\": None,\n \"upstream_branch\": None,\n \"has_upstream\": False,\n \"ahead_count\": 0,\n \"behind_count\": 0,\n \"is_diverged\": False,\n \"sync_state\": \"NO_REPO\",\n \"sync_status\": \"NO_REPO\",\n \"has_repo\": False,\n }\n# #endregion _build_no_repo_status_payload\n\n\n# #region _handle_unexpected_git_route_error [C:1] [TYPE Function]\n# @BRIEF Convert unexpected route-level exceptions to stable 500 API responses.\n# @PRE: `error` is a non-HTTPException instance.\n# @POST: Raises HTTPException(500) with route-specific context.\ndef _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:\n logger.error(f\"[{route_name}][Coherence:Failed] {error}\")\n raise HTTPException(status_code=500, detail=f\"{route_name} failed: {error!s}\")\n# #endregion _handle_unexpected_git_route_error\n\n\n# #region _resolve_repository_status [C:2] [TYPE Function]\n# @BRIEF Resolve repository status for one dashboard with graceful NO_REPO semantics.\n# @PRE: `dashboard_id` is a valid integer.\n# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent.\ndef _resolve_repository_status(dashboard_id: int) -> dict:\n git_service = get_git_service()\n repo_path = git_service._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n logger.reason(\n f\"Repository is not initialized for dashboard {dashboard_id}\",\n extra={\"src\": \"get_repository_status\"},\n )\n return _build_no_repo_status_payload()\n\n try:\n return git_service.get_status(dashboard_id)\n except HTTPException as e:\n if e.status_code == 404:\n logger.reason(\n f\"Repository is not initialized for dashboard {dashboard_id}\",\n extra={\"src\": \"get_repository_status\"},\n )\n return _build_no_repo_status_payload()\n raise\n# #endregion _resolve_repository_status\n\n\n# #region _get_git_config_or_404 [C:2] [TYPE Function]\n# @BRIEF Resolve GitServerConfig by id or raise 404.\ndef _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig:\n config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n if not config:\n raise HTTPException(status_code=404, detail=\"Git configuration not found\")\n return config\n# #endregion _get_git_config_or_404\n\n\n# #region _find_dashboard_id_by_slug [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard numeric ID by slug in a specific environment.\ndef _find_dashboard_id_by_slug(\n client: SupersetClient,\n dashboard_slug: str,\n) -> int | None:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = client.get_dashboards_page(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n return None\n# #endregion _find_dashboard_id_by_slug\n\n\n# #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard ID from slug-or-id reference for Git routes.\ndef _resolve_dashboard_id_from_ref(\n dashboard_ref: str,\n config_manager,\n env_id: str | None = None,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=400, detail=\"dashboard_ref is required\")\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n if not env_id:\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required for slug-based Git operations\",\n )\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n dashboard_id = _find_dashboard_id_by_slug(SupersetClient(env), normalized_ref)\n if dashboard_id is None:\n raise HTTPException(\n status_code=404, detail=f\"Dashboard slug '{normalized_ref}' not found\"\n )\n return dashboard_id\n# #endregion _resolve_dashboard_id_from_ref\n\n\n# #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard numeric ID by slug asynchronously for hot-path Git routes.\nasync def _find_dashboard_id_by_slug_async(\n client: \"AsyncSupersetClient\",\n dashboard_slug: str,\n) -> int | None:\n query_variants = [\n {\n \"filters\": [{\"col\": \"slug\", \"opr\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n {\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dashboard_slug}],\n \"page\": 0,\n \"page_size\": 1,\n },\n ]\n\n for query in query_variants:\n try:\n _count, dashboards = await client.get_dashboards_page_async(query=query)\n if dashboards:\n resolved_id = dashboards[0].get(\"id\")\n if resolved_id is not None:\n return int(resolved_id)\n except Exception:\n continue\n return None\n# #endregion _find_dashboard_id_by_slug_async\n\n\n# #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard ID asynchronously from slug-or-id reference for hot Git routes.\nasync def _resolve_dashboard_id_from_ref_async(\n dashboard_ref: str,\n config_manager,\n env_id: str | None = None,\n) -> int:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if not normalized_ref:\n raise HTTPException(status_code=400, detail=\"dashboard_ref is required\")\n\n if normalized_ref.isdigit():\n return int(normalized_ref)\n\n if not env_id:\n raise HTTPException(\n status_code=400,\n detail=\"env_id is required for slug-based Git operations\",\n )\n\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if not env:\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n from src.core.async_superset_client import AsyncSupersetClient\n\n client = AsyncSupersetClient(env)\n try:\n dashboard_id = await _find_dashboard_id_by_slug_async(client, normalized_ref)\n if dashboard_id is None:\n raise HTTPException(\n status_code=404, detail=f\"Dashboard slug '{normalized_ref}' not found\"\n )\n return dashboard_id\n finally:\n await client.aclose()\n# #endregion _resolve_dashboard_id_from_ref_async\n\n\n# #region _resolve_repo_key_from_ref [C:2] [TYPE Function]\n# @BRIEF Resolve repository folder key with slug-first strategy and deterministic fallback.\ndef _resolve_repo_key_from_ref(\n dashboard_ref: str,\n dashboard_id: int,\n config_manager,\n env_id: str | None = None,\n) -> str:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if normalized_ref and not normalized_ref.isdigit():\n return normalized_ref\n\n if env_id:\n try:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if env:\n payload = SupersetClient(env).get_dashboard(dashboard_id)\n dashboard_data = (\n payload.get(\"result\", payload) if isinstance(payload, dict) else {}\n )\n dashboard_slug = dashboard_data.get(\"slug\")\n if dashboard_slug:\n return str(dashboard_slug)\n except Exception:\n pass\n\n return f\"dashboard-{dashboard_id}\"\n# #endregion _resolve_repo_key_from_ref\n\n\n# #region _sanitize_optional_identity_value [C:1] [TYPE Function]\n# @BRIEF Normalize optional identity value into trimmed string or None.\ndef _sanitize_optional_identity_value(value: str | None) -> str | None:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n# #endregion _sanitize_optional_identity_value\n\n\n# #region _resolve_current_user_git_identity [C:2] [TYPE Function]\n# @BRIEF Resolve configured Git username/email from current user's profile preferences.\ndef _resolve_current_user_git_identity(\n db: Session,\n current_user: User | None,\n) -> tuple[str, str] | None:\n if db is None or not hasattr(db, \"query\"):\n return None\n\n user_id = _sanitize_optional_identity_value(getattr(current_user, \"id\", None))\n if not user_id:\n return None\n\n try:\n preference = (\n db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n except Exception as resolve_error:\n logger.explore(\n \"Failed to load profile preference for resolving git identity\",\n extra={\"src\": \"_resolve_current_user_git_identity\", \"payload\": {\"user_id\": user_id}, \"error\": str(resolve_error)},\n )\n return None\n\n if not preference:\n return None\n\n git_username = _sanitize_optional_identity_value(\n getattr(preference, \"git_username\", None)\n )\n git_email = _sanitize_optional_identity_value(\n getattr(preference, \"git_email\", None)\n )\n if not git_username or not git_email:\n return None\n return git_username, git_email\n# #endregion _resolve_current_user_git_identity\n\n\n# #region _apply_git_identity_from_profile [C:2] [TYPE Function]\n# @BRIEF Apply user-scoped Git identity to repository-local config before write/pull operations.\ndef _apply_git_identity_from_profile(\n dashboard_id: int,\n db: Session,\n current_user: User | None,\n) -> None:\n identity = _resolve_current_user_git_identity(db, current_user)\n if not identity:\n return\n\n git_service = get_git_service()\n configure_identity = getattr(git_service, \"configure_identity\", None)\n if not callable(configure_identity):\n return\n\n git_username, git_email = identity\n configure_identity(dashboard_id, git_username, git_email)\n# #endregion _apply_git_identity_from_profile\n# #endregion GitHelpers\n" }, { "contract_id": "_build_no_repo_status_payload", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_helpers.py", - "start_line": 28, - "end_line": 48, + "start_line": 22, + "end_line": 41, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns a stable payload compatible with frontend repository status parsing.", "PURPOSE": "Build a consistent status payload for dashboards without initialized repositories." }, @@ -19307,19 +17378,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_build_no_repo_status_payload:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build a consistent status payload for dashboards without initialized repositories.\n# @POST: Returns a stable payload compatible with frontend repository status parsing.\ndef _build_no_repo_status_payload() -> dict:\n return {\n \"is_dirty\": False,\n \"untracked_files\": [],\n \"modified_files\": [],\n \"staged_files\": [],\n \"current_branch\": None,\n \"upstream_branch\": None,\n \"has_upstream\": False,\n \"ahead_count\": 0,\n \"behind_count\": 0,\n \"is_diverged\": False,\n \"sync_state\": \"NO_REPO\",\n \"sync_status\": \"NO_REPO\",\n \"has_repo\": False,\n }\n# [/DEF:_build_no_repo_status_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region _build_no_repo_status_payload [C:1] [TYPE Function]\n# @BRIEF Build a consistent status payload for dashboards without initialized repositories.\n# @POST: Returns a stable payload compatible with frontend repository status parsing.\ndef _build_no_repo_status_payload() -> dict:\n return {\n \"is_dirty\": False,\n \"untracked_files\": [],\n \"modified_files\": [],\n \"staged_files\": [],\n \"current_branch\": None,\n \"upstream_branch\": None,\n \"has_upstream\": False,\n \"ahead_count\": 0,\n \"behind_count\": 0,\n \"is_diverged\": False,\n \"sync_state\": \"NO_REPO\",\n \"sync_status\": \"NO_REPO\",\n \"has_repo\": False,\n }\n# #endregion _build_no_repo_status_payload\n" }, { "contract_id": "_handle_unexpected_git_route_error", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_helpers.py", - "start_line": 51, - "end_line": 59, + "start_line": 44, + "end_line": 51, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Raises HTTPException(500) with route-specific context.", "PRE": "`error` is a non-HTTPException instance.", "PURPOSE": "Convert unexpected route-level exceptions to stable 500 API responses." @@ -19354,19 +17425,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_handle_unexpected_git_route_error:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Convert unexpected route-level exceptions to stable 500 API responses.\n# @PRE: `error` is a non-HTTPException instance.\n# @POST: Raises HTTPException(500) with route-specific context.\ndef _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:\n log.explore(f\"{route_name} failed: {error}\", error=str(error))\n raise HTTPException(status_code=500, detail=f\"{route_name} failed: {str(error)}\")\n# [/DEF:_handle_unexpected_git_route_error:Function]\n" + "anchor_syntax": "region", + "body": "# #region _handle_unexpected_git_route_error [C:1] [TYPE Function]\n# @BRIEF Convert unexpected route-level exceptions to stable 500 API responses.\n# @PRE: `error` is a non-HTTPException instance.\n# @POST: Raises HTTPException(500) with route-specific context.\ndef _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:\n logger.error(f\"[{route_name}][Coherence:Failed] {error}\")\n raise HTTPException(status_code=500, detail=f\"{route_name} failed: {error!s}\")\n# #endregion _handle_unexpected_git_route_error\n" }, { "contract_id": "_resolve_repository_status", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_helpers.py", - "start_line": 62, - "end_line": 85, + "start_line": 54, + "end_line": 78, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns standard status payload or `NO_REPO` payload when repository path is absent.", "PRE": "`dashboard_id` is a valid integer.", "PURPOSE": "Resolve repository status for one dashboard with graceful NO_REPO semantics." @@ -19392,53 +17463,53 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_repository_status:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve repository status for one dashboard with graceful NO_REPO semantics.\n# @PRE: `dashboard_id` is a valid integer.\n# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent.\ndef _resolve_repository_status(dashboard_id: int) -> dict:\n git_service = get_git_service()\n repo_path = git_service._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n log.reason(\n f\"Repository is not initialized for dashboard {dashboard_id}\"\n )\n return _build_no_repo_status_payload()\n\n try:\n return git_service.get_status(dashboard_id)\n except HTTPException as e:\n if e.status_code == 404:\n log.reason(\n f\"Repository is not initialized for dashboard {dashboard_id}\"\n )\n return _build_no_repo_status_payload()\n raise\n# [/DEF:_resolve_repository_status:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_repository_status [C:2] [TYPE Function]\n# @BRIEF Resolve repository status for one dashboard with graceful NO_REPO semantics.\n# @PRE: `dashboard_id` is a valid integer.\n# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent.\ndef _resolve_repository_status(dashboard_id: int) -> dict:\n git_service = get_git_service()\n repo_path = git_service._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n logger.reason(\n f\"Repository is not initialized for dashboard {dashboard_id}\",\n extra={\"src\": \"get_repository_status\"},\n )\n return _build_no_repo_status_payload()\n\n try:\n return git_service.get_status(dashboard_id)\n except HTTPException as e:\n if e.status_code == 404:\n logger.reason(\n f\"Repository is not initialized for dashboard {dashboard_id}\",\n extra={\"src\": \"get_repository_status\"},\n )\n return _build_no_repo_status_payload()\n raise\n# #endregion _resolve_repository_status\n" }, { "contract_id": "_get_git_config_or_404", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_helpers.py", - "start_line": 88, - "end_line": 96, + "start_line": 81, + "end_line": 88, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Resolve GitServerConfig by id or raise 404." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_get_git_config_or_404:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve GitServerConfig by id or raise 404.\ndef _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig:\n config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n if not config:\n raise HTTPException(status_code=404, detail=\"Git configuration not found\")\n return config\n# [/DEF:_get_git_config_or_404:Function]\n" + "anchor_syntax": "region", + "body": "# #region _get_git_config_or_404 [C:2] [TYPE Function]\n# @BRIEF Resolve GitServerConfig by id or raise 404.\ndef _get_git_config_or_404(db: Session, config_id: str) -> GitServerConfig:\n config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first()\n if not config:\n raise HTTPException(status_code=404, detail=\"Git configuration not found\")\n return config\n# #endregion _get_git_config_or_404\n" }, { "contract_id": "_resolve_repo_key_from_ref", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_helpers.py", - "start_line": 241, - "end_line": 270, + "start_line": 229, + "end_line": 257, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Resolve repository folder key with slug-first strategy and deterministic fallback." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_repo_key_from_ref:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve repository folder key with slug-first strategy and deterministic fallback.\ndef _resolve_repo_key_from_ref(\n dashboard_ref: str,\n dashboard_id: int,\n config_manager,\n env_id: Optional[str] = None,\n) -> str:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if normalized_ref and not normalized_ref.isdigit():\n return normalized_ref\n\n if env_id:\n try:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if env:\n payload = SupersetClient(env).get_dashboard(dashboard_id)\n dashboard_data = (\n payload.get(\"result\", payload) if isinstance(payload, dict) else {}\n )\n dashboard_slug = dashboard_data.get(\"slug\")\n if dashboard_slug:\n return str(dashboard_slug)\n except Exception:\n pass\n\n return f\"dashboard-{dashboard_id}\"\n# [/DEF:_resolve_repo_key_from_ref:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_repo_key_from_ref [C:2] [TYPE Function]\n# @BRIEF Resolve repository folder key with slug-first strategy and deterministic fallback.\ndef _resolve_repo_key_from_ref(\n dashboard_ref: str,\n dashboard_id: int,\n config_manager,\n env_id: str | None = None,\n) -> str:\n normalized_ref = str(dashboard_ref or \"\").strip()\n if normalized_ref and not normalized_ref.isdigit():\n return normalized_ref\n\n if env_id:\n try:\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n if env:\n payload = SupersetClient(env).get_dashboard(dashboard_id)\n dashboard_data = (\n payload.get(\"result\", payload) if isinstance(payload, dict) else {}\n )\n dashboard_slug = dashboard_data.get(\"slug\")\n if dashboard_slug:\n return str(dashboard_slug)\n except Exception:\n pass\n\n return f\"dashboard-{dashboard_id}\"\n# #endregion _resolve_repo_key_from_ref\n" }, { "contract_id": "_sanitize_optional_identity_value", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_helpers.py", - "start_line": 273, - "end_line": 281, + "start_line": 260, + "end_line": 267, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Normalize optional identity value into trimmed string or None." }, "relations": [], @@ -19453,61 +17524,55 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_sanitize_optional_identity_value:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Normalize optional identity value into trimmed string or None.\ndef _sanitize_optional_identity_value(value: Optional[str]) -> Optional[str]:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n# [/DEF:_sanitize_optional_identity_value:Function]\n" + "anchor_syntax": "region", + "body": "# #region _sanitize_optional_identity_value [C:1] [TYPE Function]\n# @BRIEF Normalize optional identity value into trimmed string or None.\ndef _sanitize_optional_identity_value(value: str | None) -> str | None:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n# #endregion _sanitize_optional_identity_value\n" }, { "contract_id": "_resolve_current_user_git_identity", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_helpers.py", - "start_line": 284, - "end_line": 320, + "start_line": 270, + "end_line": 308, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Resolve configured Git username/email from current user's profile preferences." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_current_user_git_identity:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve configured Git username/email from current user's profile preferences.\ndef _resolve_current_user_git_identity(\n db: Session,\n current_user: Optional[User],\n) -> Optional[tuple[str, str]]:\n if db is None or not hasattr(db, \"query\"):\n return None\n\n user_id = _sanitize_optional_identity_value(getattr(current_user, \"id\", None))\n if not user_id:\n return None\n\n try:\n preference = (\n db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n except Exception as resolve_error:\n log.explore(f\"Failed to load profile preference for user {user_id}: {resolve_error}\", error=str(resolve_error))\n return None\n\n if not preference:\n return None\n\n git_username = _sanitize_optional_identity_value(\n getattr(preference, \"git_username\", None)\n )\n git_email = _sanitize_optional_identity_value(\n getattr(preference, \"git_email\", None)\n )\n if not git_username or not git_email:\n return None\n return git_username, git_email\n# [/DEF:_resolve_current_user_git_identity:Function]\n" + "anchor_syntax": "region", + "body": "# #region _resolve_current_user_git_identity [C:2] [TYPE Function]\n# @BRIEF Resolve configured Git username/email from current user's profile preferences.\ndef _resolve_current_user_git_identity(\n db: Session,\n current_user: User | None,\n) -> tuple[str, str] | None:\n if db is None or not hasattr(db, \"query\"):\n return None\n\n user_id = _sanitize_optional_identity_value(getattr(current_user, \"id\", None))\n if not user_id:\n return None\n\n try:\n preference = (\n db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n except Exception as resolve_error:\n logger.explore(\n \"Failed to load profile preference for resolving git identity\",\n extra={\"src\": \"_resolve_current_user_git_identity\", \"payload\": {\"user_id\": user_id}, \"error\": str(resolve_error)},\n )\n return None\n\n if not preference:\n return None\n\n git_username = _sanitize_optional_identity_value(\n getattr(preference, \"git_username\", None)\n )\n git_email = _sanitize_optional_identity_value(\n getattr(preference, \"git_email\", None)\n )\n if not git_username or not git_email:\n return None\n return git_username, git_email\n# #endregion _resolve_current_user_git_identity\n" }, { "contract_id": "_apply_git_identity_from_profile", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_helpers.py", - "start_line": 323, - "end_line": 342, + "start_line": 311, + "end_line": 329, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Apply user-scoped Git identity to repository-local config before write/pull operations." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_apply_git_identity_from_profile:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Apply user-scoped Git identity to repository-local config before write/pull operations.\ndef _apply_git_identity_from_profile(\n dashboard_id: int,\n db: Session,\n current_user: Optional[User],\n) -> None:\n identity = _resolve_current_user_git_identity(db, current_user)\n if not identity:\n return\n\n git_service = get_git_service()\n configure_identity = getattr(git_service, \"configure_identity\", None)\n if not callable(configure_identity):\n return\n\n git_username, git_email = identity\n configure_identity(dashboard_id, git_username, git_email)\n# [/DEF:_apply_git_identity_from_profile:Function]\n" + "anchor_syntax": "region", + "body": "# #region _apply_git_identity_from_profile [C:2] [TYPE Function]\n# @BRIEF Apply user-scoped Git identity to repository-local config before write/pull operations.\ndef _apply_git_identity_from_profile(\n dashboard_id: int,\n db: Session,\n current_user: User | None,\n) -> None:\n identity = _resolve_current_user_git_identity(db, current_user)\n if not identity:\n return\n\n git_service = get_git_service()\n configure_identity = getattr(git_service, \"configure_identity\", None)\n if not callable(configure_identity):\n return\n\n git_username, git_email = identity\n configure_identity(dashboard_id, git_username, git_email)\n# #endregion _apply_git_identity_from_profile\n" }, { "contract_id": "GitMergeRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_merge_routes.py", "start_line": 1, - "end_line": 169, + "end_line": 160, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "API", - "PURPOSE": "FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).", - "SEMANTICS": [ - "git", - "merge", - "routes", - "conflicts" - ] + "PURPOSE": "FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue)." }, "relations": [], "schema_warnings": [ @@ -19521,53 +17586,53 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitMergeRoutes:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).\n# @LAYER: API\n# @SEMANTICS: git, merge, routes, conflicts\n\nfrom typing import List, Optional\n\nfrom fastapi import Depends, HTTPException\n\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_config_manager, has_permission\n\nfrom src.api.routes.git_schemas import (\n MergeConflictFileSchema,\n MergeContinueRequest,\n MergeResolveRequest,\n MergeStatusSchema,\n)\n\nfrom ._router import router\nfrom ._helpers import (\n _handle_unexpected_git_route_error,\n)\nfrom ._deps import get_git_service\n\n\n# [DEF:get_merge_status:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Return unfinished-merge status for repository (web-only recovery support).\n@router.get(\n \"/repositories/{dashboard_ref}/merge/status\", response_model=MergeStatusSchema\n)\nasync def get_merge_status(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_status\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_status\", e)\n# [/DEF:get_merge_status:Function]\n\n\n# [DEF:get_merge_conflicts:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Return conflicted files with mine/theirs previews for web conflict resolver.\n@router.get(\n \"/repositories/{dashboard_ref}/merge/conflicts\",\n response_model=List[MergeConflictFileSchema],\n)\nasync def get_merge_conflicts(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_conflicts(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_conflicts\", e)\n# [/DEF:get_merge_conflicts:Function]\n\n\n# [DEF:resolve_merge_conflicts:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Apply mine/theirs/manual conflict resolutions from WebUI and stage files.\n# @RELATION: CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/resolve\")\nasync def resolve_merge_conflicts(\n dashboard_ref: str,\n resolve_data: MergeResolveRequest,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"resolve_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n resolved_files = _gs.resolve_merge_conflicts(\n dashboard_id,\n [item.dict() for item in resolve_data.resolutions],\n )\n return {\"status\": \"success\", \"resolved_files\": resolved_files}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"resolve_merge_conflicts\", e)\n# [/DEF:resolve_merge_conflicts:Function]\n\n\n# [DEF:abort_merge:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Abort unfinished merge from WebUI flow.\n@router.post(\"/repositories/{dashboard_ref}/merge/abort\")\nasync def abort_merge(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"abort_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.abort_merge(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"abort_merge\", e)\n# [/DEF:abort_merge:Function]\n\n\n# [DEF:continue_merge:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Finalize unfinished merge from WebUI flow.\n# @RELATION: CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/continue\")\nasync def continue_merge(\n dashboard_ref: str,\n continue_data: MergeContinueRequest,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"continue_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.continue_merge(dashboard_id, continue_data.message)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"continue_merge\", e)\n# [/DEF:continue_merge:Function]\n# [/DEF:GitMergeRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitMergeRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]\n# @BRIEF FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException\n\nfrom src.api.routes.git_schemas import (\n MergeConflictFileSchema,\n MergeContinueRequest,\n MergeResolveRequest,\n MergeStatusSchema,\n)\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_config_manager, has_permission\n\nfrom ._deps import get_git_service\nfrom ._helpers import (\n _handle_unexpected_git_route_error,\n)\nfrom ._router import router\n\n\n# #region get_merge_status [C:2] [TYPE Function]\n# @BRIEF Return unfinished-merge status for repository (web-only recovery support).\n@router.get(\n \"/repositories/{dashboard_ref}/merge/status\", response_model=MergeStatusSchema\n)\nasync def get_merge_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_status\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_status\", e)\n# #endregion get_merge_status\n\n\n# #region get_merge_conflicts [C:2] [TYPE Function]\n# @BRIEF Return conflicted files with mine/theirs previews for web conflict resolver.\n@router.get(\n \"/repositories/{dashboard_ref}/merge/conflicts\",\n response_model=list[MergeConflictFileSchema],\n)\nasync def get_merge_conflicts(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_conflicts(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_conflicts\", e)\n# #endregion get_merge_conflicts\n\n\n# #region resolve_merge_conflicts [C:3] [TYPE Function]\n# @BRIEF Apply mine/theirs/manual conflict resolutions from WebUI and stage files.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/resolve\")\nasync def resolve_merge_conflicts(\n dashboard_ref: str,\n resolve_data: MergeResolveRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"resolve_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n resolved_files = _gs.resolve_merge_conflicts(\n dashboard_id,\n [item.dict() for item in resolve_data.resolutions],\n )\n return {\"status\": \"success\", \"resolved_files\": resolved_files}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"resolve_merge_conflicts\", e)\n# #endregion resolve_merge_conflicts\n\n\n# #region abort_merge [C:2] [TYPE Function]\n# @BRIEF Abort unfinished merge from WebUI flow.\n@router.post(\"/repositories/{dashboard_ref}/merge/abort\")\nasync def abort_merge(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"abort_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.abort_merge(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"abort_merge\", e)\n# #endregion abort_merge\n\n\n# #region continue_merge [C:3] [TYPE Function]\n# @BRIEF Finalize unfinished merge from WebUI flow.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/continue\")\nasync def continue_merge(\n dashboard_ref: str,\n continue_data: MergeContinueRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"continue_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.continue_merge(dashboard_id, continue_data.message)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"continue_merge\", e)\n# #endregion continue_merge\n# #endregion GitMergeRoutes\n" }, { "contract_id": "get_merge_status", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_merge_routes.py", - "start_line": 28, - "end_line": 53, + "start_line": 24, + "end_line": 48, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Return unfinished-merge status for repository (web-only recovery support)." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_merge_status:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Return unfinished-merge status for repository (web-only recovery support).\n@router.get(\n \"/repositories/{dashboard_ref}/merge/status\", response_model=MergeStatusSchema\n)\nasync def get_merge_status(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_status\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_status\", e)\n# [/DEF:get_merge_status:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_merge_status [C:2] [TYPE Function]\n# @BRIEF Return unfinished-merge status for repository (web-only recovery support).\n@router.get(\n \"/repositories/{dashboard_ref}/merge/status\", response_model=MergeStatusSchema\n)\nasync def get_merge_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_status\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_status\", e)\n# #endregion get_merge_status\n" }, { "contract_id": "get_merge_conflicts", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_merge_routes.py", - "start_line": 56, - "end_line": 82, + "start_line": 51, + "end_line": 76, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Return conflicted files with mine/theirs previews for web conflict resolver." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_merge_conflicts:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Return conflicted files with mine/theirs previews for web conflict resolver.\n@router.get(\n \"/repositories/{dashboard_ref}/merge/conflicts\",\n response_model=List[MergeConflictFileSchema],\n)\nasync def get_merge_conflicts(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_conflicts(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_conflicts\", e)\n# [/DEF:get_merge_conflicts:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_merge_conflicts [C:2] [TYPE Function]\n# @BRIEF Return conflicted files with mine/theirs previews for web conflict resolver.\n@router.get(\n \"/repositories/{dashboard_ref}/merge/conflicts\",\n response_model=list[MergeConflictFileSchema],\n)\nasync def get_merge_conflicts(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_conflicts(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_conflicts\", e)\n# #endregion get_merge_conflicts\n" }, { "contract_id": "resolve_merge_conflicts", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_merge_routes.py", - "start_line": 85, - "end_line": 114, + "start_line": 79, + "end_line": 107, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Apply mine/theirs/manual conflict resolutions from WebUI and stage files." }, "relations": [ @@ -19579,36 +17644,36 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:resolve_merge_conflicts:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Apply mine/theirs/manual conflict resolutions from WebUI and stage files.\n# @RELATION: CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/resolve\")\nasync def resolve_merge_conflicts(\n dashboard_ref: str,\n resolve_data: MergeResolveRequest,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"resolve_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n resolved_files = _gs.resolve_merge_conflicts(\n dashboard_id,\n [item.dict() for item in resolve_data.resolutions],\n )\n return {\"status\": \"success\", \"resolved_files\": resolved_files}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"resolve_merge_conflicts\", e)\n# [/DEF:resolve_merge_conflicts:Function]\n" + "anchor_syntax": "region", + "body": "# #region resolve_merge_conflicts [C:3] [TYPE Function]\n# @BRIEF Apply mine/theirs/manual conflict resolutions from WebUI and stage files.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/resolve\")\nasync def resolve_merge_conflicts(\n dashboard_ref: str,\n resolve_data: MergeResolveRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"resolve_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n resolved_files = _gs.resolve_merge_conflicts(\n dashboard_id,\n [item.dict() for item in resolve_data.resolutions],\n )\n return {\"status\": \"success\", \"resolved_files\": resolved_files}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"resolve_merge_conflicts\", e)\n# #endregion resolve_merge_conflicts\n" }, { "contract_id": "abort_merge", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_merge_routes.py", - "start_line": 117, - "end_line": 140, + "start_line": 110, + "end_line": 132, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Abort unfinished merge from WebUI flow." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:abort_merge:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Abort unfinished merge from WebUI flow.\n@router.post(\"/repositories/{dashboard_ref}/merge/abort\")\nasync def abort_merge(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"abort_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.abort_merge(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"abort_merge\", e)\n# [/DEF:abort_merge:Function]\n" + "anchor_syntax": "region", + "body": "# #region abort_merge [C:2] [TYPE Function]\n# @BRIEF Abort unfinished merge from WebUI flow.\n@router.post(\"/repositories/{dashboard_ref}/merge/abort\")\nasync def abort_merge(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"abort_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.abort_merge(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"abort_merge\", e)\n# #endregion abort_merge\n" }, { "contract_id": "continue_merge", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_merge_routes.py", - "start_line": 143, - "end_line": 168, + "start_line": 135, + "end_line": 159, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Finalize unfinished merge from WebUI flow." }, "relations": [ @@ -19620,28 +17685,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:continue_merge:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Finalize unfinished merge from WebUI flow.\n# @RELATION: CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/continue\")\nasync def continue_merge(\n dashboard_ref: str,\n continue_data: MergeContinueRequest,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"continue_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.continue_merge(dashboard_id, continue_data.message)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"continue_merge\", e)\n# [/DEF:continue_merge:Function]\n" + "anchor_syntax": "region", + "body": "# #region continue_merge [C:3] [TYPE Function]\n# @BRIEF Finalize unfinished merge from WebUI flow.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/continue\")\nasync def continue_merge(\n dashboard_ref: str,\n continue_data: MergeContinueRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"continue_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.continue_merge(dashboard_id, continue_data.message)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"continue_merge\", e)\n# #endregion continue_merge\n" }, { "contract_id": "GitRepoLifecycleRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_repo_lifecycle_routes.py", "start_line": 1, - "end_line": 228, + "end_line": 220, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "API", - "PURPOSE": "FastAPI endpoints for Git lifecycle operations (sync, promote, deploy).", - "SEMANTICS": [ - "git", - "lifecycle", - "sync", - "promote", - "deploy" - ] + "PURPOSE": "FastAPI endpoints for Git lifecycle operations (sync, promote, deploy)." }, "relations": [], "schema_warnings": [ @@ -19655,19 +17713,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitRepoLifecycleRoutes:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: FastAPI endpoints for Git lifecycle operations (sync, promote, deploy).\n# @LAYER: API\n# @SEMANTICS: git, lifecycle, sync, promote, deploy\n\nimport typing\nfrom typing import Optional\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.git import GitProvider, GitRepository\n\nfrom src.api.routes.git_schemas import (\n DeployRequest,\n PromoteRequest,\n PromoteResponse,\n)\n\nfrom ._router import router\nfrom ._helpers import (\n _apply_git_identity_from_profile,\n _get_git_config_or_404,\n _handle_unexpected_git_route_error,\n)\nfrom ._deps import get_git_service\n\n\n# [DEF:sync_dashboard:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Sync dashboard state from Superset to Git using the GitPlugin.\n# @RELATION: CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/sync\")\nasync def sync_dashboard(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n source_env_id: typing.Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"sync_dashboard\"):\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 from src.plugins.git_plugin import GitPlugin\n\n plugin = GitPlugin()\n return await plugin.execute(\n {\n \"operation\": \"sync\",\n \"dashboard_id\": dashboard_id,\n \"source_env_id\": source_env_id,\n }\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"sync_dashboard\", e)\n# [/DEF:sync_dashboard:Function]\n\n\n# [DEF:promote_dashboard:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Promote changes between branches via MR or direct merge.\n# @RELATION: CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/promote\", response_model=PromoteResponse)\nasync def promote_dashboard(\n dashboard_ref: str,\n payload: PromoteRequest,\n env_id: Optional[str] = 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(\"promote_dashboard\"):\n from . import _resolve_dashboard_id_from_ref\n\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n db_repo = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n raise HTTPException(\n status_code=404,\n detail=f\"Repository for dashboard {dashboard_ref} is not initialized\",\n )\n config = _get_git_config_or_404(db, db_repo.config_id)\n\n from_branch = payload.from_branch.strip()\n to_branch = payload.to_branch.strip()\n if not from_branch or not to_branch:\n raise HTTPException(\n status_code=400, detail=\"from_branch and to_branch are required\"\n )\n if from_branch == to_branch:\n raise HTTPException(\n status_code=400, detail=\"from_branch and to_branch must be different\"\n )\n\n mode = (payload.mode or \"mr\").strip().lower()\n if mode == \"direct\":\n reason = (payload.reason or \"\").strip()\n if not reason:\n raise HTTPException(\n status_code=400, detail=\"Direct promote requires non-empty reason\"\n )\n logger.warning(\n \"[promote_dashboard][PolicyViolation] Direct promote without MR by actor=unknown dashboard_ref=%s from=%s to=%s reason=%s\",\n dashboard_ref,\n from_branch,\n to_branch,\n reason,\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n result = _gs.promote_direct_merge(\n dashboard_id=dashboard_id,\n from_branch=from_branch,\n to_branch=to_branch,\n )\n return PromoteResponse(\n mode=\"direct\",\n from_branch=from_branch,\n to_branch=to_branch,\n status=result.get(\"status\", \"merged\"),\n policy_violation=True,\n )\n\n title = (payload.title or \"\").strip() or f\"Promote {from_branch} -> {to_branch}\"\n description = payload.description\n if config.provider == GitProvider.GITEA:\n pr = await _gs.create_gitea_pull_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n )\n elif config.provider == GitProvider.GITHUB:\n pr = await _gs.create_github_pull_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n draft=payload.draft,\n )\n elif config.provider == GitProvider.GITLAB:\n pr = await _gs.create_gitlab_merge_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n remove_source_branch=payload.remove_source_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} does not support promotion API\",\n )\n\n return PromoteResponse(\n mode=\"mr\",\n from_branch=from_branch,\n to_branch=to_branch,\n status=pr.get(\"status\", \"opened\"),\n url=pr.get(\"url\"),\n reference_id=str(pr.get(\"id\")) if pr.get(\"id\") is not None else None,\n policy_violation=False,\n )\n# [/DEF:promote_dashboard:Function]\n\n\n# [DEF:deploy_dashboard:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Deploy dashboard from Git to a target environment.\n# @RELATION: CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/deploy\")\nasync def deploy_dashboard(\n dashboard_ref: str,\n deploy_data: DeployRequest,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"deploy_dashboard\"):\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 from src.plugins.git_plugin import GitPlugin\n\n plugin = GitPlugin()\n return await plugin.execute(\n {\n \"operation\": \"deploy\",\n \"dashboard_id\": dashboard_id,\n \"environment_id\": deploy_data.environment_id,\n }\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"deploy_dashboard\", e)\n# [/DEF:deploy_dashboard:Function]\n# [/DEF:GitRepoLifecycleRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitRepoLifecycleRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, sync, deploy]\n# @BRIEF FastAPI endpoints for Git lifecycle operations (sync, promote, deploy).\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.git_schemas import (\n DeployRequest,\n PromoteRequest,\n PromoteResponse,\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 GitProvider, GitRepository\n\nfrom ._deps import get_git_service\nfrom ._helpers import (\n _apply_git_identity_from_profile,\n _get_git_config_or_404,\n _handle_unexpected_git_route_error,\n)\nfrom ._router import router\n\n\n# #region sync_dashboard [C:3] [TYPE Function]\n# @BRIEF Sync dashboard state from Superset to Git using the GitPlugin.\n# @RELATION CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/sync\")\nasync def sync_dashboard(\n dashboard_ref: str,\n env_id: str | None = None,\n source_env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"sync_dashboard\"):\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 from src.plugins.git_plugin import GitPlugin\n\n plugin = GitPlugin()\n return await plugin.execute(\n {\n \"operation\": \"sync\",\n \"dashboard_id\": dashboard_id,\n \"source_env_id\": source_env_id,\n }\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"sync_dashboard\", e)\n# #endregion sync_dashboard\n\n\n# #region promote_dashboard [C:3] [TYPE Function]\n# @BRIEF Promote changes between branches via MR or direct merge.\n# @RELATION CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/promote\", response_model=PromoteResponse)\nasync def promote_dashboard(\n dashboard_ref: str,\n payload: PromoteRequest,\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(\"promote_dashboard\"):\n from . import _resolve_dashboard_id_from_ref\n\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n db_repo = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n raise HTTPException(\n status_code=404,\n detail=f\"Repository for dashboard {dashboard_ref} is not initialized\",\n )\n config = _get_git_config_or_404(db, db_repo.config_id)\n\n from_branch = payload.from_branch.strip()\n to_branch = payload.to_branch.strip()\n if not from_branch or not to_branch:\n raise HTTPException(\n status_code=400, detail=\"from_branch and to_branch are required\"\n )\n if from_branch == to_branch:\n raise HTTPException(\n status_code=400, detail=\"from_branch and to_branch must be different\"\n )\n\n mode = (payload.mode or \"mr\").strip().lower()\n if mode == \"direct\":\n reason = (payload.reason or \"\").strip()\n if not reason:\n raise HTTPException(\n status_code=400, detail=\"Direct promote requires non-empty reason\"\n )\n logger.warning(\n \"[promote_dashboard][PolicyViolation] Direct promote without MR by actor=unknown dashboard_ref=%s from=%s to=%s reason=%s\",\n dashboard_ref,\n from_branch,\n to_branch,\n reason,\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n result = _gs.promote_direct_merge(\n dashboard_id=dashboard_id,\n from_branch=from_branch,\n to_branch=to_branch,\n )\n return PromoteResponse(\n mode=\"direct\",\n from_branch=from_branch,\n to_branch=to_branch,\n status=result.get(\"status\", \"merged\"),\n policy_violation=True,\n )\n\n title = (payload.title or \"\").strip() or f\"Promote {from_branch} -> {to_branch}\"\n description = payload.description\n if config.provider == GitProvider.GITEA:\n pr = await _gs.create_gitea_pull_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n )\n elif config.provider == GitProvider.GITHUB:\n pr = await _gs.create_github_pull_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n draft=payload.draft,\n )\n elif config.provider == GitProvider.GITLAB:\n pr = await _gs.create_gitlab_merge_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n remove_source_branch=payload.remove_source_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} does not support promotion API\",\n )\n\n return PromoteResponse(\n mode=\"mr\",\n from_branch=from_branch,\n to_branch=to_branch,\n status=pr.get(\"status\", \"opened\"),\n url=pr.get(\"url\"),\n reference_id=str(pr.get(\"id\")) if pr.get(\"id\") is not None else None,\n policy_violation=False,\n )\n# #endregion promote_dashboard\n\n\n# #region deploy_dashboard [C:3] [TYPE Function]\n# @BRIEF Deploy dashboard from Git to a target environment.\n# @RELATION CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/deploy\")\nasync def deploy_dashboard(\n dashboard_ref: str,\n deploy_data: DeployRequest,\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(\"deploy_dashboard\"):\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 from src.plugins.git_plugin import GitPlugin\n\n plugin = GitPlugin()\n return await plugin.execute(\n {\n \"operation\": \"deploy\",\n \"dashboard_id\": dashboard_id,\n \"environment_id\": deploy_data.environment_id,\n }\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"deploy_dashboard\", e)\n# #endregion deploy_dashboard\n# #endregion GitRepoLifecycleRoutes\n" }, { "contract_id": "sync_dashboard", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_lifecycle_routes.py", - "start_line": 34, - "end_line": 67, + "start_line": 29, + "end_line": 61, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Sync dashboard state from Superset to Git using the GitPlugin." }, "relations": [ @@ -19679,19 +17737,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:sync_dashboard:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Sync dashboard state from Superset to Git using the GitPlugin.\n# @RELATION: CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/sync\")\nasync def sync_dashboard(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n source_env_id: typing.Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"sync_dashboard\"):\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 from src.plugins.git_plugin import GitPlugin\n\n plugin = GitPlugin()\n return await plugin.execute(\n {\n \"operation\": \"sync\",\n \"dashboard_id\": dashboard_id,\n \"source_env_id\": source_env_id,\n }\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"sync_dashboard\", e)\n# [/DEF:sync_dashboard:Function]\n" + "anchor_syntax": "region", + "body": "# #region sync_dashboard [C:3] [TYPE Function]\n# @BRIEF Sync dashboard state from Superset to Git using the GitPlugin.\n# @RELATION CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/sync\")\nasync def sync_dashboard(\n dashboard_ref: str,\n env_id: str | None = None,\n source_env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"sync_dashboard\"):\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 from src.plugins.git_plugin import GitPlugin\n\n plugin = GitPlugin()\n return await plugin.execute(\n {\n \"operation\": \"sync\",\n \"dashboard_id\": dashboard_id,\n \"source_env_id\": source_env_id,\n }\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"sync_dashboard\", e)\n# #endregion sync_dashboard\n" }, { "contract_id": "promote_dashboard", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_lifecycle_routes.py", - "start_line": 70, - "end_line": 191, + "start_line": 64, + "end_line": 184, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Promote changes between branches via MR or direct merge." }, "relations": [ @@ -19703,19 +17761,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:promote_dashboard:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Promote changes between branches via MR or direct merge.\n# @RELATION: CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/promote\", response_model=PromoteResponse)\nasync def promote_dashboard(\n dashboard_ref: str,\n payload: PromoteRequest,\n env_id: Optional[str] = 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(\"promote_dashboard\"):\n from . import _resolve_dashboard_id_from_ref\n\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n db_repo = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n raise HTTPException(\n status_code=404,\n detail=f\"Repository for dashboard {dashboard_ref} is not initialized\",\n )\n config = _get_git_config_or_404(db, db_repo.config_id)\n\n from_branch = payload.from_branch.strip()\n to_branch = payload.to_branch.strip()\n if not from_branch or not to_branch:\n raise HTTPException(\n status_code=400, detail=\"from_branch and to_branch are required\"\n )\n if from_branch == to_branch:\n raise HTTPException(\n status_code=400, detail=\"from_branch and to_branch must be different\"\n )\n\n mode = (payload.mode or \"mr\").strip().lower()\n if mode == \"direct\":\n reason = (payload.reason or \"\").strip()\n if not reason:\n raise HTTPException(\n status_code=400, detail=\"Direct promote requires non-empty reason\"\n )\n logger.warning(\n \"[promote_dashboard][PolicyViolation] Direct promote without MR by actor=unknown dashboard_ref=%s from=%s to=%s reason=%s\",\n dashboard_ref,\n from_branch,\n to_branch,\n reason,\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n result = _gs.promote_direct_merge(\n dashboard_id=dashboard_id,\n from_branch=from_branch,\n to_branch=to_branch,\n )\n return PromoteResponse(\n mode=\"direct\",\n from_branch=from_branch,\n to_branch=to_branch,\n status=result.get(\"status\", \"merged\"),\n policy_violation=True,\n )\n\n title = (payload.title or \"\").strip() or f\"Promote {from_branch} -> {to_branch}\"\n description = payload.description\n if config.provider == GitProvider.GITEA:\n pr = await _gs.create_gitea_pull_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n )\n elif config.provider == GitProvider.GITHUB:\n pr = await _gs.create_github_pull_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n draft=payload.draft,\n )\n elif config.provider == GitProvider.GITLAB:\n pr = await _gs.create_gitlab_merge_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n remove_source_branch=payload.remove_source_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} does not support promotion API\",\n )\n\n return PromoteResponse(\n mode=\"mr\",\n from_branch=from_branch,\n to_branch=to_branch,\n status=pr.get(\"status\", \"opened\"),\n url=pr.get(\"url\"),\n reference_id=str(pr.get(\"id\")) if pr.get(\"id\") is not None else None,\n policy_violation=False,\n )\n# [/DEF:promote_dashboard:Function]\n" + "anchor_syntax": "region", + "body": "# #region promote_dashboard [C:3] [TYPE Function]\n# @BRIEF Promote changes between branches via MR or direct merge.\n# @RELATION CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/promote\", response_model=PromoteResponse)\nasync def promote_dashboard(\n dashboard_ref: str,\n payload: PromoteRequest,\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(\"promote_dashboard\"):\n from . import _resolve_dashboard_id_from_ref\n\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n db_repo = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n raise HTTPException(\n status_code=404,\n detail=f\"Repository for dashboard {dashboard_ref} is not initialized\",\n )\n config = _get_git_config_or_404(db, db_repo.config_id)\n\n from_branch = payload.from_branch.strip()\n to_branch = payload.to_branch.strip()\n if not from_branch or not to_branch:\n raise HTTPException(\n status_code=400, detail=\"from_branch and to_branch are required\"\n )\n if from_branch == to_branch:\n raise HTTPException(\n status_code=400, detail=\"from_branch and to_branch must be different\"\n )\n\n mode = (payload.mode or \"mr\").strip().lower()\n if mode == \"direct\":\n reason = (payload.reason or \"\").strip()\n if not reason:\n raise HTTPException(\n status_code=400, detail=\"Direct promote requires non-empty reason\"\n )\n logger.warning(\n \"[promote_dashboard][PolicyViolation] Direct promote without MR by actor=unknown dashboard_ref=%s from=%s to=%s reason=%s\",\n dashboard_ref,\n from_branch,\n to_branch,\n reason,\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n result = _gs.promote_direct_merge(\n dashboard_id=dashboard_id,\n from_branch=from_branch,\n to_branch=to_branch,\n )\n return PromoteResponse(\n mode=\"direct\",\n from_branch=from_branch,\n to_branch=to_branch,\n status=result.get(\"status\", \"merged\"),\n policy_violation=True,\n )\n\n title = (payload.title or \"\").strip() or f\"Promote {from_branch} -> {to_branch}\"\n description = payload.description\n if config.provider == GitProvider.GITEA:\n pr = await _gs.create_gitea_pull_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n )\n elif config.provider == GitProvider.GITHUB:\n pr = await _gs.create_github_pull_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n draft=payload.draft,\n )\n elif config.provider == GitProvider.GITLAB:\n pr = await _gs.create_gitlab_merge_request(\n server_url=config.url,\n pat=config.pat,\n remote_url=db_repo.remote_url,\n from_branch=from_branch,\n to_branch=to_branch,\n title=title,\n description=description,\n remove_source_branch=payload.remove_source_branch,\n )\n else:\n raise HTTPException(\n status_code=501,\n detail=f\"Provider {config.provider} does not support promotion API\",\n )\n\n return PromoteResponse(\n mode=\"mr\",\n from_branch=from_branch,\n to_branch=to_branch,\n status=pr.get(\"status\", \"opened\"),\n url=pr.get(\"url\"),\n reference_id=str(pr.get(\"id\")) if pr.get(\"id\") is not None else None,\n policy_violation=False,\n )\n# #endregion promote_dashboard\n" }, { "contract_id": "deploy_dashboard", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_lifecycle_routes.py", - "start_line": 194, - "end_line": 227, + "start_line": 187, + "end_line": 219, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Deploy dashboard from Git to a target environment." }, "relations": [ @@ -19727,30 +17785,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:deploy_dashboard:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Deploy dashboard from Git to a target environment.\n# @RELATION: CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/deploy\")\nasync def deploy_dashboard(\n dashboard_ref: str,\n deploy_data: DeployRequest,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"deploy_dashboard\"):\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 from src.plugins.git_plugin import GitPlugin\n\n plugin = GitPlugin()\n return await plugin.execute(\n {\n \"operation\": \"deploy\",\n \"dashboard_id\": dashboard_id,\n \"environment_id\": deploy_data.environment_id,\n }\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"deploy_dashboard\", e)\n# [/DEF:deploy_dashboard:Function]\n" + "anchor_syntax": "region", + "body": "# #region deploy_dashboard [C:3] [TYPE Function]\n# @BRIEF Deploy dashboard from Git to a target environment.\n# @RELATION CALLS -> [GitPlugin]\n@router.post(\"/repositories/{dashboard_ref}/deploy\")\nasync def deploy_dashboard(\n dashboard_ref: str,\n deploy_data: DeployRequest,\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(\"deploy_dashboard\"):\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 from src.plugins.git_plugin import GitPlugin\n\n plugin = GitPlugin()\n return await plugin.execute(\n {\n \"operation\": \"deploy\",\n \"dashboard_id\": dashboard_id,\n \"environment_id\": deploy_data.environment_id,\n }\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"deploy_dashboard\", e)\n# #endregion deploy_dashboard\n" }, { "contract_id": "GitRepoOperationsRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_repo_operations_routes.py", "start_line": 1, - "end_line": 352, + "end_line": 346, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "API", - "PURPOSE": "FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).", - "SEMANTICS": [ - "git", - "repository", - "operations", - "commit", - "push", - "pull", - "status" - ] + "PURPOSE": "FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history)." }, "relations": [], "schema_warnings": [ @@ -19764,53 +17813,53 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitRepoOperationsRoutes:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).\n# @LAYER: API\n# @SEMANTICS: git, repository, operations, commit, push, pull, status\n\nfrom typing import List, Optional\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\n\nlog = MarkerLogger(\"GitRepoOps\")\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 src.api.routes.git_schemas import (\n CommitCreate,\n CommitSchema,\n RepoStatusBatchRequest,\n RepoStatusBatchResponse,\n)\n\nfrom ._router import router\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 ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH\n\n\n# [DEF:commit_changes:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = 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# [/DEF:commit_changes:Function]\n\n\n# [DEF:push_changes:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = 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# [/DEF:push_changes:Function]\n\n\n# [DEF:pull_changes:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = 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 log.explore(f\"Failed to load repository binding diagnostics for dashboard {dashboard_id}: {diagnostics_error}\", error=str(diagnostics_error))\n log.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 )\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# [/DEF:pull_changes:Function]\n\n\n# [DEF:get_repository_status:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = 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# [/DEF:get_repository_status:Function]\n\n\n# [DEF:get_repository_status_batch:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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 log.explore(f\"Batch size {len(dashboard_ids)} exceeds limit {MAX_REPOSITORY_STATUS_BATCH}. Truncating request.\", error=\"Batch size exceeds limit\")\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 log.explore(f\"Failed for dashboard {dashboard_id}: {e}\", error=str(e))\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# [/DEF:get_repository_status_batch:Function]\n\n\n# [DEF:get_repository_diff:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = None,\n staged: bool = False,\n env_id: Optional[str] = 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# [/DEF:get_repository_diff:Function]\n\n\n# [DEF:get_history:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = 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# [/DEF:get_history:Function]\n\n\n# [DEF:generate_commit_message:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = 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 ...services.llm_provider import LLMProviderService\n from ...plugins.llm_analysis.service import LLMClient\n from ...plugins.llm_analysis.models import LLMProviderType\n from ...services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n resolve_bound_provider_id,\n )\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# [/DEF:generate_commit_message:Function]\n# [/DEF:GitRepoOperationsRoutes:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "commit_changes", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_operations_routes.py", - "start_line": 38, - "end_line": 68, + "start_line": 31, + "end_line": 60, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Stage and commit changes in the dashboard's repository." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:commit_changes:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = 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# [/DEF:commit_changes:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "push_changes", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_operations_routes.py", - "start_line": 71, - "end_line": 95, + "start_line": 63, + "end_line": 86, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Push local commits to the remote repository." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:push_changes:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = 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# [/DEF:push_changes:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "pull_changes", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_operations_routes.py", - "start_line": 98, - "end_line": 154, + "start_line": 89, + "end_line": 148, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Pull changes from the remote repository." }, "relations": [ @@ -19822,70 +17871,70 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:pull_changes:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = 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 log.explore(f\"Failed to load repository binding diagnostics for dashboard {dashboard_id}: {diagnostics_error}\", error=str(diagnostics_error))\n log.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 )\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# [/DEF:pull_changes:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_repository_status", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_operations_routes.py", - "start_line": 157, - "end_line": 179, + "start_line": 151, + "end_line": 172, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Get current Git status for a dashboard repository." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_repository_status:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = 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# [/DEF:get_repository_status:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_repository_status [C:2] [TYPE Function]\n# @BRIEF Get current Git status for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/status\")\nasync def get_repository_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status\"):\n from . import _resolve_dashboard_id_from_ref_async\n\n try:\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, config_manager, env_id\n )\n return _resolve_repository_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_status\", e)\n# #endregion get_repository_status\n" }, { "contract_id": "get_repository_status_batch", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_operations_routes.py", - "start_line": 182, - "end_line": 214, + "start_line": 175, + "end_line": 211, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Get Git statuses for multiple dashboard repositories in one request." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_repository_status_batch:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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 log.explore(f\"Batch size {len(dashboard_ids)} exceeds limit {MAX_REPOSITORY_STATUS_BATCH}. Truncating request.\", error=\"Batch size exceeds limit\")\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 log.explore(f\"Failed for dashboard {dashboard_id}: {e}\", error=str(e))\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# [/DEF:get_repository_status_batch:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_repository_diff", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_operations_routes.py", - "start_line": 217, - "end_line": 242, + "start_line": 214, + "end_line": 238, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Get Git diff for a dashboard repository." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_repository_diff:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = None,\n staged: bool = False,\n env_id: Optional[str] = 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# [/DEF:get_repository_diff:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "generate_commit_message", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_operations_routes.py", - "start_line": 272, - "end_line": 351, + "start_line": 267, + "end_line": 345, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Generate a suggested commit message using LLM." }, "relations": [ @@ -19897,28 +17946,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:generate_commit_message:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = 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 ...services.llm_provider import LLMProviderService\n from ...plugins.llm_analysis.service import LLMClient\n from ...plugins.llm_analysis.models import LLMProviderType\n from ...services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n resolve_bound_provider_id,\n )\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# [/DEF:generate_commit_message:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GitRepoRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_repo_routes.py", "start_line": 1, - "end_line": 265, + "end_line": 260, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "API", - "PURPOSE": "FastAPI endpoints for core Git repository operations (init, binding, branches, checkout).", - "SEMANTICS": [ - "git", - "repository", - "routes", - "init", - "branches" - ] + "PURPOSE": "FastAPI endpoints for core Git repository operations (init, binding, branches, checkout)." }, "relations": [], "schema_warnings": [ @@ -19932,19 +17974,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitRepoRoutes:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: FastAPI endpoints for core Git repository operations (init, binding, branches, checkout).\n# @LAYER: API\n# @SEMANTICS: git, repository, routes, init, branches\n\nfrom typing import List, Optional\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\n\nlog = MarkerLogger(\"GitRepoRoutes\")\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 src.api.routes.git_schemas import (\n BranchCheckout,\n BranchCreate,\n BranchSchema,\n RepoInitRequest,\n RepositoryBindingSchema,\n)\n\nfrom ._router import router\nfrom ._helpers import (\n _apply_git_identity_from_profile,\n _get_git_config_or_404,\n _handle_unexpected_git_route_error,\n)\nfrom ._deps import get_git_service\n\n\n# [DEF:init_repository:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Link a dashboard to a Git repository and perform initial clone/init.\n# @RELATION: CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/init\")\nasync def init_repository(\n dashboard_ref: str,\n init_data: RepoInitRequest,\n env_id: Optional[str] = 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(\"init_repository\"):\n from . import _resolve_dashboard_id_from_ref, _resolve_repo_key_from_ref\n\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n repo_key = _resolve_repo_key_from_ref(\n dashboard_ref, dashboard_id, config_manager, env_id\n )\n config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == init_data.config_id)\n .first()\n )\n if not config:\n raise HTTPException(status_code=404, detail=\"Git configuration not found\")\n\n try:\n log.reason(f\"Initializing repo for dashboard {dashboard_id}\")\n _gs.init_repo(\n dashboard_id,\n init_data.remote_url,\n config.pat,\n repo_key=repo_key,\n default_branch=config.default_branch,\n )\n\n repo_path = _gs._get_repo_path(dashboard_id, repo_key=repo_key)\n db_repo = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n db_repo = GitRepository(\n dashboard_id=dashboard_id,\n config_id=config.id,\n remote_url=init_data.remote_url,\n local_path=repo_path,\n current_branch=\"dev\",\n )\n db.add(db_repo)\n else:\n db_repo.config_id = config.id\n db_repo.remote_url = init_data.remote_url\n db_repo.local_path = repo_path\n db_repo.current_branch = \"dev\"\n\n db.commit()\n return {\"status\": \"success\", \"message\": \"Repository initialized\"}\n except Exception as e:\n db.rollback()\n log.explore(\"Failed to init repository\", error=str(e))\n if isinstance(e, HTTPException):\n raise\n _handle_unexpected_git_route_error(\"init_repository\", e)\n# [/DEF:init_repository:Function]\n\n\n# [DEF:get_repository_binding:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Return repository binding with provider metadata for selected dashboard.\n@router.get(\"/repositories/{dashboard_ref}\", response_model=RepositoryBindingSchema)\nasync def get_repository_binding(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_binding\"):\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 = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n raise HTTPException(\n status_code=404, detail=\"Repository not initialized\"\n )\n config = _get_git_config_or_404(db, db_repo.config_id)\n return RepositoryBindingSchema(\n dashboard_id=db_repo.dashboard_id,\n config_id=db_repo.config_id,\n provider=config.provider,\n remote_url=db_repo.remote_url,\n local_path=db_repo.local_path,\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_binding\", e)\n# [/DEF:get_repository_binding:Function]\n\n\n# [DEF:delete_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Delete local repository workspace and DB binding for selected dashboard.\n@router.delete(\"/repositories/{dashboard_ref}\")\nasync def delete_repository(\n dashboard_ref: str,\n env_id: Optional[str] = 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(\"delete_repository\"):\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.delete_repo(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"delete_repository\", e)\n# [/DEF:delete_repository:Function]\n\n\n# [DEF:get_branches:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List all branches for a dashboard's repository.\n@router.get(\"/repositories/{dashboard_ref}/branches\", response_model=List[BranchSchema])\nasync def get_branches(\n dashboard_ref: str,\n env_id: Optional[str] = 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_branches\"):\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.list_branches(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_branches\", e)\n# [/DEF:get_branches:Function]\n\n\n# [DEF:create_branch:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create a new branch in the dashboard's repository.\n@router.post(\"/repositories/{dashboard_ref}/branches\")\nasync def create_branch(\n dashboard_ref: str,\n branch_data: BranchCreate,\n env_id: Optional[str] = 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(\"create_branch\"):\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.create_branch(\n dashboard_id, branch_data.name, branch_data.from_branch\n )\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"create_branch\", e)\n# [/DEF:create_branch:Function]\n\n\n# [DEF:checkout_branch:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Switch the dashboard's repository to a specific branch.\n@router.post(\"/repositories/{dashboard_ref}/checkout\")\nasync def checkout_branch(\n dashboard_ref: str,\n checkout_data: BranchCheckout,\n env_id: Optional[str] = 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(\"checkout_branch\"):\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.checkout_branch(dashboard_id, checkout_data.name)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"checkout_branch\", e)\n# [/DEF:checkout_branch:Function]\n# [/DEF:GitRepoRoutes:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitRepoRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, search]\n# @BRIEF FastAPI endpoints for core Git repository operations (init, binding, branches, checkout).\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.git_schemas import (\n BranchCheckout,\n BranchCreate,\n BranchSchema,\n RepoInitRequest,\n RepositoryBindingSchema,\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 get_git_service\nfrom ._helpers import (\n _apply_git_identity_from_profile,\n _get_git_config_or_404,\n _handle_unexpected_git_route_error,\n)\nfrom ._router import router\n\n\n# #region init_repository [C:3] [TYPE Function]\n# @BRIEF Link a dashboard to a Git repository and perform initial clone/init.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/init\")\nasync def init_repository(\n dashboard_ref: str,\n init_data: RepoInitRequest,\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(\"init_repository\"):\n from . import _resolve_dashboard_id_from_ref, _resolve_repo_key_from_ref\n\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n repo_key = _resolve_repo_key_from_ref(\n dashboard_ref, dashboard_id, config_manager, env_id\n )\n config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == init_data.config_id)\n .first()\n )\n if not config:\n raise HTTPException(status_code=404, detail=\"Git configuration not found\")\n\n try:\n logger.reason(\n f\"Initializing repo for dashboard {dashboard_id}\",\n extra={\"src\": \"init_repository\"},\n )\n _gs.init_repo(\n dashboard_id,\n init_data.remote_url,\n config.pat,\n repo_key=repo_key,\n default_branch=config.default_branch,\n )\n\n repo_path = _gs._get_repo_path(dashboard_id, repo_key=repo_key)\n db_repo = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n db_repo = GitRepository(\n dashboard_id=dashboard_id,\n config_id=config.id,\n remote_url=init_data.remote_url,\n local_path=repo_path,\n current_branch=\"dev\",\n )\n db.add(db_repo)\n else:\n db_repo.config_id = config.id\n db_repo.remote_url = init_data.remote_url\n db_repo.local_path = repo_path\n db_repo.current_branch = \"dev\"\n\n db.commit()\n logger.info(\n f\"[init_repository][Coherence:OK] Repository initialized for dashboard {dashboard_id}\"\n )\n return {\"status\": \"success\", \"message\": \"Repository initialized\"}\n except Exception as e:\n db.rollback()\n logger.error(\n f\"[init_repository][Coherence:Failed] Failed to init repository: {e}\"\n )\n if isinstance(e, HTTPException):\n raise\n _handle_unexpected_git_route_error(\"init_repository\", e)\n# #endregion init_repository\n\n\n# #region get_repository_binding [C:2] [TYPE Function]\n# @BRIEF Return repository binding with provider metadata for selected dashboard.\n@router.get(\"/repositories/{dashboard_ref}\", response_model=RepositoryBindingSchema)\nasync def get_repository_binding(\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 with belief_scope(\"get_repository_binding\"):\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 = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n raise HTTPException(\n status_code=404, detail=\"Repository not initialized\"\n )\n config = _get_git_config_or_404(db, db_repo.config_id)\n return RepositoryBindingSchema(\n dashboard_id=db_repo.dashboard_id,\n config_id=db_repo.config_id,\n provider=config.provider,\n remote_url=db_repo.remote_url,\n local_path=db_repo.local_path,\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_binding\", e)\n# #endregion get_repository_binding\n\n\n# #region delete_repository [C:2] [TYPE Function]\n# @BRIEF Delete local repository workspace and DB binding for selected dashboard.\n@router.delete(\"/repositories/{dashboard_ref}\")\nasync def delete_repository(\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(\"delete_repository\"):\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.delete_repo(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"delete_repository\", e)\n# #endregion delete_repository\n\n\n# #region get_branches [C:2] [TYPE Function]\n# @BRIEF List all branches for a dashboard's repository.\n@router.get(\"/repositories/{dashboard_ref}/branches\", response_model=list[BranchSchema])\nasync def get_branches(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_branches\"):\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.list_branches(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_branches\", e)\n# #endregion get_branches\n\n\n# #region create_branch [C:2] [TYPE Function]\n# @BRIEF Create a new branch in the dashboard's repository.\n@router.post(\"/repositories/{dashboard_ref}/branches\")\nasync def create_branch(\n dashboard_ref: str,\n branch_data: BranchCreate,\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(\"create_branch\"):\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.create_branch(\n dashboard_id, branch_data.name, branch_data.from_branch\n )\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"create_branch\", e)\n# #endregion create_branch\n\n\n# #region checkout_branch [C:2] [TYPE Function]\n# @BRIEF Switch the dashboard's repository to a specific branch.\n@router.post(\"/repositories/{dashboard_ref}/checkout\")\nasync def checkout_branch(\n dashboard_ref: str,\n checkout_data: BranchCheckout,\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(\"checkout_branch\"):\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.checkout_branch(dashboard_id, checkout_data.name)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"checkout_branch\", e)\n# #endregion checkout_branch\n# #endregion GitRepoRoutes\n" }, { "contract_id": "init_repository", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_routes.py", - "start_line": 38, + "start_line": 31, "end_line": 108, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Link a dashboard to a Git repository and perform initial clone/init." }, "relations": [ @@ -19956,112 +17998,106 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:init_repository:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Link a dashboard to a Git repository and perform initial clone/init.\n# @RELATION: CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/init\")\nasync def init_repository(\n dashboard_ref: str,\n init_data: RepoInitRequest,\n env_id: Optional[str] = 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(\"init_repository\"):\n from . import _resolve_dashboard_id_from_ref, _resolve_repo_key_from_ref\n\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n repo_key = _resolve_repo_key_from_ref(\n dashboard_ref, dashboard_id, config_manager, env_id\n )\n config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == init_data.config_id)\n .first()\n )\n if not config:\n raise HTTPException(status_code=404, detail=\"Git configuration not found\")\n\n try:\n log.reason(f\"Initializing repo for dashboard {dashboard_id}\")\n _gs.init_repo(\n dashboard_id,\n init_data.remote_url,\n config.pat,\n repo_key=repo_key,\n default_branch=config.default_branch,\n )\n\n repo_path = _gs._get_repo_path(dashboard_id, repo_key=repo_key)\n db_repo = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n db_repo = GitRepository(\n dashboard_id=dashboard_id,\n config_id=config.id,\n remote_url=init_data.remote_url,\n local_path=repo_path,\n current_branch=\"dev\",\n )\n db.add(db_repo)\n else:\n db_repo.config_id = config.id\n db_repo.remote_url = init_data.remote_url\n db_repo.local_path = repo_path\n db_repo.current_branch = \"dev\"\n\n db.commit()\n return {\"status\": \"success\", \"message\": \"Repository initialized\"}\n except Exception as e:\n db.rollback()\n log.explore(\"Failed to init repository\", error=str(e))\n if isinstance(e, HTTPException):\n raise\n _handle_unexpected_git_route_error(\"init_repository\", e)\n# [/DEF:init_repository:Function]\n" + "anchor_syntax": "region", + "body": "# #region init_repository [C:3] [TYPE Function]\n# @BRIEF Link a dashboard to a Git repository and perform initial clone/init.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/init\")\nasync def init_repository(\n dashboard_ref: str,\n init_data: RepoInitRequest,\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(\"init_repository\"):\n from . import _resolve_dashboard_id_from_ref, _resolve_repo_key_from_ref\n\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n repo_key = _resolve_repo_key_from_ref(\n dashboard_ref, dashboard_id, config_manager, env_id\n )\n config = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == init_data.config_id)\n .first()\n )\n if not config:\n raise HTTPException(status_code=404, detail=\"Git configuration not found\")\n\n try:\n logger.reason(\n f\"Initializing repo for dashboard {dashboard_id}\",\n extra={\"src\": \"init_repository\"},\n )\n _gs.init_repo(\n dashboard_id,\n init_data.remote_url,\n config.pat,\n repo_key=repo_key,\n default_branch=config.default_branch,\n )\n\n repo_path = _gs._get_repo_path(dashboard_id, repo_key=repo_key)\n db_repo = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n db_repo = GitRepository(\n dashboard_id=dashboard_id,\n config_id=config.id,\n remote_url=init_data.remote_url,\n local_path=repo_path,\n current_branch=\"dev\",\n )\n db.add(db_repo)\n else:\n db_repo.config_id = config.id\n db_repo.remote_url = init_data.remote_url\n db_repo.local_path = repo_path\n db_repo.current_branch = \"dev\"\n\n db.commit()\n logger.info(\n f\"[init_repository][Coherence:OK] Repository initialized for dashboard {dashboard_id}\"\n )\n return {\"status\": \"success\", \"message\": \"Repository initialized\"}\n except Exception as e:\n db.rollback()\n logger.error(\n f\"[init_repository][Coherence:Failed] Failed to init repository: {e}\"\n )\n if isinstance(e, HTTPException):\n raise\n _handle_unexpected_git_route_error(\"init_repository\", e)\n# #endregion init_repository\n" }, { "contract_id": "get_repository_binding", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_routes.py", "start_line": 111, - "end_line": 150, + "end_line": 149, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Return repository binding with provider metadata for selected dashboard." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_repository_binding:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Return repository binding with provider metadata for selected dashboard.\n@router.get(\"/repositories/{dashboard_ref}\", response_model=RepositoryBindingSchema)\nasync def get_repository_binding(\n dashboard_ref: str,\n env_id: Optional[str] = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_binding\"):\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 = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n raise HTTPException(\n status_code=404, detail=\"Repository not initialized\"\n )\n config = _get_git_config_or_404(db, db_repo.config_id)\n return RepositoryBindingSchema(\n dashboard_id=db_repo.dashboard_id,\n config_id=db_repo.config_id,\n provider=config.provider,\n remote_url=db_repo.remote_url,\n local_path=db_repo.local_path,\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_binding\", e)\n# [/DEF:get_repository_binding:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_repository_binding [C:2] [TYPE Function]\n# @BRIEF Return repository binding with provider metadata for selected dashboard.\n@router.get(\"/repositories/{dashboard_ref}\", response_model=RepositoryBindingSchema)\nasync def get_repository_binding(\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 with belief_scope(\"get_repository_binding\"):\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 = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if not db_repo:\n raise HTTPException(\n status_code=404, detail=\"Repository not initialized\"\n )\n config = _get_git_config_or_404(db, db_repo.config_id)\n return RepositoryBindingSchema(\n dashboard_id=db_repo.dashboard_id,\n config_id=db_repo.config_id,\n provider=config.provider,\n remote_url=db_repo.remote_url,\n local_path=db_repo.local_path,\n )\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_binding\", e)\n# #endregion get_repository_binding\n" }, { "contract_id": "delete_repository", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_routes.py", - "start_line": 153, - "end_line": 177, + "start_line": 152, + "end_line": 175, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Delete local repository workspace and DB binding for selected dashboard." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:delete_repository:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Delete local repository workspace and DB binding for selected dashboard.\n@router.delete(\"/repositories/{dashboard_ref}\")\nasync def delete_repository(\n dashboard_ref: str,\n env_id: Optional[str] = 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(\"delete_repository\"):\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.delete_repo(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"delete_repository\", e)\n# [/DEF:delete_repository:Function]\n" + "anchor_syntax": "region", + "body": "# #region delete_repository [C:2] [TYPE Function]\n# @BRIEF Delete local repository workspace and DB binding for selected dashboard.\n@router.delete(\"/repositories/{dashboard_ref}\")\nasync def delete_repository(\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(\"delete_repository\"):\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.delete_repo(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"delete_repository\", e)\n# #endregion delete_repository\n" }, { "contract_id": "get_branches", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_routes.py", - "start_line": 180, - "end_line": 203, + "start_line": 178, + "end_line": 200, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "List all branches for a dashboard's repository." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_branches:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: List all branches for a dashboard's repository.\n@router.get(\"/repositories/{dashboard_ref}/branches\", response_model=List[BranchSchema])\nasync def get_branches(\n dashboard_ref: str,\n env_id: Optional[str] = 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_branches\"):\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.list_branches(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_branches\", e)\n# [/DEF:get_branches:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_branches [C:2] [TYPE Function]\n# @BRIEF List all branches for a dashboard's repository.\n@router.get(\"/repositories/{dashboard_ref}/branches\", response_model=list[BranchSchema])\nasync def get_branches(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_branches\"):\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.list_branches(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_branches\", e)\n# #endregion get_branches\n" }, { "contract_id": "create_branch", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_routes.py", - "start_line": 206, - "end_line": 236, + "start_line": 203, + "end_line": 232, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Create a new branch in the dashboard's repository." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:create_branch:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create a new branch in the dashboard's repository.\n@router.post(\"/repositories/{dashboard_ref}/branches\")\nasync def create_branch(\n dashboard_ref: str,\n branch_data: BranchCreate,\n env_id: Optional[str] = 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(\"create_branch\"):\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.create_branch(\n dashboard_id, branch_data.name, branch_data.from_branch\n )\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"create_branch\", e)\n# [/DEF:create_branch:Function]\n" + "anchor_syntax": "region", + "body": "# #region create_branch [C:2] [TYPE Function]\n# @BRIEF Create a new branch in the dashboard's repository.\n@router.post(\"/repositories/{dashboard_ref}/branches\")\nasync def create_branch(\n dashboard_ref: str,\n branch_data: BranchCreate,\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(\"create_branch\"):\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.create_branch(\n dashboard_id, branch_data.name, branch_data.from_branch\n )\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"create_branch\", e)\n# #endregion create_branch\n" }, { "contract_id": "checkout_branch", "contract_type": "Function", "file_path": "backend/src/api/routes/git/_repo_routes.py", - "start_line": 239, - "end_line": 264, + "start_line": 235, + "end_line": 259, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Switch the dashboard's repository to a specific branch." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:checkout_branch:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Switch the dashboard's repository to a specific branch.\n@router.post(\"/repositories/{dashboard_ref}/checkout\")\nasync def checkout_branch(\n dashboard_ref: str,\n checkout_data: BranchCheckout,\n env_id: Optional[str] = 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(\"checkout_branch\"):\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.checkout_branch(dashboard_id, checkout_data.name)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"checkout_branch\", e)\n# [/DEF:checkout_branch:Function]\n" + "anchor_syntax": "region", + "body": "# #region checkout_branch [C:2] [TYPE Function]\n# @BRIEF Switch the dashboard's repository to a specific branch.\n@router.post(\"/repositories/{dashboard_ref}/checkout\")\nasync def checkout_branch(\n dashboard_ref: str,\n checkout_data: BranchCheckout,\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(\"checkout_branch\"):\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.checkout_branch(dashboard_id, checkout_data.name)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"checkout_branch\", e)\n# #endregion checkout_branch\n" }, { "contract_id": "GitRouter", "contract_type": "Module", "file_path": "backend/src/api/routes/git/_router.py", "start_line": 1, - "end_line": 10, + "end_line": 8, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "LAYER": "API", - "PURPOSE": "Shared APIRouter for all Git route modules.", - "SEMANTICS": [ - "git", - "routes", - "fastapi", - "router" - ] + "PURPOSE": "Shared APIRouter for all Git route modules." }, "relations": [], "schema_warnings": [ @@ -20075,8 +18111,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitRouter:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: Shared APIRouter for all Git route modules.\n# @LAYER: API\n# @SEMANTICS: git, routes, fastapi, router\n\nfrom fastapi import APIRouter\n\nrouter = APIRouter(tags=[\"git\"])\n# [/DEF:GitRouter:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitRouter [C:1] [TYPE Module] [SEMANTICS fastapi, git, api]\n# @BRIEF Shared APIRouter for all Git route modules.\n# @LAYER: API\n\nfrom fastapi import APIRouter\n\nrouter = APIRouter(tags=[\"git\"])\n# #endregion GitRouter\n" }, { "contract_id": "GitSchemas", @@ -20087,17 +18123,10 @@ "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "INVARIANT": "All schemas must be compatible with the FastAPI router.", "LAYER": "API", - "PURPOSE": "Defines Pydantic models for the Git integration API layer.", - "SEMANTICS": [ - "git", - "schemas", - "pydantic", - "api", - "contracts" - ] + "PURPOSE": "Defines Pydantic models for the Git integration API layer." }, "relations": [ { @@ -20136,19 +18165,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitSchemas:Module]\n#\n# @COMPLEXITY: 1\n# @SEMANTICS: git, schemas, pydantic, api, contracts\n# @PURPOSE: Defines Pydantic models for the Git integration API layer.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> backend.src.models.git\n#\n# @INVARIANT: All schemas must be compatible with the FastAPI router.\n\nfrom pydantic import BaseModel, Field\nfrom typing import Any, Dict, List, Optional\nfrom datetime import datetime\nfrom src.models.git import GitProvider, GitStatus, SyncStatus\n\n# [DEF:GitServerConfigBase:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Base schema for Git server configuration attributes.\nclass GitServerConfigBase(BaseModel):\n name: str = Field(..., description=\"Display name for the Git server\")\n provider: GitProvider = Field(..., description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str = Field(..., description=\"Server base URL\")\n pat: str = Field(..., description=\"Personal Access Token\")\n pat: str = Field(..., description=\"Personal Access Token\")\n default_repository: Optional[str] = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: Optional[str] = Field(\"main\", description=\"Default branch logic/name\")\n# [/DEF:GitServerConfigBase:Class]\n\n# [DEF:GitServerConfigUpdate:Class]\n# @PURPOSE: Schema for updating an existing Git server configuration.\nclass GitServerConfigUpdate(BaseModel):\n name: Optional[str] = Field(None, description=\"Display name for the Git server\")\n provider: Optional[GitProvider] = Field(None, description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: Optional[str] = Field(None, description=\"Server base URL\")\n pat: Optional[str] = Field(None, description=\"Personal Access Token\")\n default_repository: Optional[str] = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: Optional[str] = Field(None, description=\"Default branch logic/name\")\n# [/DEF:GitServerConfigUpdate:Class]\n\n# [DEF:GitServerConfigCreate:Class]\n# @PURPOSE: Schema for creating a new Git server configuration.\nclass GitServerConfigCreate(GitServerConfigBase):\n \"\"\"Schema for creating a new Git server configuration.\"\"\"\n config_id: Optional[str] = Field(None, description=\"Optional config ID, useful for testing an existing config without sending its full PAT\")\n# [/DEF:GitServerConfigCreate:Class]\n\n# [DEF:GitServerConfigSchema:Class]\n# @PURPOSE: Schema for representing a Git server configuration with metadata.\nclass GitServerConfigSchema(GitServerConfigBase):\n \"\"\"Schema for representing a Git server configuration with metadata.\"\"\"\n id: str\n status: GitStatus\n last_validated: datetime\n\n class Config:\n from_attributes = True\n# [/DEF:GitServerConfigSchema:Class]\n\n# [DEF:GitRepositorySchema:Class]\n# @PURPOSE: Schema for tracking a local Git repository linked to a dashboard.\nclass GitRepositorySchema(BaseModel):\n \"\"\"Schema for tracking a local Git repository linked to a dashboard.\"\"\"\n id: str\n dashboard_id: int\n config_id: str\n remote_url: str\n local_path: str\n current_branch: str\n sync_status: SyncStatus\n\n class Config:\n from_attributes = True\n# [/DEF:GitRepositorySchema:Class]\n\n# [DEF:BranchSchema:Class]\n# @PURPOSE: Schema for representing a Git branch metadata.\nclass BranchSchema(BaseModel):\n \"\"\"Schema for representing a Git branch.\"\"\"\n name: str\n commit_hash: str\n is_remote: bool\n last_updated: datetime\n# [/DEF:BranchSchema:Class]\n\n# [DEF:CommitSchema:Class]\n# @PURPOSE: Schema for representing Git commit details.\nclass CommitSchema(BaseModel):\n \"\"\"Schema for representing a Git commit.\"\"\"\n hash: str\n author: str\n email: str\n timestamp: datetime\n message: str\n files_changed: List[str]\n# [/DEF:CommitSchema:Class]\n\n# [DEF:BranchCreate:Class]\n# @PURPOSE: Schema for branch creation requests.\nclass BranchCreate(BaseModel):\n \"\"\"Schema for branch creation requests.\"\"\"\n name: str\n from_branch: str\n# [/DEF:BranchCreate:Class]\n\n# [DEF:BranchCheckout:Class]\n# @PURPOSE: Schema for branch checkout requests.\nclass BranchCheckout(BaseModel):\n \"\"\"Schema for branch checkout requests.\"\"\"\n name: str\n# [/DEF:BranchCheckout:Class]\n\n# [DEF:CommitCreate:Class]\n# @PURPOSE: Schema for staging and committing changes.\nclass CommitCreate(BaseModel):\n \"\"\"Schema for staging and committing changes.\"\"\"\n message: str\n files: List[str]\n# [/DEF:CommitCreate:Class]\n\n# [DEF:ConflictResolution:Class]\n# @PURPOSE: Schema for resolving merge conflicts.\nclass ConflictResolution(BaseModel):\n \"\"\"Schema for resolving merge conflicts.\"\"\"\n file_path: str\n resolution: str = Field(pattern=\"^(mine|theirs|manual)$\")\n content: Optional[str] = None\n# [/DEF:ConflictResolution:Class]\n\n\n# [DEF:MergeStatusSchema:Class]\n# @PURPOSE: Schema representing unfinished merge status for repository.\nclass MergeStatusSchema(BaseModel):\n has_unfinished_merge: bool\n repository_path: str\n git_dir: str\n current_branch: str\n merge_head: Optional[str] = None\n merge_message_preview: Optional[str] = None\n conflicts_count: int = 0\n# [/DEF:MergeStatusSchema:Class]\n\n\n# [DEF:MergeConflictFileSchema:Class]\n# @PURPOSE: Schema describing one conflicted file with optional side snapshots.\nclass MergeConflictFileSchema(BaseModel):\n file_path: str\n mine: Optional[str] = None\n theirs: Optional[str] = None\n# [/DEF:MergeConflictFileSchema:Class]\n\n\n# [DEF:MergeResolveRequest:Class]\n# @PURPOSE: Request schema for resolving one or multiple merge conflicts.\nclass MergeResolveRequest(BaseModel):\n resolutions: List[ConflictResolution] = Field(default_factory=list)\n# [/DEF:MergeResolveRequest:Class]\n\n\n# [DEF:MergeContinueRequest:Class]\n# @PURPOSE: Request schema for finishing merge with optional explicit commit message.\nclass MergeContinueRequest(BaseModel):\n message: Optional[str] = None\n# [/DEF:MergeContinueRequest:Class]\n\n# [DEF:DeploymentEnvironmentSchema:Class]\n# @PURPOSE: Schema for representing a target deployment environment.\nclass DeploymentEnvironmentSchema(BaseModel):\n \"\"\"Schema for representing a target deployment environment.\"\"\"\n id: str\n name: str\n superset_url: str\n is_active: bool\n\n class Config:\n from_attributes = True\n# [/DEF:DeploymentEnvironmentSchema:Class]\n\n# [DEF:DeployRequest:Class]\n# @PURPOSE: Schema for dashboard deployment requests.\nclass DeployRequest(BaseModel):\n \"\"\"Schema for deployment requests.\"\"\"\n environment_id: str\n# [/DEF:DeployRequest:Class]\n\n# [DEF:RepoInitRequest:Class]\n# @PURPOSE: Schema for repository initialization requests.\nclass RepoInitRequest(BaseModel):\n \"\"\"Schema for repository initialization requests.\"\"\"\n config_id: str\n remote_url: str\n# [/DEF:RepoInitRequest:Class]\n\n\n# [DEF:RepositoryBindingSchema:Class]\n# @PURPOSE: Schema describing repository-to-config binding and provider metadata.\nclass RepositoryBindingSchema(BaseModel):\n dashboard_id: int\n config_id: str\n provider: GitProvider\n remote_url: str\n local_path: str\n# [/DEF:RepositoryBindingSchema:Class]\n\n# [DEF:RepoStatusBatchRequest:Class]\n# @PURPOSE: Schema for requesting repository statuses for multiple dashboards in a single call.\nclass RepoStatusBatchRequest(BaseModel):\n dashboard_ids: List[int] = Field(default_factory=list, description=\"Dashboard IDs to resolve repository statuses for\")\n# [/DEF:RepoStatusBatchRequest:Class]\n\n\n# [DEF:RepoStatusBatchResponse:Class]\n# @PURPOSE: Schema for returning repository statuses keyed by dashboard ID.\nclass RepoStatusBatchResponse(BaseModel):\n statuses: Dict[str, Dict[str, Any]]\n# [/DEF:RepoStatusBatchResponse:Class]\n\n\n# [DEF:GiteaRepoSchema:Class]\n# @PURPOSE: Schema describing a Gitea repository.\nclass GiteaRepoSchema(BaseModel):\n name: str\n full_name: str\n private: bool = False\n clone_url: Optional[str] = None\n html_url: Optional[str] = None\n ssh_url: Optional[str] = None\n default_branch: Optional[str] = None\n# [/DEF:GiteaRepoSchema:Class]\n\n\n# [DEF:GiteaRepoCreateRequest:Class]\n# @PURPOSE: Request schema for creating a Gitea repository.\nclass GiteaRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: Optional[str] = None\n auto_init: bool = True\n default_branch: Optional[str] = \"main\"\n# [/DEF:GiteaRepoCreateRequest:Class]\n\n\n# [DEF:RemoteRepoSchema:Class]\n# @PURPOSE: Provider-agnostic remote repository payload.\nclass RemoteRepoSchema(BaseModel):\n provider: GitProvider\n name: str\n full_name: str\n private: bool = False\n clone_url: Optional[str] = None\n html_url: Optional[str] = None\n ssh_url: Optional[str] = None\n default_branch: Optional[str] = None\n# [/DEF:RemoteRepoSchema:Class]\n\n\n# [DEF:RemoteRepoCreateRequest:Class]\n# @PURPOSE: Provider-agnostic repository creation request.\nclass RemoteRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: Optional[str] = None\n auto_init: bool = True\n default_branch: Optional[str] = \"main\"\n# [/DEF:RemoteRepoCreateRequest:Class]\n\n\n# [DEF:PromoteRequest:Class]\n# @PURPOSE: Request schema for branch promotion workflow.\nclass PromoteRequest(BaseModel):\n from_branch: str = Field(..., min_length=1, max_length=255)\n to_branch: str = Field(..., min_length=1, max_length=255)\n mode: str = Field(default=\"mr\", pattern=\"^(mr|direct)$\")\n title: Optional[str] = None\n description: Optional[str] = None\n reason: Optional[str] = None\n draft: bool = False\n remove_source_branch: bool = False\n# [/DEF:PromoteRequest:Class]\n\n\n# [DEF:PromoteResponse:Class]\n# @PURPOSE: Response schema for promotion operation result.\nclass PromoteResponse(BaseModel):\n mode: str\n from_branch: str\n to_branch: str\n status: str\n url: Optional[str] = None\n reference_id: Optional[str] = None\n policy_violation: bool = False\n# [/DEF:PromoteResponse:Class]\n\n# [/DEF:GitSchemas:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitSchemas [C:1] [TYPE Module] [SEMANTICS fastapi, git, api, git-server-config-base]\n#\n# @BRIEF Defines Pydantic models for the Git integration API layer.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> backend.src.models.git\n#\n# @INVARIANT: All schemas must be compatible with the FastAPI router.\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\nfrom src.models.git import GitProvider, GitStatus, SyncStatus\n\n\n# #region GitServerConfigBase [C:1] [TYPE Class]\n# @BRIEF Base schema for Git server configuration attributes.\nclass GitServerConfigBase(BaseModel):\n name: str = Field(..., description=\"Display name for the Git server\")\n provider: GitProvider = Field(..., description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str = Field(..., description=\"Server base URL\")\n pat: str = Field(..., description=\"Personal Access Token\")\n pat: str = Field(..., description=\"Personal Access Token\")\n default_repository: str | None = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: str | None = Field(\"main\", description=\"Default branch logic/name\")\n# #endregion GitServerConfigBase\n\n# #region GitServerConfigUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing Git server configuration.\nclass GitServerConfigUpdate(BaseModel):\n name: str | None = Field(None, description=\"Display name for the Git server\")\n provider: GitProvider | None = Field(None, description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str | None = Field(None, description=\"Server base URL\")\n pat: str | None = Field(None, description=\"Personal Access Token\")\n default_repository: str | None = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: str | None = Field(None, description=\"Default branch logic/name\")\n# #endregion GitServerConfigUpdate\n\n# #region GitServerConfigCreate [TYPE Class]\n# @BRIEF Schema for creating a new Git server configuration.\nclass GitServerConfigCreate(GitServerConfigBase):\n \"\"\"Schema for creating a new Git server configuration.\"\"\"\n config_id: str | None = Field(None, description=\"Optional config ID, useful for testing an existing config without sending its full PAT\")\n# #endregion GitServerConfigCreate\n\n# #region GitServerConfigSchema [TYPE Class]\n# @BRIEF Schema for representing a Git server configuration with metadata.\nclass GitServerConfigSchema(GitServerConfigBase):\n \"\"\"Schema for representing a Git server configuration with metadata.\"\"\"\n id: str\n status: GitStatus\n last_validated: datetime\n\n class Config:\n from_attributes = True\n# #endregion GitServerConfigSchema\n\n# #region GitRepositorySchema [TYPE Class]\n# @BRIEF Schema for tracking a local Git repository linked to a dashboard.\nclass GitRepositorySchema(BaseModel):\n \"\"\"Schema for tracking a local Git repository linked to a dashboard.\"\"\"\n id: str\n dashboard_id: int\n config_id: str\n remote_url: str\n local_path: str\n current_branch: str\n sync_status: SyncStatus\n\n class Config:\n from_attributes = True\n# #endregion GitRepositorySchema\n\n# #region BranchSchema [TYPE Class]\n# @BRIEF Schema for representing a Git branch metadata.\nclass BranchSchema(BaseModel):\n \"\"\"Schema for representing a Git branch.\"\"\"\n name: str\n commit_hash: str\n is_remote: bool\n last_updated: datetime\n# #endregion BranchSchema\n\n# #region CommitSchema [TYPE Class]\n# @BRIEF Schema for representing Git commit details.\nclass CommitSchema(BaseModel):\n \"\"\"Schema for representing a Git commit.\"\"\"\n hash: str\n author: str\n email: str\n timestamp: datetime\n message: str\n files_changed: list[str]\n# #endregion CommitSchema\n\n# #region BranchCreate [TYPE Class]\n# @BRIEF Schema for branch creation requests.\nclass BranchCreate(BaseModel):\n \"\"\"Schema for branch creation requests.\"\"\"\n name: str\n from_branch: str\n# #endregion BranchCreate\n\n# #region BranchCheckout [TYPE Class]\n# @BRIEF Schema for branch checkout requests.\nclass BranchCheckout(BaseModel):\n \"\"\"Schema for branch checkout requests.\"\"\"\n name: str\n# #endregion BranchCheckout\n\n# #region CommitCreate [TYPE Class]\n# @BRIEF Schema for staging and committing changes.\nclass CommitCreate(BaseModel):\n \"\"\"Schema for staging and committing changes.\"\"\"\n message: str\n files: list[str]\n# #endregion CommitCreate\n\n# #region ConflictResolution [TYPE Class]\n# @BRIEF Schema for resolving merge conflicts.\nclass ConflictResolution(BaseModel):\n \"\"\"Schema for resolving merge conflicts.\"\"\"\n file_path: str\n resolution: str = Field(pattern=\"^(mine|theirs|manual)$\")\n content: str | None = None\n# #endregion ConflictResolution\n\n\n# #region MergeStatusSchema [TYPE Class]\n# @BRIEF Schema representing unfinished merge status for repository.\nclass MergeStatusSchema(BaseModel):\n has_unfinished_merge: bool\n repository_path: str\n git_dir: str\n current_branch: str\n merge_head: str | None = None\n merge_message_preview: str | None = None\n conflicts_count: int = 0\n# #endregion MergeStatusSchema\n\n\n# #region MergeConflictFileSchema [TYPE Class]\n# @BRIEF Schema describing one conflicted file with optional side snapshots.\nclass MergeConflictFileSchema(BaseModel):\n file_path: str\n mine: str | None = None\n theirs: str | None = None\n# #endregion MergeConflictFileSchema\n\n\n# #region MergeResolveRequest [TYPE Class]\n# @BRIEF Request schema for resolving one or multiple merge conflicts.\nclass MergeResolveRequest(BaseModel):\n resolutions: list[ConflictResolution] = Field(default_factory=list)\n# #endregion MergeResolveRequest\n\n\n# #region MergeContinueRequest [TYPE Class]\n# @BRIEF Request schema for finishing merge with optional explicit commit message.\nclass MergeContinueRequest(BaseModel):\n message: str | None = None\n# #endregion MergeContinueRequest\n\n# #region DeploymentEnvironmentSchema [TYPE Class]\n# @BRIEF Schema for representing a target deployment environment.\nclass DeploymentEnvironmentSchema(BaseModel):\n \"\"\"Schema for representing a target deployment environment.\"\"\"\n id: str\n name: str\n superset_url: str\n is_active: bool\n\n class Config:\n from_attributes = True\n# #endregion DeploymentEnvironmentSchema\n\n# #region DeployRequest [TYPE Class]\n# @BRIEF Schema for dashboard deployment requests.\nclass DeployRequest(BaseModel):\n \"\"\"Schema for deployment requests.\"\"\"\n environment_id: str\n# #endregion DeployRequest\n\n# #region RepoInitRequest [TYPE Class]\n# @BRIEF Schema for repository initialization requests.\nclass RepoInitRequest(BaseModel):\n \"\"\"Schema for repository initialization requests.\"\"\"\n config_id: str\n remote_url: str\n# #endregion RepoInitRequest\n\n\n# #region RepositoryBindingSchema [TYPE Class]\n# @BRIEF Schema describing repository-to-config binding and provider metadata.\nclass RepositoryBindingSchema(BaseModel):\n dashboard_id: int\n config_id: str\n provider: GitProvider\n remote_url: str\n local_path: str\n# #endregion RepositoryBindingSchema\n\n# #region RepoStatusBatchRequest [TYPE Class]\n# @BRIEF Schema for requesting repository statuses for multiple dashboards in a single call.\nclass RepoStatusBatchRequest(BaseModel):\n dashboard_ids: list[int] = Field(default_factory=list, description=\"Dashboard IDs to resolve repository statuses for\")\n# #endregion RepoStatusBatchRequest\n\n\n# #region RepoStatusBatchResponse [TYPE Class]\n# @BRIEF Schema for returning repository statuses keyed by dashboard ID.\nclass RepoStatusBatchResponse(BaseModel):\n statuses: dict[str, dict[str, Any]]\n# #endregion RepoStatusBatchResponse\n\n\n# #region GiteaRepoSchema [TYPE Class]\n# @BRIEF Schema describing a Gitea repository.\nclass GiteaRepoSchema(BaseModel):\n name: str\n full_name: str\n private: bool = False\n clone_url: str | None = None\n html_url: str | None = None\n ssh_url: str | None = None\n default_branch: str | None = None\n# #endregion GiteaRepoSchema\n\n\n# #region GiteaRepoCreateRequest [TYPE Class]\n# @BRIEF Request schema for creating a Gitea repository.\nclass GiteaRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: str | None = None\n auto_init: bool = True\n default_branch: str | None = \"main\"\n# #endregion GiteaRepoCreateRequest\n\n\n# #region RemoteRepoSchema [TYPE Class]\n# @BRIEF Provider-agnostic remote repository payload.\nclass RemoteRepoSchema(BaseModel):\n provider: GitProvider\n name: str\n full_name: str\n private: bool = False\n clone_url: str | None = None\n html_url: str | None = None\n ssh_url: str | None = None\n default_branch: str | None = None\n# #endregion RemoteRepoSchema\n\n\n# #region RemoteRepoCreateRequest [TYPE Class]\n# @BRIEF Provider-agnostic repository creation request.\nclass RemoteRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: str | None = None\n auto_init: bool = True\n default_branch: str | None = \"main\"\n# #endregion RemoteRepoCreateRequest\n\n\n# #region PromoteRequest [TYPE Class]\n# @BRIEF Request schema for branch promotion workflow.\nclass PromoteRequest(BaseModel):\n from_branch: str = Field(..., min_length=1, max_length=255)\n to_branch: str = Field(..., min_length=1, max_length=255)\n mode: str = Field(default=\"mr\", pattern=\"^(mr|direct)$\")\n title: str | None = None\n description: str | None = None\n reason: str | None = None\n draft: bool = False\n remove_source_branch: bool = False\n# #endregion PromoteRequest\n\n\n# #region PromoteResponse [TYPE Class]\n# @BRIEF Response schema for promotion operation result.\nclass PromoteResponse(BaseModel):\n mode: str\n from_branch: str\n to_branch: str\n status: str\n url: str | None = None\n reference_id: str | None = None\n policy_violation: bool = False\n# #endregion PromoteResponse\n\n# #endregion GitSchemas\n" }, { "contract_id": "GitServerConfigBase", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 16, + "start_line": 17, "end_line": 27, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Base schema for Git server configuration attributes." }, "relations": [], @@ -20163,8 +18192,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServerConfigBase:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Base schema for Git server configuration attributes.\nclass GitServerConfigBase(BaseModel):\n name: str = Field(..., description=\"Display name for the Git server\")\n provider: GitProvider = Field(..., description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str = Field(..., description=\"Server base URL\")\n pat: str = Field(..., description=\"Personal Access Token\")\n pat: str = Field(..., description=\"Personal Access Token\")\n default_repository: Optional[str] = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: Optional[str] = Field(\"main\", description=\"Default branch logic/name\")\n# [/DEF:GitServerConfigBase:Class]\n" + "anchor_syntax": "region", + "body": "# #region GitServerConfigBase [C:1] [TYPE Class]\n# @BRIEF Base schema for Git server configuration attributes.\nclass GitServerConfigBase(BaseModel):\n name: str = Field(..., description=\"Display name for the Git server\")\n provider: GitProvider = Field(..., description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str = Field(..., description=\"Server base URL\")\n pat: str = Field(..., description=\"Personal Access Token\")\n pat: str = Field(..., description=\"Personal Access Token\")\n default_repository: str | None = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: str | None = Field(\"main\", description=\"Default branch logic/name\")\n# #endregion GitServerConfigBase\n" }, { "contract_id": "GitServerConfigUpdate", @@ -20189,8 +18218,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServerConfigUpdate:Class]\n# @PURPOSE: Schema for updating an existing Git server configuration.\nclass GitServerConfigUpdate(BaseModel):\n name: Optional[str] = Field(None, description=\"Display name for the Git server\")\n provider: Optional[GitProvider] = Field(None, description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: Optional[str] = Field(None, description=\"Server base URL\")\n pat: Optional[str] = Field(None, description=\"Personal Access Token\")\n default_repository: Optional[str] = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: Optional[str] = Field(None, description=\"Default branch logic/name\")\n# [/DEF:GitServerConfigUpdate:Class]\n" + "anchor_syntax": "region", + "body": "# #region GitServerConfigUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing Git server configuration.\nclass GitServerConfigUpdate(BaseModel):\n name: str | None = Field(None, description=\"Display name for the Git server\")\n provider: GitProvider | None = Field(None, description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str | None = Field(None, description=\"Server base URL\")\n pat: str | None = Field(None, description=\"Personal Access Token\")\n default_repository: str | None = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: str | None = Field(None, description=\"Default branch logic/name\")\n# #endregion GitServerConfigUpdate\n" }, { "contract_id": "GitServerConfigCreate", @@ -20215,8 +18244,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServerConfigCreate:Class]\n# @PURPOSE: Schema for creating a new Git server configuration.\nclass GitServerConfigCreate(GitServerConfigBase):\n \"\"\"Schema for creating a new Git server configuration.\"\"\"\n config_id: Optional[str] = Field(None, description=\"Optional config ID, useful for testing an existing config without sending its full PAT\")\n# [/DEF:GitServerConfigCreate:Class]\n" + "anchor_syntax": "region", + "body": "# #region GitServerConfigCreate [TYPE Class]\n# @BRIEF Schema for creating a new Git server configuration.\nclass GitServerConfigCreate(GitServerConfigBase):\n \"\"\"Schema for creating a new Git server configuration.\"\"\"\n config_id: str | None = Field(None, description=\"Optional config ID, useful for testing an existing config without sending its full PAT\")\n# #endregion GitServerConfigCreate\n" }, { "contract_id": "GitServerConfigSchema", @@ -20241,8 +18270,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServerConfigSchema:Class]\n# @PURPOSE: Schema for representing a Git server configuration with metadata.\nclass GitServerConfigSchema(GitServerConfigBase):\n \"\"\"Schema for representing a Git server configuration with metadata.\"\"\"\n id: str\n status: GitStatus\n last_validated: datetime\n\n class Config:\n from_attributes = True\n# [/DEF:GitServerConfigSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region GitServerConfigSchema [TYPE Class]\n# @BRIEF Schema for representing a Git server configuration with metadata.\nclass GitServerConfigSchema(GitServerConfigBase):\n \"\"\"Schema for representing a Git server configuration with metadata.\"\"\"\n id: str\n status: GitStatus\n last_validated: datetime\n\n class Config:\n from_attributes = True\n# #endregion GitServerConfigSchema\n" }, { "contract_id": "GitRepositorySchema", @@ -20267,8 +18296,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitRepositorySchema:Class]\n# @PURPOSE: Schema for tracking a local Git repository linked to a dashboard.\nclass GitRepositorySchema(BaseModel):\n \"\"\"Schema for tracking a local Git repository linked to a dashboard.\"\"\"\n id: str\n dashboard_id: int\n config_id: str\n remote_url: str\n local_path: str\n current_branch: str\n sync_status: SyncStatus\n\n class Config:\n from_attributes = True\n# [/DEF:GitRepositorySchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region GitRepositorySchema [TYPE Class]\n# @BRIEF Schema for tracking a local Git repository linked to a dashboard.\nclass GitRepositorySchema(BaseModel):\n \"\"\"Schema for tracking a local Git repository linked to a dashboard.\"\"\"\n id: str\n dashboard_id: int\n config_id: str\n remote_url: str\n local_path: str\n current_branch: str\n sync_status: SyncStatus\n\n class Config:\n from_attributes = True\n# #endregion GitRepositorySchema\n" }, { "contract_id": "BranchSchema", @@ -20293,8 +18322,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BranchSchema:Class]\n# @PURPOSE: Schema for representing a Git branch metadata.\nclass BranchSchema(BaseModel):\n \"\"\"Schema for representing a Git branch.\"\"\"\n name: str\n commit_hash: str\n is_remote: bool\n last_updated: datetime\n# [/DEF:BranchSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region BranchSchema [TYPE Class]\n# @BRIEF Schema for representing a Git branch metadata.\nclass BranchSchema(BaseModel):\n \"\"\"Schema for representing a Git branch.\"\"\"\n name: str\n commit_hash: str\n is_remote: bool\n last_updated: datetime\n# #endregion BranchSchema\n" }, { "contract_id": "CommitSchema", @@ -20319,8 +18348,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CommitSchema:Class]\n# @PURPOSE: Schema for representing Git commit details.\nclass CommitSchema(BaseModel):\n \"\"\"Schema for representing a Git commit.\"\"\"\n hash: str\n author: str\n email: str\n timestamp: datetime\n message: str\n files_changed: List[str]\n# [/DEF:CommitSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region CommitSchema [TYPE Class]\n# @BRIEF Schema for representing Git commit details.\nclass CommitSchema(BaseModel):\n \"\"\"Schema for representing a Git commit.\"\"\"\n hash: str\n author: str\n email: str\n timestamp: datetime\n message: str\n files_changed: list[str]\n# #endregion CommitSchema\n" }, { "contract_id": "BranchCreate", @@ -20345,8 +18374,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BranchCreate:Class]\n# @PURPOSE: Schema for branch creation requests.\nclass BranchCreate(BaseModel):\n \"\"\"Schema for branch creation requests.\"\"\"\n name: str\n from_branch: str\n# [/DEF:BranchCreate:Class]\n" + "anchor_syntax": "region", + "body": "# #region BranchCreate [TYPE Class]\n# @BRIEF Schema for branch creation requests.\nclass BranchCreate(BaseModel):\n \"\"\"Schema for branch creation requests.\"\"\"\n name: str\n from_branch: str\n# #endregion BranchCreate\n" }, { "contract_id": "BranchCheckout", @@ -20371,8 +18400,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BranchCheckout:Class]\n# @PURPOSE: Schema for branch checkout requests.\nclass BranchCheckout(BaseModel):\n \"\"\"Schema for branch checkout requests.\"\"\"\n name: str\n# [/DEF:BranchCheckout:Class]\n" + "anchor_syntax": "region", + "body": "# #region BranchCheckout [TYPE Class]\n# @BRIEF Schema for branch checkout requests.\nclass BranchCheckout(BaseModel):\n \"\"\"Schema for branch checkout requests.\"\"\"\n name: str\n# #endregion BranchCheckout\n" }, { "contract_id": "CommitCreate", @@ -20397,8 +18426,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CommitCreate:Class]\n# @PURPOSE: Schema for staging and committing changes.\nclass CommitCreate(BaseModel):\n \"\"\"Schema for staging and committing changes.\"\"\"\n message: str\n files: List[str]\n# [/DEF:CommitCreate:Class]\n" + "anchor_syntax": "region", + "body": "# #region CommitCreate [TYPE Class]\n# @BRIEF Schema for staging and committing changes.\nclass CommitCreate(BaseModel):\n \"\"\"Schema for staging and committing changes.\"\"\"\n message: str\n files: list[str]\n# #endregion CommitCreate\n" }, { "contract_id": "ConflictResolution", @@ -20423,8 +18452,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ConflictResolution:Class]\n# @PURPOSE: Schema for resolving merge conflicts.\nclass ConflictResolution(BaseModel):\n \"\"\"Schema for resolving merge conflicts.\"\"\"\n file_path: str\n resolution: str = Field(pattern=\"^(mine|theirs|manual)$\")\n content: Optional[str] = None\n# [/DEF:ConflictResolution:Class]\n" + "anchor_syntax": "region", + "body": "# #region ConflictResolution [TYPE Class]\n# @BRIEF Schema for resolving merge conflicts.\nclass ConflictResolution(BaseModel):\n \"\"\"Schema for resolving merge conflicts.\"\"\"\n file_path: str\n resolution: str = Field(pattern=\"^(mine|theirs|manual)$\")\n content: str | None = None\n# #endregion ConflictResolution\n" }, { "contract_id": "MergeStatusSchema", @@ -20449,8 +18478,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MergeStatusSchema:Class]\n# @PURPOSE: Schema representing unfinished merge status for repository.\nclass MergeStatusSchema(BaseModel):\n has_unfinished_merge: bool\n repository_path: str\n git_dir: str\n current_branch: str\n merge_head: Optional[str] = None\n merge_message_preview: Optional[str] = None\n conflicts_count: int = 0\n# [/DEF:MergeStatusSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region MergeStatusSchema [TYPE Class]\n# @BRIEF Schema representing unfinished merge status for repository.\nclass MergeStatusSchema(BaseModel):\n has_unfinished_merge: bool\n repository_path: str\n git_dir: str\n current_branch: str\n merge_head: str | None = None\n merge_message_preview: str | None = None\n conflicts_count: int = 0\n# #endregion MergeStatusSchema\n" }, { "contract_id": "MergeConflictFileSchema", @@ -20475,8 +18504,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MergeConflictFileSchema:Class]\n# @PURPOSE: Schema describing one conflicted file with optional side snapshots.\nclass MergeConflictFileSchema(BaseModel):\n file_path: str\n mine: Optional[str] = None\n theirs: Optional[str] = None\n# [/DEF:MergeConflictFileSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region MergeConflictFileSchema [TYPE Class]\n# @BRIEF Schema describing one conflicted file with optional side snapshots.\nclass MergeConflictFileSchema(BaseModel):\n file_path: str\n mine: str | None = None\n theirs: str | None = None\n# #endregion MergeConflictFileSchema\n" }, { "contract_id": "MergeResolveRequest", @@ -20501,8 +18530,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MergeResolveRequest:Class]\n# @PURPOSE: Request schema for resolving one or multiple merge conflicts.\nclass MergeResolveRequest(BaseModel):\n resolutions: List[ConflictResolution] = Field(default_factory=list)\n# [/DEF:MergeResolveRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region MergeResolveRequest [TYPE Class]\n# @BRIEF Request schema for resolving one or multiple merge conflicts.\nclass MergeResolveRequest(BaseModel):\n resolutions: list[ConflictResolution] = Field(default_factory=list)\n# #endregion MergeResolveRequest\n" }, { "contract_id": "MergeContinueRequest", @@ -20527,8 +18556,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MergeContinueRequest:Class]\n# @PURPOSE: Request schema for finishing merge with optional explicit commit message.\nclass MergeContinueRequest(BaseModel):\n message: Optional[str] = None\n# [/DEF:MergeContinueRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region MergeContinueRequest [TYPE Class]\n# @BRIEF Request schema for finishing merge with optional explicit commit message.\nclass MergeContinueRequest(BaseModel):\n message: str | None = None\n# #endregion MergeContinueRequest\n" }, { "contract_id": "DeploymentEnvironmentSchema", @@ -20553,8 +18582,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DeploymentEnvironmentSchema:Class]\n# @PURPOSE: Schema for representing a target deployment environment.\nclass DeploymentEnvironmentSchema(BaseModel):\n \"\"\"Schema for representing a target deployment environment.\"\"\"\n id: str\n name: str\n superset_url: str\n is_active: bool\n\n class Config:\n from_attributes = True\n# [/DEF:DeploymentEnvironmentSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region DeploymentEnvironmentSchema [TYPE Class]\n# @BRIEF Schema for representing a target deployment environment.\nclass DeploymentEnvironmentSchema(BaseModel):\n \"\"\"Schema for representing a target deployment environment.\"\"\"\n id: str\n name: str\n superset_url: str\n is_active: bool\n\n class Config:\n from_attributes = True\n# #endregion DeploymentEnvironmentSchema\n" }, { "contract_id": "DeployRequest", @@ -20579,8 +18608,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DeployRequest:Class]\n# @PURPOSE: Schema for dashboard deployment requests.\nclass DeployRequest(BaseModel):\n \"\"\"Schema for deployment requests.\"\"\"\n environment_id: str\n# [/DEF:DeployRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region DeployRequest [TYPE Class]\n# @BRIEF Schema for dashboard deployment requests.\nclass DeployRequest(BaseModel):\n \"\"\"Schema for deployment requests.\"\"\"\n environment_id: str\n# #endregion DeployRequest\n" }, { "contract_id": "RepoInitRequest", @@ -20605,8 +18634,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RepoInitRequest:Class]\n# @PURPOSE: Schema for repository initialization requests.\nclass RepoInitRequest(BaseModel):\n \"\"\"Schema for repository initialization requests.\"\"\"\n config_id: str\n remote_url: str\n# [/DEF:RepoInitRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region RepoInitRequest [TYPE Class]\n# @BRIEF Schema for repository initialization requests.\nclass RepoInitRequest(BaseModel):\n \"\"\"Schema for repository initialization requests.\"\"\"\n config_id: str\n remote_url: str\n# #endregion RepoInitRequest\n" }, { "contract_id": "RepositoryBindingSchema", @@ -20631,8 +18660,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RepositoryBindingSchema:Class]\n# @PURPOSE: Schema describing repository-to-config binding and provider metadata.\nclass RepositoryBindingSchema(BaseModel):\n dashboard_id: int\n config_id: str\n provider: GitProvider\n remote_url: str\n local_path: str\n# [/DEF:RepositoryBindingSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region RepositoryBindingSchema [TYPE Class]\n# @BRIEF Schema describing repository-to-config binding and provider metadata.\nclass RepositoryBindingSchema(BaseModel):\n dashboard_id: int\n config_id: str\n provider: GitProvider\n remote_url: str\n local_path: str\n# #endregion RepositoryBindingSchema\n" }, { "contract_id": "RepoStatusBatchRequest", @@ -20657,8 +18686,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RepoStatusBatchRequest:Class]\n# @PURPOSE: Schema for requesting repository statuses for multiple dashboards in a single call.\nclass RepoStatusBatchRequest(BaseModel):\n dashboard_ids: List[int] = Field(default_factory=list, description=\"Dashboard IDs to resolve repository statuses for\")\n# [/DEF:RepoStatusBatchRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region RepoStatusBatchRequest [TYPE Class]\n# @BRIEF Schema for requesting repository statuses for multiple dashboards in a single call.\nclass RepoStatusBatchRequest(BaseModel):\n dashboard_ids: list[int] = Field(default_factory=list, description=\"Dashboard IDs to resolve repository statuses for\")\n# #endregion RepoStatusBatchRequest\n" }, { "contract_id": "RepoStatusBatchResponse", @@ -20683,8 +18712,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RepoStatusBatchResponse:Class]\n# @PURPOSE: Schema for returning repository statuses keyed by dashboard ID.\nclass RepoStatusBatchResponse(BaseModel):\n statuses: Dict[str, Dict[str, Any]]\n# [/DEF:RepoStatusBatchResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region RepoStatusBatchResponse [TYPE Class]\n# @BRIEF Schema for returning repository statuses keyed by dashboard ID.\nclass RepoStatusBatchResponse(BaseModel):\n statuses: dict[str, dict[str, Any]]\n# #endregion RepoStatusBatchResponse\n" }, { "contract_id": "GiteaRepoSchema", @@ -20709,8 +18738,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GiteaRepoSchema:Class]\n# @PURPOSE: Schema describing a Gitea repository.\nclass GiteaRepoSchema(BaseModel):\n name: str\n full_name: str\n private: bool = False\n clone_url: Optional[str] = None\n html_url: Optional[str] = None\n ssh_url: Optional[str] = None\n default_branch: Optional[str] = None\n# [/DEF:GiteaRepoSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region GiteaRepoSchema [TYPE Class]\n# @BRIEF Schema describing a Gitea repository.\nclass GiteaRepoSchema(BaseModel):\n name: str\n full_name: str\n private: bool = False\n clone_url: str | None = None\n html_url: str | None = None\n ssh_url: str | None = None\n default_branch: str | None = None\n# #endregion GiteaRepoSchema\n" }, { "contract_id": "GiteaRepoCreateRequest", @@ -20735,8 +18764,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GiteaRepoCreateRequest:Class]\n# @PURPOSE: Request schema for creating a Gitea repository.\nclass GiteaRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: Optional[str] = None\n auto_init: bool = True\n default_branch: Optional[str] = \"main\"\n# [/DEF:GiteaRepoCreateRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region GiteaRepoCreateRequest [TYPE Class]\n# @BRIEF Request schema for creating a Gitea repository.\nclass GiteaRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: str | None = None\n auto_init: bool = True\n default_branch: str | None = \"main\"\n# #endregion GiteaRepoCreateRequest\n" }, { "contract_id": "RemoteRepoSchema", @@ -20761,8 +18790,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RemoteRepoSchema:Class]\n# @PURPOSE: Provider-agnostic remote repository payload.\nclass RemoteRepoSchema(BaseModel):\n provider: GitProvider\n name: str\n full_name: str\n private: bool = False\n clone_url: Optional[str] = None\n html_url: Optional[str] = None\n ssh_url: Optional[str] = None\n default_branch: Optional[str] = None\n# [/DEF:RemoteRepoSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region RemoteRepoSchema [TYPE Class]\n# @BRIEF Provider-agnostic remote repository payload.\nclass RemoteRepoSchema(BaseModel):\n provider: GitProvider\n name: str\n full_name: str\n private: bool = False\n clone_url: str | None = None\n html_url: str | None = None\n ssh_url: str | None = None\n default_branch: str | None = None\n# #endregion RemoteRepoSchema\n" }, { "contract_id": "RemoteRepoCreateRequest", @@ -20787,8 +18816,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RemoteRepoCreateRequest:Class]\n# @PURPOSE: Provider-agnostic repository creation request.\nclass RemoteRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: Optional[str] = None\n auto_init: bool = True\n default_branch: Optional[str] = \"main\"\n# [/DEF:RemoteRepoCreateRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region RemoteRepoCreateRequest [TYPE Class]\n# @BRIEF Provider-agnostic repository creation request.\nclass RemoteRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: str | None = None\n auto_init: bool = True\n default_branch: str | None = \"main\"\n# #endregion RemoteRepoCreateRequest\n" }, { "contract_id": "PromoteRequest", @@ -20813,8 +18842,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PromoteRequest:Class]\n# @PURPOSE: Request schema for branch promotion workflow.\nclass PromoteRequest(BaseModel):\n from_branch: str = Field(..., min_length=1, max_length=255)\n to_branch: str = Field(..., min_length=1, max_length=255)\n mode: str = Field(default=\"mr\", pattern=\"^(mr|direct)$\")\n title: Optional[str] = None\n description: Optional[str] = None\n reason: Optional[str] = None\n draft: bool = False\n remove_source_branch: bool = False\n# [/DEF:PromoteRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #region PromoteRequest [TYPE Class]\n# @BRIEF Request schema for branch promotion workflow.\nclass PromoteRequest(BaseModel):\n from_branch: str = Field(..., min_length=1, max_length=255)\n to_branch: str = Field(..., min_length=1, max_length=255)\n mode: str = Field(default=\"mr\", pattern=\"^(mr|direct)$\")\n title: str | None = None\n description: str | None = None\n reason: str | None = None\n draft: bool = False\n remove_source_branch: bool = False\n# #endregion PromoteRequest\n" }, { "contract_id": "PromoteResponse", @@ -20839,26 +18868,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PromoteResponse:Class]\n# @PURPOSE: Response schema for promotion operation result.\nclass PromoteResponse(BaseModel):\n mode: str\n from_branch: str\n to_branch: str\n status: str\n url: Optional[str] = None\n reference_id: Optional[str] = None\n policy_violation: bool = False\n# [/DEF:PromoteResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region PromoteResponse [TYPE Class]\n# @BRIEF Response schema for promotion operation result.\nclass PromoteResponse(BaseModel):\n mode: str\n from_branch: str\n to_branch: str\n status: str\n url: str | None = None\n reference_id: str | None = None\n policy_violation: bool = False\n# #endregion PromoteResponse\n" }, { "contract_id": "health_router", "contract_type": "Module", "file_path": "backend/src/api/routes/health.py", "start_line": 1, - "end_line": 66, + "end_line": 65, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI/API", - "PURPOSE": "API endpoints for dashboard health monitoring and status aggregation.", - "SEMANTICS": [ - "health", - "monitoring", - "dashboards" - ] + "PURPOSE": "API endpoints for dashboard health monitoring and status aggregation." }, "relations": [ { @@ -20869,15 +18893,15 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:health_router:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: health, monitoring, dashboards\n# @PURPOSE: API endpoints for dashboard health monitoring and status aggregation.\n# @LAYER: UI/API\n# @RELATION: DEPENDS_ON -> health_service\n\nfrom fastapi import APIRouter, Depends, Query, HTTPException, status\nfrom typing import List, Optional\nfrom sqlalchemy.orm import Session\nfrom ...core.database import get_db\nfrom ...services.health_service import HealthService\nfrom ...schemas.health import HealthSummaryResponse\nfrom ...dependencies import has_permission, get_config_manager, get_task_manager\n\nrouter = APIRouter(prefix=\"/api/health\", tags=[\"Health\"])\n\n# [DEF:get_health_summary:Function]\n# @PURPOSE: Get aggregated health status for all dashboards.\n# @PRE: Caller has read permission for dashboard health view.\n# @POST: Returns HealthSummaryResponse.\n# @RELATION: CALLS -> backend.src.services.health_service.HealthService\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: Optional[str] = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# [/DEF:get_health_summary:Function]\n\n\n# [DEF:delete_health_report:Function]\n# @PURPOSE: Delete one persisted dashboard validation report from health summary.\n# @PRE: Caller has write permission for tasks/report maintenance.\n# @POST: Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION: CALLS -> backend.src.services.health_service.HealthService\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# [/DEF:delete_health_report:Function]\n\n# [/DEF:health_router:Module]\n" + "anchor_syntax": "region", + "body": "# #region health_router [C:3] [TYPE Module] [SEMANTICS fastapi, health, api, search, dashboard]\n# @BRIEF API endpoints for dashboard health monitoring and status aggregation.\n# @LAYER: UI/API\n# @RELATION DEPENDS_ON -> health_service\n\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...dependencies import get_config_manager, get_task_manager, has_permission\nfrom ...schemas.health import HealthSummaryResponse\nfrom ...services.health_service import HealthService\n\nrouter = APIRouter(prefix=\"/api/health\", tags=[\"Health\"])\n\n# #region get_health_summary [TYPE Function]\n# @BRIEF Get aggregated health status for all dashboards.\n# @PRE: Caller has read permission for dashboard health view.\n# @POST: Returns HealthSummaryResponse.\n# @RELATION CALLS -> backend.src.services.health_service.HealthService\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: str | None = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# #endregion get_health_summary\n\n\n# #region delete_health_report [TYPE Function]\n# @BRIEF Delete one persisted dashboard validation report from health summary.\n# @PRE: Caller has write permission for tasks/report maintenance.\n# @POST: Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION CALLS -> backend.src.services.health_service.HealthService\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# #endregion delete_health_report\n\n# #endregion health_router\n" }, { "contract_id": "get_health_summary", "contract_type": "Function", "file_path": "backend/src/api/routes/health.py", - "start_line": 18, - "end_line": 38, + "start_line": 17, + "end_line": 37, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20931,15 +18955,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_health_summary:Function]\n# @PURPOSE: Get aggregated health status for all dashboards.\n# @PRE: Caller has read permission for dashboard health view.\n# @POST: Returns HealthSummaryResponse.\n# @RELATION: CALLS -> backend.src.services.health_service.HealthService\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: Optional[str] = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# [/DEF:get_health_summary:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_health_summary [TYPE Function]\n# @BRIEF Get aggregated health status for all dashboards.\n# @PRE: Caller has read permission for dashboard health view.\n# @POST: Returns HealthSummaryResponse.\n# @RELATION CALLS -> backend.src.services.health_service.HealthService\n@router.get(\"/summary\", response_model=HealthSummaryResponse)\nasync def get_health_summary(\n environment_id: str | None = Query(None),\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n \"\"\"\n @PURPOSE: Get aggregated health status for all dashboards.\n @POST: Returns HealthSummaryResponse\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n return await service.get_health_summary(environment_id=environment_id)\n# #endregion get_health_summary\n" }, { "contract_id": "delete_health_report", "contract_type": "Function", "file_path": "backend/src/api/routes/health.py", - "start_line": 41, - "end_line": 64, + "start_line": 40, + "end_line": 63, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20993,26 +19017,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:delete_health_report:Function]\n# @PURPOSE: Delete one persisted dashboard validation report from health summary.\n# @PRE: Caller has write permission for tasks/report maintenance.\n# @POST: Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION: CALLS -> backend.src.services.health_service.HealthService\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# [/DEF:delete_health_report:Function]\n" + "anchor_syntax": "region", + "body": "# #region delete_health_report [TYPE Function]\n# @BRIEF Delete one persisted dashboard validation report from health summary.\n# @PRE: Caller has write permission for tasks/report maintenance.\n# @POST: Validation record is removed; linked task/logs are cleaned when available.\n# @RELATION CALLS -> backend.src.services.health_service.HealthService\n@router.delete(\"/summary/{record_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_health_report(\n record_id: str,\n db: Session = Depends(get_db),\n config_manager = Depends(get_config_manager),\n task_manager = Depends(get_task_manager),\n _ = Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n \"\"\"\n @PURPOSE: Delete a persisted dashboard validation report from health summary.\n @POST: Validation record is removed; linked task/logs are deleted when present.\n \"\"\"\n if not config_manager.get_config().settings.features.health_monitor:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Health monitor feature is disabled\")\n service = HealthService(db, config_manager=config_manager)\n if not service.delete_validation_report(record_id, task_manager=task_manager):\n raise HTTPException(status_code=404, detail=\"Health report not found\")\n return\n# #endregion delete_health_report\n" }, { "contract_id": "LlmRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/llm.py", "start_line": 1, - "end_line": 481, + "end_line": 421, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI (API)", - "PURPOSE": "API routes for LLM provider configuration and management.", - "SEMANTICS": [ - "api", - "routes", - "llm" - ] + "PURPOSE": "API routes for LLM provider configuration and management." }, "relations": [ { @@ -21041,15 +19060,42 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:LlmRoutes:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: api, routes, llm\n# @PURPOSE: API routes for LLM provider configuration and management.\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n# @RELATION: DEPENDS_ON -> [get_current_user]\n# @RELATION: DEPENDS_ON -> [get_db]\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom typing import List, Optional\nimport httpx\nfrom ...core.cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"LlmRoutes\")\nfrom ...schemas.auth import User\nfrom ...dependencies import get_current_user as get_current_active_user\nfrom ...plugins.llm_analysis.models import (\n LLMProviderConfig,\n LLMProviderType,\n FetchModelsRequest,\n FetchModelsResponse,\n)\nfrom ...services.llm_provider import LLMProviderService\nfrom ...core.database import get_db\nfrom sqlalchemy.orm import Session\n\n# [DEF:router:Global]\n# @PURPOSE: APIRouter instance for LLM routes.\nrouter = APIRouter(tags=[\"LLM\"])\n# [/DEF:router:Global]\n\n\n# [DEF:_is_valid_runtime_api_key:Function]\n# @PURPOSE: Validate decrypted runtime API key presence/shape.\n# @PRE: value can be None.\n# @POST: Returns True only for non-placeholder key.\n# @RELATION: BINDS_TO -> [LlmRoutes]\ndef _is_valid_runtime_api_key(value: Optional[str]) -> bool:\n key = (value or \"\").strip()\n if not key:\n return False\n if key in {\"********\", \"EMPTY_OR_NONE\"}:\n return False\n return len(key) >= 16\n\n\n# [/DEF:_is_valid_runtime_api_key:Function]\n\n\n# [DEF:_build_provider_auth_headers:Function]\n# @PURPOSE: Build auth headers for provider model listing based on provider type.\n# @PRE: provider_type is a valid LLMProviderType.\n# @POST: Returns dict of HTTP headers for the provider's model list API.\ndef _build_provider_auth_headers(api_key: Optional[str], provider_type: LLMProviderType) -> dict:\n headers = {}\n if not api_key:\n return headers\n if provider_type == LLMProviderType.KILO:\n headers[\"Authentication\"] = f\"Bearer {api_key}\"\n headers[\"X-API-Key\"] = api_key\n else:\n # OpenAI, OpenRouter — standard Bearer token\n headers[\"Authorization\"] = f\"Bearer {api_key}\"\n return headers\n\n\n# [/DEF:_build_provider_auth_headers:Function]\n\n\n# [DEF:get_providers:Function]\n# @PURPOSE: Retrieve all LLM provider configurations.\n# @PRE: User is authenticated.\n# @POST: Returns list of LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.get(\"/providers\", response_model=List[LLMProviderConfig])\nasync def get_providers(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n \"\"\"\n Get all LLM provider configurations.\n \"\"\"\n log.reason(\n f\"Fetching providers for user: {current_user.username}\"\n )\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n return [\n LLMProviderConfig(\n id=p.id,\n provider_type=LLMProviderType(p.provider_type),\n name=p.name,\n base_url=p.base_url,\n api_key=\"********\",\n default_model=p.default_model,\n is_active=p.is_active,\n )\n for p in providers\n ]\n\n\n# [/DEF:get_providers:Function]\n\n\n# [DEF:get_llm_status:Function]\n# @PURPOSE: Returns whether LLM runtime is configured for dashboard validation.\n# @PRE: User is authenticated.\n# @POST: configured=true only when an active provider with valid decrypted key exists.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: CALLS -> [_is_valid_runtime_api_key]\n@router.get(\"/status\")\nasync def get_llm_status(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n active_provider = next((p for p in providers if p.is_active), None)\n\n if not providers:\n return {\n \"configured\": False,\n \"reason\": \"no_providers_configured\",\n \"provider_count\": 0,\n \"active_provider_count\": 0,\n }\n\n if not active_provider:\n return {\n \"configured\": False,\n \"reason\": \"no_active_provider\",\n \"provider_count\": len(providers),\n \"active_provider_count\": 0,\n \"providers\": [\n {\n \"id\": provider.id,\n \"name\": provider.name,\n \"provider_type\": provider.provider_type,\n \"is_active\": bool(provider.is_active),\n }\n for provider in providers\n ],\n }\n\n api_key = service.get_decrypted_api_key(active_provider.id)\n if not _is_valid_runtime_api_key(api_key):\n return {\n \"configured\": False,\n \"reason\": \"invalid_api_key\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n return {\n \"configured\": True,\n \"reason\": \"ok\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n\n# [/DEF:get_llm_status:Function]\n\n\n# [DEF:create_provider:Function]\n# @PURPOSE: Create a new LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the created LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\n \"/providers\", response_model=LLMProviderConfig, status_code=status.HTTP_201_CREATED\n)\nasync def create_provider(\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Create a new LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.create_provider(config)\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=\"********\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# [/DEF:create_provider:Function]\n\n\n# [DEF:update_provider:Function]\n# @PURPOSE: Update an existing LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the updated LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.put(\"/providers/{provider_id}\", response_model=LLMProviderConfig)\nasync def update_provider(\n provider_id: str,\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Update an existing LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.update_provider(provider_id, config)\n if not provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=\"********\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# [/DEF:update_provider:Function]\n\n\n# [DEF:delete_provider:Function]\n# @PURPOSE: Delete an LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns success status.\n# @RELATION: CALLS -> [LLMProviderService]\n@router.delete(\"/providers/{provider_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_provider(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Delete an LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n if not service.delete_provider(provider_id):\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n return\n\n\n# [/DEF:delete_provider:Function]\n\n\n# [DEF:fetch_models_for_provider:Function]\n# @PURPOSE: Fetch available models from a provider's API endpoint.\n# @PRE: valid base_url and optional api_key/provider_id.\n# @POST: Returns list of model IDs sorted alphabetically.\n# @SIDE_EFFECT: Makes HTTP GET to multiple possible model endpoints.\n@router.post(\"/providers/fetch-models\", response_model=FetchModelsResponse)\nasync def fetch_models_for_provider(\n request: FetchModelsRequest,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Fetch available models from a provider's models endpoint.\n\n Resolves API key in priority order:\n 1. Direct api_key from request body\n 2. Decrypted api_key from DB via provider_id (for existing providers)\n 3. No auth (for local providers without auth)\n\n Tries multiple paths in order:\n 1. {base_url}/models (OpenAI-compatible, stripped of /v1)\n 2. {base_url}/v1/models (OpenAI-compatible, with /v1)\n 3. {base_url}/api/tags (Ollama format)\n Returns empty list if none respond — user can enter model name manually.\n \"\"\"\n log.reason(\n f\"Fetching models for {request.base_url}\"\n )\n\n # Resolve API key: prefer direct, fall back to stored key via provider_id\n api_key = request.api_key\n if not api_key and request.provider_id:\n try:\n svc = LLMProviderService(db)\n api_key = svc.get_decrypted_api_key(request.provider_id)\n except Exception:\n log.explore(f\"Could not resolve API key for provider {request.provider_id}\", error=\"API key resolution failed\")\n\n base = request.base_url.rstrip(\"/\")\n\n # Collect possible model list endpoints\n candidates = []\n\n # If base ends with /v1, try both /models (after stripping /v1) and /v1/models\n if base.endswith(\"/v1\"):\n candidates.append(base[:-3] + \"/models\") # base without /v1 + /models\n candidates.append(base + \"/models\") # base with /v1 + /models (e.g. /v1/models)\n else:\n candidates.append(base + \"/models\") # /models\n candidates.append(base + \"/v1/models\") # /v1/models\n\n candidates.append(base + \"/api/tags\") # Ollama format\n\n headers = _build_provider_auth_headers(api_key, request.provider_type)\n last_error = None\n\n try:\n async with httpx.AsyncClient(timeout=10.0) as client:\n for url in candidates:\n try:\n response = await client.get(url, headers=headers)\n\n if response.status_code != 200:\n if response.status_code != 404:\n last_error = f\"HTTP {response.status_code} from {url}: {response.text[:200]}\"\n continue\n\n body = response.json()\n raw = body.get(\"data\") if isinstance(body, dict) else body\n\n # Try OpenAI format: [{\"id\": \"model-name\", ...}, ...] or list of strings\n if isinstance(raw, list):\n models = sorted(\n m.get(\"id\", str(m)) if isinstance(m, dict) else str(m)\n for m in raw\n )\n if models:\n log.reason(\n f\"Found {len(models)} models at {url}\"\n )\n return FetchModelsResponse(\n models=models, total=len(models)\n )\n\n # Try Ollama format: {\"models\": [{\"name\": \"model-name\", ...}, ...]}\n if isinstance(body, dict):\n models_list = body.get(\"models\", [])\n if isinstance(models_list, list):\n models = sorted(\n m.get(\"name\", str(m)) if isinstance(m, dict) else str(m)\n for m in models_list\n )\n if models:\n log.reason(\n f\"Found {len(models)} Ollama models at {url}\"\n )\n return FetchModelsResponse(\n models=models, total=len(models)\n )\n\n except httpx.TimeoutException:\n last_error = f\"Timeout at {url}\"\n except httpx.ConnectError:\n last_error = f\"Cannot connect at {url}\"\n except Exception as e:\n last_error = f\"Error at {url}: {str(e)[:100]}\"\n\n # All endpoints failed — return empty list with no error (user can type manually)\n log.explore(f\"No model endpoint responded for {request.base_url}. Last error: {last_error}\", error=f\"No model endpoint responded for {request.base_url}\")\n return FetchModelsResponse(models=[], total=0)\n\n except Exception as e:\n log.explore(f\"Unexpected error for {request.base_url}: {e}\", error=str(e))\n return FetchModelsResponse(models=[], total=0)\n\n\n# [/DEF:fetch_models_for_provider:Function]\n\n\n# [DEF:test_connection:Function]\n# @PURPOSE: Test connection to an LLM provider.\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMClient]\n@router.post(\"/providers/{provider_id}/test\")\nasync def test_connection(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n log.reason(\n f\"Testing connection for provider_id: {provider_id}\"\n )\n \"\"\"\n Test connection to an LLM provider.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n service = LLMProviderService(db)\n db_provider = service.get_provider(provider_id)\n if not db_provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n api_key = service.get_decrypted_api_key(provider_id)\n\n # Check if API key was successfully decrypted\n if not api_key:\n log.explore(f\"Failed to decrypt API key for provider {provider_id}\", error=\"API key decryption failed\")\n raise HTTPException(\n status_code=500,\n detail=\"Failed to decrypt API key. The provider may have been encrypted with a different encryption key. Please update the provider with a new API key.\",\n )\n\n client = LLMClient(\n provider_type=LLMProviderType(db_provider.provider_type),\n api_key=api_key,\n base_url=db_provider.base_url,\n default_model=db_provider.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# [/DEF:test_connection:Function]\n\n\n# [DEF:test_provider_config:Function]\n# @PURPOSE: Test connection with a provided configuration (not yet saved).\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION: DEPENDS_ON -> [LLMClient]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\"/providers/test\")\nasync def test_provider_config(\n config: LLMProviderConfig, current_user: User = Depends(get_current_active_user)\n):\n \"\"\"\n Test connection with a provided configuration.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n log.reason(\n f\"Testing config for {config.name}\"\n )\n\n # Check if API key is provided\n if not config.api_key or config.api_key == \"********\":\n raise HTTPException(\n status_code=400, detail=\"API key is required for testing connection\"\n )\n\n client = LLMClient(\n provider_type=config.provider_type,\n api_key=config.api_key,\n base_url=config.base_url,\n default_model=config.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# [/DEF:test_provider_config:Function]\n\n# [/DEF:LlmRoutes:Module]\n" + "anchor_syntax": "region", + "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\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 [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 [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=\"********\",\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 [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 # 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 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 [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 [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=\"********\",\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 [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=\"********\",\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 [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 [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 [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\n if not config.api_key or config.api_key == \"********\":\n raise HTTPException(\n status_code=400, detail=\"API key is required for testing connection\"\n )\n\n client = LLMClient(\n provider_type=config.provider_type,\n api_key=config.api_key,\n base_url=config.base_url,\n default_model=config.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# #endregion test_provider_config\n\n# #endregion LlmRoutes\n" + }, + { + "contract_id": "FetchModelsRequest", + "contract_type": "Class", + "file_path": "backend/src/api/routes/llm.py", + "start_line": 22, + "end_line": 31, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Pydantic request model for the fetch-models endpoint." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "router", "contract_type": "Global", "file_path": "backend/src/api/routes/llm.py", - "start_line": 29, - "end_line": 32, + "start_line": 33, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21085,15 +19131,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:router:Global]\n# @PURPOSE: APIRouter instance for LLM routes.\nrouter = APIRouter(tags=[\"LLM\"])\n# [/DEF:router:Global]\n" + "anchor_syntax": "region", + "body": "# #region router [TYPE Global]\n# @BRIEF APIRouter instance for LLM routes.\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": 35, - "end_line": 49, + "start_line": 39, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21147,61 +19193,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_is_valid_runtime_api_key:Function]\n# @PURPOSE: Validate decrypted runtime API key presence/shape.\n# @PRE: value can be None.\n# @POST: Returns True only for non-placeholder key.\n# @RELATION: BINDS_TO -> [LlmRoutes]\ndef _is_valid_runtime_api_key(value: Optional[str]) -> bool:\n key = (value or \"\").strip()\n if not key:\n return False\n if key in {\"********\", \"EMPTY_OR_NONE\"}:\n return False\n return len(key) >= 16\n\n\n# [/DEF:_is_valid_runtime_api_key:Function]\n" - }, - { - "contract_id": "_build_provider_auth_headers", - "contract_type": "Function", - "file_path": "backend/src/api/routes/llm.py", - "start_line": 52, - "end_line": 69, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns dict of HTTP headers for the provider's model list API.", - "PRE": "provider_type is a valid LLMProviderType.", - "PURPOSE": "Build auth headers for provider model listing based on provider type." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_build_provider_auth_headers:Function]\n# @PURPOSE: Build auth headers for provider model listing based on provider type.\n# @PRE: provider_type is a valid LLMProviderType.\n# @POST: Returns dict of HTTP headers for the provider's model list API.\ndef _build_provider_auth_headers(api_key: Optional[str], provider_type: LLMProviderType) -> dict:\n headers = {}\n if not api_key:\n return headers\n if provider_type == LLMProviderType.KILO:\n headers[\"Authentication\"] = f\"Bearer {api_key}\"\n headers[\"X-API-Key\"] = api_key\n else:\n # OpenAI, OpenRouter — standard Bearer token\n headers[\"Authorization\"] = f\"Bearer {api_key}\"\n return headers\n\n\n# [/DEF:_build_provider_auth_headers:Function]\n" + "anchor_syntax": "region", + "body": "# #region _is_valid_runtime_api_key [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" }, { "contract_id": "get_providers", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 72, - "end_line": 104, + "start_line": 56, + "end_line": 89, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21261,15 +19261,83 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_providers:Function]\n# @PURPOSE: Retrieve all LLM provider configurations.\n# @PRE: User is authenticated.\n# @POST: Returns list of LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.get(\"/providers\", response_model=List[LLMProviderConfig])\nasync def get_providers(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n \"\"\"\n Get all LLM provider configurations.\n \"\"\"\n log.reason(\n f\"Fetching providers for user: {current_user.username}\"\n )\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n return [\n LLMProviderConfig(\n id=p.id,\n provider_type=LLMProviderType(p.provider_type),\n name=p.name,\n base_url=p.base_url,\n api_key=\"********\",\n default_model=p.default_model,\n is_active=p.is_active,\n )\n for p in providers\n ]\n\n\n# [/DEF:get_providers:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_providers [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=\"********\",\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" + }, + { + "contract_id": "fetch_models", + "contract_type": "Function", + "file_path": "backend/src/api/routes/llm.py", + "start_line": 92, + "end_line": 159, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Returns a list of available model IDs.", + "PRE": "User is authenticated. Either provider_id or base_url+provider_type must be provided.", + "PURPOSE": "Fetch available models from an LLM provider by base_url+provider_type, or by provider_id." + }, + "relations": [ + { + "source_id": "fetch_models", + "relation_type": "CALLS", + "target_id": "LLMProviderService", + "target_ref": "[LLMProviderService]" + }, + { + "source_id": "fetch_models", + "relation_type": "CALLS", + "target_id": "LLMClient", + "target_ref": "[LLMClient]" + } + ], + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "body": "# #region fetch_models [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 # 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 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" }, { "contract_id": "get_llm_status", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 107, - "end_line": 175, + "start_line": 162, + "end_line": 230, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21329,15 +19397,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_llm_status:Function]\n# @PURPOSE: Returns whether LLM runtime is configured for dashboard validation.\n# @PRE: User is authenticated.\n# @POST: configured=true only when an active provider with valid decrypted key exists.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: CALLS -> [_is_valid_runtime_api_key]\n@router.get(\"/status\")\nasync def get_llm_status(\n current_user: User = Depends(get_current_active_user), db: Session = Depends(get_db)\n):\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n active_provider = next((p for p in providers if p.is_active), None)\n\n if not providers:\n return {\n \"configured\": False,\n \"reason\": \"no_providers_configured\",\n \"provider_count\": 0,\n \"active_provider_count\": 0,\n }\n\n if not active_provider:\n return {\n \"configured\": False,\n \"reason\": \"no_active_provider\",\n \"provider_count\": len(providers),\n \"active_provider_count\": 0,\n \"providers\": [\n {\n \"id\": provider.id,\n \"name\": provider.name,\n \"provider_type\": provider.provider_type,\n \"is_active\": bool(provider.is_active),\n }\n for provider in providers\n ],\n }\n\n api_key = service.get_decrypted_api_key(active_provider.id)\n if not _is_valid_runtime_api_key(api_key):\n return {\n \"configured\": False,\n \"reason\": \"invalid_api_key\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n return {\n \"configured\": True,\n \"reason\": \"ok\",\n \"provider_count\": len(providers),\n \"active_provider_count\": len(\n [provider for provider in providers if provider.is_active]\n ),\n \"provider_id\": active_provider.id,\n \"provider_name\": active_provider.name,\n \"provider_type\": active_provider.provider_type,\n \"default_model\": active_provider.default_model,\n }\n\n\n# [/DEF:get_llm_status:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_llm_status [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" }, { "contract_id": "create_provider", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 178, - "end_line": 208, + "start_line": 233, + "end_line": 263, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21397,15 +19465,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:create_provider:Function]\n# @PURPOSE: Create a new LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the created LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\n \"/providers\", response_model=LLMProviderConfig, status_code=status.HTTP_201_CREATED\n)\nasync def create_provider(\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Create a new LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.create_provider(config)\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=\"********\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# [/DEF:create_provider:Function]\n" + "anchor_syntax": "region", + "body": "# #region create_provider [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=\"********\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# #endregion create_provider\n" }, { "contract_id": "update_provider", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 211, - "end_line": 243, + "start_line": 266, + "end_line": 298, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21465,15 +19533,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_provider:Function]\n# @PURPOSE: Update an existing LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns the updated LLMProviderConfig.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.put(\"/providers/{provider_id}\", response_model=LLMProviderConfig)\nasync def update_provider(\n provider_id: str,\n config: LLMProviderConfig,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Update an existing LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n provider = service.update_provider(provider_id, config)\n if not provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n return LLMProviderConfig(\n id=provider.id,\n provider_type=LLMProviderType(provider.provider_type),\n name=provider.name,\n base_url=provider.base_url,\n api_key=\"********\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# [/DEF:update_provider:Function]\n" + "anchor_syntax": "region", + "body": "# #region update_provider [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=\"********\",\n default_model=provider.default_model,\n is_active=provider.is_active,\n )\n\n\n# #endregion update_provider\n" }, { "contract_id": "delete_provider", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 246, - "end_line": 266, + "start_line": 301, + "end_line": 321, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21527,71 +19595,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:delete_provider:Function]\n# @PURPOSE: Delete an LLM provider configuration.\n# @PRE: User is authenticated and has admin permissions.\n# @POST: Returns success status.\n# @RELATION: CALLS -> [LLMProviderService]\n@router.delete(\"/providers/{provider_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_provider(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Delete an LLM provider configuration.\n \"\"\"\n service = LLMProviderService(db)\n if not service.delete_provider(provider_id):\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n return\n\n\n# [/DEF:delete_provider:Function]\n" - }, - { - "contract_id": "fetch_models_for_provider", - "contract_type": "Function", - "file_path": "backend/src/api/routes/llm.py", - "start_line": 269, - "end_line": 385, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns list of model IDs sorted alphabetically.", - "PRE": "valid base_url and optional api_key/provider_id.", - "PURPOSE": "Fetch available models from a provider's API endpoint.", - "SIDE_EFFECT": "Makes HTTP GET to multiple possible model endpoints." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:fetch_models_for_provider:Function]\n# @PURPOSE: Fetch available models from a provider's API endpoint.\n# @PRE: valid base_url and optional api_key/provider_id.\n# @POST: Returns list of model IDs sorted alphabetically.\n# @SIDE_EFFECT: Makes HTTP GET to multiple possible model endpoints.\n@router.post(\"/providers/fetch-models\", response_model=FetchModelsResponse)\nasync def fetch_models_for_provider(\n request: FetchModelsRequest,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n \"\"\"\n Fetch available models from a provider's models endpoint.\n\n Resolves API key in priority order:\n 1. Direct api_key from request body\n 2. Decrypted api_key from DB via provider_id (for existing providers)\n 3. No auth (for local providers without auth)\n\n Tries multiple paths in order:\n 1. {base_url}/models (OpenAI-compatible, stripped of /v1)\n 2. {base_url}/v1/models (OpenAI-compatible, with /v1)\n 3. {base_url}/api/tags (Ollama format)\n Returns empty list if none respond — user can enter model name manually.\n \"\"\"\n log.reason(\n f\"Fetching models for {request.base_url}\"\n )\n\n # Resolve API key: prefer direct, fall back to stored key via provider_id\n api_key = request.api_key\n if not api_key and request.provider_id:\n try:\n svc = LLMProviderService(db)\n api_key = svc.get_decrypted_api_key(request.provider_id)\n except Exception:\n log.explore(f\"Could not resolve API key for provider {request.provider_id}\", error=\"API key resolution failed\")\n\n base = request.base_url.rstrip(\"/\")\n\n # Collect possible model list endpoints\n candidates = []\n\n # If base ends with /v1, try both /models (after stripping /v1) and /v1/models\n if base.endswith(\"/v1\"):\n candidates.append(base[:-3] + \"/models\") # base without /v1 + /models\n candidates.append(base + \"/models\") # base with /v1 + /models (e.g. /v1/models)\n else:\n candidates.append(base + \"/models\") # /models\n candidates.append(base + \"/v1/models\") # /v1/models\n\n candidates.append(base + \"/api/tags\") # Ollama format\n\n headers = _build_provider_auth_headers(api_key, request.provider_type)\n last_error = None\n\n try:\n async with httpx.AsyncClient(timeout=10.0) as client:\n for url in candidates:\n try:\n response = await client.get(url, headers=headers)\n\n if response.status_code != 200:\n if response.status_code != 404:\n last_error = f\"HTTP {response.status_code} from {url}: {response.text[:200]}\"\n continue\n\n body = response.json()\n raw = body.get(\"data\") if isinstance(body, dict) else body\n\n # Try OpenAI format: [{\"id\": \"model-name\", ...}, ...] or list of strings\n if isinstance(raw, list):\n models = sorted(\n m.get(\"id\", str(m)) if isinstance(m, dict) else str(m)\n for m in raw\n )\n if models:\n log.reason(\n f\"Found {len(models)} models at {url}\"\n )\n return FetchModelsResponse(\n models=models, total=len(models)\n )\n\n # Try Ollama format: {\"models\": [{\"name\": \"model-name\", ...}, ...]}\n if isinstance(body, dict):\n models_list = body.get(\"models\", [])\n if isinstance(models_list, list):\n models = sorted(\n m.get(\"name\", str(m)) if isinstance(m, dict) else str(m)\n for m in models_list\n )\n if models:\n log.reason(\n f\"Found {len(models)} Ollama models at {url}\"\n )\n return FetchModelsResponse(\n models=models, total=len(models)\n )\n\n except httpx.TimeoutException:\n last_error = f\"Timeout at {url}\"\n except httpx.ConnectError:\n last_error = f\"Cannot connect at {url}\"\n except Exception as e:\n last_error = f\"Error at {url}: {str(e)[:100]}\"\n\n # All endpoints failed — return empty list with no error (user can type manually)\n log.explore(f\"No model endpoint responded for {request.base_url}. Last error: {last_error}\", error=f\"No model endpoint responded for {request.base_url}\")\n return FetchModelsResponse(models=[], total=0)\n\n except Exception as e:\n log.explore(f\"Unexpected error for {request.base_url}: {e}\", error=str(e))\n return FetchModelsResponse(models=[], total=0)\n\n\n# [/DEF:fetch_models_for_provider:Function]\n" + "anchor_syntax": "region", + "body": "# #region delete_provider [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" }, { "contract_id": "test_connection", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 388, - "end_line": 437, + "start_line": 324, + "end_line": 376, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21651,15 +19663,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_connection:Function]\n# @PURPOSE: Test connection to an LLM provider.\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION: CALLS -> [LLMProviderService]\n# @RELATION: DEPENDS_ON -> [LLMClient]\n@router.post(\"/providers/{provider_id}/test\")\nasync def test_connection(\n provider_id: str,\n current_user: User = Depends(get_current_active_user),\n db: Session = Depends(get_db),\n):\n log.reason(\n f\"Testing connection for provider_id: {provider_id}\"\n )\n \"\"\"\n Test connection to an LLM provider.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n service = LLMProviderService(db)\n db_provider = service.get_provider(provider_id)\n if not db_provider:\n raise HTTPException(status_code=404, detail=\"Provider not found\")\n\n api_key = service.get_decrypted_api_key(provider_id)\n\n # Check if API key was successfully decrypted\n if not api_key:\n log.explore(f\"Failed to decrypt API key for provider {provider_id}\", error=\"API key decryption failed\")\n raise HTTPException(\n status_code=500,\n detail=\"Failed to decrypt API key. The provider may have been encrypted with a different encryption key. Please update the provider with a new API key.\",\n )\n\n client = LLMClient(\n provider_type=LLMProviderType(db_provider.provider_type),\n api_key=api_key,\n base_url=db_provider.base_url,\n default_model=db_provider.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# [/DEF:test_connection:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_connection [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" }, { "contract_id": "test_provider_config", "contract_type": "Function", "file_path": "backend/src/api/routes/llm.py", - "start_line": 440, - "end_line": 479, + "start_line": 379, + "end_line": 419, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21719,28 +19731,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_provider_config:Function]\n# @PURPOSE: Test connection with a provided configuration (not yet saved).\n# @PRE: User is authenticated.\n# @POST: Returns success status and message.\n# @RELATION: DEPENDS_ON -> [LLMClient]\n# @RELATION: DEPENDS_ON -> [LLMProviderConfig]\n@router.post(\"/providers/test\")\nasync def test_provider_config(\n config: LLMProviderConfig, current_user: User = Depends(get_current_active_user)\n):\n \"\"\"\n Test connection with a provided configuration.\n \"\"\"\n from ...plugins.llm_analysis.service import LLMClient\n\n log.reason(\n f\"Testing config for {config.name}\"\n )\n\n # Check if API key is provided\n if not config.api_key or config.api_key == \"********\":\n raise HTTPException(\n status_code=400, detail=\"API key is required for testing connection\"\n )\n\n client = LLMClient(\n provider_type=config.provider_type,\n api_key=config.api_key,\n base_url=config.base_url,\n default_model=config.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# [/DEF:test_provider_config:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_provider_config [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\n if not config.api_key or config.api_key == \"********\":\n raise HTTPException(\n status_code=400, detail=\"API key is required for testing connection\"\n )\n\n client = LLMClient(\n provider_type=config.provider_type,\n api_key=config.api_key,\n base_url=config.base_url,\n default_model=config.default_model,\n )\n\n try:\n await client.test_runtime_connection()\n return {\"success\": True, \"message\": \"Connection successful\"}\n except Exception as e:\n return {\"success\": False, \"error\": str(e)}\n\n\n# #endregion test_provider_config\n" }, { "contract_id": "MappingsApi", "contract_type": "Module", "file_path": "backend/src/api/routes/mappings.py", "start_line": 1, - "end_line": 130, + "end_line": 127, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Mappings are persisted in the SQLite database.", "LAYER": "API", - "PURPOSE": "API endpoints for managing database mappings and getting suggestions.", - "SEMANTICS": [ - "api", - "mappings", - "database", - "fuzzy-matching" - ] + "PURPOSE": "API endpoints for managing database mappings and getting suggestions." }, "relations": [ { @@ -21773,57 +19779,57 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MappingsApi:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: api, mappings, database, fuzzy-matching\n# @PURPOSE: API endpoints for managing database mappings and getting suggestions.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [AppDependencies]\n# @RELATION: DEPENDS_ON -> [DatabaseModule]\n# @RELATION: DEPENDS_ON -> [mapping_service]\n#\n# @INVARIANT: Mappings are persisted in the SQLite database.\n\n# [SECTION: IMPORTS]\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom typing import List, Optional\nfrom ...core.logger import belief_scope\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...core.database import get_db\nfrom ...models.mapping import DatabaseMapping\nfrom pydantic import BaseModel\n# [/SECTION]\n\nrouter = APIRouter(tags=[\"mappings\"])\n\n# [DEF:MappingCreate:DataClass]\nclass MappingCreate(BaseModel):\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: Optional[str] = None\n# [/DEF:MappingCreate:DataClass]\n\n# [DEF:MappingResponse:DataClass]\nclass MappingResponse(BaseModel):\n id: str\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: Optional[str] = None\n\n class Config:\n from_attributes = True\n# [/DEF:MappingResponse:DataClass]\n\n# [DEF:SuggestRequest:DataClass]\nclass SuggestRequest(BaseModel):\n source_env_id: str\n target_env_id: str\n# [/DEF:SuggestRequest:DataClass]\n\n# [DEF:get_mappings:Function]\n# @PURPOSE: List all saved database mappings.\n# @PRE: db session is injected.\n# @POST: Returns filtered list of DatabaseMapping records.\n@router.get(\"\", response_model=List[MappingResponse])\nasync def get_mappings(\n source_env_id: Optional[str] = None,\n target_env_id: Optional[str] = None,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"get_mappings\"):\n query = db.query(DatabaseMapping)\n if source_env_id:\n query = query.filter(DatabaseMapping.source_env_id == source_env_id)\n if target_env_id:\n query = query.filter(DatabaseMapping.target_env_id == target_env_id)\n return query.all()\n# [/DEF:get_mappings:Function]\n\n# [DEF:create_mapping:Function]\n# @PURPOSE: Create or update a database mapping.\n# @PRE: mapping is valid MappingCreate, db session is injected.\n# @POST: DatabaseMapping created or updated in database.\n@router.post(\"\", response_model=MappingResponse)\nasync def create_mapping(\n mapping: MappingCreate,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"create_mapping\"):\n # Check if mapping already exists\n existing = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == mapping.source_env_id,\n DatabaseMapping.target_env_id == mapping.target_env_id,\n DatabaseMapping.source_db_uuid == mapping.source_db_uuid\n ).first()\n \n if existing:\n existing.target_db_uuid = mapping.target_db_uuid\n existing.target_db_name = mapping.target_db_name\n existing.engine = mapping.engine\n db.commit()\n db.refresh(existing)\n return existing\n \n new_mapping = DatabaseMapping(**mapping.dict())\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n# [/DEF:create_mapping:Function]\n\n# [DEF:suggest_mappings_api:Function]\n# @PURPOSE: Get suggested mappings based on fuzzy matching.\n# @PRE: request is valid SuggestRequest, config_manager is injected.\n# @POST: Returns mapping suggestions.\n@router.post(\"/suggest\")\nasync def suggest_mappings_api(\n request: SuggestRequest,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"suggest_mappings_api\"):\n from ...services.mapping_service import MappingService\n service = MappingService(config_manager)\n try:\n return await service.get_suggestions(request.source_env_id, request.target_env_id)\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n# [/DEF:suggest_mappings_api:Function]\n\n# [/DEF:MappingsApi:Module]\n" + "anchor_syntax": "region", + "body": "# #region MappingsApi [C:3] [TYPE Module] [SEMANTICS fastapi, mapping, api, mapping-create]\n#\n# @BRIEF API endpoints for managing database mappings and getting suggestions.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [DatabaseModule]\n# @RELATION DEPENDS_ON -> [mapping_service]\n#\n# @INVARIANT: Mappings are persisted in the SQLite database.\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...models.mapping import DatabaseMapping\n\nrouter = APIRouter(tags=[\"mappings\"])\n\n# #region MappingCreate [TYPE DataClass]\nclass MappingCreate(BaseModel):\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n# #endregion MappingCreate\n\n# #region MappingResponse [TYPE DataClass]\nclass MappingResponse(BaseModel):\n id: str\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion MappingResponse\n\n# #region SuggestRequest [TYPE DataClass]\nclass SuggestRequest(BaseModel):\n source_env_id: str\n target_env_id: str\n# #endregion SuggestRequest\n\n# #region get_mappings [TYPE Function]\n# @BRIEF List all saved database mappings.\n# @PRE: db session is injected.\n# @POST: Returns filtered list of DatabaseMapping records.\n@router.get(\"\", response_model=list[MappingResponse])\nasync def get_mappings(\n source_env_id: str | None = None,\n target_env_id: str | None = None,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"get_mappings\"):\n query = db.query(DatabaseMapping)\n if source_env_id:\n query = query.filter(DatabaseMapping.source_env_id == source_env_id)\n if target_env_id:\n query = query.filter(DatabaseMapping.target_env_id == target_env_id)\n return query.all()\n# #endregion get_mappings\n\n# #region create_mapping [TYPE Function]\n# @BRIEF Create or update a database mapping.\n# @PRE: mapping is valid MappingCreate, db session is injected.\n# @POST: DatabaseMapping created or updated in database.\n@router.post(\"\", response_model=MappingResponse)\nasync def create_mapping(\n mapping: MappingCreate,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"create_mapping\"):\n # Check if mapping already exists\n existing = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == mapping.source_env_id,\n DatabaseMapping.target_env_id == mapping.target_env_id,\n DatabaseMapping.source_db_uuid == mapping.source_db_uuid\n ).first()\n\n if existing:\n existing.target_db_uuid = mapping.target_db_uuid\n existing.target_db_name = mapping.target_db_name\n existing.engine = mapping.engine\n db.commit()\n db.refresh(existing)\n return existing\n\n new_mapping = DatabaseMapping(**mapping.dict())\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n# #endregion create_mapping\n\n# #region suggest_mappings_api [TYPE Function]\n# @BRIEF Get suggested mappings based on fuzzy matching.\n# @PRE: request is valid SuggestRequest, config_manager is injected.\n# @POST: Returns mapping suggestions.\n@router.post(\"/suggest\")\nasync def suggest_mappings_api(\n request: SuggestRequest,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"suggest_mappings_api\"):\n from ...services.mapping_service import MappingService\n service = MappingService(config_manager)\n try:\n return await service.get_suggestions(request.source_env_id, request.target_env_id)\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n# #endregion suggest_mappings_api\n\n# #endregion MappingsApi\n" }, { "contract_id": "MappingCreate", "contract_type": "DataClass", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 26, - "end_line": 35, + "start_line": 23, + "end_line": 32, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:MappingCreate:DataClass]\nclass MappingCreate(BaseModel):\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: Optional[str] = None\n# [/DEF:MappingCreate:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region MappingCreate [TYPE DataClass]\nclass MappingCreate(BaseModel):\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n# #endregion MappingCreate\n" }, { "contract_id": "MappingResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 37, - "end_line": 50, + "start_line": 34, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:MappingResponse:DataClass]\nclass MappingResponse(BaseModel):\n id: str\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: Optional[str] = None\n\n class Config:\n from_attributes = True\n# [/DEF:MappingResponse:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region MappingResponse [TYPE DataClass]\nclass MappingResponse(BaseModel):\n id: str\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion MappingResponse\n" }, { "contract_id": "SuggestRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 52, - "end_line": 56, + "start_line": 49, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SuggestRequest:DataClass]\nclass SuggestRequest(BaseModel):\n source_env_id: str\n target_env_id: str\n# [/DEF:SuggestRequest:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region SuggestRequest [TYPE DataClass]\nclass SuggestRequest(BaseModel):\n source_env_id: str\n target_env_id: str\n# #endregion SuggestRequest\n" }, { "contract_id": "get_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 58, - "end_line": 76, + "start_line": 55, + "end_line": 73, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21861,15 +19867,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_mappings:Function]\n# @PURPOSE: List all saved database mappings.\n# @PRE: db session is injected.\n# @POST: Returns filtered list of DatabaseMapping records.\n@router.get(\"\", response_model=List[MappingResponse])\nasync def get_mappings(\n source_env_id: Optional[str] = None,\n target_env_id: Optional[str] = None,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"get_mappings\"):\n query = db.query(DatabaseMapping)\n if source_env_id:\n query = query.filter(DatabaseMapping.source_env_id == source_env_id)\n if target_env_id:\n query = query.filter(DatabaseMapping.target_env_id == target_env_id)\n return query.all()\n# [/DEF:get_mappings:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_mappings [TYPE Function]\n# @BRIEF List all saved database mappings.\n# @PRE: db session is injected.\n# @POST: Returns filtered list of DatabaseMapping records.\n@router.get(\"\", response_model=list[MappingResponse])\nasync def get_mappings(\n source_env_id: str | None = None,\n target_env_id: str | None = None,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"get_mappings\"):\n query = db.query(DatabaseMapping)\n if source_env_id:\n query = query.filter(DatabaseMapping.source_env_id == source_env_id)\n if target_env_id:\n query = query.filter(DatabaseMapping.target_env_id == target_env_id)\n return query.all()\n# #endregion get_mappings\n" }, { "contract_id": "create_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 78, - "end_line": 109, + "start_line": 75, + "end_line": 106, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21907,15 +19913,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:create_mapping:Function]\n# @PURPOSE: Create or update a database mapping.\n# @PRE: mapping is valid MappingCreate, db session is injected.\n# @POST: DatabaseMapping created or updated in database.\n@router.post(\"\", response_model=MappingResponse)\nasync def create_mapping(\n mapping: MappingCreate,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"create_mapping\"):\n # Check if mapping already exists\n existing = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == mapping.source_env_id,\n DatabaseMapping.target_env_id == mapping.target_env_id,\n DatabaseMapping.source_db_uuid == mapping.source_db_uuid\n ).first()\n \n if existing:\n existing.target_db_uuid = mapping.target_db_uuid\n existing.target_db_name = mapping.target_db_name\n existing.engine = mapping.engine\n db.commit()\n db.refresh(existing)\n return existing\n \n new_mapping = DatabaseMapping(**mapping.dict())\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n# [/DEF:create_mapping:Function]\n" + "anchor_syntax": "region", + "body": "# #region create_mapping [TYPE Function]\n# @BRIEF Create or update a database mapping.\n# @PRE: mapping is valid MappingCreate, db session is injected.\n# @POST: DatabaseMapping created or updated in database.\n@router.post(\"\", response_model=MappingResponse)\nasync def create_mapping(\n mapping: MappingCreate,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"create_mapping\"):\n # Check if mapping already exists\n existing = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == mapping.source_env_id,\n DatabaseMapping.target_env_id == mapping.target_env_id,\n DatabaseMapping.source_db_uuid == mapping.source_db_uuid\n ).first()\n\n if existing:\n existing.target_db_uuid = mapping.target_db_uuid\n existing.target_db_name = mapping.target_db_name\n existing.engine = mapping.engine\n db.commit()\n db.refresh(existing)\n return existing\n\n new_mapping = DatabaseMapping(**mapping.dict())\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n# #endregion create_mapping\n" }, { "contract_id": "suggest_mappings_api", "contract_type": "Function", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 111, - "end_line": 128, + "start_line": 108, + "end_line": 125, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21953,32 +19959,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:suggest_mappings_api:Function]\n# @PURPOSE: Get suggested mappings based on fuzzy matching.\n# @PRE: request is valid SuggestRequest, config_manager is injected.\n# @POST: Returns mapping suggestions.\n@router.post(\"/suggest\")\nasync def suggest_mappings_api(\n request: SuggestRequest,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"suggest_mappings_api\"):\n from ...services.mapping_service import MappingService\n service = MappingService(config_manager)\n try:\n return await service.get_suggestions(request.source_env_id, request.target_env_id)\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n# [/DEF:suggest_mappings_api:Function]\n" + "anchor_syntax": "region", + "body": "# #region suggest_mappings_api [TYPE Function]\n# @BRIEF Get suggested mappings based on fuzzy matching.\n# @PRE: request is valid SuggestRequest, config_manager is injected.\n# @POST: Returns mapping suggestions.\n@router.post(\"/suggest\")\nasync def suggest_mappings_api(\n request: SuggestRequest,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"suggest_mappings_api\"):\n from ...services.mapping_service import MappingService\n service = MappingService(config_manager)\n try:\n return await service.get_suggestions(request.source_env_id, request.target_env_id)\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n# #endregion suggest_mappings_api\n" }, { "contract_id": "MigrationApi", "contract_type": "Module", "file_path": "backend/src/api/routes/migration.py", "start_line": 1, - "end_line": 399, + "end_line": 394, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "[DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]", "INVARIANT": "Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.", "LAYER": "Infra", "POST": "Migration tasks are enqueued or dry-run results are computed and returned.", "PRE": "Backend core services initialized and Database session available.", "PURPOSE": "HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints.", - "SEMANTICS": [ - "api", - "migration", - "dashboards", - "sync", - "dry-run" - ], "SIDE_EFFECT": "Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.", "TEST_CONTRACT": "[DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]", "TEST_EDGE": "[external_fail] ->[HTTP_500]", @@ -22112,19 +20111,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationApi:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: api, migration, dashboards, sync, dry-run\n# @PURPOSE: HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON ->[AppDependencies]\n# @RELATION: DEPENDS_ON ->[DatabaseModule]\n# @RELATION: DEPENDS_ON ->[DashboardSelection]\n# @RELATION: DEPENDS_ON ->[DashboardMetadata]\n# @RELATION: DEPENDS_ON ->[MigrationDryRunService]\n# @RELATION: DEPENDS_ON ->[IdMappingService]\n# @RELATION: DEPENDS_ON ->[ResourceMapping]\n# @INVARIANT: Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.\n# @PRE: Backend core services initialized and Database session available.\n# @POST: Migration tasks are enqueued or dry-run results are computed and returned.\n# @SIDE_EFFECT: Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.\n# @DATA_CONTRACT: [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]\n# @TEST_CONTRACT: [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]\n# @TEST_SCENARIO: [invalid_environment] -> [HTTP_400_or_404]\n# @TEST_SCENARIO: [valid_execution] -> [success_payload_with_required_fields]\n# @TEST_EDGE: [missing_field] ->[HTTP_400]\n# @TEST_EDGE: [invalid_type] ->[validation_error]\n# @TEST_EDGE: [external_fail] ->[HTTP_500]\n# @TEST_INVARIANT: [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom typing import List, Dict, Any, Optional, cast\nfrom sqlalchemy.orm import Session\nfrom ...dependencies import get_config_manager, get_task_manager, has_permission\nfrom ...core.database import get_db\nfrom ...models.dashboard import DashboardMetadata, DashboardSelection\nfrom ...core.superset_client import SupersetClient\nfrom ...core.logger import logger, belief_scope\nfrom ...core.migration.dry_run_orchestrator import MigrationDryRunService\nfrom ...core.mapping_service import IdMappingService\nfrom ...models.mapping import ResourceMapping\n\nlogger = cast(Any, logger)\n\nrouter = APIRouter(prefix=\"/api\", tags=[\"migration\"])\n\n\n# [DEF:get_dashboards:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Fetch dashboard metadata from a requested environment for migration selection UI.\n# @PRE: env_id is provided and exists in configured environments.\n# @POST: Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.\n# @SIDE_EFFECT: Reads environment configuration and performs remote Superset metadata retrieval over network.\n# @DATA_CONTRACT: Input[str env_id] -> Output[List[DashboardMetadata]]\n# @RELATION: CALLS ->[SupersetClient.get_dashboards_summary]\n@router.get(\"/environments/{env_id}/dashboards\", response_model=List[DashboardMetadata])\nasync def get_dashboards(\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"get_dashboards\", f\"env_id={env_id}\"):\n logger.reason(f\"Fetching dashboards for environment: {env_id}\")\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n\n if not env:\n logger.explore(f\"Environment {env_id} not found in configuration\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary()\n logger.reflect(f\"Retrieved {len(dashboards)} dashboards from {env_id}\")\n return dashboards\n\n\n# [/DEF:get_dashboards:Function]\n\n\n# [DEF:execute_migration:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Validate migration selection and enqueue asynchronous migration task execution.\n# @PRE: DashboardSelection payload is valid and both source/target environments exist.\n# @POST: Returns {\"task_id\": str, \"message\": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.\n# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]]\n# @RELATION: CALLS ->[create_task]\n# @RELATION: DEPENDS_ON ->[DashboardSelection]\n# @INVARIANT: Migration task dispatch never occurs before source and target environment ids pass guard validation.\n@router.post(\"/migration/execute\")\nasync def execute_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"execute_migration\"):\n logger.reason(\n f\"Initiating migration from {selection.source_env_id} to {selection.target_env_id}\"\n )\n\n # Validate environments exist\n environments = config_manager.get_environments()\n env_ids = {e.id for e in environments}\n\n if (\n selection.source_env_id not in env_ids\n or selection.target_env_id not in env_ids\n ):\n logger.explore(\n \"Invalid environment selection\",\n extra={\n \"source\": selection.source_env_id,\n \"target\": selection.target_env_id,\n },\n )\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n # Include replace_db_config and fix_cross_filters in the task parameters\n task_params = selection.dict()\n task_params[\"replace_db_config\"] = selection.replace_db_config\n task_params[\"fix_cross_filters\"] = selection.fix_cross_filters\n\n logger.reason(\n f\"Creating migration task with {len(selection.selected_ids)} dashboards\"\n )\n\n try:\n task = await task_manager.create_task(\"superset-migration\", task_params)\n logger.reflect(f\"Migration task created: {task.id}\")\n return {\"task_id\": task.id, \"message\": \"Migration initiated\"}\n except Exception as e:\n logger.explore(f\"Task creation failed: {e}\")\n raise HTTPException(\n status_code=500, detail=f\"Failed to create migration task: {str(e)}\"\n )\n\n\n# [/DEF:execute_migration:Function]\n\n\n# [DEF:dry_run_migration:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Build pre-flight migration diff and risk summary without mutating target systems.\n# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.\n# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.\n# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]]\n# @RELATION: DEPENDS_ON ->[DashboardSelection]\n# @RELATION: DEPENDS_ON ->[MigrationDryRunService]\n# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution.\n@router.post(\"/migration/dry-run\", response_model=Dict[str, Any])\nasync def dry_run_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"dry_run_migration\"):\n logger.reason(\n f\"Starting dry run: {selection.source_env_id} -> {selection.target_env_id}\"\n )\n\n environments = config_manager.get_environments()\n env_map = {env.id: env for env in environments}\n source_env = env_map.get(selection.source_env_id)\n target_env = env_map.get(selection.target_env_id)\n\n if not source_env or not target_env:\n logger.explore(\"Invalid environment selection for dry run\")\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n if selection.source_env_id == selection.target_env_id:\n logger.explore(\"Source and target environments are identical\")\n raise HTTPException(\n status_code=400,\n detail=\"Source and target environments must be different\",\n )\n\n if not selection.selected_ids:\n logger.explore(\"No dashboards selected for dry run\")\n raise HTTPException(\n status_code=400, detail=\"No dashboards selected for dry run\"\n )\n\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n\n try:\n result = service.run(\n selection=selection,\n source_client=source_client,\n target_client=target_client,\n db=db,\n )\n logger.reflect(\"Dry run analysis complete\")\n return result\n except ValueError as exc:\n logger.explore(f\"Dry run orchestrator failed: {exc}\")\n raise HTTPException(status_code=500, detail=str(exc)) from exc\n\n\n# [/DEF:dry_run_migration:Function]\n\n\n# [DEF:get_migration_settings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Read and return configured migration synchronization cron expression.\n# @PRE: Configuration store is available and requester has READ permission.\n# @POST: Returns {\"cron\": str} reflecting current persisted settings value.\n# @SIDE_EFFECT: Reads configuration from config manager.\n# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]]\n# @RELATION: DEPENDS_ON ->[AppDependencies]\n@router.get(\"/migration/settings\", response_model=Dict[str, str])\nasync def get_migration_settings(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_migration_settings\"):\n config = config_manager.get_config()\n cron = config.settings.migration_sync_cron\n return {\"cron\": cron}\n\n\n# [/DEF:get_migration_settings:Function]\n\n\n# [DEF:update_migration_settings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Validate and persist migration synchronization cron expression update.\n# @PRE: Payload includes \"cron\" key and requester has WRITE permission.\n# @POST: Returns {\"cron\": str, \"status\": \"updated\"} and persists updated cron value.\n# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager.\n# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]]\n# @RELATION: DEPENDS_ON ->[AppDependencies]\n@router.put(\"/migration/settings\", response_model=Dict[str, str])\nasync def update_migration_settings(\n payload: Dict[str, str],\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"WRITE\")),\n):\n with belief_scope(\"update_migration_settings\"):\n if \"cron\" not in payload:\n raise HTTPException(\n status_code=400, detail=\"Missing 'cron' field in payload\"\n )\n\n cron_expr = payload[\"cron\"]\n\n config = config_manager.get_config()\n config.settings.migration_sync_cron = cron_expr\n config_manager.save_config(config)\n\n return {\"cron\": cron_expr, \"status\": \"updated\"}\n\n\n# [/DEF:update_migration_settings:Function]\n\n\n# [DEF:get_resource_mappings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.\n# @PRE: skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.\n# @POST: Returns {\"items\": [...], \"total\": int} where items reflect applied filters and pagination.\n# @SIDE_EFFECT: Executes database read queries against ResourceMapping table.\n# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]]\n# @RELATION: DEPENDS_ON ->[ResourceMapping]\n@router.get(\"/migration/mappings-data\", response_model=Dict[str, Any])\nasync def get_resource_mappings(\n skip: int = Query(0, ge=0),\n limit: int = Query(50, ge=1, le=500),\n search: Optional[str] = Query(None, description=\"Search by resource name or UUID\"),\n env_id: Optional[str] = Query(None, description=\"Filter by environment ID\"),\n resource_type: Optional[str] = Query(None, description=\"Filter by resource type\"),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_resource_mappings\"):\n query = db.query(ResourceMapping)\n\n if env_id:\n query = query.filter(ResourceMapping.environment_id == env_id)\n\n if resource_type:\n query = query.filter(ResourceMapping.resource_type == resource_type.upper())\n\n if search:\n search_term = f\"%{search}%\"\n query = query.filter(\n (ResourceMapping.resource_name.ilike(search_term))\n | (ResourceMapping.uuid.ilike(search_term))\n )\n\n total = query.count()\n mappings = (\n query.order_by(ResourceMapping.resource_type, ResourceMapping.resource_name)\n .offset(skip)\n .limit(limit)\n .all()\n )\n\n items = []\n for m in mappings:\n mapping = cast(Any, m)\n resource_type_value = (\n mapping.resource_type.value\n if mapping.resource_type is not None\n else None\n )\n last_synced_at = (\n mapping.last_synced_at.isoformat()\n if mapping.last_synced_at is not None\n else None\n )\n items.append(\n {\n \"id\": mapping.id,\n \"environment_id\": mapping.environment_id,\n \"resource_type\": resource_type_value,\n \"uuid\": mapping.uuid,\n \"remote_id\": mapping.remote_integer_id,\n \"resource_name\": mapping.resource_name,\n \"last_synced_at\": last_synced_at,\n }\n )\n\n return {\"items\": items, \"total\": total}\n\n\n# [/DEF:get_resource_mappings:Function]\n\n\n# [DEF:trigger_sync_now:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Trigger immediate ID synchronization for every configured environment.\n# @PRE: At least one environment is configured and requester has EXECUTE permission.\n# @POST: Returns sync summary with synced/failed counts after attempting all environments.\n# @SIDE_EFFECT: Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.\n# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]]\n# @RELATION: DEPENDS_ON ->[IdMappingService]\n# @RELATION: CALLS ->[sync_environment]\n@router.post(\"/migration/sync-now\", response_model=Dict[str, Any])\nasync def trigger_sync_now(\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"trigger_sync_now\"):\n from ...models.mapping import Environment as EnvironmentModel\n\n config = config_manager.get_config()\n environments = config.environments\n\n if not environments:\n raise HTTPException(status_code=400, detail=\"No environments configured\")\n\n # Ensure each environment exists in DB (upsert) to satisfy FK constraints\n for env in environments:\n existing = db.query(EnvironmentModel).filter_by(id=env.id).first()\n if not existing:\n db_env = EnvironmentModel(\n id=env.id,\n name=env.name,\n url=env.url,\n credentials_id=env.id, # Use env.id as credentials reference\n )\n db.add(db_env)\n logger.reason(\n f\"Created environment row for {env.id}\"\n )\n else:\n existing.name = env.name\n existing.url = env.url\n db.commit()\n\n service = IdMappingService(db)\n results = {\"synced\": [], \"failed\": []}\n\n for env in environments:\n try:\n client = SupersetClient(env)\n service.sync_environment(env.id, client)\n results[\"synced\"].append(env.id)\n logger.reason(f\"Synced environment {env.id}\")\n except Exception as e:\n results[\"failed\"].append({\"env_id\": env.id, \"error\": str(e)})\n logger.explore(f\"Failed to sync {env.id}\", error=str(e))\n\n return {\n \"status\": \"completed\",\n \"synced_count\": len(results[\"synced\"]),\n \"failed_count\": len(results[\"failed\"]),\n \"details\": results,\n }\n\n\n# [/DEF:trigger_sync_now:Function]\n\n# [/DEF:MigrationApi:Module]\n" + "anchor_syntax": "region", + "body": "# #region MigrationApi [C:5] [TYPE Module] [SEMANTICS fastapi, migration, api, sync, search, mapping]\n# @BRIEF HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [DatabaseModule]\n# @RELATION DEPENDS_ON -> [DashboardSelection]\n# @RELATION DEPENDS_ON -> [DashboardMetadata]\n# @RELATION DEPENDS_ON -> [MigrationDryRunService]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION DEPENDS_ON -> [ResourceMapping]\n# @INVARIANT: Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.\n# @PRE: Backend core services initialized and Database session available.\n# @POST: Migration tasks are enqueued or dry-run results are computed and returned.\n# @SIDE_EFFECT: Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.\n# @DATA_CONTRACT: [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]\n# @TEST_CONTRACT: [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]\n# @TEST_SCENARIO: [invalid_environment] -> [HTTP_400_or_404]\n# @TEST_SCENARIO: [valid_execution] -> [success_payload_with_required_fields]\n# @TEST_EDGE: [missing_field] ->[HTTP_400]\n# @TEST_EDGE: [invalid_type] ->[validation_error]\n# @TEST_EDGE: [external_fail] ->[HTTP_500]\n# @TEST_INVARIANT: [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]\n\nfrom typing import Any, cast\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...core.mapping_service import IdMappingService\nfrom ...core.migration.dry_run_orchestrator import MigrationDryRunService\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_task_manager, has_permission\nfrom ...models.dashboard import DashboardMetadata, DashboardSelection\nfrom ...models.mapping import ResourceMapping\n\nlogger = cast(Any, logger)\n\nrouter = APIRouter(prefix=\"/api\", tags=[\"migration\"])\n\n\n# #region get_dashboards [C:3] [TYPE Function]\n# @BRIEF Fetch dashboard metadata from a requested environment for migration selection UI.\n# @PRE: env_id is provided and exists in configured environments.\n# @POST: Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.\n# @SIDE_EFFECT: Reads environment configuration and performs remote Superset metadata retrieval over network.\n# @DATA_CONTRACT: Input[str env_id] -> Output[List[DashboardMetadata]]\n# @RELATION CALLS -> [SupersetClient.get_dashboards_summary]\n@router.get(\"/environments/{env_id}/dashboards\", response_model=list[DashboardMetadata])\nasync def get_dashboards(\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"get_dashboards\", f\"env_id={env_id}\"):\n logger.reason(f\"Fetching dashboards for environment: {env_id}\")\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n\n if not env:\n logger.explore(f\"Environment {env_id} not found in configuration\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary()\n logger.reflect(f\"Retrieved {len(dashboards)} dashboards from {env_id}\")\n return dashboards\n\n\n# #endregion get_dashboards\n\n\n# #region execute_migration [C:5] [TYPE Function]\n# @BRIEF Validate migration selection and enqueue asynchronous migration task execution.\n# @PRE: DashboardSelection payload is valid and both source/target environments exist.\n# @POST: Returns {\"task_id\": str, \"message\": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.\n# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]]\n# @RELATION CALLS -> [create_task]\n# @RELATION DEPENDS_ON -> [DashboardSelection]\n# @INVARIANT: Migration task dispatch never occurs before source and target environment ids pass guard validation.\n@router.post(\"/migration/execute\")\nasync def execute_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"execute_migration\"):\n logger.reason(\n f\"Initiating migration from {selection.source_env_id} to {selection.target_env_id}\"\n )\n\n # Validate environments exist\n environments = config_manager.get_environments()\n env_ids = {e.id for e in environments}\n\n if (\n selection.source_env_id not in env_ids\n or selection.target_env_id not in env_ids\n ):\n logger.explore(\n \"Invalid environment selection\",\n extra={\n \"source\": selection.source_env_id,\n \"target\": selection.target_env_id,\n },\n )\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n # Include replace_db_config and fix_cross_filters in the task parameters\n task_params = selection.dict()\n task_params[\"replace_db_config\"] = selection.replace_db_config\n task_params[\"fix_cross_filters\"] = selection.fix_cross_filters\n\n logger.reason(\n f\"Creating migration task with {len(selection.selected_ids)} dashboards\"\n )\n\n try:\n task = await task_manager.create_task(\"superset-migration\", task_params)\n logger.reflect(f\"Migration task created: {task.id}\")\n return {\"task_id\": task.id, \"message\": \"Migration initiated\"}\n except Exception as e:\n logger.explore(f\"Task creation failed: {e}\")\n raise HTTPException(\n status_code=500, detail=f\"Failed to create migration task: {e!s}\"\n )\n\n\n# #endregion execute_migration\n\n\n# #region dry_run_migration [C:5] [TYPE Function]\n# @BRIEF Build pre-flight migration diff and risk summary without mutating target systems.\n# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.\n# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.\n# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [DashboardSelection]\n# @RELATION DEPENDS_ON -> [MigrationDryRunService]\n# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution.\n@router.post(\"/migration/dry-run\", response_model=dict[str, Any])\nasync def dry_run_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"dry_run_migration\"):\n logger.reason(\n f\"Starting dry run: {selection.source_env_id} -> {selection.target_env_id}\"\n )\n\n environments = config_manager.get_environments()\n env_map = {env.id: env for env in environments}\n source_env = env_map.get(selection.source_env_id)\n target_env = env_map.get(selection.target_env_id)\n\n if not source_env or not target_env:\n logger.explore(\"Invalid environment selection for dry run\")\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n if selection.source_env_id == selection.target_env_id:\n logger.explore(\"Source and target environments are identical\")\n raise HTTPException(\n status_code=400,\n detail=\"Source and target environments must be different\",\n )\n\n if not selection.selected_ids:\n logger.explore(\"No dashboards selected for dry run\")\n raise HTTPException(\n status_code=400, detail=\"No dashboards selected for dry run\"\n )\n\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n\n try:\n result = service.run(\n selection=selection,\n source_client=source_client,\n target_client=target_client,\n db=db,\n )\n logger.reflect(\"Dry run analysis complete\")\n return result\n except ValueError as exc:\n logger.explore(f\"Dry run orchestrator failed: {exc}\")\n raise HTTPException(status_code=500, detail=str(exc)) from exc\n\n\n# #endregion dry_run_migration\n\n\n# #region get_migration_settings [C:3] [TYPE Function]\n# @BRIEF Read and return configured migration synchronization cron expression.\n# @PRE: Configuration store is available and requester has READ permission.\n# @POST: Returns {\"cron\": str} reflecting current persisted settings value.\n# @SIDE_EFFECT: Reads configuration from config manager.\n# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n@router.get(\"/migration/settings\", response_model=dict[str, str])\nasync def get_migration_settings(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_migration_settings\"):\n config = config_manager.get_config()\n cron = config.settings.migration_sync_cron\n return {\"cron\": cron}\n\n\n# #endregion get_migration_settings\n\n\n# #region update_migration_settings [C:3] [TYPE Function]\n# @BRIEF Validate and persist migration synchronization cron expression update.\n# @PRE: Payload includes \"cron\" key and requester has WRITE permission.\n# @POST: Returns {\"cron\": str, \"status\": \"updated\"} and persists updated cron value.\n# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager.\n# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n@router.put(\"/migration/settings\", response_model=dict[str, str])\nasync def update_migration_settings(\n payload: dict[str, str],\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"WRITE\")),\n):\n with belief_scope(\"update_migration_settings\"):\n if \"cron\" not in payload:\n raise HTTPException(\n status_code=400, detail=\"Missing 'cron' field in payload\"\n )\n\n cron_expr = payload[\"cron\"]\n\n config = config_manager.get_config()\n config.settings.migration_sync_cron = cron_expr\n config_manager.save_config(config)\n\n return {\"cron\": cron_expr, \"status\": \"updated\"}\n\n\n# #endregion update_migration_settings\n\n\n# #region get_resource_mappings [C:3] [TYPE Function]\n# @BRIEF Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.\n# @PRE: skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.\n# @POST: Returns {\"items\": [...], \"total\": int} where items reflect applied filters and pagination.\n# @SIDE_EFFECT: Executes database read queries against ResourceMapping table.\n# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [ResourceMapping]\n@router.get(\"/migration/mappings-data\", response_model=dict[str, Any])\nasync def get_resource_mappings(\n skip: int = Query(0, ge=0),\n limit: int = Query(50, ge=1, le=500),\n search: str | None = Query(None, description=\"Search by resource name or UUID\"),\n env_id: str | None = Query(None, description=\"Filter by environment ID\"),\n resource_type: str | None = Query(None, description=\"Filter by resource type\"),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_resource_mappings\"):\n query = db.query(ResourceMapping)\n\n if env_id:\n query = query.filter(ResourceMapping.environment_id == env_id)\n\n if resource_type:\n query = query.filter(ResourceMapping.resource_type == resource_type.upper())\n\n if search:\n search_term = f\"%{search}%\"\n query = query.filter(\n (ResourceMapping.resource_name.ilike(search_term))\n | (ResourceMapping.uuid.ilike(search_term))\n )\n\n total = query.count()\n mappings = (\n query.order_by(ResourceMapping.resource_type, ResourceMapping.resource_name)\n .offset(skip)\n .limit(limit)\n .all()\n )\n\n items = []\n for m in mappings:\n mapping = cast(Any, m)\n resource_type_value = (\n mapping.resource_type.value\n if mapping.resource_type is not None\n else None\n )\n last_synced_at = (\n mapping.last_synced_at.isoformat()\n if mapping.last_synced_at is not None\n else None\n )\n items.append(\n {\n \"id\": mapping.id,\n \"environment_id\": mapping.environment_id,\n \"resource_type\": resource_type_value,\n \"uuid\": mapping.uuid,\n \"remote_id\": mapping.remote_integer_id,\n \"resource_name\": mapping.resource_name,\n \"last_synced_at\": last_synced_at,\n }\n )\n\n return {\"items\": items, \"total\": total}\n\n\n# #endregion get_resource_mappings\n\n\n# #region trigger_sync_now [C:3] [TYPE Function]\n# @BRIEF Trigger immediate ID synchronization for every configured environment.\n# @PRE: At least one environment is configured and requester has EXECUTE permission.\n# @POST: Returns sync summary with synced/failed counts after attempting all environments.\n# @SIDE_EFFECT: Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.\n# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION CALLS -> [sync_environment]\n@router.post(\"/migration/sync-now\", response_model=dict[str, Any])\nasync def trigger_sync_now(\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"trigger_sync_now\"):\n from ...core.logger import logger\n from ...models.mapping import Environment as EnvironmentModel\n\n config = config_manager.get_config()\n environments = config.environments\n\n if not environments:\n raise HTTPException(status_code=400, detail=\"No environments configured\")\n\n # Ensure each environment exists in DB (upsert) to satisfy FK constraints\n for env in environments:\n existing = db.query(EnvironmentModel).filter_by(id=env.id).first()\n if not existing:\n db_env = EnvironmentModel(\n id=env.id,\n name=env.name,\n url=env.url,\n credentials_id=env.id, # Use env.id as credentials reference\n )\n db.add(db_env)\n logger.reason(\n f\"Created environment row for {env.id}\",\n extra={\"src\": \"trigger_sync_now\"},\n )\n else:\n existing.name = env.name\n existing.url = env.url\n db.commit()\n\n service = IdMappingService(db)\n results = {\"synced\": [], \"failed\": []}\n\n for env in environments:\n try:\n client = SupersetClient(env)\n service.sync_environment(env.id, client)\n results[\"synced\"].append(env.id)\n logger.reason(f\"Synced environment {env.id}\", extra={\"src\": \"trigger_sync_now\"})\n except Exception as e:\n results[\"failed\"].append({\"env_id\": env.id, \"error\": str(e)})\n logger.error(f\"[trigger_sync_now][Error] Failed to sync {env.id}: {e}\")\n\n return {\n \"status\": \"completed\",\n \"synced_count\": len(results[\"synced\"]),\n \"failed_count\": len(results[\"failed\"]),\n \"details\": results,\n }\n\n\n# #endregion trigger_sync_now\n\n# #endregion MigrationApi\n" }, { "contract_id": "execute_migration", "contract_type": "Function", "file_path": "backend/src/api/routes/migration.py", - "start_line": 75, - "end_line": 136, + "start_line": 74, + "end_line": 134, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[DashboardSelection] -> Output[Dict[str, str]]", "INVARIANT": "Migration task dispatch never occurs before source and target environment ids pass guard validation.", "POST": "Returns {\"task_id\": str, \"message\": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.", @@ -22166,19 +20165,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:execute_migration:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Validate migration selection and enqueue asynchronous migration task execution.\n# @PRE: DashboardSelection payload is valid and both source/target environments exist.\n# @POST: Returns {\"task_id\": str, \"message\": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.\n# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]]\n# @RELATION: CALLS ->[create_task]\n# @RELATION: DEPENDS_ON ->[DashboardSelection]\n# @INVARIANT: Migration task dispatch never occurs before source and target environment ids pass guard validation.\n@router.post(\"/migration/execute\")\nasync def execute_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"execute_migration\"):\n logger.reason(\n f\"Initiating migration from {selection.source_env_id} to {selection.target_env_id}\"\n )\n\n # Validate environments exist\n environments = config_manager.get_environments()\n env_ids = {e.id for e in environments}\n\n if (\n selection.source_env_id not in env_ids\n or selection.target_env_id not in env_ids\n ):\n logger.explore(\n \"Invalid environment selection\",\n extra={\n \"source\": selection.source_env_id,\n \"target\": selection.target_env_id,\n },\n )\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n # Include replace_db_config and fix_cross_filters in the task parameters\n task_params = selection.dict()\n task_params[\"replace_db_config\"] = selection.replace_db_config\n task_params[\"fix_cross_filters\"] = selection.fix_cross_filters\n\n logger.reason(\n f\"Creating migration task with {len(selection.selected_ids)} dashboards\"\n )\n\n try:\n task = await task_manager.create_task(\"superset-migration\", task_params)\n logger.reflect(f\"Migration task created: {task.id}\")\n return {\"task_id\": task.id, \"message\": \"Migration initiated\"}\n except Exception as e:\n logger.explore(f\"Task creation failed: {e}\")\n raise HTTPException(\n status_code=500, detail=f\"Failed to create migration task: {str(e)}\"\n )\n\n\n# [/DEF:execute_migration:Function]\n" + "anchor_syntax": "region", + "body": "# #region execute_migration [C:5] [TYPE Function]\n# @BRIEF Validate migration selection and enqueue asynchronous migration task execution.\n# @PRE: DashboardSelection payload is valid and both source/target environments exist.\n# @POST: Returns {\"task_id\": str, \"message\": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.\n# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]]\n# @RELATION CALLS -> [create_task]\n# @RELATION DEPENDS_ON -> [DashboardSelection]\n# @INVARIANT: Migration task dispatch never occurs before source and target environment ids pass guard validation.\n@router.post(\"/migration/execute\")\nasync def execute_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"execute_migration\"):\n logger.reason(\n f\"Initiating migration from {selection.source_env_id} to {selection.target_env_id}\"\n )\n\n # Validate environments exist\n environments = config_manager.get_environments()\n env_ids = {e.id for e in environments}\n\n if (\n selection.source_env_id not in env_ids\n or selection.target_env_id not in env_ids\n ):\n logger.explore(\n \"Invalid environment selection\",\n extra={\n \"source\": selection.source_env_id,\n \"target\": selection.target_env_id,\n },\n )\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n # Include replace_db_config and fix_cross_filters in the task parameters\n task_params = selection.dict()\n task_params[\"replace_db_config\"] = selection.replace_db_config\n task_params[\"fix_cross_filters\"] = selection.fix_cross_filters\n\n logger.reason(\n f\"Creating migration task with {len(selection.selected_ids)} dashboards\"\n )\n\n try:\n task = await task_manager.create_task(\"superset-migration\", task_params)\n logger.reflect(f\"Migration task created: {task.id}\")\n return {\"task_id\": task.id, \"message\": \"Migration initiated\"}\n except Exception as e:\n logger.explore(f\"Task creation failed: {e}\")\n raise HTTPException(\n status_code=500, detail=f\"Failed to create migration task: {e!s}\"\n )\n\n\n# #endregion execute_migration\n" }, { "contract_id": "dry_run_migration", "contract_type": "Function", "file_path": "backend/src/api/routes/migration.py", - "start_line": 139, - "end_line": 203, + "start_line": 137, + "end_line": 200, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[DashboardSelection] -> Output[Dict[str, Any]]", "INVARIANT": "Dry-run flow remains read-only and rejects identical source/target environments before service execution.", "POST": "Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.", @@ -22220,19 +20219,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:dry_run_migration:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Build pre-flight migration diff and risk summary without mutating target systems.\n# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.\n# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.\n# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]]\n# @RELATION: DEPENDS_ON ->[DashboardSelection]\n# @RELATION: DEPENDS_ON ->[MigrationDryRunService]\n# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution.\n@router.post(\"/migration/dry-run\", response_model=Dict[str, Any])\nasync def dry_run_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"dry_run_migration\"):\n logger.reason(\n f\"Starting dry run: {selection.source_env_id} -> {selection.target_env_id}\"\n )\n\n environments = config_manager.get_environments()\n env_map = {env.id: env for env in environments}\n source_env = env_map.get(selection.source_env_id)\n target_env = env_map.get(selection.target_env_id)\n\n if not source_env or not target_env:\n logger.explore(\"Invalid environment selection for dry run\")\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n if selection.source_env_id == selection.target_env_id:\n logger.explore(\"Source and target environments are identical\")\n raise HTTPException(\n status_code=400,\n detail=\"Source and target environments must be different\",\n )\n\n if not selection.selected_ids:\n logger.explore(\"No dashboards selected for dry run\")\n raise HTTPException(\n status_code=400, detail=\"No dashboards selected for dry run\"\n )\n\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n\n try:\n result = service.run(\n selection=selection,\n source_client=source_client,\n target_client=target_client,\n db=db,\n )\n logger.reflect(\"Dry run analysis complete\")\n return result\n except ValueError as exc:\n logger.explore(f\"Dry run orchestrator failed: {exc}\")\n raise HTTPException(status_code=500, detail=str(exc)) from exc\n\n\n# [/DEF:dry_run_migration:Function]\n" + "anchor_syntax": "region", + "body": "# #region dry_run_migration [C:5] [TYPE Function]\n# @BRIEF Build pre-flight migration diff and risk summary without mutating target systems.\n# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.\n# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.\n# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API.\n# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [DashboardSelection]\n# @RELATION DEPENDS_ON -> [MigrationDryRunService]\n# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution.\n@router.post(\"/migration/dry-run\", response_model=dict[str, Any])\nasync def dry_run_migration(\n selection: DashboardSelection,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"dry_run_migration\"):\n logger.reason(\n f\"Starting dry run: {selection.source_env_id} -> {selection.target_env_id}\"\n )\n\n environments = config_manager.get_environments()\n env_map = {env.id: env for env in environments}\n source_env = env_map.get(selection.source_env_id)\n target_env = env_map.get(selection.target_env_id)\n\n if not source_env or not target_env:\n logger.explore(\"Invalid environment selection for dry run\")\n raise HTTPException(\n status_code=400, detail=\"Invalid source or target environment\"\n )\n\n if selection.source_env_id == selection.target_env_id:\n logger.explore(\"Source and target environments are identical\")\n raise HTTPException(\n status_code=400,\n detail=\"Source and target environments must be different\",\n )\n\n if not selection.selected_ids:\n logger.explore(\"No dashboards selected for dry run\")\n raise HTTPException(\n status_code=400, detail=\"No dashboards selected for dry run\"\n )\n\n service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n\n try:\n result = service.run(\n selection=selection,\n source_client=source_client,\n target_client=target_client,\n db=db,\n )\n logger.reflect(\"Dry run analysis complete\")\n return result\n except ValueError as exc:\n logger.explore(f\"Dry run orchestrator failed: {exc}\")\n raise HTTPException(status_code=500, detail=str(exc)) from exc\n\n\n# #endregion dry_run_migration\n" }, { "contract_id": "get_migration_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/migration.py", - "start_line": 206, - "end_line": 225, + "start_line": 203, + "end_line": 221, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input[None] -> Output[Dict[str, str]]", "POST": "Returns {\"cron\": str} reflecting current persisted settings value.", "PRE": "Configuration store is available and requester has READ permission.", @@ -22285,19 +20284,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_migration_settings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Read and return configured migration synchronization cron expression.\n# @PRE: Configuration store is available and requester has READ permission.\n# @POST: Returns {\"cron\": str} reflecting current persisted settings value.\n# @SIDE_EFFECT: Reads configuration from config manager.\n# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]]\n# @RELATION: DEPENDS_ON ->[AppDependencies]\n@router.get(\"/migration/settings\", response_model=Dict[str, str])\nasync def get_migration_settings(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_migration_settings\"):\n config = config_manager.get_config()\n cron = config.settings.migration_sync_cron\n return {\"cron\": cron}\n\n\n# [/DEF:get_migration_settings:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_migration_settings [C:3] [TYPE Function]\n# @BRIEF Read and return configured migration synchronization cron expression.\n# @PRE: Configuration store is available and requester has READ permission.\n# @POST: Returns {\"cron\": str} reflecting current persisted settings value.\n# @SIDE_EFFECT: Reads configuration from config manager.\n# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n@router.get(\"/migration/settings\", response_model=dict[str, str])\nasync def get_migration_settings(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_migration_settings\"):\n config = config_manager.get_config()\n cron = config.settings.migration_sync_cron\n return {\"cron\": cron}\n\n\n# #endregion get_migration_settings\n" }, { "contract_id": "update_migration_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/migration.py", - "start_line": 228, - "end_line": 257, + "start_line": 224, + "end_line": 252, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input[Dict[str, str]] -> Output[Dict[str, str]]", "POST": "Returns {\"cron\": str, \"status\": \"updated\"} and persists updated cron value.", "PRE": "Payload includes \"cron\" key and requester has WRITE permission.", @@ -22350,19 +20349,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_migration_settings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Validate and persist migration synchronization cron expression update.\n# @PRE: Payload includes \"cron\" key and requester has WRITE permission.\n# @POST: Returns {\"cron\": str, \"status\": \"updated\"} and persists updated cron value.\n# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager.\n# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]]\n# @RELATION: DEPENDS_ON ->[AppDependencies]\n@router.put(\"/migration/settings\", response_model=Dict[str, str])\nasync def update_migration_settings(\n payload: Dict[str, str],\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"WRITE\")),\n):\n with belief_scope(\"update_migration_settings\"):\n if \"cron\" not in payload:\n raise HTTPException(\n status_code=400, detail=\"Missing 'cron' field in payload\"\n )\n\n cron_expr = payload[\"cron\"]\n\n config = config_manager.get_config()\n config.settings.migration_sync_cron = cron_expr\n config_manager.save_config(config)\n\n return {\"cron\": cron_expr, \"status\": \"updated\"}\n\n\n# [/DEF:update_migration_settings:Function]\n" + "anchor_syntax": "region", + "body": "# #region update_migration_settings [C:3] [TYPE Function]\n# @BRIEF Validate and persist migration synchronization cron expression update.\n# @PRE: Payload includes \"cron\" key and requester has WRITE permission.\n# @POST: Returns {\"cron\": str, \"status\": \"updated\"} and persists updated cron value.\n# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager.\n# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n@router.put(\"/migration/settings\", response_model=dict[str, str])\nasync def update_migration_settings(\n payload: dict[str, str],\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"WRITE\")),\n):\n with belief_scope(\"update_migration_settings\"):\n if \"cron\" not in payload:\n raise HTTPException(\n status_code=400, detail=\"Missing 'cron' field in payload\"\n )\n\n cron_expr = payload[\"cron\"]\n\n config = config_manager.get_config()\n config.settings.migration_sync_cron = cron_expr\n config_manager.save_config(config)\n\n return {\"cron\": cron_expr, \"status\": \"updated\"}\n\n\n# #endregion update_migration_settings\n" }, { "contract_id": "get_resource_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/migration.py", - "start_line": 260, - "end_line": 330, + "start_line": 255, + "end_line": 324, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input[QueryParams] -> Output[Dict[str, Any]]", "POST": "Returns {\"items\": [...], \"total\": int} where items reflect applied filters and pagination.", "PRE": "skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.", @@ -22415,19 +20414,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_resource_mappings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.\n# @PRE: skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.\n# @POST: Returns {\"items\": [...], \"total\": int} where items reflect applied filters and pagination.\n# @SIDE_EFFECT: Executes database read queries against ResourceMapping table.\n# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]]\n# @RELATION: DEPENDS_ON ->[ResourceMapping]\n@router.get(\"/migration/mappings-data\", response_model=Dict[str, Any])\nasync def get_resource_mappings(\n skip: int = Query(0, ge=0),\n limit: int = Query(50, ge=1, le=500),\n search: Optional[str] = Query(None, description=\"Search by resource name or UUID\"),\n env_id: Optional[str] = Query(None, description=\"Filter by environment ID\"),\n resource_type: Optional[str] = Query(None, description=\"Filter by resource type\"),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_resource_mappings\"):\n query = db.query(ResourceMapping)\n\n if env_id:\n query = query.filter(ResourceMapping.environment_id == env_id)\n\n if resource_type:\n query = query.filter(ResourceMapping.resource_type == resource_type.upper())\n\n if search:\n search_term = f\"%{search}%\"\n query = query.filter(\n (ResourceMapping.resource_name.ilike(search_term))\n | (ResourceMapping.uuid.ilike(search_term))\n )\n\n total = query.count()\n mappings = (\n query.order_by(ResourceMapping.resource_type, ResourceMapping.resource_name)\n .offset(skip)\n .limit(limit)\n .all()\n )\n\n items = []\n for m in mappings:\n mapping = cast(Any, m)\n resource_type_value = (\n mapping.resource_type.value\n if mapping.resource_type is not None\n else None\n )\n last_synced_at = (\n mapping.last_synced_at.isoformat()\n if mapping.last_synced_at is not None\n else None\n )\n items.append(\n {\n \"id\": mapping.id,\n \"environment_id\": mapping.environment_id,\n \"resource_type\": resource_type_value,\n \"uuid\": mapping.uuid,\n \"remote_id\": mapping.remote_integer_id,\n \"resource_name\": mapping.resource_name,\n \"last_synced_at\": last_synced_at,\n }\n )\n\n return {\"items\": items, \"total\": total}\n\n\n# [/DEF:get_resource_mappings:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_resource_mappings [C:3] [TYPE Function]\n# @BRIEF Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.\n# @PRE: skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.\n# @POST: Returns {\"items\": [...], \"total\": int} where items reflect applied filters and pagination.\n# @SIDE_EFFECT: Executes database read queries against ResourceMapping table.\n# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [ResourceMapping]\n@router.get(\"/migration/mappings-data\", response_model=dict[str, Any])\nasync def get_resource_mappings(\n skip: int = Query(0, ge=0),\n limit: int = Query(50, ge=1, le=500),\n search: str | None = Query(None, description=\"Search by resource name or UUID\"),\n env_id: str | None = Query(None, description=\"Filter by environment ID\"),\n resource_type: str | None = Query(None, description=\"Filter by resource type\"),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_resource_mappings\"):\n query = db.query(ResourceMapping)\n\n if env_id:\n query = query.filter(ResourceMapping.environment_id == env_id)\n\n if resource_type:\n query = query.filter(ResourceMapping.resource_type == resource_type.upper())\n\n if search:\n search_term = f\"%{search}%\"\n query = query.filter(\n (ResourceMapping.resource_name.ilike(search_term))\n | (ResourceMapping.uuid.ilike(search_term))\n )\n\n total = query.count()\n mappings = (\n query.order_by(ResourceMapping.resource_type, ResourceMapping.resource_name)\n .offset(skip)\n .limit(limit)\n .all()\n )\n\n items = []\n for m in mappings:\n mapping = cast(Any, m)\n resource_type_value = (\n mapping.resource_type.value\n if mapping.resource_type is not None\n else None\n )\n last_synced_at = (\n mapping.last_synced_at.isoformat()\n if mapping.last_synced_at is not None\n else None\n )\n items.append(\n {\n \"id\": mapping.id,\n \"environment_id\": mapping.environment_id,\n \"resource_type\": resource_type_value,\n \"uuid\": mapping.uuid,\n \"remote_id\": mapping.remote_integer_id,\n \"resource_name\": mapping.resource_name,\n \"last_synced_at\": last_synced_at,\n }\n )\n\n return {\"items\": items, \"total\": total}\n\n\n# #endregion get_resource_mappings\n" }, { "contract_id": "trigger_sync_now", "contract_type": "Function", "file_path": "backend/src/api/routes/migration.py", - "start_line": 333, - "end_line": 397, + "start_line": 327, + "end_line": 392, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input[None] -> Output[Dict[str, Any]]", "POST": "Returns sync summary with synced/failed counts after attempting all environments.", "PRE": "At least one environment is configured and requester has EXECUTE permission.", @@ -22486,27 +20485,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:trigger_sync_now:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Trigger immediate ID synchronization for every configured environment.\n# @PRE: At least one environment is configured and requester has EXECUTE permission.\n# @POST: Returns sync summary with synced/failed counts after attempting all environments.\n# @SIDE_EFFECT: Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.\n# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]]\n# @RELATION: DEPENDS_ON ->[IdMappingService]\n# @RELATION: CALLS ->[sync_environment]\n@router.post(\"/migration/sync-now\", response_model=Dict[str, Any])\nasync def trigger_sync_now(\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"trigger_sync_now\"):\n from ...models.mapping import Environment as EnvironmentModel\n\n config = config_manager.get_config()\n environments = config.environments\n\n if not environments:\n raise HTTPException(status_code=400, detail=\"No environments configured\")\n\n # Ensure each environment exists in DB (upsert) to satisfy FK constraints\n for env in environments:\n existing = db.query(EnvironmentModel).filter_by(id=env.id).first()\n if not existing:\n db_env = EnvironmentModel(\n id=env.id,\n name=env.name,\n url=env.url,\n credentials_id=env.id, # Use env.id as credentials reference\n )\n db.add(db_env)\n logger.reason(\n f\"Created environment row for {env.id}\"\n )\n else:\n existing.name = env.name\n existing.url = env.url\n db.commit()\n\n service = IdMappingService(db)\n results = {\"synced\": [], \"failed\": []}\n\n for env in environments:\n try:\n client = SupersetClient(env)\n service.sync_environment(env.id, client)\n results[\"synced\"].append(env.id)\n logger.reason(f\"Synced environment {env.id}\")\n except Exception as e:\n results[\"failed\"].append({\"env_id\": env.id, \"error\": str(e)})\n logger.explore(f\"Failed to sync {env.id}\", error=str(e))\n\n return {\n \"status\": \"completed\",\n \"synced_count\": len(results[\"synced\"]),\n \"failed_count\": len(results[\"failed\"]),\n \"details\": results,\n }\n\n\n# [/DEF:trigger_sync_now:Function]\n" + "anchor_syntax": "region", + "body": "# #region trigger_sync_now [C:3] [TYPE Function]\n# @BRIEF Trigger immediate ID synchronization for every configured environment.\n# @PRE: At least one environment is configured and requester has EXECUTE permission.\n# @POST: Returns sync summary with synced/failed counts after attempting all environments.\n# @SIDE_EFFECT: Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.\n# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION CALLS -> [sync_environment]\n@router.post(\"/migration/sync-now\", response_model=dict[str, Any])\nasync def trigger_sync_now(\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"trigger_sync_now\"):\n from ...core.logger import logger\n from ...models.mapping import Environment as EnvironmentModel\n\n config = config_manager.get_config()\n environments = config.environments\n\n if not environments:\n raise HTTPException(status_code=400, detail=\"No environments configured\")\n\n # Ensure each environment exists in DB (upsert) to satisfy FK constraints\n for env in environments:\n existing = db.query(EnvironmentModel).filter_by(id=env.id).first()\n if not existing:\n db_env = EnvironmentModel(\n id=env.id,\n name=env.name,\n url=env.url,\n credentials_id=env.id, # Use env.id as credentials reference\n )\n db.add(db_env)\n logger.reason(\n f\"Created environment row for {env.id}\",\n extra={\"src\": \"trigger_sync_now\"},\n )\n else:\n existing.name = env.name\n existing.url = env.url\n db.commit()\n\n service = IdMappingService(db)\n results = {\"synced\": [], \"failed\": []}\n\n for env in environments:\n try:\n client = SupersetClient(env)\n service.sync_environment(env.id, client)\n results[\"synced\"].append(env.id)\n logger.reason(f\"Synced environment {env.id}\", extra={\"src\": \"trigger_sync_now\"})\n except Exception as e:\n results[\"failed\"].append({\"env_id\": env.id, \"error\": str(e)})\n logger.error(f\"[trigger_sync_now][Error] Failed to sync {env.id}: {e}\")\n\n return {\n \"status\": \"completed\",\n \"synced_count\": len(results[\"synced\"]),\n \"failed_count\": len(results[\"failed\"]),\n \"details\": results,\n }\n\n\n# #endregion trigger_sync_now\n" }, { "contract_id": "PluginsRouter", "contract_type": "Module", "file_path": "backend/src/api/routes/plugins.py", "start_line": 1, - "end_line": 39, + "end_line": 36, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI (API)", - "PURPOSE": "Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.", - "SEMANTICS": [ - "api", - "router", - "plugins", - "list" - ] + "PURPOSE": "Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins." }, "relations": [ { @@ -22529,22 +20522,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:PluginsRouter:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: api, router, plugins, list\n# @PURPOSE: Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [PluginConfig]\n# @RELATION: DEPENDS_ON -> [get_plugin_loader]\n# @RELATION: BINDS_TO -> [API_Routes]\nfrom typing import List\nfrom fastapi import APIRouter, Depends\n\nfrom ...core.plugin_base import PluginConfig\nfrom ...dependencies import get_plugin_loader, has_permission\nfrom ...core.logger import belief_scope\n\nrouter = APIRouter()\n\n\n# [DEF:list_plugins:Function]\n# @PURPOSE: Retrieve a list of all available plugins.\n# @PRE: plugin_loader is injected via Depends.\n# @POST: Returns a list of PluginConfig objects.\n# @RETURN: List[PluginConfig] - List of registered plugins.\n# @RELATION: CALLS -> [get_plugin_loader]\n# @RELATION: DEPENDS_ON -> [PluginConfig]\n@router.get(\"\", response_model=List[PluginConfig])\nasync def list_plugins(\n plugin_loader=Depends(get_plugin_loader),\n _=Depends(has_permission(\"plugins\", \"READ\")),\n):\n with belief_scope(\"list_plugins\"):\n \"\"\"\n Retrieve a list of all available plugins.\n \"\"\"\n return plugin_loader.get_all_plugin_configs()\n\n\n# [/DEF:list_plugins:Function]\n# [/DEF:PluginsRouter:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "list_plugins", "contract_type": "Function", "file_path": "backend/src/api/routes/plugins.py", - "start_line": 19, - "end_line": 38, + "start_line": 17, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Returns a list of PluginConfig objects.", "PRE": "plugin_loader is injected via Depends.", - "PURPOSE": "Retrieve a list of all available plugins.", - "RETURN": "List[PluginConfig] - List of registered plugins." + "PURPOSE": "Retrieve a list of all available plugins." }, "relations": [ { @@ -22588,12 +20580,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -22604,29 +20590,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:list_plugins:Function]\n# @PURPOSE: Retrieve a list of all available plugins.\n# @PRE: plugin_loader is injected via Depends.\n# @POST: Returns a list of PluginConfig objects.\n# @RETURN: List[PluginConfig] - List of registered plugins.\n# @RELATION: CALLS -> [get_plugin_loader]\n# @RELATION: DEPENDS_ON -> [PluginConfig]\n@router.get(\"\", response_model=List[PluginConfig])\nasync def list_plugins(\n plugin_loader=Depends(get_plugin_loader),\n _=Depends(has_permission(\"plugins\", \"READ\")),\n):\n with belief_scope(\"list_plugins\"):\n \"\"\"\n Retrieve a list of all available plugins.\n \"\"\"\n return plugin_loader.get_all_plugin_configs()\n\n\n# [/DEF:list_plugins:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ProfileApiModule", "contract_type": "Module", "file_path": "backend/src/api/routes/profile.py", "start_line": 1, - "end_line": 154, + "end_line": 146, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Endpoints are self-scoped and never mutate another user preference.", "LAYER": "API", "PURPOSE": "Exposes self-scoped profile preference endpoints and environment-based Superset account lookup.", - "SEMANTICS": [ - "api", - "profile", - "preferences", - "self-service", - "account-lookup" - ], "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." @@ -22634,19 +20613,19 @@ "relations": [ { "source_id": "ProfileApiModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ProfileService", "target_ref": "[ProfileService]" }, { "source_id": "ProfileApiModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "get_current_user", "target_ref": "[get_current_user]" }, { "source_id": "ProfileApiModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "get_db", "target_ref": "[get_db]" } @@ -22720,71 +20699,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ProfileApiModule:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: api, profile, preferences, self-service, account-lookup\n# @PURPOSE: 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# [SECTION: IMPORTS]\nfrom typing import Optional\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\nfrom ...core.cot_logger import MarkerLogger\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# [/SECTION]\n\nlog = MarkerLogger(\"Profile\")\n\nrouter = APIRouter(prefix=\"/api/profile\", tags=[\"profile\"])\n\n\n# [DEF:_get_profile_service:Function]\n# @RELATION: CALLS -> ProfileService\n# @PURPOSE: 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# [/DEF:_get_profile_service:Function]\n\n\n# [DEF:get_preferences:Function]\n# @RELATION: CALLS -> ProfileService\n# @PURPOSE: 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 log.reason(\"Resolving current user preference\")\n service = _get_profile_service(db, config_manager, plugin_loader)\n return service.get_my_preference(current_user)\n# [/DEF:get_preferences:Function]\n\n\n# [DEF:update_preferences:Function]\n# @RELATION: CALLS -> ProfileService\n# @PURPOSE: 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 log.reason(\"Attempting preference save\")\n return service.update_my_preference(current_user=current_user, payload=payload)\n except ProfileValidationError as exc:\n log.reflect(\"Preference validation failed\")\n raise HTTPException(status_code=422, detail=exc.errors) from exc\n except ProfileAuthorizationError as exc:\n log.explore(\"Cross-user mutation guard blocked request\", error=\"User attempted to mutate another user's resource\")\n raise HTTPException(status_code=403, detail=str(exc)) from exc\n# [/DEF:update_preferences:Function]\n\n\n# [DEF:lookup_superset_accounts:Function]\n# @RELATION: CALLS -> ProfileService\n# @PURPOSE: 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: Optional[str] = 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 log.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 log.explore(\"Lookup request references unknown environment\", error=\"EnvironmentNotFoundError raised during Superset account lookup\")\n raise HTTPException(status_code=404, detail=str(exc)) from exc\n# [/DEF:lookup_superset_accounts:Function]\n\n# [/DEF:ProfileApiModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_get_profile_service", "contract_type": "Function", "file_path": "backend/src/api/routes/profile.py", - "start_line": 52, - "end_line": 63, + "start_line": 44, + "end_line": 55, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -22838,15 +20763,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_profile_service:Function]\n# @RELATION: CALLS -> ProfileService\n# @PURPOSE: 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# [/DEF:_get_profile_service:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_preferences", "contract_type": "Function", "file_path": "backend/src/api/routes/profile.py", - "start_line": 66, - "end_line": 82, + "start_line": 58, + "end_line": 74, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -22900,15 +20825,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_preferences:Function]\n# @RELATION: CALLS -> ProfileService\n# @PURPOSE: 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 log.reason(\"Resolving current user preference\")\n service = _get_profile_service(db, config_manager, plugin_loader)\n return service.get_my_preference(current_user)\n# [/DEF:get_preferences:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "update_preferences", "contract_type": "Function", "file_path": "backend/src/api/routes/profile.py", - "start_line": 85, - "end_line": 109, + "start_line": 77, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -22962,15 +20887,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_preferences:Function]\n# @RELATION: CALLS -> ProfileService\n# @PURPOSE: 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 log.reason(\"Attempting preference save\")\n return service.update_my_preference(current_user=current_user, payload=payload)\n except ProfileValidationError as exc:\n log.reflect(\"Preference validation failed\")\n raise HTTPException(status_code=422, detail=exc.errors) from exc\n except ProfileAuthorizationError as exc:\n log.explore(\"Cross-user mutation guard blocked request\", error=\"User attempted to mutate another user's resource\")\n raise HTTPException(status_code=403, detail=str(exc)) from exc\n# [/DEF:update_preferences:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "lookup_superset_accounts", "contract_type": "Function", "file_path": "backend/src/api/routes/profile.py", - "start_line": 112, - "end_line": 152, + "start_line": 104, + "end_line": 144, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -23024,57 +20949,49 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:lookup_superset_accounts:Function]\n# @RELATION: CALLS -> ProfileService\n# @PURPOSE: 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: Optional[str] = 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 log.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 log.explore(\"Lookup request references unknown environment\", error=\"EnvironmentNotFoundError raised during Superset account lookup\")\n raise HTTPException(status_code=404, detail=str(exc)) from exc\n# [/DEF:lookup_superset_accounts:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ReportsRouter", "contract_type": "Module", "file_path": "backend/src/api/routes/reports.py", "start_line": 1, - "end_line": 187, + "end_line": 175, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "[ReportQuery] -> [ReportCollection | ReportDetailView]", "INVARIANT": "Endpoints are read-only and do not trigger long-running tasks.", "LAYER": "UI (API)", "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.", - "SEMANTICS": [ - "api", - "reports", - "list", - "detail", - "pagination", - "filters" - ], "SIDE_EFFECT": "None" }, "relations": [ { "source_id": "ReportsRouter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ReportsService:Class", "target_ref": "[ReportsService:Class]" }, { "source_id": "ReportsRouter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "get_task_manager:Function", "target_ref": "[get_task_manager:Function]" }, { "source_id": "ReportsRouter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "get_clean_release_repository:Function", "target_ref": "[get_clean_release_repository:Function]" }, { "source_id": "ReportsRouter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "has_permission:Function", "target_ref": "[has_permission:Function]" } @@ -23097,98 +21014,24 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReportsRouter:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: api, reports, list, detail, pagination, filters\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom typing import List, Optional\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\n\nfrom ...dependencies import (\n get_task_manager,\n has_permission,\n get_clean_release_repository,\n)\nfrom ...core.task_manager import TaskManager\nfrom ...core.logger import belief_scope\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# [/SECTION]\n\nrouter = APIRouter(prefix=\"/api/reports\", tags=[\"Reports\"])\n\n\n# [DEF:_parse_csv_enum_list:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# @PARAM: raw (Optional[str]) - Comma-separated enum values.\n# @PARAM: enum_cls (type) - Enum class for validation.\n# @PARAM: field_name (str) - Query field name for diagnostics.\n# @RETURN: List - Parsed enum values.\n# @RELATION: BINDS_TO -> [ReportsRouter]\ndef _parse_csv_enum_list(raw: Optional[str], 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# [/DEF:_parse_csv_enum_list:Function]\n\n\n# [DEF:list_reports:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = Query(None, description=\"Comma-separated task types\"),\n statuses: Optional[str] = Query(None, description=\"Comma-separated statuses\"),\n time_from: Optional[datetime] = Query(None),\n time_to: Optional[datetime] = Query(None),\n search: Optional[str] = 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# [/DEF:list_reports:Function]\n\n\n# [DEF:get_report_detail:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:get_report_detail:Function]\n\n# [/DEF:ReportsRouter:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_parse_csv_enum_list", "contract_type": "Function", "file_path": "backend/src/api/routes/reports.py", - "start_line": 43, - "end_line": 78, + "start_line": 38, + "end_line": 68, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PARAM": "field_name (str) - Query field name for diagnostics.", + "COMPLEXITY": 1, "POST": "Returns enum list or raises HTTP 400 with deterministic machine-readable payload.", "PRE": "raw may be None/empty or comma-separated values.", - "PURPOSE": "Parse comma-separated query value into enum list.", - "RETURN": "List - Parsed enum values." + "PURPOSE": "Parse comma-separated query value into enum list." }, "relations": [ { @@ -23199,12 +21042,6 @@ } ], "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", @@ -23232,12 +21069,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -23248,19 +21079,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_parse_csv_enum_list:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# @PARAM: raw (Optional[str]) - Comma-separated enum values.\n# @PARAM: enum_cls (type) - Enum class for validation.\n# @PARAM: field_name (str) - Query field name for diagnostics.\n# @RETURN: List - Parsed enum values.\n# @RELATION: BINDS_TO -> [ReportsRouter]\ndef _parse_csv_enum_list(raw: Optional[str], 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# [/DEF:_parse_csv_enum_list:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "list_reports", "contract_type": "Function", "file_path": "backend/src/api/routes/reports.py", - "start_line": 81, - "end_line": 154, + "start_line": 71, + "end_line": 143, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "deterministic error payload for invalid filters.", "PRE": "authenticated/authorized request and validated query params.", "PURPOSE": "Return paginated unified reports list.", @@ -23269,19 +21100,19 @@ "relations": [ { "source_id": "list_reports", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "_parse_csv_enum_list:Function", "target_ref": "[_parse_csv_enum_list:Function]" }, { "source_id": "list_reports", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ReportQuery:Class", "target_ref": "[ReportQuery:Class]" }, { "source_id": "list_reports", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ReportsService:Class", "target_ref": "[ReportsService:Class]" } @@ -23313,75 +21144,21 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:list_reports:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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: Optional[str] = Query(None, description=\"Comma-separated task types\"),\n statuses: Optional[str] = Query(None, description=\"Comma-separated statuses\"),\n time_from: Optional[datetime] = Query(None),\n time_to: Optional[datetime] = Query(None),\n search: Optional[str] = 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# [/DEF:list_reports:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_report_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/reports.py", - "start_line": 157, - "end_line": 185, + "start_line": 146, + "end_line": 173, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "returns normalized detail envelope or 404 when report is not found.", "PRE": "authenticated/authorized request and existing report_id.", "PURPOSE": "Return one normalized report detail with diagnostics and next actions." @@ -23423,46 +21200,40 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_report_detail:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:get_report_detail:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SettingsRouter", "contract_type": "Module", "file_path": "backend/src/api/routes/settings.py", "start_line": 1, - "end_line": 644, + "end_line": 640, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "All settings changes must be persisted via ConfigManager.", "LAYER": "API", "PUBLIC_API": "router", - "PURPOSE": "Provides API endpoints for managing application settings and Superset environments.", - "SEMANTICS": [ - "settings", - "api", - "router", - "fastapi" - ] + "PURPOSE": "Provides API endpoints for managing application settings and Superset environments." }, "relations": [ { "source_id": "SettingsRouter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ConfigManager", "target_ref": "[ConfigManager]" }, { "source_id": "SettingsRouter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "get_config_manager", "target_ref": "[get_config_manager]" }, { "source_id": "SettingsRouter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "has_permission", "target_ref": "[has_permission]" } @@ -23482,81 +21253,22 @@ "tag": "PUBLIC_API", "message": "@PUBLIC_API is not defined in axiom_config.yaml tags", "detail": null - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:SettingsRouter:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: settings, api, router, fastapi\n# @PURPOSE: 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# [SECTION: IMPORTS]\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom typing import List\nfrom pydantic import BaseModel\nfrom ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig\nfrom ...models.storage import StorageConfig\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope\nfrom ...core.cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"Settings\")\nfrom ...core.superset_client import SupersetClient\nfrom ...services.llm_prompt_templates import normalize_llm_settings\nfrom ...models.llm import ValidationPolicy\nfrom ...models.config import AppConfigRecord\nfrom ...schemas.settings import (\n ValidationPolicyCreate,\n ValidationPolicyUpdate,\n ValidationPolicyResponse,\n)\nfrom ...core.database import get_db\nfrom sqlalchemy.orm import Session\n# [/SECTION]\n\n\n# [DEF:LoggingConfigResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response model for logging configuration with current task log level.\n# @SEMANTICS: logging, config, response\nclass LoggingConfigResponse(BaseModel):\n level: str\n task_log_level: str\n enable_belief_state: bool\n\n\n# [/DEF:LoggingConfigResponse:Class]\n\nrouter = APIRouter()\n\n\n# [DEF:_normalize_superset_env_url:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:_normalize_superset_env_url:Function]\n\n\n# [DEF:_validate_superset_connection_fast:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_validate_superset_connection_fast:Function]\n\n\n# [DEF:get_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieves all application settings.\n# @PRE: Config manager is available.\n# @POST: Returns masked AppConfig.\n# @RETURN: AppConfig - The current configuration.\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 log.reason(\"Fetching all 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# [/DEF:get_settings:Function]\n\n\n# [DEF:get_features:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:get_features:Function]\n\n\n# [DEF:update_global_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Updates global application settings.\n# @PRE: New settings are provided.\n# @POST: Global settings are updated.\n# @PARAM: settings (GlobalSettings) - The new global settings.\n# @RETURN: GlobalSettings - The updated settings.\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 log.reason(\"Updating global settings\")\n\n config_manager.update_global_settings(settings)\n return settings\n\n\n# [/DEF:update_global_settings:Function]\n\n\n# [DEF:get_storage_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieves storage-specific settings.\n# @RETURN: StorageConfig - The storage configuration.\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# [/DEF:get_storage_settings:Function]\n\n\n# [DEF:update_storage_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Updates storage-specific settings.\n# @PARAM: storage (StorageConfig) - The new storage settings.\n# @POST: Storage settings are updated and saved.\n# @RETURN: StorageConfig - The updated storage settings.\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# [/DEF:update_storage_settings:Function]\n\n\n# [DEF:get_environments:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Lists all configured Superset environments.\n# @PRE: Config manager is available.\n# @POST: Returns list of environments.\n# @RETURN: List[Environment] - 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 log.reason(\"Fetching 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# [/DEF:get_environments:Function]\n\n\n# [DEF:add_environment:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Adds a new Superset environment.\n# @PRE: Environment data is valid and reachable.\n# @POST: Environment is added to config.\n# @PARAM: env (Environment) - The environment to add.\n# @RETURN: Environment - The added environment.\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 log.reason(\"Adding environment\", payload={\"env_id\": env.id})\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 log.explore(\"Connection validation failed\", payload={\"env_id\": env.id}, error=str(e))\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# [/DEF:add_environment:Function]\n\n\n# [DEF:update_environment:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Updates an existing Superset environment.\n# @PRE: ID and valid environment data are provided.\n# @POST: Environment is updated in config.\n# @PARAM: id (str) - The ID of the environment to update.\n# @PARAM: env (Environment) - The updated environment data.\n# @RETURN: Environment - The updated environment.\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 log.reason(\"Updating environment\", payload={\"id\": id})\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 log.explore(\"Connection validation failed\", payload={\"id\": id}, error=str(e))\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# [/DEF:update_environment:Function]\n\n\n# [DEF:delete_environment:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Deletes a Superset environment.\n# @PRE: ID is provided.\n# @POST: Environment is removed from config.\n# @PARAM: id (str) - The ID of the environment to delete.\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 log.reason(\"Deleting environment\", payload={\"id\": id})\n config_manager.delete_environment(id)\n return {\"message\": f\"Environment {id} deleted\"}\n\n\n# [/DEF:delete_environment:Function]\n\n\n# [DEF:test_environment_connection:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Tests the connection to a Superset environment.\n# @PRE: ID is provided.\n# @POST: Returns success or error status.\n# @PARAM: id (str) - The ID of the environment to test.\n# @RETURN: dict - Success message or error.\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 log.reason(\"Testing environment connection\", payload={\"id\": id})\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 log.reflect(\"Connection successful\", payload={\"id\": id})\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n except Exception as e:\n log.explore(\"Connection failed\", payload={\"id\": id}, error=str(e))\n return {\"status\": \"error\", \"message\": str(e)}\n\n\n# [/DEF:test_environment_connection:Function]\n\n\n# [DEF:get_logging_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieves current logging configuration.\n# @PRE: Config manager is available.\n# @POST: Returns logging configuration.\n# @RETURN: LoggingConfigResponse - The current logging config.\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# [/DEF:get_logging_config:Function]\n\n\n# [DEF:update_logging_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Updates logging configuration.\n# @PRE: New logging config is provided.\n# @POST: Logging configuration is updated and saved.\n# @PARAM: config (LoggingConfig) - The new logging configuration.\n# @RETURN: LoggingConfigResponse - The updated logging config.\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 log.reason(\"Updating logging config\", payload={\"level\": config.level, \"task_log_level\": config.task_log_level})\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# [/DEF:update_logging_config:Function]\n\n\n# [DEF:ConsolidatedSettingsResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response model for consolidated application settings.\nclass ConsolidatedSettingsResponse(BaseModel):\n environments: List[dict]\n connections: List[dict]\n llm: dict\n llm_providers: List[dict]\n logging: dict\n storage: dict\n notifications: dict = {}\n features: dict = {}\n\n\n# [/DEF:ConsolidatedSettingsResponse:Class]\n\n\n# [DEF:get_consolidated_settings:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: 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 log.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...services.llm_provider import LLMProviderService\n from ...core.database import SessionLocal\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 log.reflect(\n \"Consolidated settings payload assembled\",\n payload={\n \"environment_count\": len(response_payload.environments),\n \"provider_count\": len(response_payload.llm_providers),\n },\n )\n return response_payload\n\n\n# [/DEF:get_consolidated_settings:Function]\n\n\n# [DEF:update_consolidated_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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 log.reason(\"Applying consolidated settings patch\")\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# [/DEF:update_consolidated_settings:Function]\n\n\n# [DEF:get_validation_policies:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Lists all validation policies.\n# @RETURN: List[ValidationPolicyResponse] - List of 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# [/DEF:get_validation_policies:Function]\n\n\n# [DEF:create_validation_policy:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Creates a new validation policy.\n# @PARAM: policy (ValidationPolicyCreate) - The policy data.\n# @RETURN: ValidationPolicyResponse - The created 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# [/DEF:create_validation_policy:Function]\n\n\n# [DEF:update_validation_policy:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Updates an existing validation policy.\n# @PARAM: id (str) - The ID of the policy to update.\n# @PARAM: policy (ValidationPolicyUpdate) - The updated policy data.\n# @RETURN: ValidationPolicyResponse - The updated 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# [/DEF:update_validation_policy:Function]\n\n\n# [DEF:delete_validation_policy:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Deletes a validation policy.\n# @PARAM: id (str) - The ID of the policy to delete.\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# [/DEF:delete_validation_policy:Function]\n\n# [/DEF:SettingsRouter:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "LoggingConfigResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/settings.py", - "start_line": 40, - "end_line": 50, + "start_line": 36, + "end_line": 44, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Response model for logging configuration with current task log level.", - "SEMANTICS": [ - "logging", - "config", - "response" - ] + "COMPLEXITY": 1, + "PURPOSE": "Response model for logging configuration with current task log level." }, "relations": [], "schema_warnings": [ @@ -23568,44 +21280,21 @@ "actual_complexity": 1, "contract_type": "Class" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:LoggingConfigResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response model for logging configuration with current task log level.\n# @SEMANTICS: logging, config, response\nclass LoggingConfigResponse(BaseModel):\n level: str\n task_log_level: str\n enable_belief_state: bool\n\n\n# [/DEF:LoggingConfigResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_validate_superset_connection_fast", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 70, - "end_line": 89, + "start_line": 63, + "end_line": 81, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Raises on auth/API failures; returns None on success.", "PRE": "env contains valid URL and credentials.", "PURPOSE": "Run lightweight Superset connectivity validation without full pagination scan." @@ -23631,23 +21320,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_validate_superset_connection_fast:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:_validate_superset_connection_fast:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 92, - "end_line": 114, + "start_line": 84, + "end_line": 104, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns masked AppConfig.", "PRE": "Config manager is available.", - "PURPOSE": "Retrieves all application settings.", - "RETURN": "AppConfig - The current configuration." + "PURPOSE": "Retrieves all application settings." }, "relations": [], "schema_warnings": [ @@ -23668,27 +21356,21 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:get_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieves all application settings.\n# @PRE: Config manager is available.\n# @POST: Returns masked AppConfig.\n# @RETURN: AppConfig - The current configuration.\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 log.reason(\"Fetching all 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# [/DEF:get_settings:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_features", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 117, - "end_line": 130, + "start_line": 107, + "end_line": 119, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns dict with dataset_review and health_monitor booleans.", "PRE": "Config manager is available.", "PURPOSE": "Public endpoint returning feature flags for frontend sidebar filtering.", @@ -23733,57 +21415,41 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_features:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:get_features:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_storage_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 156, - "end_line": 169, + "start_line": 142, + "end_line": 153, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Retrieves storage-specific settings.", - "RETURN": "StorageConfig - The storage configuration." + "COMPLEXITY": 2, + "PURPOSE": "Retrieves storage-specific settings." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": "# [DEF:get_storage_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieves storage-specific settings.\n# @RETURN: StorageConfig - The storage configuration.\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# [/DEF:get_storage_settings:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "update_storage_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 172, - "end_line": 195, + "start_line": 156, + "end_line": 176, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "storage (StorageConfig) - The new storage settings.", + "COMPLEXITY": 2, "POST": "Storage settings are updated and saved.", - "PURPOSE": "Updates storage-specific settings.", - "RETURN": "StorageConfig - The updated storage settings." + "PURPOSE": "Updates storage-specific settings." }, "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", @@ -23792,41 +21458,27 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:update_storage_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Updates storage-specific settings.\n# @PARAM: storage (StorageConfig) - The new storage settings.\n# @POST: Storage settings are updated and saved.\n# @RETURN: StorageConfig - The updated storage settings.\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# [/DEF:update_storage_settings:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "test_environment_connection", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 318, - "end_line": 347, + "start_line": 292, + "end_line": 322, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "id (str) - The ID of the environment to test.", + "COMPLEXITY": 2, "POST": "Returns success or error status.", "PRE": "ID is provided.", - "PURPOSE": "Tests the connection to a Superset environment.", - "RETURN": "dict - Success message or error." + "PURPOSE": "Tests the connection to a Superset environment." }, "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", @@ -23844,31 +21496,24 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:test_environment_connection:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Tests the connection to a Superset environment.\n# @PRE: ID is provided.\n# @POST: Returns success or error status.\n# @PARAM: id (str) - The ID of the environment to test.\n# @RETURN: dict - Success message or error.\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 log.reason(\"Testing environment connection\", payload={\"id\": id})\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 log.reflect(\"Connection successful\", payload={\"id\": id})\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n except Exception as e:\n log.explore(\"Connection failed\", payload={\"id\": id}, error=str(e))\n return {\"status\": \"error\", \"message\": str(e)}\n\n\n# [/DEF:test_environment_connection:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_logging_config", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 350, - "end_line": 370, + "start_line": 325, + "end_line": 343, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Returns logging configuration.", "PRE": "Config manager is available.", - "PURPOSE": "Retrieves current logging configuration.", - "RETURN": "LoggingConfigResponse - The current logging config." + "PURPOSE": "Retrieves current logging configuration." }, "relations": [], "schema_warnings": [ @@ -23889,41 +21534,27 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:get_logging_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieves current logging configuration.\n# @PRE: Config manager is available.\n# @POST: Returns logging configuration.\n# @RETURN: LoggingConfigResponse - The current logging config.\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# [/DEF:get_logging_config:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "update_logging_config", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 373, - "end_line": 401, + "start_line": 346, + "end_line": 373, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "config (LoggingConfig) - The new logging configuration.", + "COMPLEXITY": 2, "POST": "Logging configuration is updated and saved.", "PRE": "New logging config is provided.", - "PURPOSE": "Updates logging configuration.", - "RETURN": "LoggingConfigResponse - The updated logging config." + "PURPOSE": "Updates logging configuration." }, "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", @@ -23941,27 +21572,21 @@ "actual_complexity": 2, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:update_logging_config:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Updates logging configuration.\n# @PRE: New logging config is provided.\n# @POST: Logging configuration is updated and saved.\n# @PARAM: config (LoggingConfig) - The new logging configuration.\n# @RETURN: LoggingConfigResponse - The updated logging config.\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 log.reason(\"Updating logging config\", payload={\"level\": config.level, \"task_log_level\": config.task_log_level})\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# [/DEF:update_logging_config:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ConsolidatedSettingsResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/settings.py", - "start_line": 404, - "end_line": 418, + "start_line": 376, + "end_line": 389, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Response model for consolidated application settings." }, "relations": [], @@ -23976,19 +21601,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ConsolidatedSettingsResponse:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Response model for consolidated application settings.\nclass ConsolidatedSettingsResponse(BaseModel):\n environments: List[dict]\n connections: List[dict]\n llm: dict\n llm_providers: List[dict]\n logging: dict\n storage: dict\n notifications: dict = {}\n features: dict = {}\n\n\n# [/DEF:ConsolidatedSettingsResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_consolidated_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 421, - "end_line": 497, + "start_line": 392, + "end_line": 467, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "DATA_CONTRACT": "Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]", "POST": "Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.", "PRE": "Config manager is available and the caller holds admin settings read permission.", @@ -23998,37 +21623,37 @@ "relations": [ { "source_id": "get_consolidated_settings", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ConfigManager", "target_ref": "[ConfigManager]" }, { "source_id": "get_consolidated_settings", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LLMProviderService", "target_ref": "[LLMProviderService]" }, { "source_id": "get_consolidated_settings", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AppConfigRecord", "target_ref": "[AppConfigRecord]" }, { "source_id": "get_consolidated_settings", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SessionLocal", "target_ref": "[SessionLocal]" }, { "source_id": "get_consolidated_settings", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "has_permission", "target_ref": "[has_permission]" }, { "source_id": "get_consolidated_settings", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "normalize_llm_settings", "target_ref": "[normalize_llm_settings]" } @@ -24042,129 +21667,21 @@ "actual_complexity": 4, "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_consolidated_settings:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: 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 log.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...services.llm_provider import LLMProviderService\n from ...core.database import SessionLocal\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 log.reflect(\n \"Consolidated settings payload assembled\",\n payload={\n \"environment_count\": len(response_payload.environments),\n \"provider_count\": len(response_payload.llm_providers),\n },\n )\n return response_payload\n\n\n# [/DEF:get_consolidated_settings:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_consolidated_settings [C:4] [TYPE Function]\n# @BRIEF Retrieves all settings categories in a single call.\n# @PRE: Config manager is available and the caller holds admin settings read permission.\n# @POST: Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.\n# @SIDE_EFFECT: Opens one database session to read LLM providers and config-backed notification payload, then closes it.\n# @DATA_CONTRACT: Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [normalize_llm_settings]\n@router.get(\"/consolidated\", response_model=ConsolidatedSettingsResponse)\nasync def get_consolidated_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_consolidated_settings\"):\n logger.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n notifications_payload = {}\n try:\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_providers_list = [\n {\n \"id\": p.id,\n \"provider_type\": p.provider_type,\n \"name\": p.name,\n \"base_url\": p.base_url,\n \"api_key\": \"********\",\n \"default_model\": p.default_model,\n \"is_active\": p.is_active,\n }\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" }, { "contract_id": "update_consolidated_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 500, - "end_line": 552, + "start_line": 470, + "end_line": 523, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "POST": "Settings are updated and saved via ConfigManager.", "PRE": "User has admin permissions, config is valid.", "PURPOSE": "Bulk update application settings from the consolidated view." @@ -24190,144 +21707,107 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_consolidated_settings:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: 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 log.reason(\"Applying consolidated settings patch\")\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# [/DEF:update_consolidated_settings:Function]\n" + "anchor_syntax": "region", + "body": "# #region update_consolidated_settings [C:2] [TYPE Function]\n# @BRIEF Bulk update application settings from the consolidated view.\n# @PRE: User has admin permissions, config is valid.\n# @POST: Settings are updated and saved via ConfigManager.\n@router.patch(\"/consolidated\")\nasync def update_consolidated_settings(\n settings_patch: dict,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_consolidated_settings\"):\n logger.reason(\n \"Applying consolidated settings patch\", extra={\"src\": \"update_consolidated_settings\"}\n )\n\n current_config = config_manager.get_config()\n current_settings = current_config.settings\n\n # Update connections if provided\n if \"connections\" in settings_patch:\n current_settings.connections = settings_patch[\"connections\"]\n\n # Update LLM if provided\n if \"llm\" in settings_patch:\n current_settings.llm = normalize_llm_settings(settings_patch[\"llm\"])\n\n # Update Logging if provided\n if \"logging\" in settings_patch:\n current_settings.logging = LoggingConfig(**settings_patch[\"logging\"])\n\n # Update Storage if provided\n if \"storage\" in settings_patch:\n new_storage = StorageConfig(**settings_patch[\"storage\"])\n is_valid, message = config_manager.validate_path(new_storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n current_settings.storage = new_storage\n\n # Update Features if provided\n if \"features\" in settings_patch:\n from ...core.config_models import FeaturesConfig\n\n current_settings.features = FeaturesConfig(**settings_patch[\"features\"])\n\n if \"notifications\" in settings_patch:\n payload = config_manager.get_payload()\n payload[\"notifications\"] = settings_patch[\"notifications\"]\n config_manager.save_config(payload)\n\n config_manager.update_global_settings(current_settings)\n return {\"status\": \"success\", \"message\": \"Settings updated\"}\n\n\n# #endregion update_consolidated_settings\n" }, { "contract_id": "get_validation_policies", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 555, - "end_line": 567, + "start_line": 526, + "end_line": 536, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Lists all validation policies.", - "RETURN": "List[ValidationPolicyResponse] - List of policies." + "COMPLEXITY": 2, + "PURPOSE": "Lists all validation policies." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": "# [DEF:get_validation_policies:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Lists all validation policies.\n# @RETURN: List[ValidationPolicyResponse] - List of 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# [/DEF:get_validation_policies:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "create_validation_policy", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 570, - "end_line": 589, + "start_line": 539, + "end_line": 555, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "policy (ValidationPolicyCreate) - The policy data.", - "PURPOSE": "Creates a new validation policy.", - "RETURN": "ValidationPolicyResponse - The created policy." + "COMPLEXITY": 2, + "PURPOSE": "Creates a new validation policy." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": "# [DEF:create_validation_policy:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Creates a new validation policy.\n# @PARAM: policy (ValidationPolicyCreate) - The policy data.\n# @RETURN: ValidationPolicyResponse - The created 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# [/DEF:create_validation_policy:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region create_validation_policy [C:2] [TYPE Function]\n# @BRIEF Creates a new validation policy.\n@router.post(\"/automation/policies\", response_model=ValidationPolicyResponse)\nasync def create_validation_policy(\n policy: ValidationPolicyCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_validation_policy\"):\n db_policy = ValidationPolicy(**policy.dict())\n db.add(db_policy)\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion create_validation_policy\n" }, { "contract_id": "update_validation_policy", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 592, - "end_line": 619, + "start_line": 558, + "end_line": 581, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "policy (ValidationPolicyUpdate) - The updated policy data.", - "PURPOSE": "Updates an existing validation policy.", - "RETURN": "ValidationPolicyResponse - The updated policy." + "COMPLEXITY": 2, + "PURPOSE": "Updates an existing validation policy." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": "# [DEF:update_validation_policy:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Updates an existing validation policy.\n# @PARAM: id (str) - The ID of the policy to update.\n# @PARAM: policy (ValidationPolicyUpdate) - The updated policy data.\n# @RETURN: ValidationPolicyResponse - The updated 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# [/DEF:update_validation_policy:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "delete_validation_policy", "contract_type": "Function", "file_path": "backend/src/api/routes/settings.py", - "start_line": 622, - "end_line": 642, + "start_line": 584, + "end_line": 602, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "id (str) - The ID of the policy to delete.", + "COMPLEXITY": 2, "PURPOSE": "Deletes a validation policy." }, "relations": [], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "PARAM", - "message": "@PARAM is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": "# [DEF:delete_validation_policy:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Deletes a validation policy.\n# @PARAM: id (str) - The ID of the policy to delete.\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# [/DEF:delete_validation_policy:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" + }, + { + "contract_id": "get_translation_schedules", + "contract_type": "Function", + "file_path": "backend/src/api/routes/settings.py", + "start_line": 605, + "end_line": 638, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Lists all translation schedules joined with translation job names." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "storage_routes", "contract_type": "Module", "file_path": "backend/src/api/routes/storage.py", "start_line": 1, - "end_line": 193, + "end_line": 170, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "All paths must be validated against path traversal.", "LAYER": "API", - "PURPOSE": "API endpoints for file storage management (backups and repositories).", - "SEMANTICS": [ - "storage", - "files", - "upload", - "download", - "backup", - "repository" - ] + "PURPOSE": "API endpoints for file storage management (backups and repositories)." }, "relations": [ { @@ -24354,24 +21834,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:storage_routes:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: storage, files, upload, download, backup, repository\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom pathlib import Path\nfrom fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException\nfrom fastapi.responses import FileResponse\nfrom typing import List, Optional\nfrom ...models.storage import StoredFile, FileCategory\nfrom ...dependencies import get_plugin_loader, has_permission\nfrom ...plugins.storage.plugin import StoragePlugin\nfrom ...core.logger import belief_scope\n# [/SECTION]\n\nrouter = APIRouter(tags=[\"storage\"])\n\n# [DEF:list_files:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: List all files and directories in the storage system.\n#\n# @PRE: None.\n# @POST: Returns a list of StoredFile objects.\n#\n# @PARAM: category (Optional[FileCategory]) - Filter by category.\n# @PARAM: path (Optional[str]) - Subpath within the category.\n# @RETURN: List[StoredFile] - List of files/directories.\n# @RELATION: DEPENDS_ON -> [StoragePlugin]\n@router.get(\"/files\", response_model=List[StoredFile])\nasync def list_files(\n category: Optional[FileCategory] = None,\n path: Optional[str] = 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# [/DEF:list_files:Function]\n\n# [DEF:upload_file:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: category (FileCategory) - Target category.\n# @PARAM: path (Optional[str]) - Target subpath.\n# @PARAM: file (UploadFile) - The file content.\n# @RETURN: StoredFile - Metadata of the uploaded file.\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: Optional[str] = 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# [/DEF:upload_file:Function]\n\n# [DEF:delete_file:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Delete a specific file or directory.\n#\n# @PRE: category must be a valid FileCategory.\n# @POST: Item is removed from storage.\n#\n# @PARAM: category (FileCategory) - File category.\n# @PARAM: path (str) - Relative path of the item.\n# @RETURN: None\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# [/DEF:delete_file:Function]\n\n# [DEF:download_file:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Retrieve a file for download.\n#\n# @PRE: category must be a valid FileCategory.\n# @POST: Returns a FileResponse.\n#\n# @PARAM: category (FileCategory) - File category.\n# @PARAM: path (str) - Relative path of the file.\n# @RETURN: FileResponse - The file content.\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# [/DEF:download_file:Function]\n\n# [DEF:get_file_by_path:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: path (str) - Absolute or storage-root-relative file path.\n# @RETURN: FileResponse - The file content.\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# [/DEF:get_file_by_path:Function]\n\n# [/DEF:storage_routes:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "list_files", "contract_type": "Function", "file_path": "backend/src/api/routes/storage.py", - "start_line": 25, - "end_line": 49, + "start_line": 22, + "end_line": 42, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "path (Optional[str]) - Subpath within the category.", + "COMPLEXITY": 3, "POST": "Returns a list of StoredFile objects.", "PRE": "None.", - "PURPOSE": "List all files and directories in the storage system.", - "RETURN": "List[StoredFile] - List of files/directories." + "PURPOSE": "List all files and directories in the storage system." }, "relations": [ { @@ -24382,12 +21860,6 @@ } ], "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", @@ -24405,32 +21877,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:list_files:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: List all files and directories in the storage system.\n#\n# @PRE: None.\n# @POST: Returns a list of StoredFile objects.\n#\n# @PARAM: category (Optional[FileCategory]) - Filter by category.\n# @PARAM: path (Optional[str]) - Subpath within the category.\n# @RETURN: List[StoredFile] - List of files/directories.\n# @RELATION: DEPENDS_ON -> [StoragePlugin]\n@router.get(\"/files\", response_model=List[StoredFile])\nasync def list_files(\n category: Optional[FileCategory] = None,\n path: Optional[str] = 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# [/DEF:list_files:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "delete_file", "contract_type": "Function", "file_path": "backend/src/api/routes/storage.py", - "start_line": 85, - "end_line": 116, + "start_line": 73, + "end_line": 100, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "path (str) - Relative path of the item.", + "COMPLEXITY": 3, "POST": "Item is removed from storage.", "PRE": "category must be a valid FileCategory.", "PURPOSE": "Delete a specific file or directory.", - "RETURN": "None", "SIDE_EFFECT": "Deletes item from the filesystem." }, "relations": [ @@ -24442,12 +21906,6 @@ } ], "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", @@ -24466,12 +21924,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -24482,24 +21934,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:delete_file:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Delete a specific file or directory.\n#\n# @PRE: category must be a valid FileCategory.\n# @POST: Item is removed from storage.\n#\n# @PARAM: category (FileCategory) - File category.\n# @PARAM: path (str) - Relative path of the item.\n# @RETURN: None\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# [/DEF:delete_file:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "download_file", "contract_type": "Function", "file_path": "backend/src/api/routes/storage.py", - "start_line": 118, - "end_line": 149, + "start_line": 102, + "end_line": 129, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "path (str) - Relative path of the file.", + "COMPLEXITY": 3, "POST": "Returns a FileResponse.", "PRE": "category must be a valid FileCategory.", - "PURPOSE": "Retrieve a file for download.", - "RETURN": "FileResponse - The file content." + "PURPOSE": "Retrieve a file for download." }, "relations": [ { @@ -24510,12 +21960,6 @@ } ], "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", @@ -24533,32 +21977,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:download_file:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Retrieve a file for download.\n#\n# @PRE: category must be a valid FileCategory.\n# @POST: Returns a FileResponse.\n#\n# @PARAM: category (FileCategory) - File category.\n# @PARAM: path (str) - Relative path of the file.\n# @RETURN: FileResponse - The file content.\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# [/DEF:download_file:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "get_file_by_path", "contract_type": "Function", "file_path": "backend/src/api/routes/storage.py", - "start_line": 151, - "end_line": 191, + "start_line": 131, + "end_line": 168, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "path (str) - Absolute or storage-root-relative file path.", + "COMPLEXITY": 3, "POST": "Returns a FileResponse for existing files.", "PRE": "path must resolve under configured storage root.", - "PURPOSE": "Retrieve a file by validated absolute/relative path under storage root.", - "RETURN": "FileResponse - The file content." + "PURPOSE": "Retrieve a file by validated absolute/relative path under storage root." }, "relations": [ { @@ -24569,12 +22005,6 @@ } ], "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", @@ -24592,38 +22022,23 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:get_file_by_path:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: path (str) - Absolute or storage-root-relative file path.\n# @RETURN: FileResponse - The file content.\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# [/DEF:get_file_by_path:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "TasksRouter", "contract_type": "Module", "file_path": "backend/src/api/routes/tasks.py", "start_line": 1, - "end_line": 423, + "end_line": 376, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI (API)", - "PURPOSE": "Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks.", - "SEMANTICS": [ - "api", - "router", - "tasks", - "create", - "list", - "get", - "logs" - ] + "PURPOSE": "Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks." }, "relations": [ { @@ -24646,24 +22061,79 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TasksRouter:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: api, router, tasks, create, list, get, logs\n# @PURPOSE: Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks.\n# @LAYER: UI (API)\n# @RELATION: DEPENDS_ON -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\n# @RELATION: DEPENDS_ON -> [LLMProviderService]\n\n# [SECTION: IMPORTS]\nfrom typing import List, Dict, Any, Optional\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom pydantic import BaseModel\nfrom ...core.logger import belief_scope\n\nfrom ...core.task_manager import TaskManager, Task, TaskStatus, LogEntry\nfrom ...core.task_manager.models import LogFilter, LogStats\nfrom ...dependencies import (\n get_task_manager,\n has_permission,\n get_current_user,\n get_config_manager,\n)\nfrom ...core.config_manager import ConfigManager\nfrom ...services.llm_prompt_templates import (\n is_multimodal_model,\n normalize_llm_settings,\n resolve_bound_provider_id,\n)\n# [/SECTION]\n\nrouter = APIRouter()\n\nTASK_TYPE_PLUGIN_MAP = {\n \"llm_validation\": [\"llm_dashboard_validation\"],\n \"backup\": [\"superset-backup\"],\n \"migration\": [\"superset-migration\"],\n}\n\n\nclass CreateTaskRequest(BaseModel):\n plugin_id: str\n params: Dict[str, Any]\n\n\nclass ResolveTaskRequest(BaseModel):\n resolution_params: Dict[str, Any]\n\n\nclass ResumeTaskRequest(BaseModel):\n passwords: Dict[str, str]\n\n\n# [DEF:create_task:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Create and start a new task for a given plugin.\n# @PARAM: request (CreateTaskRequest) - The request body containing plugin_id and params.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: plugin_id must exist and params must be valid for that plugin.\n# @POST: A new task is created and started.\n# @RETURN: Task - The created task instance.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\n# @RELATION: DEPENDS_ON -> [LLMProviderService]\n@router.post(\"\", response_model=Task, status_code=status.HTTP_201_CREATED)\nasync def create_task(\n request: CreateTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n current_user=Depends(get_current_user),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n # Dynamic permission check based on plugin_id\n has_permission(f\"plugin:{request.plugin_id}\", \"EXECUTE\")(current_user)\n with belief_scope(\"create_task\"):\n try:\n # Special handling for LLM tasks to resolve provider config by task binding.\n if request.plugin_id in {\"llm_dashboard_validation\", \"llm_documentation\"}:\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n try:\n llm_service = LLMProviderService(db)\n provider_id = request.params.get(\"provider_id\")\n if not provider_id:\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n binding_key = (\n \"dashboard_validation\"\n if request.plugin_id == \"llm_dashboard_validation\"\n else \"documentation\"\n )\n provider_id = resolve_bound_provider_id(\n llm_settings, binding_key\n )\n if provider_id:\n request.params[\"provider_id\"] = provider_id\n if not provider_id:\n providers = llm_service.get_all_providers()\n active_provider = next(\n (p for p in providers if p.is_active), None\n )\n if active_provider:\n provider_id = active_provider.id\n request.params[\"provider_id\"] = provider_id\n\n if provider_id:\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n if (\n request.plugin_id == \"llm_dashboard_validation\"\n and not is_multimodal_model(\n db_provider.default_model,\n db_provider.provider_type,\n )\n ):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"Selected provider model is not multimodal for dashboard validation\",\n )\n finally:\n db.close()\n\n task = await task_manager.create_task(\n plugin_id=request.plugin_id, params=request.params\n )\n return task\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n\n\n# [/DEF:create_task:Function]\n\n\n# [DEF:list_tasks:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieve a list of tasks with pagination and optional status filter.\n# @PARAM: limit (int) - Maximum number of tasks to return.\n# @PARAM: offset (int) - Number of tasks to skip.\n# @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_manager must be available.\n# @POST: Returns a list of tasks.\n# @RETURN: List[Task] - List of tasks.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]\n@router.get(\"\", response_model=List[Task])\nasync def list_tasks(\n limit: int = 10,\n offset: int = 0,\n status_filter: Optional[TaskStatus] = Query(None, alias=\"status\"),\n task_type: Optional[str] = Query(\n None, description=\"Task category: llm_validation, backup, migration\"\n ),\n plugin_id: Optional[List[str]] = Query(\n None, description=\"Filter by plugin_id (repeatable query param)\"\n ),\n completed_only: bool = Query(\n False, description=\"Return only completed tasks (SUCCESS/FAILED)\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"list_tasks\"):\n plugin_filters = list(plugin_id) if plugin_id else []\n if task_type:\n if task_type not in TASK_TYPE_PLUGIN_MAP:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"Unsupported task_type '{task_type}'. Allowed: {', '.join(TASK_TYPE_PLUGIN_MAP.keys())}\",\n )\n plugin_filters.extend(TASK_TYPE_PLUGIN_MAP[task_type])\n\n return task_manager.get_tasks(\n limit=limit,\n offset=offset,\n status=status_filter,\n plugin_ids=plugin_filters or None,\n completed_only=completed_only,\n )\n\n\n# [/DEF:list_tasks:Function]\n\n\n# [DEF:get_task:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieve the details of a specific task.\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_id must exist.\n# @POST: Returns task details or raises 404.\n# @RETURN: Task - The task details.\n# @RELATION: CALLS -> [TaskManager]\n@router.get(\"/{task_id}\", response_model=Task)\nasync def get_task(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"get_task\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task\n\n\n# [/DEF:get_task:Function]\n\n\n# [DEF:get_task_logs:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Retrieve logs for a specific task with optional filtering.\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: level (Optional[str]) - Filter by log level (DEBUG, INFO, WARNING, ERROR).\n# @PARAM: source (Optional[str]) - Filter by source component.\n# @PARAM: search (Optional[str]) - Text search in message.\n# @PARAM: offset (int) - Number of logs to skip.\n# @PARAM: limit (int) - Maximum number of logs to return.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_id must exist.\n# @POST: Returns a list of log entries or raises 404.\n# @RETURN: List[LogEntry] - List of log entries.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [LogFilter]\n# @TEST_CONTRACT: TaskLogQueryInput -> List[LogEntry]\n# @TEST_SCENARIO: existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.\n# @TEST_FIXTURE: valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json\n# @TEST_EDGE: missing_task -> Unknown task_id returns 404 Task not found.\n# @TEST_EDGE: invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.\n# @TEST_EDGE: pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.\n# @TEST_INVARIANT: logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]\n@router.get(\"/{task_id}/logs\")\nasync def get_task_logs(\n task_id: str,\n level: Optional[str] = Query(\n None, description=\"Filter by log level (DEBUG, INFO, WARNING, ERROR)\"\n ),\n source: Optional[str] = Query(None, description=\"Filter by source component\"),\n search: Optional[str] = Query(None, description=\"Text search in message\"),\n offset: int = Query(0, ge=0, description=\"Number of logs to skip\"),\n limit: int = Query(\n 100, ge=1, le=1000, description=\"Maximum number of logs to return\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_logs\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n\n log_filter = LogFilter(\n level=level.upper() if level else None,\n source=source,\n search=search,\n offset=offset,\n limit=limit,\n )\n return task_manager.get_task_logs(task_id, log_filter)\n\n\n# [/DEF:get_task_logs:Function]\n\n\n# [DEF:get_task_log_stats:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Get statistics about logs for a task (counts by level and source).\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_id must exist.\n# @POST: Returns log statistics or raises 404.\n# @RETURN: LogStats - Statistics about task logs.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [LogStats]\n@router.get(\"/{task_id}/logs/stats\", response_model=LogStats)\nasync def get_task_log_stats(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_stats\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n stats_payload = task_manager.get_task_log_stats(task_id)\n if isinstance(stats_payload, LogStats):\n return stats_payload\n if isinstance(stats_payload, dict) and (\n \"total_count\" in stats_payload\n or \"by_level\" in stats_payload\n or \"by_source\" in stats_payload\n ):\n return LogStats(\n total_count=int(stats_payload.get(\"total_count\", 0) or 0),\n by_level=dict(stats_payload.get(\"by_level\") or {}),\n by_source=dict(stats_payload.get(\"by_source\") or {}),\n )\n flat_by_level = (\n dict(stats_payload or {}) if isinstance(stats_payload, dict) else {}\n )\n return LogStats(\n total_count=sum(int(value or 0) for value in flat_by_level.values()),\n by_level={\n str(key): int(value or 0) for key, value in flat_by_level.items()\n },\n by_source={},\n )\n\n\n# [/DEF:get_task_log_stats:Function]\n\n\n# [DEF:get_task_log_sources:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Get unique sources for a task's logs.\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_id must exist.\n# @POST: Returns list of unique source names or raises 404.\n# @RETURN: List[str] - Unique source names.\n# @RELATION: CALLS -> [TaskManager]\n@router.get(\"/{task_id}/logs/sources\", response_model=List[str])\nasync def get_task_log_sources(\n task_id: str,\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"get_task_log_sources\"):\n task = task_manager.get_task(task_id)\n if not task:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND, detail=\"Task not found\"\n )\n return task_manager.get_task_log_sources(task_id)\n\n\n# [/DEF:get_task_log_sources:Function]\n\n\n# [DEF:resolve_task:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve a task that is awaiting mapping.\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: request (ResolveTaskRequest) - The resolution parameters.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task must be in AWAITING_MAPPING status.\n# @POST: Task is resolved and resumes execution.\n# @RETURN: Task - The updated task object.\n# @RELATION: CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resolve\", response_model=Task)\nasync def resolve_task(\n task_id: str,\n request: ResolveTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resolve_task\"):\n try:\n await task_manager.resolve_task(task_id, request.resolution_params)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# [/DEF:resolve_task:Function]\n\n\n# [DEF:resume_task:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resume a task that is awaiting input (e.g., passwords).\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: request (ResumeTaskRequest) - The input (passwords).\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task must be in AWAITING_INPUT status.\n# @POST: Task resumes execution with provided input.\n# @RETURN: Task - The updated task object.\n# @RELATION: CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resume\", response_model=Task)\nasync def resume_task(\n task_id: str,\n request: ResumeTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resume_task\"):\n try:\n task_manager.resume_task_with_password(task_id, request.passwords)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# [/DEF:resume_task:Function]\n\n\n# [DEF:clear_tasks:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Clear tasks matching the status filter.\n# @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_manager is available.\n# @POST: Tasks are removed from memory/persistence.\n# @RELATION: CALLS -> [TaskManager]\n@router.delete(\"\", status_code=status.HTTP_204_NO_CONTENT)\nasync def clear_tasks(\n status: Optional[TaskStatus] = None,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"clear_tasks\", f\"status={status}\"):\n task_manager.clear_tasks(status)\n return\n\n\n# [/DEF:clear_tasks:Function]\n\n# [/DEF:TasksRouter:Module]\n" + "anchor_syntax": "region", + "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", + "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" }, { "contract_id": "list_tasks", "contract_type": "Function", "file_path": "backend/src/api/routes/tasks.py", - "start_line": 137, - "end_line": 185, + "start_line": 130, + "end_line": 172, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "task_manager (TaskManager) - The task manager instance.", + "COMPLEXITY": 2, "POST": "Returns a list of tasks.", "PRE": "task_manager must be available.", - "PURPOSE": "Retrieve a list of tasks with pagination and optional status filter.", - "RETURN": "List[Task] - List of tasks." + "PURPOSE": "Retrieve a list of tasks with pagination and optional status filter." }, "relations": [ { @@ -24681,11 +22151,59 @@ ], "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 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", + "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", @@ -24705,10 +22223,167 @@ } }, { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null + "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", + "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": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Function' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Function" + } + }, + { + "code": "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", + "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", @@ -24720,24 +22395,130 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:list_tasks:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Retrieve a list of tasks with pagination and optional status filter.\n# @PARAM: limit (int) - Maximum number of tasks to return.\n# @PARAM: offset (int) - Number of tasks to skip.\n# @PARAM: status (Optional[TaskStatus]) - Filter by task status.\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task_manager must be available.\n# @POST: Returns a list of tasks.\n# @RETURN: List[Task] - List of tasks.\n# @RELATION: CALLS -> [TaskManager]\n# @RELATION: BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]\n@router.get(\"\", response_model=List[Task])\nasync def list_tasks(\n limit: int = 10,\n offset: int = 0,\n status_filter: Optional[TaskStatus] = Query(None, alias=\"status\"),\n task_type: Optional[str] = Query(\n None, description=\"Task category: llm_validation, backup, migration\"\n ),\n plugin_id: Optional[List[str]] = Query(\n None, description=\"Filter by plugin_id (repeatable query param)\"\n ),\n completed_only: bool = Query(\n False, description=\"Return only completed tasks (SUCCESS/FAILED)\"\n ),\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"list_tasks\"):\n plugin_filters = list(plugin_id) if plugin_id else []\n if task_type:\n if task_type not in TASK_TYPE_PLUGIN_MAP:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"Unsupported task_type '{task_type}'. Allowed: {', '.join(TASK_TYPE_PLUGIN_MAP.keys())}\",\n )\n plugin_filters.extend(TASK_TYPE_PLUGIN_MAP[task_type])\n\n return task_manager.get_tasks(\n limit=limit,\n offset=offset,\n status=status_filter,\n plugin_ids=plugin_filters or None,\n completed_only=completed_only,\n )\n\n\n# [/DEF:list_tasks:Function]\n" + "anchor_syntax": "region", + "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", + "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", + "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", "file_path": "backend/src/api/routes/tasks.py", - "start_line": 374, - "end_line": 399, + "start_line": 335, + "end_line": 355, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PARAM": "task_manager (TaskManager) - The task manager instance.", + "COMPLEXITY": 2, "POST": "Task resumes execution with provided input.", "PRE": "task must be in AWAITING_INPUT status.", - "PURPOSE": "Resume a task that is awaiting input (e.g., passwords).", - "RETURN": "Task - The updated task object." + "PURPOSE": "Resume a task that is awaiting input (e.g., passwords)." }, "relations": [ { @@ -24749,11 +22530,59 @@ ], "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 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", + "body": "# #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" + }, + { + "contract_id": "clear_tasks", + "contract_type": "Function", + "file_path": "backend/src/api/routes/tasks.py", + "start_line": 358, + "end_line": 374, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "POST": "Tasks are removed from memory/persistence.", + "PRE": "task_manager is available.", + "PURPOSE": "Clear tasks matching the status filter." + }, + "relations": [ + { + "source_id": "clear_tasks", + "relation_type": "CALLS", + "target_id": "TaskManager", + "target_ref": "[TaskManager]" + } + ], + "schema_warnings": [ { "code": "tag_forbidden_by_complexity", "tag": "POST", @@ -24772,12 +22601,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -24788,24 +22611,23 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:resume_task:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resume a task that is awaiting input (e.g., passwords).\n# @PARAM: task_id (str) - The unique identifier of the task.\n# @PARAM: request (ResumeTaskRequest) - The input (passwords).\n# @PARAM: task_manager (TaskManager) - The task manager instance.\n# @PRE: task must be in AWAITING_INPUT status.\n# @POST: Task resumes execution with provided input.\n# @RETURN: Task - The updated task object.\n# @RELATION: CALLS -> [TaskManager]\n@router.post(\"/{task_id}/resume\", response_model=Task)\nasync def resume_task(\n task_id: str,\n request: ResumeTaskRequest,\n task_manager: TaskManager = Depends(get_task_manager),\n _=Depends(has_permission(\"tasks\", \"WRITE\")),\n):\n with belief_scope(\"resume_task\"):\n try:\n task_manager.resume_task_with_password(task_id, request.passwords)\n return task_manager.get_task(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n\n\n# [/DEF:resume_task:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "TranslateRoutes", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/__init__.py", "start_line": 1, - "end_line": 36, + "end_line": 37, "tier": "TIER_2", "complexity": 4, "metadata": { "COMPLEXITY": 4, - "LAYER": "UI", - "POST": "Package imports all route modules and re-exports router. Sub-routes registered on shared router.", - "PRE": "All sub-modules importable. Router instance available from ._router.", + "LAYER": "UI (API)", "PURPOSE": "API routes for LLM-based SQL/dashboard translation management including terminology dictionary CRUD and import.", - "SIDE_EFFECT": "Registers route handlers on shared APIRouter at import time." + "RATIONALE": "Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.", + "REJECTED": "Invalidating in-progress runs on config edit would break scheduled run continuity." }, "relations": [ { @@ -24817,8 +22639,8 @@ { "source_id": "TranslateRoutes", "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "target_id": "DictionaryManager:Class", + "target_ref": "[DictionaryManager:Class]" }, { "source_id": "TranslateRoutes", @@ -24851,438 +22673,695 @@ "target_ref": "[SupersetClient]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "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", - "body": "# #region TranslateRoutes [C:4] [TYPE Module] [SEMANTICS api,routes,translate]\n# @BRIEF API routes for LLM-based SQL/dashboard translation management including terminology dictionary CRUD and import.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslateJobResponse]\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @PRE All sub-modules importable. Router instance available from ._router.\n# @POST Package imports all route modules and re-exports router. Sub-routes registered on shared router.\n# @SIDE_EFFECT Registers route handlers on shared APIRouter at import time.\n\n\"\"\"\nTranslate routes package.\n\nRe-exports the shared router and all route handler modules.\nAll sub-modules register their handlers on the router at import time.\n\"\"\"\n\nfrom ._router import router\nfrom . import _job_routes # noqa: F401 — registers job handlers\nfrom . import _run_routes # noqa: F401 — registers run handlers\nfrom . import _run_list_routes # noqa: F401 — registers run list/detail/csv handlers\nfrom . import _preview_routes # noqa: F401 — registers preview handlers\nfrom . import _dictionary_routes # noqa: F401 — registers dictionary handlers\nfrom . import _schedule_routes # noqa: F401 — registers schedule handlers\nfrom . import _metrics_routes # noqa: F401 — registers metrics handlers\nfrom . import _correction_routes # noqa: F401 — registers correction handlers\n\n__all__ = [\n \"router\",\n]\n\n# #endregion TranslateRoutes\n" + "body": "# #region TranslateRoutes [C:4] [TYPE Module] [SEMANTICS translate, api, package, schedule, dashboard, llm]\n# @BRIEF API routes for LLM-based SQL/dashboard translation management including terminology dictionary CRUD and import.\n# @LAYER: UI (API)\n# @RELATION DEPENDS_ON -> [TranslateJobResponse]\n# @RELATION DEPENDS_ON -> [DictionaryManager:Class]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @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\n\"\"\"\nTranslate routes package.\n\nRe-exports the shared router and all route handler modules.\nAll sub-modules register their handlers on the router at import time.\n\"\"\"\n\nfrom . import (\n _correction_routes, # noqa: F401 — registers correction handlers\n _dictionary_routes, # noqa: F401 — registers dictionary handlers\n _job_routes, # noqa: F401 — registers job handlers\n _metrics_routes, # noqa: F401 — registers metrics handlers\n _preview_routes, # noqa: F401 — registers preview handlers\n _run_list_routes, # noqa: F401 — registers run list/detail/csv handlers\n _run_routes, # noqa: F401 — registers run handlers\n _schedule_routes, # noqa: F401 — registers schedule handlers\n)\nfrom ._router import router\n\n__all__ = [\n \"router\",\n]\n\n# #endregion TranslateRoutes\n" }, { "contract_id": "TranslateCorrectionRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_correction_routes.py", "start_line": 1, - "end_line": 106, - "tier": "TIER_2", - "complexity": 4, + "end_line": 91, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": 4, - "LAYER": "UI", - "POST": "Term corrections submitted and applied to dictionary with conflict detection.", - "PRE": "DB session initialized. User authenticated with translate.dictionary.edit permissions.", - "PURPOSE": "Term correction submission endpoints for applying user feedback to dictionary entries.", - "SIDE_EFFECT": "Creates/updates DictionaryEntry records in DB based on correction submissions." + "COMPLEXITY": 2, + "LAYER": "API", + "PURPOSE": "Term correction submission endpoints." }, - "relations": [ - { - "source_id": "TranslateCorrectionRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" - }, - { - "source_id": "TranslateCorrectionRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" - }, - { - "source_id": "TranslateCorrectionRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_db", - "target_ref": "[get_db]" - } - ], + "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslateCorrectionRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,corrections]\n# @BRIEF Term correction submission endpoints for applying user feedback to dictionary entries.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE DB session initialized. User authenticated with translate.dictionary.edit permissions.\n# @POST Term corrections submitted and applied to dictionary with conflict detection.\n# @SIDE_EFFECT Creates/updates DictionaryEntry records in DB based on correction submissions.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.dictionary import DictionaryManager\nfrom ....schemas.translate import (\n TermCorrectionSubmit,\n TermCorrectionBulkSubmit,\n CorrectionSubmitResponse,\n)\n\nfrom ._router import router\n\nlog = MarkerLogger(\"TranslateCorrectionRoutes\")\n\n# ============================================================\n# Corrections\n# ============================================================\n\n# #region submit_correction [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]\n# @BRIEF Submit a single term correction from a run result review.\n# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.\n# @POST Correction applied with conflict detection. Result returned.\n# @SIDE_EFFECT Creates/updates DictionaryEntry record in DB; logs correction origin.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/corrections\")\nasync def submit_correction(\n payload: TermCorrectionSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit a term correction from a run result review.\"\"\"\n log.reason(\"Correction request\", payload={\"user\": current_user.username})\n if not payload.dictionary_id:\n raise HTTPException(status_code=422, detail=\"dictionary_id is required\")\n try:\n result = DictionaryManager.submit_correction(\n db,\n dict_id=payload.dictionary_id,\n source_term=payload.source_term,\n incorrect_target_term=payload.incorrect_target_term,\n corrected_target_term=payload.corrected_target_term,\n origin_run_id=payload.origin_run_id,\n origin_row_key=payload.origin_row_key,\n origin_user_id=current_user.username,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_correction\n\n\n# #region submit_bulk_corrections [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]\n# @BRIEF Submit multiple term corrections atomically with full conflict reporting.\n# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.\n# @POST All corrections applied or none with conflict list returned.\n# @SIDE_EFFECT Creates/updates multiple DictionaryEntry records in DB atomically.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/corrections/bulk\")\nasync def submit_bulk_corrections(\n payload: TermCorrectionBulkSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit multiple term corrections atomically.\"\"\"\n log.reason(\"Bulk correction request\", payload={\"user\": current_user.username})\n try:\n corr_list = []\n for c in payload.corrections:\n corr_list.append({\n \"source_term\": c.source_term,\n \"incorrect_target_term\": c.incorrect_target_term,\n \"corrected_target_term\": c.corrected_target_term,\n \"origin_run_id\": c.origin_run_id,\n \"origin_row_key\": c.origin_row_key,\n })\n result = DictionaryManager.submit_bulk_corrections(\n db,\n dict_id=payload.dictionary_id,\n corrections=corr_list,\n origin_user_id=current_user.username,\n )\n if result.get(\"status\") == \"conflicts\":\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=result)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_bulk_corrections\n\n# #endregion TranslateCorrectionRoutesModule\n" + "body": "# #region TranslateCorrectionRoutesModule [C:2] [TYPE Module] [SEMANTICS fastapi, translate, api, search]\n# @BRIEF Term correction submission endpoints.\n# @LAYER: API\n\nfrom fastapi import Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.logger import logger\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.dictionary import DictionaryManager\nfrom ....schemas.auth import User\nfrom ....schemas.translate import (\n TermCorrectionBulkSubmit,\n TermCorrectionSubmit,\n)\nfrom ._router import router\n\n# ============================================================\n# Corrections\n# ============================================================\n\n# #region submit_correction [TYPE Function]\n# @BRIEF Submit a term correction from a run result review.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Correction applied with conflict detection.\n@router.post(\"/corrections\")\nasync def submit_correction(\n payload: TermCorrectionSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit a term correction from a run result review.\"\"\"\n logger.reason(f\"submit_correction — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n if not payload.dictionary_id:\n raise HTTPException(status_code=422, detail=\"dictionary_id is required\")\n try:\n result = DictionaryManager.submit_correction(\n db,\n dict_id=payload.dictionary_id,\n source_term=payload.source_term,\n incorrect_target_term=payload.incorrect_target_term,\n corrected_target_term=payload.corrected_target_term,\n origin_run_id=payload.origin_run_id,\n origin_row_key=payload.origin_row_key,\n origin_user_id=current_user.username,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_correction\n\n\n# #region submit_bulk_corrections [TYPE Function]\n# @BRIEF Submit multiple term corrections atomically.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: All corrections applied or none with conflict list.\n@router.post(\"/corrections/bulk\")\nasync def submit_bulk_corrections(\n payload: TermCorrectionBulkSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit multiple term corrections atomically.\"\"\"\n logger.reason(f\"submit_bulk_corrections — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n corr_list = []\n for c in payload.corrections:\n corr_list.append({\n \"source_term\": c.source_term,\n \"incorrect_target_term\": c.incorrect_target_term,\n \"corrected_target_term\": c.corrected_target_term,\n \"origin_run_id\": c.origin_run_id,\n \"origin_row_key\": c.origin_row_key,\n })\n result = DictionaryManager.submit_bulk_corrections(\n db,\n dict_id=payload.dictionary_id,\n corrections=corr_list,\n origin_user_id=current_user.username,\n )\n if result.get(\"status\") == \"conflicts\":\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=result)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_bulk_corrections\n\n# #endregion TranslateCorrectionRoutesModule\n" }, { "contract_id": "submit_correction", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_correction_routes.py", - "start_line": 34, - "end_line": 65, - "tier": "TIER_2", - "complexity": 4, + "start_line": 23, + "end_line": 52, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Correction applied with conflict detection. Result returned.", - "PRE": "User has translate.dictionary.edit permission. Dictionary ID is provided.", - "PURPOSE": "Submit a single term correction from a run result review.", - "SIDE_EFFECT": "Creates/updates DictionaryEntry record in DB; logs correction origin." + "POST": "Correction applied with conflict detection.", + "PRE": "User has translate.dictionary.edit permission.", + "PURPOSE": "Submit a term correction from a run result review." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "submit_correction", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region submit_correction [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]\n# @BRIEF Submit a single term correction from a run result review.\n# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.\n# @POST Correction applied with conflict detection. Result returned.\n# @SIDE_EFFECT Creates/updates DictionaryEntry record in DB; logs correction origin.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/corrections\")\nasync def submit_correction(\n payload: TermCorrectionSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit a term correction from a run result review.\"\"\"\n log.reason(\"Correction request\", payload={\"user\": current_user.username})\n if not payload.dictionary_id:\n raise HTTPException(status_code=422, detail=\"dictionary_id is required\")\n try:\n result = DictionaryManager.submit_correction(\n db,\n dict_id=payload.dictionary_id,\n source_term=payload.source_term,\n incorrect_target_term=payload.incorrect_target_term,\n corrected_target_term=payload.corrected_target_term,\n origin_run_id=payload.origin_run_id,\n origin_row_key=payload.origin_row_key,\n origin_user_id=current_user.username,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_correction\n" + "body": "# #region submit_correction [TYPE Function]\n# @BRIEF Submit a term correction from a run result review.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Correction applied with conflict detection.\n@router.post(\"/corrections\")\nasync def submit_correction(\n payload: TermCorrectionSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit a term correction from a run result review.\"\"\"\n logger.reason(f\"submit_correction — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n if not payload.dictionary_id:\n raise HTTPException(status_code=422, detail=\"dictionary_id is required\")\n try:\n result = DictionaryManager.submit_correction(\n db,\n dict_id=payload.dictionary_id,\n source_term=payload.source_term,\n incorrect_target_term=payload.incorrect_target_term,\n corrected_target_term=payload.corrected_target_term,\n origin_run_id=payload.origin_run_id,\n origin_row_key=payload.origin_row_key,\n origin_user_id=current_user.username,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_correction\n" }, { "contract_id": "submit_bulk_corrections", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_correction_routes.py", - "start_line": 68, - "end_line": 104, - "tier": "TIER_2", - "complexity": 4, + "start_line": 55, + "end_line": 89, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "All corrections applied or none with conflict list returned.", - "PRE": "User has translate.dictionary.edit permission. Dictionary ID is provided.", - "PURPOSE": "Submit multiple term corrections atomically with full conflict reporting.", - "SIDE_EFFECT": "Creates/updates multiple DictionaryEntry records in DB atomically." + "POST": "All corrections applied or none with conflict list.", + "PRE": "User has translate.dictionary.edit permission.", + "PURPOSE": "Submit multiple term corrections atomically." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "submit_bulk_corrections", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region submit_bulk_corrections [C:4] [TYPE Function] [SEMANTICS api,translate,corrections]\n# @BRIEF Submit multiple term corrections atomically with full conflict reporting.\n# @PRE User has translate.dictionary.edit permission. Dictionary ID is provided.\n# @POST All corrections applied or none with conflict list returned.\n# @SIDE_EFFECT Creates/updates multiple DictionaryEntry records in DB atomically.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/corrections/bulk\")\nasync def submit_bulk_corrections(\n payload: TermCorrectionBulkSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit multiple term corrections atomically.\"\"\"\n log.reason(\"Bulk correction request\", payload={\"user\": current_user.username})\n try:\n corr_list = []\n for c in payload.corrections:\n corr_list.append({\n \"source_term\": c.source_term,\n \"incorrect_target_term\": c.incorrect_target_term,\n \"corrected_target_term\": c.corrected_target_term,\n \"origin_run_id\": c.origin_run_id,\n \"origin_row_key\": c.origin_row_key,\n })\n result = DictionaryManager.submit_bulk_corrections(\n db,\n dict_id=payload.dictionary_id,\n corrections=corr_list,\n origin_user_id=current_user.username,\n )\n if result.get(\"status\") == \"conflicts\":\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=result)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_bulk_corrections\n" + "body": "# #region submit_bulk_corrections [TYPE Function]\n# @BRIEF Submit multiple term corrections atomically.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: All corrections applied or none with conflict list.\n@router.post(\"/corrections/bulk\")\nasync def submit_bulk_corrections(\n payload: TermCorrectionBulkSubmit,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Submit multiple term corrections atomically.\"\"\"\n logger.reason(f\"submit_bulk_corrections — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n corr_list = []\n for c in payload.corrections:\n corr_list.append({\n \"source_term\": c.source_term,\n \"incorrect_target_term\": c.incorrect_target_term,\n \"corrected_target_term\": c.corrected_target_term,\n \"origin_run_id\": c.origin_run_id,\n \"origin_row_key\": c.origin_row_key,\n })\n result = DictionaryManager.submit_bulk_corrections(\n db,\n dict_id=payload.dictionary_id,\n corrections=corr_list,\n origin_user_id=current_user.username,\n )\n if result.get(\"status\") == \"conflicts\":\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=result)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion submit_bulk_corrections\n" }, { "contract_id": "TranslateDictionaryRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", "start_line": 1, - "end_line": 383, + "end_line": 349, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": 4, - "LAYER": "UI", - "POST": "Dictionaries and entries CRUD completed. Import executed with conflict resolution.", - "PRE": "DB session initialized. User authenticated with translate.dictionary permissions.", - "PURPOSE": "Terminology Dictionary CRUD, entries management, and import routes.", - "SIDE_EFFECT": "Reads/writes TerminologyDictionary and DictionaryEntry records to DB." + "COMPLEXITY": 3, + "LAYER": "API", + "PURPOSE": "Terminology Dictionary CRUD, entries management, and import routes." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "TranslateDictionaryRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" - }, - { - "source_id": "TranslateDictionaryRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" - }, - { - "source_id": "TranslateDictionaryRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_db", - "target_ref": "[get_db]" + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslateDictionaryRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,dictionaries]\n# @BRIEF Terminology Dictionary CRUD, entries management, and import routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE DB session initialized. User authenticated with translate.dictionary permissions.\n# @POST Dictionaries and entries CRUD completed. Import executed with conflict resolution.\n# @SIDE_EFFECT Reads/writes TerminologyDictionary and DictionaryEntry records to DB.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.logger import belief_scope\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateDictRoutes\")\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.dictionary import DictionaryManager\nfrom ....schemas.translate import (\n DictionaryCreate,\n DictionaryEntryCreate,\n DictionaryEntryResponse,\n DictionaryImport,\n DictionaryResponse,\n)\n\nfrom ._router import router\nfrom ._helpers import _dict_to_response, _get_dictionary_entry_counts\n\n\n# ============================================================\n# Terminology Dictionaries\n# ============================================================\n\n# #region list_dictionaries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF List all terminology dictionaries with pagination and entry counts.\n# @PRE User has translate.dictionary.view permission.\n# @POST Returns paginated list of dictionaries with total count.\n# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries\")\nasync def list_dictionaries(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all terminology dictionaries.\"\"\"\n with belief_scope(\"list_dictionaries\"):\n log.reason(\"List dictionaries\", payload={\"user\": current_user.username})\n dicts, total = DictionaryManager.list_dictionaries(db, page=page, page_size=page_size)\n dict_ids = [d.id for d in dicts]\n counts = _get_dictionary_entry_counts(db, dict_ids) if dict_ids else {}\n items = [_dict_to_response(d, counts.get(d.id, 0)) for d in dicts]\n log.reflect(\"Dictionaries listed\", payload={\"total\": total, \"returned\": len(items)})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n# #endregion list_dictionaries\n\n\n# #region get_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Get a single terminology dictionary by ID with entry count.\n# @PRE User has translate.dictionary.view permission.\n# @POST Returns the dictionary with entry_count or 404.\n# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries/{dictionary_id}\")\nasync def get_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get a terminology dictionary by ID.\"\"\"\n with belief_scope(\"get_dictionary\"):\n log.reason(\"Get dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n d = DictionaryManager.get_dictionary(db, dictionary_id)\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n log.reflect(\"Dictionary found\", payload={\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_dictionary\n\n\n# #region create_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Create a new terminology dictionary.\n# @PRE User has translate.dictionary.create permission. Payload validated.\n# @POST Dictionary created and returned with zero entries.\n# @SIDE_EFFECT Creates TerminologyDictionary record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries\", status_code=status.HTTP_201_CREATED)\nasync def create_dictionary(\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"CREATE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Create a new terminology dictionary.\"\"\"\n with belief_scope(\"create_dictionary\"):\n log.reason(\"Create dictionary\", payload={\"user\": current_user.username})\n d = DictionaryManager.create_dictionary(\n db,\n name=payload.name,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n created_by=current_user.username,\n description=payload.description,\n is_active=payload.is_active,\n )\n log.reflect(\"Dictionary created\", payload={\"id\": d.id})\n return _dict_to_response(d, 0)\n# #endregion create_dictionary\n\n\n# #region update_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Update an existing terminology dictionary.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists.\n# @POST Dictionary updated and returned with current entry count.\n# @SIDE_EFFECT Updates TerminologyDictionary record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.put(\"/dictionaries/{dictionary_id}\")\nasync def update_dictionary(\n dictionary_id: str,\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update a terminology dictionary.\"\"\"\n with belief_scope(\"update_dictionary\"):\n log.reason(\"Update dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n d = DictionaryManager.update_dictionary(\n db,\n dict_id=dictionary_id,\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n is_active=payload.is_active,\n )\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n log.reflect(\"Dictionary updated\", payload={\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_dictionary\n\n\n# #region delete_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.\n# @PRE User has translate.dictionary.delete permission. Dictionary exists and is not in use.\n# @POST Dictionary is deleted or 409 if attached to active jobs.\n# @SIDE_EFFECT Deletes TerminologyDictionary record from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.delete(\"/dictionaries/{dictionary_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"DELETE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a terminology dictionary.\"\"\"\n with belief_scope(\"delete_dictionary\"):\n log.reason(\"Delete dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n DictionaryManager.delete_dictionary(db, dictionary_id)\n log.reflect(\"Dictionary deleted\", payload={\"id\": dictionary_id})\n return None\n except ValueError as e:\n if \"active/scheduled\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary\n\n\n# ============================================================\n# Dictionary Entries CRUD\n# ============================================================\n\n# #region list_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF List entries for a dictionary with pagination.\n# @PRE User has translate.dictionary.view permission. Dictionary exists.\n# @POST Returns paginated list of entries.\n# @SIDE_EFFECT Reads DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries/{dictionary_id}/entries\")\nasync def list_dictionary_entries(\n dictionary_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List entries for a dictionary.\"\"\"\n with belief_scope(\"list_dictionary_entries\"):\n log.reason(\"List dictionary entries\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n entries, total = DictionaryManager.list_entries(db, dictionary_id, page=page, page_size=page_size)\n items = [\n {\n \"id\": e.id,\n \"dictionary_id\": e.dictionary_id,\n \"source_term\": e.source_term,\n \"source_term_normalized\": e.source_term_normalized,\n \"target_term\": e.target_term,\n \"context_notes\": e.context_notes,\n \"created_at\": e.created_at,\n \"updated_at\": e.updated_at,\n }\n for e in entries\n ]\n log.reflect(\"Entries listed\", payload={\"dict_id\": dictionary_id, \"total\": total})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion list_dictionary_entries\n\n\n# #region add_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Add a new entry to a dictionary.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists.\n# @POST Entry created and returned.\n# @SIDE_EFFECT Creates DictionaryEntry record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries/{dictionary_id}/entries\", status_code=status.HTTP_201_CREATED)\nasync def add_dictionary_entry(\n dictionary_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Add a new entry to a dictionary.\"\"\"\n with belief_scope(\"add_dictionary_entry\"):\n log.reason(\"Add dictionary entry\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n entry = DictionaryManager.add_entry(\n db, dictionary_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n log.reflect(\"Entry added\", payload={\"entry_id\": entry.id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion add_dictionary_entry\n\n\n# #region edit_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Update an existing dictionary entry.\n# @PRE User has translate.dictionary.edit permission. Entry exists.\n# @POST Entry updated and returned.\n# @SIDE_EFFECT Updates DictionaryEntry record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.put(\"/dictionaries/{dictionary_id}/entries/{entry_id}\")\nasync def edit_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update an existing dictionary entry.\"\"\"\n with belief_scope(\"edit_dictionary_entry\"):\n log.reason(\"Edit dictionary entry\", payload={\"entry_id\": entry_id, \"user\": current_user.username})\n try:\n entry = DictionaryManager.edit_entry(\n db, entry_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n log.reflect(\"Entry updated\", payload={\"entry_id\": entry_id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion edit_dictionary_entry\n\n\n# #region delete_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Delete a dictionary entry.\n# @PRE User has translate.dictionary.edit permission. Entry exists.\n# @POST Entry is deleted.\n# @SIDE_EFFECT Deletes DictionaryEntry record from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.delete(\"/dictionaries/{dictionary_id}/entries/{entry_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a dictionary entry.\"\"\"\n with belief_scope(\"delete_dictionary_entry\"):\n log.reason(\"Delete dictionary entry\", payload={\"entry_id\": entry_id, \"user\": current_user.username})\n try:\n DictionaryManager.delete_entry(db, entry_id)\n log.reflect(\"Entry deleted\", payload={\"entry_id\": entry_id})\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary_entry\n\n\n# #region import_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,import]\n# @BRIEF Import entries into a terminology dictionary from CSV/TSV content.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists. Content is valid CSV/TSV.\n# @POST Entries imported per conflict mode (overwrite/keep_existing/cancel). Import result returned.\n# @SIDE_EFFECT Creates/updates DictionaryEntry records in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries/{dictionary_id}/import\")\nasync def import_dictionary_entries(\n dictionary_id: str,\n payload: DictionaryImport,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Import entries into a terminology dictionary from CSV/TSV content.\"\"\"\n with belief_scope(\"import_dictionary_entries\"):\n log.reason(\"Import dictionary entries\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n\n if payload.on_conflict not in (\"overwrite\", \"keep_existing\", \"cancel\"):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"on_conflict must be 'overwrite', 'keep_existing', or 'cancel'\",\n )\n\n try:\n result = DictionaryManager.import_entries(\n db,\n dict_id=dictionary_id,\n content=payload.content,\n delimiter=payload.delimiter,\n on_conflict=payload.on_conflict,\n preview_only=payload.preview_only,\n )\n log.reflect(\"Import completed\", payload={\n \"dict_id\": dictionary_id,\n \"total\": result[\"total\"],\n \"errors\": len(result[\"errors\"]),\n })\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion import_dictionary_entries\n\n# #endregion TranslateDictionaryRoutesModule\n" + "body": "# #region TranslateDictionaryRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search]\n# @BRIEF Terminology Dictionary CRUD, entries management, and import routes.\n# @LAYER: API\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.logger import belief_scope, logger\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.dictionary import DictionaryManager\nfrom ....schemas.auth import User\nfrom ....schemas.translate import (\n DictionaryCreate,\n DictionaryEntryCreate,\n DictionaryImport,\n)\nfrom ._helpers import _dict_to_response, _get_dictionary_entry_counts\nfrom ._router import router\n\n# ============================================================\n# Terminology Dictionaries\n# ============================================================\n\n# #region list_dictionaries [TYPE Function]\n# @BRIEF List all terminology dictionaries.\n# @PRE: User has translate.dictionary.view permission.\n# @POST: Returns list of dictionaries with total count.\n@router.get(\"/dictionaries\")\nasync def list_dictionaries(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all terminology dictionaries.\"\"\"\n with belief_scope(\"list_dictionaries\"):\n logger.reason(f\"User: {current_user.username}\")\n dicts, total = DictionaryManager.list_dictionaries(db, page=page, page_size=page_size)\n dict_ids = [d.id for d in dicts]\n counts = _get_dictionary_entry_counts(db, dict_ids) if dict_ids else {}\n items = [_dict_to_response(d, counts.get(d.id, 0)) for d in dicts]\n logger.reflect(\"Dictionaries listed\", {\"total\": total, \"returned\": len(items)})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n# #endregion list_dictionaries\n\n\n# #region get_dictionary [TYPE Function]\n# @BRIEF Get a single terminology dictionary by ID.\n# @PRE: User has translate.dictionary.view permission.\n# @POST: Returns the dictionary with entry_count.\n@router.get(\"/dictionaries/{dictionary_id}\")\nasync def get_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get a terminology dictionary by ID.\"\"\"\n with belief_scope(\"get_dictionary\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n d = DictionaryManager.get_dictionary(db, dictionary_id)\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n logger.reflect(\"Dictionary found\", {\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_dictionary\n\n\n# #region create_dictionary [TYPE Function]\n# @BRIEF Create a new terminology dictionary.\n# @PRE: User has translate.dictionary.create permission.\n# @POST: Returns the created dictionary.\n@router.post(\"/dictionaries\", status_code=status.HTTP_201_CREATED)\nasync def create_dictionary(\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"CREATE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Create a new terminology dictionary.\"\"\"\n with belief_scope(\"create_dictionary\"):\n logger.reason(f\"User: {current_user.username}\")\n d = DictionaryManager.create_dictionary(\n db,\n name=payload.name,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n created_by=current_user.username,\n description=payload.description,\n is_active=payload.is_active,\n )\n logger.reflect(\"Dictionary created\", {\"id\": d.id})\n return _dict_to_response(d, 0)\n# #endregion create_dictionary\n\n\n# #region update_dictionary [TYPE Function]\n# @BRIEF Update an existing terminology dictionary.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Returns the updated dictionary.\n@router.put(\"/dictionaries/{dictionary_id}\")\nasync def update_dictionary(\n dictionary_id: str,\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update a terminology dictionary.\"\"\"\n with belief_scope(\"update_dictionary\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n d = DictionaryManager.update_dictionary(\n db,\n dict_id=dictionary_id,\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n is_active=payload.is_active,\n )\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n logger.reflect(\"Dictionary updated\", {\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_dictionary\n\n\n# #region delete_dictionary [TYPE Function]\n# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.\n# @PRE: User has translate.dictionary.delete permission.\n# @POST: Dictionary is deleted.\n@router.delete(\"/dictionaries/{dictionary_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"DELETE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a terminology dictionary.\"\"\"\n with belief_scope(\"delete_dictionary\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n DictionaryManager.delete_dictionary(db, dictionary_id)\n logger.reflect(\"Dictionary deleted\", {\"id\": dictionary_id})\n return None\n except ValueError as e:\n if \"active/scheduled\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary\n\n\n# ============================================================\n# Dictionary Entries CRUD\n# ============================================================\n\n# #region list_dictionary_entries [TYPE Function]\n# @BRIEF List entries for a dictionary.\n# @PRE: User has translate.dictionary.view permission.\n# @POST: Returns paginated list of entries.\n@router.get(\"/dictionaries/{dictionary_id}/entries\")\nasync def list_dictionary_entries(\n dictionary_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List entries for a dictionary.\"\"\"\n with belief_scope(\"list_dictionary_entries\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n entries, total = DictionaryManager.list_entries(db, dictionary_id, page=page, page_size=page_size)\n items = [\n {\n \"id\": e.id,\n \"dictionary_id\": e.dictionary_id,\n \"source_term\": e.source_term,\n \"source_term_normalized\": e.source_term_normalized,\n \"target_term\": e.target_term,\n \"context_notes\": e.context_notes,\n \"created_at\": e.created_at,\n \"updated_at\": e.updated_at,\n }\n for e in entries\n ]\n logger.reflect(\"Entries listed\", {\"dict_id\": dictionary_id, \"total\": total})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion list_dictionary_entries\n\n\n# #region add_dictionary_entry [TYPE Function]\n# @BRIEF Add a new entry to a dictionary.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Entry is created.\n@router.post(\"/dictionaries/{dictionary_id}/entries\", status_code=status.HTTP_201_CREATED)\nasync def add_dictionary_entry(\n dictionary_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Add a new entry to a dictionary.\"\"\"\n with belief_scope(\"add_dictionary_entry\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n entry = DictionaryManager.add_entry(\n db, dictionary_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n logger.reflect(\"Entry added\", {\"entry_id\": entry.id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion add_dictionary_entry\n\n\n# #region edit_dictionary_entry [TYPE Function]\n# @BRIEF Update an existing dictionary entry.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Entry is updated.\n@router.put(\"/dictionaries/{dictionary_id}/entries/{entry_id}\")\nasync def edit_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update an existing dictionary entry.\"\"\"\n with belief_scope(\"edit_dictionary_entry\"):\n logger.reason(f\"Entry: {entry_id}, User: {current_user.username}\")\n try:\n entry = DictionaryManager.edit_entry(\n db, entry_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n logger.reflect(\"Entry updated\", {\"entry_id\": entry_id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion edit_dictionary_entry\n\n\n# #region delete_dictionary_entry [TYPE Function]\n# @BRIEF Delete a dictionary entry.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Entry is deleted.\n@router.delete(\"/dictionaries/{dictionary_id}/entries/{entry_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a dictionary entry.\"\"\"\n with belief_scope(\"delete_dictionary_entry\"):\n logger.reason(f\"Entry: {entry_id}, User: {current_user.username}\")\n try:\n DictionaryManager.delete_entry(db, entry_id)\n logger.reflect(\"Entry deleted\", {\"entry_id\": entry_id})\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary_entry\n\n\n# #region import_dictionary_entries [TYPE Function]\n# @BRIEF Import entries into a terminology dictionary from CSV/TSV content.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Entries are imported per conflict mode.\n@router.post(\"/dictionaries/{dictionary_id}/import\")\nasync def import_dictionary_entries(\n dictionary_id: str,\n payload: DictionaryImport,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Import entries into a terminology dictionary from CSV/TSV content.\"\"\"\n with belief_scope(\"import_dictionary_entries\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n\n if payload.on_conflict not in (\"overwrite\", \"keep_existing\", \"cancel\"):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"on_conflict must be 'overwrite', 'keep_existing', or 'cancel'\",\n )\n\n try:\n result = DictionaryManager.import_entries(\n db,\n dict_id=dictionary_id,\n content=payload.content,\n delimiter=payload.delimiter,\n on_conflict=payload.on_conflict,\n preview_only=payload.preview_only,\n )\n logger.reflect(\"Import completed\", {\n \"dict_id\": dictionary_id,\n \"total\": result[\"total\"],\n \"errors\": len(result[\"errors\"]),\n })\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion import_dictionary_entries\n\n# #endregion TranslateDictionaryRoutesModule\n" }, { "contract_id": "list_dictionaries", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 39, - "end_line": 62, - "tier": "TIER_2", - "complexity": 4, + "start_line": 25, + "end_line": 46, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Returns paginated list of dictionaries with total count.", + "POST": "Returns list of dictionaries with total count.", "PRE": "User has translate.dictionary.view permission.", - "PURPOSE": "List all terminology dictionaries with pagination and entry counts.", - "SIDE_EFFECT": "Reads TerminologyDictionary and DictionaryEntry records from DB." + "PURPOSE": "List all terminology dictionaries." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "list_dictionaries", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region list_dictionaries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF List all terminology dictionaries with pagination and entry counts.\n# @PRE User has translate.dictionary.view permission.\n# @POST Returns paginated list of dictionaries with total count.\n# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries\")\nasync def list_dictionaries(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all terminology dictionaries.\"\"\"\n with belief_scope(\"list_dictionaries\"):\n log.reason(\"List dictionaries\", payload={\"user\": current_user.username})\n dicts, total = DictionaryManager.list_dictionaries(db, page=page, page_size=page_size)\n dict_ids = [d.id for d in dicts]\n counts = _get_dictionary_entry_counts(db, dict_ids) if dict_ids else {}\n items = [_dict_to_response(d, counts.get(d.id, 0)) for d in dicts]\n log.reflect(\"Dictionaries listed\", payload={\"total\": total, \"returned\": len(items)})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n# #endregion list_dictionaries\n" + "body": "# #region list_dictionaries [TYPE Function]\n# @BRIEF List all terminology dictionaries.\n# @PRE: User has translate.dictionary.view permission.\n# @POST: Returns list of dictionaries with total count.\n@router.get(\"/dictionaries\")\nasync def list_dictionaries(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all terminology dictionaries.\"\"\"\n with belief_scope(\"list_dictionaries\"):\n logger.reason(f\"User: {current_user.username}\")\n dicts, total = DictionaryManager.list_dictionaries(db, page=page, page_size=page_size)\n dict_ids = [d.id for d in dicts]\n counts = _get_dictionary_entry_counts(db, dict_ids) if dict_ids else {}\n items = [_dict_to_response(d, counts.get(d.id, 0)) for d in dicts]\n logger.reflect(\"Dictionaries listed\", {\"total\": total, \"returned\": len(items)})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n# #endregion list_dictionaries\n" }, { "contract_id": "get_dictionary", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 65, - "end_line": 89, - "tier": "TIER_2", - "complexity": 4, + "start_line": 49, + "end_line": 71, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Returns the dictionary with entry_count or 404.", + "POST": "Returns the dictionary with entry_count.", "PRE": "User has translate.dictionary.view permission.", - "PURPOSE": "Get a single terminology dictionary by ID with entry count.", - "SIDE_EFFECT": "Reads TerminologyDictionary and DictionaryEntry records from DB." + "PURPOSE": "Get a single terminology dictionary by ID." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_dictionary", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Get a single terminology dictionary by ID with entry count.\n# @PRE User has translate.dictionary.view permission.\n# @POST Returns the dictionary with entry_count or 404.\n# @SIDE_EFFECT Reads TerminologyDictionary and DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries/{dictionary_id}\")\nasync def get_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get a terminology dictionary by ID.\"\"\"\n with belief_scope(\"get_dictionary\"):\n log.reason(\"Get dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n d = DictionaryManager.get_dictionary(db, dictionary_id)\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n log.reflect(\"Dictionary found\", payload={\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_dictionary\n" + "body": "# #region get_dictionary [TYPE Function]\n# @BRIEF Get a single terminology dictionary by ID.\n# @PRE: User has translate.dictionary.view permission.\n# @POST: Returns the dictionary with entry_count.\n@router.get(\"/dictionaries/{dictionary_id}\")\nasync def get_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get a terminology dictionary by ID.\"\"\"\n with belief_scope(\"get_dictionary\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n d = DictionaryManager.get_dictionary(db, dictionary_id)\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n logger.reflect(\"Dictionary found\", {\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_dictionary\n" }, { "contract_id": "create_dictionary", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 92, - "end_line": 119, - "tier": "TIER_2", - "complexity": 4, + "start_line": 74, + "end_line": 99, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Dictionary created and returned with zero entries.", - "PRE": "User has translate.dictionary.create permission. Payload validated.", - "PURPOSE": "Create a new terminology dictionary.", - "SIDE_EFFECT": "Creates TerminologyDictionary record in DB." + "POST": "Returns the created dictionary.", + "PRE": "User has translate.dictionary.create permission.", + "PURPOSE": "Create a new terminology dictionary." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "create_dictionary", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region create_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Create a new terminology dictionary.\n# @PRE User has translate.dictionary.create permission. Payload validated.\n# @POST Dictionary created and returned with zero entries.\n# @SIDE_EFFECT Creates TerminologyDictionary record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries\", status_code=status.HTTP_201_CREATED)\nasync def create_dictionary(\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"CREATE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Create a new terminology dictionary.\"\"\"\n with belief_scope(\"create_dictionary\"):\n log.reason(\"Create dictionary\", payload={\"user\": current_user.username})\n d = DictionaryManager.create_dictionary(\n db,\n name=payload.name,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n created_by=current_user.username,\n description=payload.description,\n is_active=payload.is_active,\n )\n log.reflect(\"Dictionary created\", payload={\"id\": d.id})\n return _dict_to_response(d, 0)\n# #endregion create_dictionary\n" + "body": "# #region create_dictionary [TYPE Function]\n# @BRIEF Create a new terminology dictionary.\n# @PRE: User has translate.dictionary.create permission.\n# @POST: Returns the created dictionary.\n@router.post(\"/dictionaries\", status_code=status.HTTP_201_CREATED)\nasync def create_dictionary(\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"CREATE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Create a new terminology dictionary.\"\"\"\n with belief_scope(\"create_dictionary\"):\n logger.reason(f\"User: {current_user.username}\")\n d = DictionaryManager.create_dictionary(\n db,\n name=payload.name,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n created_by=current_user.username,\n description=payload.description,\n is_active=payload.is_active,\n )\n logger.reflect(\"Dictionary created\", {\"id\": d.id})\n return _dict_to_response(d, 0)\n# #endregion create_dictionary\n" }, { "contract_id": "update_dictionary", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 122, - "end_line": 155, - "tier": "TIER_2", - "complexity": 4, + "start_line": 102, + "end_line": 133, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Dictionary updated and returned with current entry count.", - "PRE": "User has translate.dictionary.edit permission. Dictionary exists.", - "PURPOSE": "Update an existing terminology dictionary.", - "SIDE_EFFECT": "Updates TerminologyDictionary record in DB." + "POST": "Returns the updated dictionary.", + "PRE": "User has translate.dictionary.edit permission.", + "PURPOSE": "Update an existing terminology dictionary." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "update_dictionary", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region update_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Update an existing terminology dictionary.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists.\n# @POST Dictionary updated and returned with current entry count.\n# @SIDE_EFFECT Updates TerminologyDictionary record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.put(\"/dictionaries/{dictionary_id}\")\nasync def update_dictionary(\n dictionary_id: str,\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update a terminology dictionary.\"\"\"\n with belief_scope(\"update_dictionary\"):\n log.reason(\"Update dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n d = DictionaryManager.update_dictionary(\n db,\n dict_id=dictionary_id,\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n is_active=payload.is_active,\n )\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n log.reflect(\"Dictionary updated\", payload={\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_dictionary\n" + "body": "# #region update_dictionary [TYPE Function]\n# @BRIEF Update an existing terminology dictionary.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Returns the updated dictionary.\n@router.put(\"/dictionaries/{dictionary_id}\")\nasync def update_dictionary(\n dictionary_id: str,\n payload: DictionaryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update a terminology dictionary.\"\"\"\n with belief_scope(\"update_dictionary\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n d = DictionaryManager.update_dictionary(\n db,\n dict_id=dictionary_id,\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n is_active=payload.is_active,\n )\n from ....models.translate import DictionaryEntry\n entry_count = db.query(DictionaryEntry.id).filter(DictionaryEntry.dictionary_id == dictionary_id).count()\n logger.reflect(\"Dictionary updated\", {\"id\": dictionary_id})\n return _dict_to_response(d, entry_count)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_dictionary\n" }, { "contract_id": "delete_dictionary", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 158, - "end_line": 182, - "tier": "TIER_2", - "complexity": 4, + "start_line": 136, + "end_line": 158, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Dictionary is deleted or 409 if attached to active jobs.", - "PRE": "User has translate.dictionary.delete permission. Dictionary exists and is not in use.", - "PURPOSE": "Delete a terminology dictionary, blocked if attached to active/scheduled jobs.", - "SIDE_EFFECT": "Deletes TerminologyDictionary record from DB." + "POST": "Dictionary is deleted.", + "PRE": "User has translate.dictionary.delete permission.", + "PURPOSE": "Delete a terminology dictionary, blocked if attached to active/scheduled jobs." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "delete_dictionary", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region delete_dictionary [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries]\n# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.\n# @PRE User has translate.dictionary.delete permission. Dictionary exists and is not in use.\n# @POST Dictionary is deleted or 409 if attached to active jobs.\n# @SIDE_EFFECT Deletes TerminologyDictionary record from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.delete(\"/dictionaries/{dictionary_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"DELETE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a terminology dictionary.\"\"\"\n with belief_scope(\"delete_dictionary\"):\n log.reason(\"Delete dictionary\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n DictionaryManager.delete_dictionary(db, dictionary_id)\n log.reflect(\"Dictionary deleted\", payload={\"id\": dictionary_id})\n return None\n except ValueError as e:\n if \"active/scheduled\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary\n" + "body": "# #region delete_dictionary [TYPE Function]\n# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.\n# @PRE: User has translate.dictionary.delete permission.\n# @POST: Dictionary is deleted.\n@router.delete(\"/dictionaries/{dictionary_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary(\n dictionary_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"DELETE\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a terminology dictionary.\"\"\"\n with belief_scope(\"delete_dictionary\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n DictionaryManager.delete_dictionary(db, dictionary_id)\n logger.reflect(\"Dictionary deleted\", {\"id\": dictionary_id})\n return None\n except ValueError as e:\n if \"active/scheduled\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary\n" }, { "contract_id": "list_dictionary_entries", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 189, - "end_line": 226, - "tier": "TIER_2", - "complexity": 4, + "start_line": 165, + "end_line": 200, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, "POST": "Returns paginated list of entries.", - "PRE": "User has translate.dictionary.view permission. Dictionary exists.", - "PURPOSE": "List entries for a dictionary with pagination.", - "SIDE_EFFECT": "Reads DictionaryEntry records from DB." + "PRE": "User has translate.dictionary.view permission.", + "PURPOSE": "List entries for a dictionary." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "list_dictionary_entries", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region list_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF List entries for a dictionary with pagination.\n# @PRE User has translate.dictionary.view permission. Dictionary exists.\n# @POST Returns paginated list of entries.\n# @SIDE_EFFECT Reads DictionaryEntry records from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.get(\"/dictionaries/{dictionary_id}/entries\")\nasync def list_dictionary_entries(\n dictionary_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List entries for a dictionary.\"\"\"\n with belief_scope(\"list_dictionary_entries\"):\n log.reason(\"List dictionary entries\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n entries, total = DictionaryManager.list_entries(db, dictionary_id, page=page, page_size=page_size)\n items = [\n {\n \"id\": e.id,\n \"dictionary_id\": e.dictionary_id,\n \"source_term\": e.source_term,\n \"source_term_normalized\": e.source_term_normalized,\n \"target_term\": e.target_term,\n \"context_notes\": e.context_notes,\n \"created_at\": e.created_at,\n \"updated_at\": e.updated_at,\n }\n for e in entries\n ]\n log.reflect(\"Entries listed\", payload={\"dict_id\": dictionary_id, \"total\": total})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion list_dictionary_entries\n" + "body": "# #region list_dictionary_entries [TYPE Function]\n# @BRIEF List entries for a dictionary.\n# @PRE: User has translate.dictionary.view permission.\n# @POST: Returns paginated list of entries.\n@router.get(\"/dictionaries/{dictionary_id}/entries\")\nasync def list_dictionary_entries(\n dictionary_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List entries for a dictionary.\"\"\"\n with belief_scope(\"list_dictionary_entries\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n entries, total = DictionaryManager.list_entries(db, dictionary_id, page=page, page_size=page_size)\n items = [\n {\n \"id\": e.id,\n \"dictionary_id\": e.dictionary_id,\n \"source_term\": e.source_term,\n \"source_term_normalized\": e.source_term_normalized,\n \"target_term\": e.target_term,\n \"context_notes\": e.context_notes,\n \"created_at\": e.created_at,\n \"updated_at\": e.updated_at,\n }\n for e in entries\n ]\n logger.reflect(\"Entries listed\", {\"dict_id\": dictionary_id, \"total\": total})\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion list_dictionary_entries\n" }, { "contract_id": "add_dictionary_entry", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 229, - "end_line": 268, - "tier": "TIER_2", - "complexity": 4, + "start_line": 203, + "end_line": 240, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Entry created and returned.", - "PRE": "User has translate.dictionary.edit permission. Dictionary exists.", - "PURPOSE": "Add a new entry to a dictionary.", - "SIDE_EFFECT": "Creates DictionaryEntry record in DB." + "POST": "Entry is created.", + "PRE": "User has translate.dictionary.edit permission.", + "PURPOSE": "Add a new entry to a dictionary." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "add_dictionary_entry", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region add_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Add a new entry to a dictionary.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists.\n# @POST Entry created and returned.\n# @SIDE_EFFECT Creates DictionaryEntry record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries/{dictionary_id}/entries\", status_code=status.HTTP_201_CREATED)\nasync def add_dictionary_entry(\n dictionary_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Add a new entry to a dictionary.\"\"\"\n with belief_scope(\"add_dictionary_entry\"):\n log.reason(\"Add dictionary entry\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n try:\n entry = DictionaryManager.add_entry(\n db, dictionary_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n log.reflect(\"Entry added\", payload={\"entry_id\": entry.id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion add_dictionary_entry\n" + "body": "# #region add_dictionary_entry [TYPE Function]\n# @BRIEF Add a new entry to a dictionary.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Entry is created.\n@router.post(\"/dictionaries/{dictionary_id}/entries\", status_code=status.HTTP_201_CREATED)\nasync def add_dictionary_entry(\n dictionary_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Add a new entry to a dictionary.\"\"\"\n with belief_scope(\"add_dictionary_entry\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n try:\n entry = DictionaryManager.add_entry(\n db, dictionary_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n logger.reflect(\"Entry added\", {\"entry_id\": entry.id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion add_dictionary_entry\n" }, { "contract_id": "edit_dictionary_entry", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 271, - "end_line": 311, - "tier": "TIER_2", - "complexity": 4, + "start_line": 243, + "end_line": 281, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Entry updated and returned.", - "PRE": "User has translate.dictionary.edit permission. Entry exists.", - "PURPOSE": "Update an existing dictionary entry.", - "SIDE_EFFECT": "Updates DictionaryEntry record in DB." + "POST": "Entry is updated.", + "PRE": "User has translate.dictionary.edit permission.", + "PURPOSE": "Update an existing dictionary entry." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "edit_dictionary_entry", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region edit_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Update an existing dictionary entry.\n# @PRE User has translate.dictionary.edit permission. Entry exists.\n# @POST Entry updated and returned.\n# @SIDE_EFFECT Updates DictionaryEntry record in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.put(\"/dictionaries/{dictionary_id}/entries/{entry_id}\")\nasync def edit_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update an existing dictionary entry.\"\"\"\n with belief_scope(\"edit_dictionary_entry\"):\n log.reason(\"Edit dictionary entry\", payload={\"entry_id\": entry_id, \"user\": current_user.username})\n try:\n entry = DictionaryManager.edit_entry(\n db, entry_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n log.reflect(\"Entry updated\", payload={\"entry_id\": entry_id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion edit_dictionary_entry\n" + "body": "# #region edit_dictionary_entry [TYPE Function]\n# @BRIEF Update an existing dictionary entry.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Entry is updated.\n@router.put(\"/dictionaries/{dictionary_id}/entries/{entry_id}\")\nasync def edit_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n payload: DictionaryEntryCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Update an existing dictionary entry.\"\"\"\n with belief_scope(\"edit_dictionary_entry\"):\n logger.reason(f\"Entry: {entry_id}, User: {current_user.username}\")\n try:\n entry = DictionaryManager.edit_entry(\n db, entry_id,\n source_term=payload.source_term,\n target_term=payload.target_term,\n context_notes=payload.context_notes,\n )\n logger.reflect(\"Entry updated\", {\"entry_id\": entry_id})\n return {\n \"id\": entry.id,\n \"dictionary_id\": entry.dictionary_id,\n \"source_term\": entry.source_term,\n \"source_term_normalized\": entry.source_term_normalized,\n \"target_term\": entry.target_term,\n \"context_notes\": entry.context_notes,\n \"created_at\": entry.created_at,\n \"updated_at\": entry.updated_at,\n }\n except ValueError as e:\n if \"already exists\" in str(e):\n raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion edit_dictionary_entry\n" }, { "contract_id": "delete_dictionary_entry", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 314, - "end_line": 337, - "tier": "TIER_2", - "complexity": 4, + "start_line": 284, + "end_line": 305, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, "POST": "Entry is deleted.", - "PRE": "User has translate.dictionary.edit permission. Entry exists.", - "PURPOSE": "Delete a dictionary entry.", - "SIDE_EFFECT": "Deletes DictionaryEntry record from DB." + "PRE": "User has translate.dictionary.edit permission.", + "PURPOSE": "Delete a dictionary entry." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "delete_dictionary_entry", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region delete_dictionary_entry [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,entries]\n# @BRIEF Delete a dictionary entry.\n# @PRE User has translate.dictionary.edit permission. Entry exists.\n# @POST Entry is deleted.\n# @SIDE_EFFECT Deletes DictionaryEntry record from DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.delete(\"/dictionaries/{dictionary_id}/entries/{entry_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a dictionary entry.\"\"\"\n with belief_scope(\"delete_dictionary_entry\"):\n log.reason(\"Delete dictionary entry\", payload={\"entry_id\": entry_id, \"user\": current_user.username})\n try:\n DictionaryManager.delete_entry(db, entry_id)\n log.reflect(\"Entry deleted\", payload={\"entry_id\": entry_id})\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary_entry\n" + "body": "# #region delete_dictionary_entry [TYPE Function]\n# @BRIEF Delete a dictionary entry.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Entry is deleted.\n@router.delete(\"/dictionaries/{dictionary_id}/entries/{entry_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_dictionary_entry(\n dictionary_id: str,\n entry_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Delete a dictionary entry.\"\"\"\n with belief_scope(\"delete_dictionary_entry\"):\n logger.reason(f\"Entry: {entry_id}, User: {current_user.username}\")\n try:\n DictionaryManager.delete_entry(db, entry_id)\n logger.reflect(\"Entry deleted\", {\"entry_id\": entry_id})\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_dictionary_entry\n" }, { "contract_id": "import_dictionary_entries", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_dictionary_routes.py", - "start_line": 340, - "end_line": 381, - "tier": "TIER_2", - "complexity": 4, + "start_line": 308, + "end_line": 347, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Entries imported per conflict mode (overwrite/keep_existing/cancel). Import result returned.", - "PRE": "User has translate.dictionary.edit permission. Dictionary exists. Content is valid CSV/TSV.", - "PURPOSE": "Import entries into a terminology dictionary from CSV/TSV content.", - "SIDE_EFFECT": "Creates/updates DictionaryEntry records in DB." + "POST": "Entries are imported per conflict mode.", + "PRE": "User has translate.dictionary.edit permission.", + "PURPOSE": "Import entries into a terminology dictionary from CSV/TSV content." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "import_dictionary_entries", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryManager", - "target_ref": "[DictionaryManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region import_dictionary_entries [C:4] [TYPE Function] [SEMANTICS api,translate,dictionaries,import]\n# @BRIEF Import entries into a terminology dictionary from CSV/TSV content.\n# @PRE User has translate.dictionary.edit permission. Dictionary exists. Content is valid CSV/TSV.\n# @POST Entries imported per conflict mode (overwrite/keep_existing/cancel). Import result returned.\n# @SIDE_EFFECT Creates/updates DictionaryEntry records in DB.\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n@router.post(\"/dictionaries/{dictionary_id}/import\")\nasync def import_dictionary_entries(\n dictionary_id: str,\n payload: DictionaryImport,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Import entries into a terminology dictionary from CSV/TSV content.\"\"\"\n with belief_scope(\"import_dictionary_entries\"):\n log.reason(\"Import dictionary entries\", payload={\"dict_id\": dictionary_id, \"user\": current_user.username})\n\n if payload.on_conflict not in (\"overwrite\", \"keep_existing\", \"cancel\"):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"on_conflict must be 'overwrite', 'keep_existing', or 'cancel'\",\n )\n\n try:\n result = DictionaryManager.import_entries(\n db,\n dict_id=dictionary_id,\n content=payload.content,\n delimiter=payload.delimiter,\n on_conflict=payload.on_conflict,\n preview_only=payload.preview_only,\n )\n log.reflect(\"Import completed\", payload={\n \"dict_id\": dictionary_id,\n \"total\": result[\"total\"],\n \"errors\": len(result[\"errors\"]),\n })\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion import_dictionary_entries\n" + "body": "# #region import_dictionary_entries [TYPE Function]\n# @BRIEF Import entries into a terminology dictionary from CSV/TSV content.\n# @PRE: User has translate.dictionary.edit permission.\n# @POST: Entries are imported per conflict mode.\n@router.post(\"/dictionaries/{dictionary_id}/import\")\nasync def import_dictionary_entries(\n dictionary_id: str,\n payload: DictionaryImport,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.dictionary\", \"EDIT\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Import entries into a terminology dictionary from CSV/TSV content.\"\"\"\n with belief_scope(\"import_dictionary_entries\"):\n logger.reason(f\"Dict: {dictionary_id}, User: {current_user.username}\")\n\n if payload.on_conflict not in (\"overwrite\", \"keep_existing\", \"cancel\"):\n raise HTTPException(\n status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,\n detail=\"on_conflict must be 'overwrite', 'keep_existing', or 'cancel'\",\n )\n\n try:\n result = DictionaryManager.import_entries(\n db,\n dict_id=dictionary_id,\n content=payload.content,\n delimiter=payload.delimiter,\n on_conflict=payload.on_conflict,\n preview_only=payload.preview_only,\n )\n logger.reflect(\"Import completed\", {\n \"dict_id\": dictionary_id,\n \"total\": result[\"total\"],\n \"errors\": len(result[\"errors\"]),\n })\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion import_dictionary_entries\n" }, { "contract_id": "TranslateHelpersModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_helpers.py", "start_line": 1, - "end_line": 69, + "end_line": 72, "tier": "TIER_1", "complexity": 2, "metadata": { "COMPLEXITY": 2, - "LAYER": "UI", + "LAYER": "API", "PURPOSE": "Shared helper functions for translate route handlers." }, - "relations": [], - "schema_warnings": [], + "relations": [ + { + "source_id": "TranslateHelpersModule", + "relation_type": "DEPENDS_ON", + "target_id": "models.translate", + "target_ref": "[models.translate]" + } + ], + "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", - "body": "# #region TranslateHelpersModule [C:2] [TYPE Module] [SEMANTICS api,routes,translate,helpers]\n# @BRIEF Shared helper functions for translate route handlers.\n# @LAYER UI\n\nfrom typing import Any, Dict, List\nfrom sqlalchemy.orm import Session\n\n\n# #region _run_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Convert TranslationRun ORM to response dict.\ndef _run_to_response(run: Any) -> dict:\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"config_snapshot\": run.config_snapshot,\n \"key_hash\": run.key_hash,\n \"config_hash\": run.config_hash,\n \"dict_snapshot_hash\": run.dict_snapshot_hash,\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n# #endregion _run_to_response\n\n\n# #region _dict_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.\n@staticmethod\ndef _dict_to_response(d: Any, entry_count: int = 0) -> dict:\n return {\n \"id\": d.id,\n \"name\": d.name,\n \"description\": d.description,\n \"source_dialect\": d.source_dialect,\n \"target_dialect\": d.target_dialect,\n \"is_active\": d.is_active,\n \"created_by\": d.created_by,\n \"created_at\": d.created_at,\n \"updated_at\": d.updated_at,\n \"entry_count\": entry_count,\n }\n# #endregion _dict_to_response\n\n\n# #region _get_dictionary_entry_counts [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Get entry counts for a list of dictionary IDs.\ndef _get_dictionary_entry_counts(db: Session, dict_ids: List[str]) -> Dict[str, int]:\n from sqlalchemy import func\n from ....models.translate import DictionaryEntry\n counts = (\n db.query(DictionaryEntry.dictionary_id, func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id.in_(dict_ids))\n .group_by(DictionaryEntry.dictionary_id)\n .all()\n )\n return {row[0]: row[1] for row in counts}\n# #endregion _get_dictionary_entry_counts\n\n# #endregion TranslateHelpersModule\n" + "body": "# #region TranslateHelpersModule [C:2] [TYPE Module] [SEMANTICS sqlalchemy, translate, helper, query, mapping]\n# @BRIEF Shared helper functions for translate route handlers.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [models.translate]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\n\n# #region _run_to_response [C:2] [TYPE Function]\n# @BRIEF Convert TranslationRun ORM to response dict.\ndef _run_to_response(run: Any) -> dict:\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"config_snapshot\": run.config_snapshot,\n \"key_hash\": run.key_hash,\n \"config_hash\": run.config_hash,\n \"dict_snapshot_hash\": run.dict_snapshot_hash,\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n# #endregion _run_to_response\n\n\n# #region _dict_to_response [C:2] [TYPE Function]\n# @BRIEF Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.\n@staticmethod\ndef _dict_to_response(d: Any, entry_count: int = 0) -> dict:\n return {\n \"id\": d.id,\n \"name\": d.name,\n \"description\": d.description,\n \"source_dialect\": d.source_dialect,\n \"target_dialect\": d.target_dialect,\n \"is_active\": d.is_active,\n \"created_by\": d.created_by,\n \"created_at\": d.created_at,\n \"updated_at\": d.updated_at,\n \"entry_count\": entry_count,\n }\n# #endregion _dict_to_response\n\n\n# #region _get_dictionary_entry_counts [C:2] [TYPE Function]\n# @BRIEF Get entry counts for a list of dictionary IDs.\ndef _get_dictionary_entry_counts(db: Session, dict_ids: list[str]) -> dict[str, int]:\n from sqlalchemy import func\n\n from ....models.translate import DictionaryEntry\n counts = (\n db.query(DictionaryEntry.dictionary_id, func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id.in_(dict_ids))\n .group_by(DictionaryEntry.dictionary_id)\n .all()\n )\n return {row[0]: row[1] for row in counts}\n# #endregion _get_dictionary_entry_counts\n\n# #endregion TranslateHelpersModule\n" }, { "contract_id": "_run_to_response", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_helpers.py", - "start_line": 9, - "end_line": 33, + "start_line": 11, + "end_line": 35, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -25292,14 +23371,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region _run_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Convert TranslationRun ORM to response dict.\ndef _run_to_response(run: Any) -> dict:\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"config_snapshot\": run.config_snapshot,\n \"key_hash\": run.key_hash,\n \"config_hash\": run.config_hash,\n \"dict_snapshot_hash\": run.dict_snapshot_hash,\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n# #endregion _run_to_response\n" + "body": "# #region _run_to_response [C:2] [TYPE Function]\n# @BRIEF Convert TranslationRun ORM to response dict.\ndef _run_to_response(run: Any) -> dict:\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"config_snapshot\": run.config_snapshot,\n \"key_hash\": run.key_hash,\n \"config_hash\": run.config_hash,\n \"dict_snapshot_hash\": run.dict_snapshot_hash,\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n# #endregion _run_to_response\n" }, { "contract_id": "_dict_to_response", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_helpers.py", - "start_line": 36, - "end_line": 52, + "start_line": 38, + "end_line": 54, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -25309,14 +23388,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region _dict_to_response [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.\n@staticmethod\ndef _dict_to_response(d: Any, entry_count: int = 0) -> dict:\n return {\n \"id\": d.id,\n \"name\": d.name,\n \"description\": d.description,\n \"source_dialect\": d.source_dialect,\n \"target_dialect\": d.target_dialect,\n \"is_active\": d.is_active,\n \"created_by\": d.created_by,\n \"created_at\": d.created_at,\n \"updated_at\": d.updated_at,\n \"entry_count\": entry_count,\n }\n# #endregion _dict_to_response\n" + "body": "# #region _dict_to_response [C:2] [TYPE Function]\n# @BRIEF Convert a TerminologyDictionary ORM model to a response dict with computed entry_count.\n@staticmethod\ndef _dict_to_response(d: Any, entry_count: int = 0) -> dict:\n return {\n \"id\": d.id,\n \"name\": d.name,\n \"description\": d.description,\n \"source_dialect\": d.source_dialect,\n \"target_dialect\": d.target_dialect,\n \"is_active\": d.is_active,\n \"created_by\": d.created_by,\n \"created_at\": d.created_at,\n \"updated_at\": d.updated_at,\n \"entry_count\": entry_count,\n }\n# #endregion _dict_to_response\n" }, { "contract_id": "_get_dictionary_entry_counts", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_helpers.py", - "start_line": 55, - "end_line": 67, + "start_line": 57, + "end_line": 70, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -25326,591 +23405,831 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region _get_dictionary_entry_counts [C:2] [TYPE Function] [SEMANTICS helpers,translate]\n# @BRIEF Get entry counts for a list of dictionary IDs.\ndef _get_dictionary_entry_counts(db: Session, dict_ids: List[str]) -> Dict[str, int]:\n from sqlalchemy import func\n from ....models.translate import DictionaryEntry\n counts = (\n db.query(DictionaryEntry.dictionary_id, func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id.in_(dict_ids))\n .group_by(DictionaryEntry.dictionary_id)\n .all()\n )\n return {row[0]: row[1] for row in counts}\n# #endregion _get_dictionary_entry_counts\n" + "body": "# #region _get_dictionary_entry_counts [C:2] [TYPE Function]\n# @BRIEF Get entry counts for a list of dictionary IDs.\ndef _get_dictionary_entry_counts(db: Session, dict_ids: list[str]) -> dict[str, int]:\n from sqlalchemy import func\n\n from ....models.translate import DictionaryEntry\n counts = (\n db.query(DictionaryEntry.dictionary_id, func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id.in_(dict_ids))\n .group_by(DictionaryEntry.dictionary_id)\n .all()\n )\n return {row[0]: row[1] for row in counts}\n# #endregion _get_dictionary_entry_counts\n" }, { "contract_id": "TranslateJobRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_job_routes.py", "start_line": 1, - "end_line": 272, + "end_line": 248, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": 4, - "LAYER": "UI", - "POST": "Translation job CRUD completed or datasource columns fetched.", - "PRE": "ConfigManager and DB session initialized. User authenticated with translate.job permissions.", - "PURPOSE": "Translation Job CRUD and datasource column routes.", - "SIDE_EFFECT": "Reads/writes TranslationJob records to DB; fetches datasource metadata from Superset API." + "COMPLEXITY": 3, + "LAYER": "API", + "PURPOSE": "Translation Job CRUD and datasource column routes." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "TranslateJobRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslateJobService", - "target_ref": "[TranslateJobService]" - }, - { - "source_id": "TranslateJobRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "ConfigManager", - "target_ref": "[ConfigManager]" - }, - { - "source_id": "TranslateJobRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetClient", - "target_ref": "[SupersetClient]" - }, - { - "source_id": "TranslateJobRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" - }, - { - "source_id": "TranslateJobRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_db", - "target_ref": "[get_db]" + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslateJobRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,jobs]\n# @BRIEF Translation Job CRUD and datasource column routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE ConfigManager and DB session initialized. User authenticated with translate.job permissions.\n# @POST Translation job CRUD completed or datasource columns fetched.\n# @SIDE_EFFECT Reads/writes TranslationJob records to DB; fetches datasource metadata from Superset API.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.logger import belief_scope\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateJobRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.service import (\n TranslateJobService,\n job_to_response,\n get_datasource_columns as fetch_datasource_columns_service,\n)\nfrom ....schemas.translate import (\n TranslateJobCreate,\n TranslateJobUpdate,\n TranslateJobResponse,\n DatasourceColumnsResponse,\n DuplicateJobResponse,\n)\n\nfrom ._router import router\n\n\n# ============================================================\n# Translation Job CRUD\n# ============================================================\n\n# #region list_jobs [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF List all translation jobs with pagination and optional status filter.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.get(\"/jobs\", response_model=List[TranslateJobResponse])\nasync def list_jobs(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n status: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all translation jobs.\"\"\"\n log.reason(\"Listing jobs\", payload={\"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n total, jobs = service.list_jobs(\n page=page,\n page_size=page_size,\n status_filter=status,\n )\n results = []\n for job in jobs:\n dict_ids = service.get_job_dictionary_ids(job.id)\n results.append(job_to_response(job, dict_ids))\n return results\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_jobs\n\n\n# #region get_job [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Get a single translation job by ID.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.get(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def get_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get a translation job by ID.\"\"\"\n log.reason(\"Getting job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.get_job(job_id)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job\n\n\n# #region create_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Create a new translation job.\n# @PRE User has translate.job.create permission. Payload validated.\n# @POST Translation job created and returned.\n# @SIDE_EFFECT Creates TranslationJob record in DB; validates columns via SupersetClient; caches database_dialect.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.post(\"/jobs\", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def create_job(\n payload: TranslateJobCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Create a new translation job.\"\"\"\n log.reason(\"Creating job\", payload={\"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.create_job(payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion create_job\n\n\n# #region update_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Update an existing translation job.\n# @PRE User has translate.job.edit permission. Job exists.\n# @POST Translation job updated and returned.\n# @SIDE_EFFECT Updates TranslationJob record in DB; re-detects database_dialect if datasource changed.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.put(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def update_job(\n job_id: str,\n payload: TranslateJobUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EDIT\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Update a translation job.\"\"\"\n log.reason(\"Updating job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.update_job(job_id, payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_job\n\n\n# #region delete_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Delete a translation job.\n# @PRE User has translate.job.delete permission. Job exists.\n# @POST Translation job is deleted from DB.\n# @SIDE_EFFECT Deletes TranslationJob and associated records from DB.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.delete(\"/jobs/{job_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"DELETE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete a translation job.\"\"\"\n log.reason(\"Deleting job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n service.delete_job(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_job\n\n\n# #region duplicate_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Duplicate a translation job.\n# @PRE User has translate.job.create permission. Source job exists.\n# @POST Duplicated job created with status DRAFT and returned.\n# @SIDE_EFFECT Creates a new TranslationJob record in DB as a copy of the source.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.post(\"/jobs/{job_id}/duplicate\", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def duplicate_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Duplicate a translation job.\"\"\"\n log.reason(\"Duplicating job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n new_job = service.duplicate_job(job_id)\n return DuplicateJobResponse(\n id=new_job.id,\n name=new_job.name,\n message=\"Job duplicated successfully\",\n )\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion duplicate_job\n\n\n# #region list_datasources [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]\n# @BRIEF List all Superset datasets available for translation jobs.\n# @PRE User has translate.job.view permission.\n# @POST Returns list of datasets with metadata.\n# @SIDE_EFFECT Queries Superset API for available datasets.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n@router.get(\"/datasources\")\nasync def list_datasources(\n env_id: str = Query(..., description=\"Superset environment ID\"),\n search: Optional[str] = Query(None, description=\"Search by table name\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all Superset datasets available for translation jobs.\"\"\"\n with belief_scope(\"list_datasources\"):\n try:\n service = TranslateJobService(db, config_manager)\n datasets = service.fetch_available_datasources(env_id, search)\n return datasets\n except Exception as e:\n log.explore(\"List datasources failed\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))\n# #endregion list_datasources\n\n\n# #region get_datasource_columns [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]\n# @BRIEF Get column metadata and database dialect for a Superset datasource.\n# @PRE User has translate.job.view permission. Datasource exists in Superset.\n# @POST Returns column list with metadata and database_dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n@router.get(\"/datasources/{datasource_id}/columns\", response_model=DatasourceColumnsResponse)\nasync def get_datasource_columns(\n datasource_id: int,\n env_id: str = Query(..., description=\"Superset environment ID\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get column metadata and database dialect for a Superset datasource.\"\"\"\n log.reason(\"Getting datasource columns\", payload={\n \"datasource_id\": datasource_id,\n \"env_id\": env_id,\n \"user\": current_user.username,\n })\n try:\n result = fetch_datasource_columns_service(\n datasource_id=datasource_id,\n env_id=env_id,\n config_manager=config_manager,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Get datasource columns failed\", error=str(e))\n raise HTTPException(\n status_code=status.HTTP_502_BAD_GATEWAY,\n detail=f\"Failed to fetch datasource columns from Superset: {e}\",\n )\n# #endregion get_datasource_columns\n\n# #endregion TranslateJobRoutesModule\n" + "body": "# #region TranslateJobRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search]\n# @BRIEF Translation Job CRUD and datasource column routes.\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.config_manager import ConfigManager\nfrom ....core.database import get_db\nfrom ....core.logger import belief_scope, logger\nfrom ....dependencies import get_config_manager, get_current_user, has_permission\nfrom ....plugins.translate.service import (\n TranslateJobService,\n job_to_response,\n)\nfrom ....plugins.translate.service import (\n get_datasource_columns as fetch_datasource_columns_service,\n)\nfrom ....schemas.auth import User\nfrom ....schemas.translate import (\n DatasourceColumnsResponse,\n DuplicateJobResponse,\n TranslateJobCreate,\n TranslateJobResponse,\n TranslateJobUpdate,\n)\nfrom ._router import router\n\n# ============================================================\n# Translation Job CRUD\n# ============================================================\n\n# #region list_jobs [TYPE Function]\n# @BRIEF List all translation jobs.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns list of translation jobs.\n@router.get(\"/jobs\", response_model=list[TranslateJobResponse])\nasync def list_jobs(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n status: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all translation jobs.\"\"\"\n logger.reason(f\"list_jobs — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n total, jobs = service.list_jobs(\n page=page,\n page_size=page_size,\n status_filter=status,\n )\n results = []\n for job in jobs:\n dict_ids = service.get_job_dictionary_ids(job.id)\n results.append(job_to_response(job, dict_ids))\n return results\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_jobs\n\n\n# #region get_job [TYPE Function]\n# @BRIEF Get a single translation job by ID.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns the translation job.\n@router.get(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def get_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get a translation job by ID.\"\"\"\n logger.reason(f\"get_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.get_job(job_id)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job\n\n\n# #region create_job [TYPE Function]\n# @BRIEF Create a new translation job.\n# @PRE: User has translate.job.create permission.\n# @POST: Returns the created translation job.\n# @SIDE_EFFECT: Validates columns via SupersetClient; caches database_dialect.\n@router.post(\"/jobs\", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def create_job(\n payload: TranslateJobCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Create a new translation job.\"\"\"\n logger.reason(f\"create_job — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.create_job(payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion create_job\n\n\n# #region update_job [TYPE Function]\n# @BRIEF Update an existing translation job.\n# @PRE: User has translate.job.edit permission.\n# @POST: Returns the updated translation job.\n# @SIDE_EFFECT: Re-detects database_dialect if datasource changed.\n@router.put(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def update_job(\n job_id: str,\n payload: TranslateJobUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EDIT\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Update a translation job.\"\"\"\n logger.reason(f\"update_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.update_job(job_id, payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_job\n\n\n# #region delete_job [TYPE Function]\n# @BRIEF Delete a translation job.\n# @PRE: User has translate.job.delete permission.\n# @POST: Job is deleted.\n@router.delete(\"/jobs/{job_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"DELETE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete a translation job.\"\"\"\n logger.reason(f\"delete_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n service.delete_job(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_job\n\n\n# #region duplicate_job [TYPE Function]\n# @BRIEF Duplicate a translation job.\n# @PRE: User has translate.job.create permission.\n# @POST: Returns the duplicated job with status DRAFT.\n@router.post(\"/jobs/{job_id}/duplicate\", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def duplicate_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Duplicate a translation job.\"\"\"\n logger.reason(f\"duplicate_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n new_job = service.duplicate_job(job_id)\n return DuplicateJobResponse(\n id=new_job.id,\n name=new_job.name,\n message=\"Job duplicated successfully\",\n )\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion duplicate_job\n\n\n# #region get_datasource_columns [TYPE Function]\n# @BRIEF Get column metadata and database dialect for a Superset datasource.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns column list with metadata and database_dialect.\n# @SIDE_EFFECT: Queries Superset API for dataset detail and database info.\n# @RATIONALE: Dialect detection from Superset connection cached on TranslationJob at save time.\n# @REJECTED: Hardcoding dialect list per database — would drift from actual Superset config.\n\n@router.get(\"/datasources\")\nasync def list_datasources(\n env_id: str = Query(..., description=\"Superset environment ID\"),\n search: str | None = Query(None, description=\"Search by table name\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all Superset datasets available for translation jobs.\"\"\"\n with belief_scope(\"list_datasources\"):\n try:\n service = TranslateJobService(db, config_manager)\n datasets = service.fetch_available_datasources(env_id, search)\n return datasets\n except Exception as e:\n logger.explore(\"list_datasources failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))\n\n@router.get(\"/datasources/{datasource_id}/columns\", response_model=DatasourceColumnsResponse)\nasync def get_datasource_columns(\n datasource_id: int,\n env_id: str = Query(..., description=\"Superset environment ID\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get column metadata and database dialect for a Superset datasource.\"\"\"\n logger.reason(\n f\"get_datasource_columns — Datasource: {datasource_id}, Env: {env_id}, User: {current_user.username}\",\n extra={\"src\": \"translate_routes\"}\n )\n try:\n result = fetch_datasource_columns_service(\n datasource_id=datasource_id,\n env_id=env_id,\n config_manager=config_manager,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"get_datasource_columns failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(\n status_code=status.HTTP_502_BAD_GATEWAY,\n detail=f\"Failed to fetch datasource columns from Superset: {e}\",\n )\n# #endregion get_datasource_columns\n\n# #endregion TranslateJobRoutesModule\n" }, { "contract_id": "list_jobs", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_job_routes.py", - "start_line": 45, - "end_line": 74, - "tier": "TIER_2", - "complexity": 3, + "start_line": 34, + "end_line": 64, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "List all translation jobs with pagination and optional status filter." + "POST": "Returns list of translation jobs.", + "PRE": "User has translate.job.view permission.", + "PURPOSE": "List all translation jobs." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "list_jobs", - "relation_type": "DEPENDS_ON", - "target_id": "TranslateJobService", - "target_ref": "[TranslateJobService]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region list_jobs [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF List all translation jobs with pagination and optional status filter.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.get(\"/jobs\", response_model=List[TranslateJobResponse])\nasync def list_jobs(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n status: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all translation jobs.\"\"\"\n log.reason(\"Listing jobs\", payload={\"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n total, jobs = service.list_jobs(\n page=page,\n page_size=page_size,\n status_filter=status,\n )\n results = []\n for job in jobs:\n dict_ids = service.get_job_dictionary_ids(job.id)\n results.append(job_to_response(job, dict_ids))\n return results\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_jobs\n" + "body": "# #region list_jobs [TYPE Function]\n# @BRIEF List all translation jobs.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns list of translation jobs.\n@router.get(\"/jobs\", response_model=list[TranslateJobResponse])\nasync def list_jobs(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n status: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all translation jobs.\"\"\"\n logger.reason(f\"list_jobs — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n total, jobs = service.list_jobs(\n page=page,\n page_size=page_size,\n status_filter=status,\n )\n results = []\n for job in jobs:\n dict_ids = service.get_job_dictionary_ids(job.id)\n results.append(job_to_response(job, dict_ids))\n return results\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_jobs\n" }, { "contract_id": "get_job", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_job_routes.py", - "start_line": 77, - "end_line": 97, - "tier": "TIER_2", - "complexity": 3, + "start_line": 67, + "end_line": 88, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns the translation job.", + "PRE": "User has translate.job.view permission.", "PURPOSE": "Get a single translation job by ID." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_job", - "relation_type": "DEPENDS_ON", - "target_id": "TranslateJobService", - "target_ref": "[TranslateJobService]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_job [C:3] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Get a single translation job by ID.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.get(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def get_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get a translation job by ID.\"\"\"\n log.reason(\"Getting job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.get_job(job_id)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job\n" + "body": "# #region get_job [TYPE Function]\n# @BRIEF Get a single translation job by ID.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns the translation job.\n@router.get(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def get_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get a translation job by ID.\"\"\"\n logger.reason(f\"get_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.get_job(job_id)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job\n" }, { "contract_id": "create_job", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_job_routes.py", - "start_line": 100, - "end_line": 123, - "tier": "TIER_2", - "complexity": 4, + "start_line": 91, + "end_line": 113, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Translation job created and returned.", - "PRE": "User has translate.job.create permission. Payload validated.", + "POST": "Returns the created translation job.", + "PRE": "User has translate.job.create permission.", "PURPOSE": "Create a new translation job.", - "SIDE_EFFECT": "Creates TranslationJob record in DB; validates columns via SupersetClient; caches database_dialect." + "SIDE_EFFECT": "Validates columns via SupersetClient; caches database_dialect." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "create_job", - "relation_type": "DEPENDS_ON", - "target_id": "TranslateJobService", - "target_ref": "[TranslateJobService]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region create_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Create a new translation job.\n# @PRE User has translate.job.create permission. Payload validated.\n# @POST Translation job created and returned.\n# @SIDE_EFFECT Creates TranslationJob record in DB; validates columns via SupersetClient; caches database_dialect.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.post(\"/jobs\", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def create_job(\n payload: TranslateJobCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Create a new translation job.\"\"\"\n log.reason(\"Creating job\", payload={\"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.create_job(payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion create_job\n" + "body": "# #region create_job [TYPE Function]\n# @BRIEF Create a new translation job.\n# @PRE: User has translate.job.create permission.\n# @POST: Returns the created translation job.\n# @SIDE_EFFECT: Validates columns via SupersetClient; caches database_dialect.\n@router.post(\"/jobs\", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def create_job(\n payload: TranslateJobCreate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Create a new translation job.\"\"\"\n logger.reason(f\"create_job — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.create_job(payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion create_job\n" }, { "contract_id": "update_job", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_job_routes.py", - "start_line": 126, - "end_line": 150, - "tier": "TIER_2", - "complexity": 4, + "start_line": 116, + "end_line": 139, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Translation job updated and returned.", - "PRE": "User has translate.job.edit permission. Job exists.", + "POST": "Returns the updated translation job.", + "PRE": "User has translate.job.edit permission.", "PURPOSE": "Update an existing translation job.", - "SIDE_EFFECT": "Updates TranslationJob record in DB; re-detects database_dialect if datasource changed." + "SIDE_EFFECT": "Re-detects database_dialect if datasource changed." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "update_job", - "relation_type": "DEPENDS_ON", - "target_id": "TranslateJobService", - "target_ref": "[TranslateJobService]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region update_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Update an existing translation job.\n# @PRE User has translate.job.edit permission. Job exists.\n# @POST Translation job updated and returned.\n# @SIDE_EFFECT Updates TranslationJob record in DB; re-detects database_dialect if datasource changed.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.put(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def update_job(\n job_id: str,\n payload: TranslateJobUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EDIT\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Update a translation job.\"\"\"\n log.reason(\"Updating job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.update_job(job_id, payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_job\n" + "body": "# #region update_job [TYPE Function]\n# @BRIEF Update an existing translation job.\n# @PRE: User has translate.job.edit permission.\n# @POST: Returns the updated translation job.\n# @SIDE_EFFECT: Re-detects database_dialect if datasource changed.\n@router.put(\"/jobs/{job_id}\", response_model=TranslateJobResponse)\nasync def update_job(\n job_id: str,\n payload: TranslateJobUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EDIT\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Update a translation job.\"\"\"\n logger.reason(f\"update_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n job = service.update_job(job_id, payload)\n dict_ids = service.get_job_dictionary_ids(job.id)\n return job_to_response(job, dict_ids)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_job\n" }, { "contract_id": "delete_job", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_job_routes.py", - "start_line": 153, - "end_line": 174, - "tier": "TIER_2", - "complexity": 4, + "start_line": 142, + "end_line": 161, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Translation job is deleted from DB.", - "PRE": "User has translate.job.delete permission. Job exists.", - "PURPOSE": "Delete a translation job.", - "SIDE_EFFECT": "Deletes TranslationJob and associated records from DB." + "POST": "Job is deleted.", + "PRE": "User has translate.job.delete permission.", + "PURPOSE": "Delete a translation job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "delete_job", - "relation_type": "DEPENDS_ON", - "target_id": "TranslateJobService", - "target_ref": "[TranslateJobService]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region delete_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Delete a translation job.\n# @PRE User has translate.job.delete permission. Job exists.\n# @POST Translation job is deleted from DB.\n# @SIDE_EFFECT Deletes TranslationJob and associated records from DB.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.delete(\"/jobs/{job_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"DELETE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete a translation job.\"\"\"\n log.reason(\"Deleting job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n service.delete_job(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_job\n" + "body": "# #region delete_job [TYPE Function]\n# @BRIEF Delete a translation job.\n# @PRE: User has translate.job.delete permission.\n# @POST: Job is deleted.\n@router.delete(\"/jobs/{job_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"DELETE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete a translation job.\"\"\"\n logger.reason(f\"delete_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n service.delete_job(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_job\n" }, { "contract_id": "duplicate_job", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_job_routes.py", - "start_line": 177, - "end_line": 203, - "tier": "TIER_2", - "complexity": 4, + "start_line": 164, + "end_line": 188, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Duplicated job created with status DRAFT and returned.", - "PRE": "User has translate.job.create permission. Source job exists.", - "PURPOSE": "Duplicate a translation job.", - "SIDE_EFFECT": "Creates a new TranslationJob record in DB as a copy of the source." + "POST": "Returns the duplicated job with status DRAFT.", + "PRE": "User has translate.job.create permission.", + "PURPOSE": "Duplicate a translation job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "duplicate_job", - "relation_type": "DEPENDS_ON", - "target_id": "TranslateJobService", - "target_ref": "[TranslateJobService]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region duplicate_job [C:4] [TYPE Function] [SEMANTICS api,translate,jobs]\n# @BRIEF Duplicate a translation job.\n# @PRE User has translate.job.create permission. Source job exists.\n# @POST Duplicated job created with status DRAFT and returned.\n# @SIDE_EFFECT Creates a new TranslationJob record in DB as a copy of the source.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n@router.post(\"/jobs/{job_id}/duplicate\", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def duplicate_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Duplicate a translation job.\"\"\"\n log.reason(\"Duplicating job\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n new_job = service.duplicate_job(job_id)\n return DuplicateJobResponse(\n id=new_job.id,\n name=new_job.name,\n message=\"Job duplicated successfully\",\n )\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion duplicate_job\n" - }, - { - "contract_id": "list_datasources", - "contract_type": "Function", - "file_path": "backend/src/api/routes/translate/_job_routes.py", - "start_line": 206, - "end_line": 231, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns list of datasets with metadata.", - "PRE": "User has translate.job.view permission.", - "PURPOSE": "List all Superset datasets available for translation jobs.", - "SIDE_EFFECT": "Queries Superset API for available datasets." - }, - "relations": [ - { - "source_id": "list_datasources", - "relation_type": "DEPENDS_ON", - "target_id": "TranslateJobService", - "target_ref": "[TranslateJobService]" + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { - "source_id": "list_datasources", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetClient", - "target_ref": "[SupersetClient]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region list_datasources [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]\n# @BRIEF List all Superset datasets available for translation jobs.\n# @PRE User has translate.job.view permission.\n# @POST Returns list of datasets with metadata.\n# @SIDE_EFFECT Queries Superset API for available datasets.\n# @RELATION DEPENDS_ON -> [TranslateJobService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n@router.get(\"/datasources\")\nasync def list_datasources(\n env_id: str = Query(..., description=\"Superset environment ID\"),\n search: Optional[str] = Query(None, description=\"Search by table name\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all Superset datasets available for translation jobs.\"\"\"\n with belief_scope(\"list_datasources\"):\n try:\n service = TranslateJobService(db, config_manager)\n datasets = service.fetch_available_datasources(env_id, search)\n return datasets\n except Exception as e:\n log.explore(\"List datasources failed\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))\n# #endregion list_datasources\n" + "body": "# #region duplicate_job [TYPE Function]\n# @BRIEF Duplicate a translation job.\n# @PRE: User has translate.job.create permission.\n# @POST: Returns the duplicated job with status DRAFT.\n@router.post(\"/jobs/{job_id}/duplicate\", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)\nasync def duplicate_job(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"CREATE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Duplicate a translation job.\"\"\"\n logger.reason(f\"duplicate_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n service = TranslateJobService(db, config_manager, current_user.username)\n new_job = service.duplicate_job(job_id)\n return DuplicateJobResponse(\n id=new_job.id,\n name=new_job.name,\n message=\"Job duplicated successfully\",\n )\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion duplicate_job\n" }, { "contract_id": "get_datasource_columns", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_job_routes.py", - "start_line": 234, - "end_line": 270, - "tier": "TIER_2", - "complexity": 4, + "start_line": 191, + "end_line": 246, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, "POST": "Returns column list with metadata and database_dialect.", - "PRE": "User has translate.job.view permission. Datasource exists in Superset.", + "PRE": "User has translate.job.view permission.", "PURPOSE": "Get column metadata and database dialect for a Superset datasource.", + "RATIONALE": "Dialect detection from Superset connection cached on TranslationJob at save time.", + "REJECTED": "Hardcoding dialect list per database — would drift from actual Superset config.", "SIDE_EFFECT": "Queries Superset API for dataset detail and database info." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_datasource_columns", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetClient", - "target_ref": "[SupersetClient]" + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { - "source_id": "get_datasource_columns", - "relation_type": "DEPENDS_ON", - "target_id": "ConfigManager", - "target_ref": "[ConfigManager]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_datasource_columns [C:4] [TYPE Function] [SEMANTICS api,translate,datasources]\n# @BRIEF Get column metadata and database dialect for a Superset datasource.\n# @PRE User has translate.job.view permission. Datasource exists in Superset.\n# @POST Returns column list with metadata and database_dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n@router.get(\"/datasources/{datasource_id}/columns\", response_model=DatasourceColumnsResponse)\nasync def get_datasource_columns(\n datasource_id: int,\n env_id: str = Query(..., description=\"Superset environment ID\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get column metadata and database dialect for a Superset datasource.\"\"\"\n log.reason(\"Getting datasource columns\", payload={\n \"datasource_id\": datasource_id,\n \"env_id\": env_id,\n \"user\": current_user.username,\n })\n try:\n result = fetch_datasource_columns_service(\n datasource_id=datasource_id,\n env_id=env_id,\n config_manager=config_manager,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Get datasource columns failed\", error=str(e))\n raise HTTPException(\n status_code=status.HTTP_502_BAD_GATEWAY,\n detail=f\"Failed to fetch datasource columns from Superset: {e}\",\n )\n# #endregion get_datasource_columns\n" + "body": "# #region get_datasource_columns [TYPE Function]\n# @BRIEF Get column metadata and database dialect for a Superset datasource.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns column list with metadata and database_dialect.\n# @SIDE_EFFECT: Queries Superset API for dataset detail and database info.\n# @RATIONALE: Dialect detection from Superset connection cached on TranslationJob at save time.\n# @REJECTED: Hardcoding dialect list per database — would drift from actual Superset config.\n\n@router.get(\"/datasources\")\nasync def list_datasources(\n env_id: str = Query(..., description=\"Superset environment ID\"),\n search: str | None = Query(None, description=\"Search by table name\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"List all Superset datasets available for translation jobs.\"\"\"\n with belief_scope(\"list_datasources\"):\n try:\n service = TranslateJobService(db, config_manager)\n datasets = service.fetch_available_datasources(env_id, search)\n return datasets\n except Exception as e:\n logger.explore(\"list_datasources failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))\n\n@router.get(\"/datasources/{datasource_id}/columns\", response_model=DatasourceColumnsResponse)\nasync def get_datasource_columns(\n datasource_id: int,\n env_id: str = Query(..., description=\"Superset environment ID\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get column metadata and database dialect for a Superset datasource.\"\"\"\n logger.reason(\n f\"get_datasource_columns — Datasource: {datasource_id}, Env: {env_id}, User: {current_user.username}\",\n extra={\"src\": \"translate_routes\"}\n )\n try:\n result = fetch_datasource_columns_service(\n datasource_id=datasource_id,\n env_id=env_id,\n config_manager=config_manager,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"get_datasource_columns failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(\n status_code=status.HTTP_502_BAD_GATEWAY,\n detail=f\"Failed to fetch datasource columns from Superset: {e}\",\n )\n# #endregion get_datasource_columns\n" }, { "contract_id": "TranslateMetricsRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_metrics_routes.py", "start_line": 1, - "end_line": 67, - "tier": "TIER_2", - "complexity": 3, + "end_line": 63, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": 3, - "LAYER": "UI", - "PURPOSE": "Translation Metrics endpoints providing aggregated stats on runs and corrections." + "COMPLEXITY": 2, + "LAYER": "API", + "PURPOSE": "Translation Metrics endpoints." }, - "relations": [ - { - "source_id": "TranslateMetricsRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationMetrics", - "target_ref": "[TranslationMetrics]" - }, - { - "source_id": "TranslateMetricsRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" - }, - { - "source_id": "TranslateMetricsRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_db", - "target_ref": "[get_db]" - } - ], + "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslateMetricsRoutesModule [C:3] [TYPE Module] [SEMANTICS api,routes,translate,metrics]\n# @BRIEF Translation Metrics endpoints providing aggregated stats on runs and corrections.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.metrics import TranslationMetrics\n\nfrom ._router import router\n\nlog = MarkerLogger(\"TranslateMetricsRoutes\")\n\n# ============================================================\n# Metrics\n# ============================================================\n\n# #region get_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]\n# @BRIEF Get translation metrics across all jobs, optionally filtered by job.\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n@router.get(\"/metrics\")\nasync def get_metrics(\n job_id: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get translation metrics.\"\"\"\n log.reason(\"Metrics request\", payload={\"user\": current_user.username})\n try:\n metrics_svc = TranslationMetrics(db)\n if job_id:\n return metrics_svc.get_job_metrics(job_id)\n return metrics_svc.get_all_metrics()\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_metrics\n\n\n# #region get_job_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]\n# @BRIEF Get aggregated metrics for a specific translation job.\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n@router.get(\"/jobs/{job_id}/metrics\")\nasync def get_job_metrics(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get aggregated metrics for a specific job.\"\"\"\n log.reason(\"Job metrics request\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n metrics_svc = TranslationMetrics(db)\n return metrics_svc.get_job_metrics(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job_metrics\n\n# #endregion TranslateMetricsRoutesModule\n" + "body": "# #region TranslateMetricsRoutesModule [C:2] [TYPE Module] [SEMANTICS fastapi, translate, api, search]\n# @BRIEF Translation Metrics endpoints.\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.logger import logger\nfrom ....dependencies import get_current_user, has_permission\nfrom ....plugins.translate.metrics import TranslationMetrics\nfrom ....schemas.auth import User\nfrom ._router import router\n\n# ============================================================\n# Metrics\n# ============================================================\n\n# #region get_metrics [TYPE Function]\n# @BRIEF Get translation metrics, optionally filtered by job.\n# @PRE: User has translate.metrics.view permission.\n# @POST: Returns metrics data.\n@router.get(\"/metrics\")\nasync def get_metrics(\n job_id: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get translation metrics.\"\"\"\n logger.reason(f\"get_metrics — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n metrics_svc = TranslationMetrics(db)\n if job_id:\n return metrics_svc.get_job_metrics(job_id)\n return metrics_svc.get_all_metrics()\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_metrics\n\n\n# #region get_job_metrics [TYPE Function]\n# @BRIEF Get aggregated metrics for a specific job.\n# @PRE: User has translate.metrics.view permission.\n# @POST: Returns metrics dict.\n@router.get(\"/jobs/{job_id}/metrics\")\nasync def get_job_metrics(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get aggregated metrics for a specific job.\"\"\"\n logger.reason(f\"get_job_metrics — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n metrics_svc = TranslationMetrics(db)\n return metrics_svc.get_job_metrics(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job_metrics\n\n# #endregion TranslateMetricsRoutesModule\n" }, { "contract_id": "get_metrics", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_metrics_routes.py", - "start_line": 26, - "end_line": 45, - "tier": "TIER_2", - "complexity": 3, + "start_line": 20, + "end_line": 40, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Get translation metrics across all jobs, optionally filtered by job." + "POST": "Returns metrics data.", + "PRE": "User has translate.metrics.view permission.", + "PURPOSE": "Get translation metrics, optionally filtered by job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_metrics", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationMetrics", - "target_ref": "[TranslationMetrics]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]\n# @BRIEF Get translation metrics across all jobs, optionally filtered by job.\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n@router.get(\"/metrics\")\nasync def get_metrics(\n job_id: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get translation metrics.\"\"\"\n log.reason(\"Metrics request\", payload={\"user\": current_user.username})\n try:\n metrics_svc = TranslationMetrics(db)\n if job_id:\n return metrics_svc.get_job_metrics(job_id)\n return metrics_svc.get_all_metrics()\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_metrics\n" + "body": "# #region get_metrics [TYPE Function]\n# @BRIEF Get translation metrics, optionally filtered by job.\n# @PRE: User has translate.metrics.view permission.\n# @POST: Returns metrics data.\n@router.get(\"/metrics\")\nasync def get_metrics(\n job_id: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get translation metrics.\"\"\"\n logger.reason(f\"get_metrics — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n metrics_svc = TranslationMetrics(db)\n if job_id:\n return metrics_svc.get_job_metrics(job_id)\n return metrics_svc.get_all_metrics()\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_metrics\n" }, { "contract_id": "get_job_metrics", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_metrics_routes.py", - "start_line": 48, - "end_line": 65, - "tier": "TIER_2", - "complexity": 3, + "start_line": 43, + "end_line": 61, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Get aggregated metrics for a specific translation job." + "POST": "Returns metrics dict.", + "PRE": "User has translate.metrics.view permission.", + "PURPOSE": "Get aggregated metrics for a specific job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_job_metrics", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationMetrics", - "target_ref": "[TranslationMetrics]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_job_metrics [C:3] [TYPE Function] [SEMANTICS api,translate,metrics]\n# @BRIEF Get aggregated metrics for a specific translation job.\n# @RELATION DEPENDS_ON -> [TranslationMetrics]\n@router.get(\"/jobs/{job_id}/metrics\")\nasync def get_job_metrics(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get aggregated metrics for a specific job.\"\"\"\n log.reason(\"Job metrics request\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n metrics_svc = TranslationMetrics(db)\n return metrics_svc.get_job_metrics(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job_metrics\n" + "body": "# #region get_job_metrics [TYPE Function]\n# @BRIEF Get aggregated metrics for a specific job.\n# @PRE: User has translate.metrics.view permission.\n# @POST: Returns metrics dict.\n@router.get(\"/jobs/{job_id}/metrics\")\nasync def get_job_metrics(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.metrics\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get aggregated metrics for a specific job.\"\"\"\n logger.reason(f\"get_job_metrics — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n metrics_svc = TranslationMetrics(db)\n return metrics_svc.get_job_metrics(job_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_job_metrics\n" }, { "contract_id": "TranslatePreviewRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_preview_routes.py", "start_line": 1, - "end_line": 210, + "end_line": 190, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": 4, - "LAYER": "UI", - "POST": "Preview sessions created, rows reviewed/accepted, records queried.", - "PRE": "ConfigManager and DB session initialized. User authenticated with translate permissions.", - "PURPOSE": "Translation Preview session management routes.", - "SIDE_EFFECT": "Creates/updates preview sessions and rows in DB; fetches sample data from Superset; calls LLM provider." + "COMPLEXITY": 3, + "LAYER": "API", + "PURPOSE": "Translation Preview session management routes." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "TranslatePreviewRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreview", - "target_ref": "[TranslationPreview]" - }, - { - "source_id": "TranslatePreviewRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "ConfigManager", - "target_ref": "[ConfigManager]" - }, - { - "source_id": "TranslatePreviewRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" - }, - { - "source_id": "TranslatePreviewRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_db", - "target_ref": "[get_db]" + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslatePreviewRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,preview]\n# @BRIEF Translation Preview session management routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE ConfigManager and DB session initialized. User authenticated with translate permissions.\n# @POST Preview sessions created, rows reviewed/accepted, records queried.\n# @SIDE_EFFECT Creates/updates preview sessions and rows in DB; fetches sample data from Superset; calls LLM provider.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslatePreviewRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.preview import TranslationPreview\nfrom ....schemas.translate import (\n PreviewRequest,\n PreviewRowUpdate,\n PreviewAcceptResponse,\n PreviewRow,\n)\n\nfrom ._router import router\n\n\n# ============================================================\n# Preview\n# ============================================================\n\n# #region preview_translation [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Preview a translation before applying it.\n# @PRE User has translate.job.execute permission. Job exists.\n# @POST Preview session created with sample rows and cost estimation.\n# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates PreviewSession and PreviewRecord rows in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n@router.post(\"/jobs/{job_id}/preview\", status_code=status.HTTP_201_CREATED)\nasync def preview_translation(\n job_id: str,\n payload: PreviewRequest = None,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview a translation before applying it.\"\"\"\n log.reason(\"Preview translation\", payload={\"job_id\": job_id, \"user\": current_user.username})\n if payload is None:\n payload = PreviewRequest()\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.preview_rows(\n job_id=job_id,\n sample_size=payload.sample_size,\n prompt_template=payload.prompt_template,\n env_id=payload.env_id,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Preview translation error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Preview failed: {e}\")\n# #endregion preview_translation\n\n\n# #region update_preview_row [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Approve, edit, or reject a preview row.\n# @PRE User has translate.job.execute permission. Preview row exists.\n# @POST Preview row status updated to APPROVED/REJECTED/EDITED.\n# @SIDE_EFFECT Updates PreviewRecord status in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.put(\"/jobs/{job_id}/preview/rows/{row_key}\")\nasync def update_preview_row(\n job_id: str,\n row_key: str,\n payload: PreviewRowUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Approve, edit, or reject a preview row.\"\"\"\n log.reason(\"Update preview row\", payload={\"job_id\": job_id, \"row\": row_key, \"action\": payload.action})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.update_preview_row(\n job_id=job_id,\n row_id=row_key,\n action=payload.action,\n translation=payload.translation,\n feedback=payload.feedback,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion update_preview_row\n\n\n# #region accept_preview_session [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Accept a preview session, marking it as the quality gate for full execution.\n# @PRE User has translate.job.execute permission. Job has an ACTIVE preview session.\n# @POST Preview session status set to APPLIED. Full execution can proceed.\n# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.post(\"/jobs/{job_id}/preview/accept\")\nasync def accept_preview_session(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Accept a preview session, enabling full translation execution.\"\"\"\n log.reason(\"Accept preview session\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion accept_preview_session\n\n\n# #region apply_preview [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Apply a preview session (alias for accept when accepting at session level).\n# @PRE User has translate.job.execute permission. Session exists.\n# @POST Preview session applied and marked as quality gate.\n# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.post(\"/preview/{session_id}/apply\")\nasync def apply_preview(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Apply a preview session by session ID.\"\"\"\n log.reason(\"Apply preview\", payload={\"session_id\": session_id, \"user\": current_user.username})\n # Find job_id from session\n from ....models.translate import TranslationPreviewSession\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=session.job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion apply_preview\n\n\n# ============================================================\n# Preview Records\n# ============================================================\n\n# #region get_preview_records [C:3] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Get records for a preview session.\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n@router.get(\"/preview/{session_id}/records\", response_model=List[PreviewRow])\nasync def get_preview_records(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get records for a preview session.\"\"\"\n log.reason(\"Get preview records\", payload={\"session_id\": session_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationPreviewSession, TranslationPreviewRecord\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session_id)\n .all()\n )\n return [\n PreviewRow(\n id=r.id,\n source_sql=r.source_sql,\n target_sql=r.target_sql,\n source_object_type=r.source_object_type,\n source_object_id=r.source_object_id,\n source_object_name=r.source_object_name,\n status=r.status,\n feedback=r.feedback,\n )\n for r in records\n ]\n except HTTPException:\n raise\n except Exception as e:\n log.explore(\"Get preview records error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_preview_records\n\n# #endregion TranslatePreviewRoutesModule\n" + "body": "# #region TranslatePreviewRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, preview, review, api, search]\n# @BRIEF Translation Preview session management routes.\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.config_manager import ConfigManager\nfrom ....core.database import get_db\nfrom ....core.logger import logger\nfrom ....dependencies import get_config_manager, get_current_user, has_permission\nfrom ....plugins.translate.preview import TranslationPreview\nfrom ....schemas.auth import User\nfrom ....schemas.translate import (\n PreviewRequest,\n PreviewRow,\n PreviewRowUpdate,\n)\nfrom ._router import router\n\n# ============================================================\n# Preview\n# ============================================================\n\n# #region preview_translation [TYPE Function]\n# @BRIEF Preview a translation before applying it.\n# @PRE: User has translate.job.execute permission.\n# @POST: Returns a preview session with records and cost estimation.\n# @SIDE_EFFECT: Fetches sample data from Superset; calls LLM provider; creates DB rows.\n@router.post(\"/jobs/{job_id}/preview\", status_code=status.HTTP_201_CREATED)\nasync def preview_translation(\n job_id: str,\n payload: PreviewRequest = None,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview a translation before applying it.\"\"\"\n logger.reason(f\"preview_translation — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n if payload is None:\n payload = PreviewRequest()\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.preview_rows(\n job_id=job_id,\n sample_size=payload.sample_size,\n prompt_template=payload.prompt_template,\n env_id=payload.env_id,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"preview_translation failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Preview failed: {e}\")\n# #endregion preview_translation\n\n\n# #region update_preview_row [TYPE Function]\n# @BRIEF Approve, edit, or reject a preview row.\n# @PRE: User has translate.job.execute permission.\n# @POST: Preview row status is updated.\n@router.put(\"/jobs/{job_id}/preview/rows/{row_key}\")\nasync def update_preview_row(\n job_id: str,\n row_key: str,\n payload: PreviewRowUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Approve, edit, or reject a preview row.\"\"\"\n logger.reason(f\"update_preview_row — Job: {job_id}, Row: {row_key}, Action: {payload.action}\", extra={\"src\": \"translate_routes\"})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.update_preview_row(\n job_id=job_id,\n row_id=row_key,\n action=payload.action,\n translation=payload.translation,\n feedback=payload.feedback,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion update_preview_row\n\n\n# #region accept_preview_session [TYPE Function]\n# @BRIEF Accept a preview session, marking it as the quality gate for full execution.\n# @PRE: User has translate.job.execute permission. Job has an ACTIVE preview session.\n# @POST: Preview session is marked as APPLIED; full execution can proceed.\n@router.post(\"/jobs/{job_id}/preview/accept\")\nasync def accept_preview_session(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Accept a preview session, enabling full translation execution.\"\"\"\n logger.reason(f\"accept_preview_session — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion accept_preview_session\n\n\n# #region apply_preview [TYPE Function]\n# @BRIEF Apply a preview session (alias for accept when accepting at session level).\n# @PRE: User has translate.job.execute permission.\n# @POST: Preview is applied.\n@router.post(\"/preview/{session_id}/apply\")\nasync def apply_preview(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Apply a preview session by session ID.\"\"\"\n logger.reason(f\"apply_preview — Session: {session_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n # Find job_id from session\n from ....models.translate import TranslationPreviewSession\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=session.job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion apply_preview\n\n\n# ============================================================\n# Preview Records\n# ============================================================\n\n# #region get_preview_records [TYPE Function]\n# @BRIEF Get records for a preview session.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns list of preview records.\n@router.get(\"/preview/{session_id}/records\", response_model=list[PreviewRow])\nasync def get_preview_records(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get records for a preview session.\"\"\"\n logger.reason(f\"get_preview_records — Session: {session_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n from ....models.translate import TranslationPreviewRecord, TranslationPreviewSession\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session_id)\n .all()\n )\n return [\n PreviewRow(\n id=r.id,\n source_sql=r.source_sql,\n target_sql=r.target_sql,\n source_object_type=r.source_object_type,\n source_object_id=r.source_object_id,\n source_object_name=r.source_object_name,\n status=r.status,\n feedback=r.feedback,\n )\n for r in records\n ]\n except HTTPException:\n raise\n except Exception as e:\n logger.explore(\"get_preview_records failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_preview_records\n\n# #endregion TranslatePreviewRoutesModule\n" }, { "contract_id": "preview_translation", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_preview_routes.py", - "start_line": 38, - "end_line": 72, - "tier": "TIER_2", - "complexity": 4, + "start_line": 26, + "end_line": 58, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Preview session created with sample rows and cost estimation.", - "PRE": "User has translate.job.execute permission. Job exists.", + "POST": "Returns a preview session with records and cost estimation.", + "PRE": "User has translate.job.execute permission.", "PURPOSE": "Preview a translation before applying it.", - "SIDE_EFFECT": "Fetches sample data from Superset; calls LLM provider; creates PreviewSession and PreviewRecord rows in DB." + "SIDE_EFFECT": "Fetches sample data from Superset; calls LLM provider; creates DB rows." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "preview_translation", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreview", - "target_ref": "[TranslationPreview]" + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { - "source_id": "preview_translation", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetClient", - "target_ref": "[SupersetClient]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region preview_translation [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Preview a translation before applying it.\n# @PRE User has translate.job.execute permission. Job exists.\n# @POST Preview session created with sample rows and cost estimation.\n# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates PreviewSession and PreviewRecord rows in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n@router.post(\"/jobs/{job_id}/preview\", status_code=status.HTTP_201_CREATED)\nasync def preview_translation(\n job_id: str,\n payload: PreviewRequest = None,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview a translation before applying it.\"\"\"\n log.reason(\"Preview translation\", payload={\"job_id\": job_id, \"user\": current_user.username})\n if payload is None:\n payload = PreviewRequest()\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.preview_rows(\n job_id=job_id,\n sample_size=payload.sample_size,\n prompt_template=payload.prompt_template,\n env_id=payload.env_id,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Preview translation error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Preview failed: {e}\")\n# #endregion preview_translation\n" + "body": "# #region preview_translation [TYPE Function]\n# @BRIEF Preview a translation before applying it.\n# @PRE: User has translate.job.execute permission.\n# @POST: Returns a preview session with records and cost estimation.\n# @SIDE_EFFECT: Fetches sample data from Superset; calls LLM provider; creates DB rows.\n@router.post(\"/jobs/{job_id}/preview\", status_code=status.HTTP_201_CREATED)\nasync def preview_translation(\n job_id: str,\n payload: PreviewRequest = None,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview a translation before applying it.\"\"\"\n logger.reason(f\"preview_translation — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n if payload is None:\n payload = PreviewRequest()\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.preview_rows(\n job_id=job_id,\n sample_size=payload.sample_size,\n prompt_template=payload.prompt_template,\n env_id=payload.env_id,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"preview_translation failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Preview failed: {e}\")\n# #endregion preview_translation\n" }, { "contract_id": "update_preview_row", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_preview_routes.py", - "start_line": 75, - "end_line": 105, - "tier": "TIER_2", - "complexity": 4, + "start_line": 61, + "end_line": 89, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Preview row status updated to APPROVED/REJECTED/EDITED.", - "PRE": "User has translate.job.execute permission. Preview row exists.", - "PURPOSE": "Approve, edit, or reject a preview row.", - "SIDE_EFFECT": "Updates PreviewRecord status in DB." + "POST": "Preview row status is updated.", + "PRE": "User has translate.job.execute permission.", + "PURPOSE": "Approve, edit, or reject a preview row." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "update_preview_row", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreview", - "target_ref": "[TranslationPreview]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region update_preview_row [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Approve, edit, or reject a preview row.\n# @PRE User has translate.job.execute permission. Preview row exists.\n# @POST Preview row status updated to APPROVED/REJECTED/EDITED.\n# @SIDE_EFFECT Updates PreviewRecord status in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.put(\"/jobs/{job_id}/preview/rows/{row_key}\")\nasync def update_preview_row(\n job_id: str,\n row_key: str,\n payload: PreviewRowUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Approve, edit, or reject a preview row.\"\"\"\n log.reason(\"Update preview row\", payload={\"job_id\": job_id, \"row\": row_key, \"action\": payload.action})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.update_preview_row(\n job_id=job_id,\n row_id=row_key,\n action=payload.action,\n translation=payload.translation,\n feedback=payload.feedback,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion update_preview_row\n" + "body": "# #region update_preview_row [TYPE Function]\n# @BRIEF Approve, edit, or reject a preview row.\n# @PRE: User has translate.job.execute permission.\n# @POST: Preview row status is updated.\n@router.put(\"/jobs/{job_id}/preview/rows/{row_key}\")\nasync def update_preview_row(\n job_id: str,\n row_key: str,\n payload: PreviewRowUpdate,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Approve, edit, or reject a preview row.\"\"\"\n logger.reason(f\"update_preview_row — Job: {job_id}, Row: {row_key}, Action: {payload.action}\", extra={\"src\": \"translate_routes\"})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.update_preview_row(\n job_id=job_id,\n row_id=row_key,\n action=payload.action,\n translation=payload.translation,\n feedback=payload.feedback,\n )\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion update_preview_row\n" }, { "contract_id": "accept_preview_session", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_preview_routes.py", - "start_line": 108, - "end_line": 130, - "tier": "TIER_2", - "complexity": 4, + "start_line": 92, + "end_line": 112, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Preview session status set to APPLIED. Full execution can proceed.", + "POST": "Preview session is marked as APPLIED; full execution can proceed.", "PRE": "User has translate.job.execute permission. Job has an ACTIVE preview session.", - "PURPOSE": "Accept a preview session, marking it as the quality gate for full execution.", - "SIDE_EFFECT": "Updates PreviewSession status to APPLIED in DB." + "PURPOSE": "Accept a preview session, marking it as the quality gate for full execution." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "accept_preview_session", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreview", - "target_ref": "[TranslationPreview]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region accept_preview_session [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Accept a preview session, marking it as the quality gate for full execution.\n# @PRE User has translate.job.execute permission. Job has an ACTIVE preview session.\n# @POST Preview session status set to APPLIED. Full execution can proceed.\n# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.post(\"/jobs/{job_id}/preview/accept\")\nasync def accept_preview_session(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Accept a preview session, enabling full translation execution.\"\"\"\n log.reason(\"Accept preview session\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion accept_preview_session\n" + "body": "# #region accept_preview_session [TYPE Function]\n# @BRIEF Accept a preview session, marking it as the quality gate for full execution.\n# @PRE: User has translate.job.execute permission. Job has an ACTIVE preview session.\n# @POST: Preview session is marked as APPLIED; full execution can proceed.\n@router.post(\"/jobs/{job_id}/preview/accept\")\nasync def accept_preview_session(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Accept a preview session, enabling full translation execution.\"\"\"\n logger.reason(f\"accept_preview_session — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion accept_preview_session\n" }, { "contract_id": "apply_preview", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_preview_routes.py", - "start_line": 133, - "end_line": 160, - "tier": "TIER_2", - "complexity": 4, + "start_line": 115, + "end_line": 140, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Preview session applied and marked as quality gate.", - "PRE": "User has translate.job.execute permission. Session exists.", - "PURPOSE": "Apply a preview session (alias for accept when accepting at session level).", - "SIDE_EFFECT": "Updates PreviewSession status to APPLIED in DB." + "POST": "Preview is applied.", + "PRE": "User has translate.job.execute permission.", + "PURPOSE": "Apply a preview session (alias for accept when accepting at session level)." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "apply_preview", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreview", - "target_ref": "[TranslationPreview]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region apply_preview [C:4] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Apply a preview session (alias for accept when accepting at session level).\n# @PRE User has translate.job.execute permission. Session exists.\n# @POST Preview session applied and marked as quality gate.\n# @SIDE_EFFECT Updates PreviewSession status to APPLIED in DB.\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n@router.post(\"/preview/{session_id}/apply\")\nasync def apply_preview(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Apply a preview session by session ID.\"\"\"\n log.reason(\"Apply preview\", payload={\"session_id\": session_id, \"user\": current_user.username})\n # Find job_id from session\n from ....models.translate import TranslationPreviewSession\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=session.job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion apply_preview\n" + "body": "# #region apply_preview [TYPE Function]\n# @BRIEF Apply a preview session (alias for accept when accepting at session level).\n# @PRE: User has translate.job.execute permission.\n# @POST: Preview is applied.\n@router.post(\"/preview/{session_id}/apply\")\nasync def apply_preview(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Apply a preview session by session ID.\"\"\"\n logger.reason(f\"apply_preview — Session: {session_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n # Find job_id from session\n from ....models.translate import TranslationPreviewSession\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n try:\n preview_service = TranslationPreview(db, config_manager, current_user.username)\n result = preview_service.accept_preview_session(job_id=session.job_id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion apply_preview\n" }, { "contract_id": "get_preview_records", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_preview_routes.py", - "start_line": 167, - "end_line": 208, - "tier": "TIER_2", - "complexity": 3, + "start_line": 147, + "end_line": 188, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns list of preview records.", + "PRE": "User has translate.job.view permission.", "PURPOSE": "Get records for a preview session." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_preview_records", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreviewSession", - "target_ref": "[TranslationPreviewSession]" + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { - "source_id": "get_preview_records", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreviewRecord", - "target_ref": "[TranslationPreviewRecord]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_preview_records [C:3] [TYPE Function] [SEMANTICS api,translate,preview]\n# @BRIEF Get records for a preview session.\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n@router.get(\"/preview/{session_id}/records\", response_model=List[PreviewRow])\nasync def get_preview_records(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get records for a preview session.\"\"\"\n log.reason(\"Get preview records\", payload={\"session_id\": session_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationPreviewSession, TranslationPreviewRecord\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session_id)\n .all()\n )\n return [\n PreviewRow(\n id=r.id,\n source_sql=r.source_sql,\n target_sql=r.target_sql,\n source_object_type=r.source_object_type,\n source_object_id=r.source_object_id,\n source_object_name=r.source_object_name,\n status=r.status,\n feedback=r.feedback,\n )\n for r in records\n ]\n except HTTPException:\n raise\n except Exception as e:\n log.explore(\"Get preview records error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_preview_records\n" + "body": "# #region get_preview_records [TYPE Function]\n# @BRIEF Get records for a preview session.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns list of preview records.\n@router.get(\"/preview/{session_id}/records\", response_model=list[PreviewRow])\nasync def get_preview_records(\n session_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get records for a preview session.\"\"\"\n logger.reason(f\"get_preview_records — Session: {session_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n from ....models.translate import TranslationPreviewRecord, TranslationPreviewSession\n session = db.query(TranslationPreviewSession).filter(TranslationPreviewSession.id == session_id).first()\n if not session:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f\"Preview session '{session_id}' not found\")\n records = (\n db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session_id)\n .all()\n )\n return [\n PreviewRow(\n id=r.id,\n source_sql=r.source_sql,\n target_sql=r.target_sql,\n source_object_type=r.source_object_type,\n source_object_id=r.source_object_id,\n source_object_name=r.source_object_name,\n status=r.status,\n feedback=r.feedback,\n )\n for r in records\n ]\n except HTTPException:\n raise\n except Exception as e:\n logger.explore(\"get_preview_records failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_preview_records\n" }, { "contract_id": "TranslateRouterModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_router.py", "start_line": 1, - "end_line": 10, + "end_line": 12, "tier": "TIER_1", "complexity": 1, "metadata": { "COMPLEXITY": 1, - "LAYER": "UI" + "LAYER": "API", + "PURPOSE": "APIRouter instance for translate routes." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslateRouterModule [C:1] [TYPE Module] [SEMANTICS api,routes,translate,router]\n# @LAYER UI\n\nfrom fastapi import APIRouter\n\n# #region translate_router [C:1] [TYPE Global]\nrouter = APIRouter(prefix=\"/api/translate\", tags=[\"translate\"])\n# #endregion translate_router\n\n# #endregion TranslateRouterModule\n" + "body": "# #region TranslateRouterModule [C:1] [TYPE Module] [SEMANTICS fastapi, translate, api]\n# @BRIEF APIRouter instance for translate routes.\n# @LAYER: API\n\nfrom fastapi import APIRouter\n\n# #region translate_router [TYPE Global]\n# @BRIEF APIRouter instance for all translate sub-routes.\nrouter = APIRouter(prefix=\"/api/translate\", tags=[\"translate\"])\n# #endregion translate_router\n\n# #endregion TranslateRouterModule\n" }, { "contract_id": "translate_router", "contract_type": "Global", "file_path": "backend/src/api/routes/translate/_router.py", - "start_line": 6, - "end_line": 8, + "start_line": 7, + "end_line": 10, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "APIRouter instance for all translate sub-routes." }, "relations": [], "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Global'", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Global'", "detail": { "actual_type": "Global", "allowed_types": [ @@ -25919,6 +24238,7 @@ "Class", "Component", "Block", + "ADR", "Skill", "Agent" ] @@ -25926,8 +24246,8 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Global' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Global' at C1", "detail": { "actual_complexity": 1, "contract_type": "Global" @@ -25935,621 +24255,902 @@ } ], "anchor_syntax": "region", - "body": "# #region translate_router [C:1] [TYPE Global]\nrouter = APIRouter(prefix=\"/api/translate\", tags=[\"translate\"])\n# #endregion translate_router\n" + "body": "# #region translate_router [TYPE Global]\n# @BRIEF APIRouter instance for all translate sub-routes.\nrouter = APIRouter(prefix=\"/api/translate\", tags=[\"translate\"])\n# #endregion translate_router\n" }, { "contract_id": "TranslateRunListRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_run_list_routes.py", "start_line": 1, - "end_line": 209, + "end_line": 204, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, - "LAYER": "UI", + "LAYER": "API", "PURPOSE": "Translation Run listing, detail, and CSV download routes (cross-job)." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "TranslateRunListRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" - }, - { - "source_id": "TranslateRunListRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationRun", - "target_ref": "[TranslationRun]" - }, - { - "source_id": "TranslateRunListRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" - }, - { - "source_id": "TranslateRunListRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_db", - "target_ref": "[get_db]" + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslateRunListRoutesModule [C:3] [TYPE Module] [SEMANTICS api,routes,translate,runs,list,detail]\n# @BRIEF Translation Run listing, detail, and CSV download routes (cross-job).\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateRunListRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.orchestrator import TranslationOrchestrator\nfrom ....plugins.translate.events import TranslationEventLog\n\nfrom ._router import router\n\n\n# ============================================================\n# List Runs (cross-job)\n# ============================================================\n\n# #region list_runs [C:3] [TYPE Function] [SEMANTICS api,translate,runs,list]\n# @BRIEF List all runs with cross-job filtering and pagination.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n@router.get(\"/runs\")\nasync def list_runs(\n job_id: Optional[str] = Query(None),\n run_status: Optional[str] = Query(None, alias=\"status\"),\n trigger_type: Optional[str] = Query(None),\n created_by: Optional[str] = Query(None),\n date_from: Optional[str] = Query(None),\n date_to: Optional[str] = Query(None),\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all runs with cross-job filtering and pagination.\"\"\"\n log.reason(\"Listing runs\", payload={\"user\": current_user.username})\n try:\n from ....models.translate import TranslationRun\n query = db.query(TranslationRun)\n\n if job_id:\n query = query.filter(TranslationRun.job_id == job_id)\n if run_status:\n query = query.filter(TranslationRun.status == run_status)\n if trigger_type:\n query = query.filter(TranslationRun.trigger_type == trigger_type)\n if created_by:\n query = query.filter(TranslationRun.created_by == created_by)\n if date_from:\n try:\n from datetime import datetime\n dt_from = datetime.fromisoformat(date_from)\n query = query.filter(TranslationRun.created_at >= dt_from)\n except ValueError:\n pass\n if date_to:\n try:\n from datetime import datetime\n dt_to = datetime.fromisoformat(date_to)\n query = query.filter(TranslationRun.created_at <= dt_to)\n except ValueError:\n pass\n\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n items = []\n for r in runs:\n items.append({\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"trigger_type\": r.trigger_type,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"config_hash\": r.config_hash,\n \"dict_snapshot_hash\": r.dict_snapshot_hash,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n })\n\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except Exception as e:\n log.explore(\"List runs error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_runs\n\n\n# ============================================================\n# Run Detail\n# ============================================================\n\n# #region get_run_detail [C:3] [TYPE Function] [SEMANTICS api,translate,runs,detail]\n# @BRIEF Get detailed run info with config_snapshot, records, events.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n@router.get(\"/runs/{run_id}/detail\")\nasync def get_run_detail(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get detailed run info with config_snapshot, records, events.\"\"\"\n log.reason(\"Get run detail\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n status_info = orch.get_run_status(run_id)\n records = orch.get_run_records(run_id, page=1, page_size=50)\n events = TranslationEventLog(db).query_events(run_id=run_id)\n event_summary = TranslationEventLog(db).get_run_event_summary(run_id)\n\n # Get config snapshot from run\n from ....models.translate import TranslationRun\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n config_snapshot = run.config_snapshot if run else None\n\n return {\n **status_info,\n \"config_snapshot\": config_snapshot,\n \"config_hash\": run.config_hash if run else None,\n \"records\": records,\n \"events\": events,\n \"event_invariants\": event_summary,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_detail\n\n\n# ============================================================\n# Download Skipped CSV\n# ============================================================\n\n# #region download_skipped_csv [C:3] [TYPE Function] [SEMANTICS api,translate,runs,csv]\n# @BRIEF Download a CSV of skipped translation records from a run.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n@router.get(\"/runs/{run_id}/skipped.csv\")\nasync def download_skipped_csv(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Download skipped translation records as CSV.\"\"\"\n log.reason(\"Download skipped CSV\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationRecord\n from fastapi.responses import StreamingResponse\n import csv\n import io\n\n records = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run_id,\n TranslationRecord.status == \"SKIPPED\",\n )\n .all()\n )\n\n output = io.StringIO()\n writer = csv.writer(output)\n writer.writerow([\"id\", \"source_sql\", \"target_sql\", \"source_object_type\",\n \"source_object_id\", \"source_object_name\", \"error_message\"])\n for rec in records:\n writer.writerow([\n rec.id, rec.source_sql, rec.target_sql, rec.source_object_type,\n rec.source_object_id, rec.source_object_name, rec.error_message,\n ])\n\n output.seek(0)\n return StreamingResponse(\n iter([output.getvalue()]),\n media_type=\"text/csv\",\n headers={\"Content-Disposition\": f\"attachment; filename=skipped_{run_id}.csv\"},\n )\n except Exception as e:\n log.explore(\"Download skipped CSV error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion download_skipped_csv\n\n# #endregion TranslateRunListRoutesModule\n" + "body": "# #region TranslateRunListRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, download, search]\n# @BRIEF Translation Run listing, detail, and CSV download routes (cross-job).\n# @LAYER: API\n\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.config_manager import ConfigManager\nfrom ....core.database import get_db\nfrom ....core.logger import logger\nfrom ....dependencies import get_config_manager, get_current_user, has_permission\nfrom ....plugins.translate.events import TranslationEventLog\nfrom ....plugins.translate.orchestrator import TranslationOrchestrator\nfrom ....schemas.auth import User\nfrom ._router import router\n\n# ============================================================\n# List Runs (cross-job)\n# ============================================================\n\n# #region list_runs [TYPE Function]\n# @BRIEF List all runs with cross-job filtering and pagination.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns paginated list of runs.\n@router.get(\"/runs\")\nasync def list_runs(\n job_id: str | None = Query(None),\n run_status: str | None = Query(None),\n trigger_type: str | None = Query(None),\n created_by: str | None = Query(None),\n date_from: str | None = Query(None),\n date_to: str | None = Query(None),\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all runs with cross-job filtering and pagination.\"\"\"\n logger.reason(f\"list_runs — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n from ....models.translate import TranslationRun\n query = db.query(TranslationRun)\n\n if job_id:\n query = query.filter(TranslationRun.job_id == job_id)\n if run_status:\n query = query.filter(TranslationRun.status == run_status)\n if trigger_type:\n query = query.filter(TranslationRun.trigger_type == trigger_type)\n if created_by:\n query = query.filter(TranslationRun.created_by == created_by)\n if date_from:\n try:\n from datetime import datetime\n dt_from = datetime.fromisoformat(date_from)\n query = query.filter(TranslationRun.created_at >= dt_from)\n except ValueError:\n pass\n if date_to:\n try:\n from datetime import datetime\n dt_to = datetime.fromisoformat(date_to)\n query = query.filter(TranslationRun.created_at <= dt_to)\n except ValueError:\n pass\n\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n items = []\n for r in runs:\n items.append({\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"trigger_type\": r.trigger_type,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"config_hash\": r.config_hash,\n \"dict_snapshot_hash\": r.dict_snapshot_hash,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n })\n\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except Exception as e:\n logger.explore(\"list_runs failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_runs\n\n\n# ============================================================\n# Run Detail\n# ============================================================\n\n# #region get_run_detail [TYPE Function]\n# @BRIEF Get detailed run info with config_snapshot, records, events.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns run detail with records and events.\n@router.get(\"/runs/{run_id}/detail\")\nasync def get_run_detail(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get detailed run info with config_snapshot, records, events.\"\"\"\n logger.reason(f\"get_run_detail — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n status_info = orch.get_run_status(run_id)\n records = orch.get_run_records(run_id, page=1, page_size=50)\n events = TranslationEventLog(db).query_events(run_id=run_id)\n event_summary = TranslationEventLog(db).get_run_event_summary(run_id)\n\n # Get config snapshot from run\n from ....models.translate import TranslationRun\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n config_snapshot = run.config_snapshot if run else None\n\n return {\n **status_info,\n \"config_snapshot\": config_snapshot,\n \"config_hash\": run.config_hash if run else None,\n \"records\": records,\n \"events\": events,\n \"event_invariants\": event_summary,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_detail\n\n\n# ============================================================\n# Download Skipped CSV\n# ============================================================\n\n# #region download_skipped_csv [TYPE Function]\n# @BRIEF Download a CSV of skipped translation records from a run.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns CSV file of skipped records.\n@router.get(\"/runs/{run_id}/skipped.csv\")\nasync def download_skipped_csv(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Download skipped translation records as CSV.\"\"\"\n logger.reason(f\"download_skipped_csv — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n import csv\n import io\n\n from fastapi.responses import StreamingResponse\n\n from ....models.translate import TranslationRecord\n\n records = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run_id,\n TranslationRecord.status == \"SKIPPED\",\n )\n .all()\n )\n\n output = io.StringIO()\n writer = csv.writer(output)\n writer.writerow([\"id\", \"source_sql\", \"target_sql\", \"source_object_type\",\n \"source_object_id\", \"source_object_name\", \"error_message\"])\n for rec in records:\n writer.writerow([\n rec.id, rec.source_sql, rec.target_sql, rec.source_object_type,\n rec.source_object_id, rec.source_object_name, rec.error_message,\n ])\n\n output.seek(0)\n return StreamingResponse(\n iter([output.getvalue()]),\n media_type=\"text/csv\",\n headers={\"Content-Disposition\": f\"attachment; filename=skipped_{run_id}.csv\"},\n )\n except Exception as e:\n logger.explore(\"download_skipped_csv failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion download_skipped_csv\n\n# #endregion TranslateRunListRoutesModule\n" }, { "contract_id": "list_runs", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_list_routes.py", - "start_line": 30, - "end_line": 110, - "tier": "TIER_2", - "complexity": 3, + "start_line": 22, + "end_line": 103, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns paginated list of runs.", + "PRE": "User has translate.history.view permission.", "PURPOSE": "List all runs with cross-job filtering and pagination." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "list_runs", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationRun", - "target_ref": "[TranslationRun]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region list_runs [C:3] [TYPE Function] [SEMANTICS api,translate,runs,list]\n# @BRIEF List all runs with cross-job filtering and pagination.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n@router.get(\"/runs\")\nasync def list_runs(\n job_id: Optional[str] = Query(None),\n run_status: Optional[str] = Query(None, alias=\"status\"),\n trigger_type: Optional[str] = Query(None),\n created_by: Optional[str] = Query(None),\n date_from: Optional[str] = Query(None),\n date_to: Optional[str] = Query(None),\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all runs with cross-job filtering and pagination.\"\"\"\n log.reason(\"Listing runs\", payload={\"user\": current_user.username})\n try:\n from ....models.translate import TranslationRun\n query = db.query(TranslationRun)\n\n if job_id:\n query = query.filter(TranslationRun.job_id == job_id)\n if run_status:\n query = query.filter(TranslationRun.status == run_status)\n if trigger_type:\n query = query.filter(TranslationRun.trigger_type == trigger_type)\n if created_by:\n query = query.filter(TranslationRun.created_by == created_by)\n if date_from:\n try:\n from datetime import datetime\n dt_from = datetime.fromisoformat(date_from)\n query = query.filter(TranslationRun.created_at >= dt_from)\n except ValueError:\n pass\n if date_to:\n try:\n from datetime import datetime\n dt_to = datetime.fromisoformat(date_to)\n query = query.filter(TranslationRun.created_at <= dt_to)\n except ValueError:\n pass\n\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n items = []\n for r in runs:\n items.append({\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"trigger_type\": r.trigger_type,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"config_hash\": r.config_hash,\n \"dict_snapshot_hash\": r.dict_snapshot_hash,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n })\n\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except Exception as e:\n log.explore(\"List runs error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_runs\n" + "body": "# #region list_runs [TYPE Function]\n# @BRIEF List all runs with cross-job filtering and pagination.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns paginated list of runs.\n@router.get(\"/runs\")\nasync def list_runs(\n job_id: str | None = Query(None),\n run_status: str | None = Query(None),\n trigger_type: str | None = Query(None),\n created_by: str | None = Query(None),\n date_from: str | None = Query(None),\n date_to: str | None = Query(None),\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"List all runs with cross-job filtering and pagination.\"\"\"\n logger.reason(f\"list_runs — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n from ....models.translate import TranslationRun\n query = db.query(TranslationRun)\n\n if job_id:\n query = query.filter(TranslationRun.job_id == job_id)\n if run_status:\n query = query.filter(TranslationRun.status == run_status)\n if trigger_type:\n query = query.filter(TranslationRun.trigger_type == trigger_type)\n if created_by:\n query = query.filter(TranslationRun.created_by == created_by)\n if date_from:\n try:\n from datetime import datetime\n dt_from = datetime.fromisoformat(date_from)\n query = query.filter(TranslationRun.created_at >= dt_from)\n except ValueError:\n pass\n if date_to:\n try:\n from datetime import datetime\n dt_to = datetime.fromisoformat(date_to)\n query = query.filter(TranslationRun.created_at <= dt_to)\n except ValueError:\n pass\n\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n items = []\n for r in runs:\n items.append({\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"trigger_type\": r.trigger_type,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"config_hash\": r.config_hash,\n \"dict_snapshot_hash\": r.dict_snapshot_hash,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n })\n\n return {\"items\": items, \"total\": total, \"page\": page, \"page_size\": page_size}\n except Exception as e:\n logger.explore(\"list_runs failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_runs\n" }, { "contract_id": "get_run_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_list_routes.py", - "start_line": 117, - "end_line": 154, - "tier": "TIER_2", - "complexity": 3, + "start_line": 110, + "end_line": 146, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns run detail with records and events.", + "PRE": "User has translate.history.view permission.", "PURPOSE": "Get detailed run info with config_snapshot, records, events." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_run_detail", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { - "source_id": "get_run_detail", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationEventLog", - "target_ref": "[TranslationEventLog]" + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { - "source_id": "get_run_detail", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationRun", - "target_ref": "[TranslationRun]" + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_run_detail [C:3] [TYPE Function] [SEMANTICS api,translate,runs,detail]\n# @BRIEF Get detailed run info with config_snapshot, records, events.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n@router.get(\"/runs/{run_id}/detail\")\nasync def get_run_detail(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get detailed run info with config_snapshot, records, events.\"\"\"\n log.reason(\"Get run detail\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n status_info = orch.get_run_status(run_id)\n records = orch.get_run_records(run_id, page=1, page_size=50)\n events = TranslationEventLog(db).query_events(run_id=run_id)\n event_summary = TranslationEventLog(db).get_run_event_summary(run_id)\n\n # Get config snapshot from run\n from ....models.translate import TranslationRun\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n config_snapshot = run.config_snapshot if run else None\n\n return {\n **status_info,\n \"config_snapshot\": config_snapshot,\n \"config_hash\": run.config_hash if run else None,\n \"records\": records,\n \"events\": events,\n \"event_invariants\": event_summary,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_detail\n" + "body": "# #region get_run_detail [TYPE Function]\n# @BRIEF Get detailed run info with config_snapshot, records, events.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns run detail with records and events.\n@router.get(\"/runs/{run_id}/detail\")\nasync def get_run_detail(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get detailed run info with config_snapshot, records, events.\"\"\"\n logger.reason(f\"get_run_detail — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n status_info = orch.get_run_status(run_id)\n records = orch.get_run_records(run_id, page=1, page_size=50)\n events = TranslationEventLog(db).query_events(run_id=run_id)\n event_summary = TranslationEventLog(db).get_run_event_summary(run_id)\n\n # Get config snapshot from run\n from ....models.translate import TranslationRun\n run = db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n config_snapshot = run.config_snapshot if run else None\n\n return {\n **status_info,\n \"config_snapshot\": config_snapshot,\n \"config_hash\": run.config_hash if run else None,\n \"records\": records,\n \"events\": events,\n \"event_invariants\": event_summary,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_detail\n" }, { "contract_id": "download_skipped_csv", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_list_routes.py", - "start_line": 161, - "end_line": 207, - "tier": "TIER_2", - "complexity": 3, + "start_line": 153, + "end_line": 202, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns CSV file of skipped records.", + "PRE": "User has translate.job.view permission.", "PURPOSE": "Download a CSV of skipped translation records from a run." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "download_skipped_csv", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationRecord", - "target_ref": "[TranslationRecord]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region download_skipped_csv [C:3] [TYPE Function] [SEMANTICS api,translate,runs,csv]\n# @BRIEF Download a CSV of skipped translation records from a run.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n@router.get(\"/runs/{run_id}/skipped.csv\")\nasync def download_skipped_csv(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Download skipped translation records as CSV.\"\"\"\n log.reason(\"Download skipped CSV\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationRecord\n from fastapi.responses import StreamingResponse\n import csv\n import io\n\n records = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run_id,\n TranslationRecord.status == \"SKIPPED\",\n )\n .all()\n )\n\n output = io.StringIO()\n writer = csv.writer(output)\n writer.writerow([\"id\", \"source_sql\", \"target_sql\", \"source_object_type\",\n \"source_object_id\", \"source_object_name\", \"error_message\"])\n for rec in records:\n writer.writerow([\n rec.id, rec.source_sql, rec.target_sql, rec.source_object_type,\n rec.source_object_id, rec.source_object_name, rec.error_message,\n ])\n\n output.seek(0)\n return StreamingResponse(\n iter([output.getvalue()]),\n media_type=\"text/csv\",\n headers={\"Content-Disposition\": f\"attachment; filename=skipped_{run_id}.csv\"},\n )\n except Exception as e:\n log.explore(\"Download skipped CSV error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion download_skipped_csv\n" + "body": "# #region download_skipped_csv [TYPE Function]\n# @BRIEF Download a CSV of skipped translation records from a run.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns CSV file of skipped records.\n@router.get(\"/runs/{run_id}/skipped.csv\")\nasync def download_skipped_csv(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Download skipped translation records as CSV.\"\"\"\n logger.reason(f\"download_skipped_csv — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n import csv\n import io\n\n from fastapi.responses import StreamingResponse\n\n from ....models.translate import TranslationRecord\n\n records = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run_id,\n TranslationRecord.status == \"SKIPPED\",\n )\n .all()\n )\n\n output = io.StringIO()\n writer = csv.writer(output)\n writer.writerow([\"id\", \"source_sql\", \"target_sql\", \"source_object_type\",\n \"source_object_id\", \"source_object_name\", \"error_message\"])\n for rec in records:\n writer.writerow([\n rec.id, rec.source_sql, rec.target_sql, rec.source_object_type,\n rec.source_object_id, rec.source_object_name, rec.error_message,\n ])\n\n output.seek(0)\n return StreamingResponse(\n iter([output.getvalue()]),\n media_type=\"text/csv\",\n headers={\"Content-Disposition\": f\"attachment; filename=skipped_{run_id}.csv\"},\n )\n except Exception as e:\n logger.explore(\"download_skipped_csv failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion download_skipped_csv\n" }, { "contract_id": "TranslateRunRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_run_routes.py", "start_line": 1, - "end_line": 281, + "end_line": 344, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": 4, - "LAYER": "UI", - "POST": "Translation run executed, manipulated, or queried. Results returned to caller.", - "PRE": "ConfigManager and DB session initialized. User authenticated with translate permissions.", - "PURPOSE": "Translation Run execution, history, status, records and batches routes.", - "SIDE_EFFECT": "Creates/updates TranslationRun records in DB; spawns background threads for execution; queries Superset API." + "COMPLEXITY": 3, + "LAYER": "API", + "PURPOSE": "Translation Run execution, history, status, records and batches routes." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "TranslateRunRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" - }, - { - "source_id": "TranslateRunRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "ConfigManager", - "target_ref": "[ConfigManager]" - }, - { - "source_id": "TranslateRunRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" - }, - { - "source_id": "TranslateRunRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_db", - "target_ref": "[get_db]" + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslateRunRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,runs]\n# @BRIEF Translation Run execution, history, status, records and batches routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE ConfigManager and DB session initialized. User authenticated with translate permissions.\n# @POST Translation run executed, manipulated, or queried. Results returned to caller.\n# @SIDE_EFFECT Creates/updates TranslationRun records in DB; spawns background threads for execution; queries Superset API.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db, SessionLocal\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateRunRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.orchestrator import TranslationOrchestrator\nfrom ....plugins.translate.events import TranslationEventLog\nfrom ....models.translate import TranslationRun\n\nfrom ._router import router\nfrom ._helpers import _run_to_response\n\n\n# ============================================================\n# Translation Run / Execute\n# ============================================================\n\n# #region run_translation [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Execute a translation job (trigger a run).\n# @PRE User has translate.job.execute permission. Job exists and preview is accepted.\n# @POST Translation run created and started in background. Run object returned.\n# @SIDE_EFFECT Creates TranslationRun record in DB; spawns background thread for translation execution.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/jobs/{job_id}/run\", status_code=status.HTTP_201_CREATED)\nasync def run_translation(\n job_id: str,\n full_translation: bool = Query(False, description=\"If True, fetch ALL rows from Superset dataset instead of preview-only rows\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Execute a translation job (trigger a run).\n\n By default runs translation on preview-approved rows only.\n Set full_translation=true to fetch ALL rows from the Superset source dataset.\n \"\"\"\n log.reason(\"Run translation\", payload={\"job_id\": job_id, \"user\": current_user.username, \"full_translation\": full_translation})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.start_run(job_id=job_id, is_scheduled=False)\n # Execute asynchronously in background with a FRESH session.\n # The request-scoped session from Depends(get_db) is closed after the handler returns,\n # so the background thread must create its own session to avoid \"This transaction is closed\".\n import threading\n\n def _execute_background():\n thread_db = SessionLocal()\n try:\n thread_orch = TranslationOrchestrator(thread_db, config_manager, current_user.username)\n # Reload run from DB with the new session (the passed run object is detached)\n thread_run = thread_db.query(TranslationRun).filter(TranslationRun.id == run.id).first()\n if thread_run:\n thread_orch.execute_run(thread_run, full_translation=full_translation)\n except Exception as e:\n log.explore(\"Background execution error\", error=str(e))\n finally:\n thread_db.close()\n\n threading.Thread(target=_execute_background, daemon=True).start()\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Run translation error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Run failed: {e}\")\n# #endregion run_translation\n\n\n# #region retry_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Retry failed batches in a translation run.\n# @PRE User has translate.job.execute permission. Run exists and has failed batches.\n# @POST Failed batches re-executed. Updated run returned.\n# @SIDE_EFFECT Updates TranslationRun and TranslationBatch records in DB.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/retry\")\nasync def retry_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry failed batches in a translation run.\"\"\"\n log.reason(\"Retry run\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_failed_batches(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Retry run error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry failed: {e}\")\n# #endregion retry_run\n\n\n# #region retry_insert [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Retry the SQL insert phase for a completed run.\n# @PRE User has translate.job.execute permission. Run completed with insert_status FAILED.\n# @POST SQL insert phase re-executed. Updated run returned.\n# @SIDE_EFFECT Re-executes SQL insert into Superset; updates TranslationRun insert_status.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/retry-insert\")\nasync def retry_insert(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry the SQL insert phase for a completed run.\"\"\"\n log.reason(\"Retry insert\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_insert(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Retry insert error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry insert failed: {e}\")\n# #endregion retry_insert\n\n\n# #region cancel_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Cancel a running translation.\n# @PRE User has translate.job.execute permission. Run is in RUNNING state.\n# @POST Run is cancelled. Updated run returned.\n# @SIDE_EFFECT Updates TranslationRun status to CANCELLED in DB.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/cancel\")\nasync def cancel_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Cancel a running translation.\"\"\"\n log.reason(\"Cancel run\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.cancel_run(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion cancel_run\n\n\n# #region get_run_history [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get run history for a translation job.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/jobs/{job_id}/runs\")\nasync def get_run_history(\n job_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get run history for a translation job.\"\"\"\n log.reason(\"Get run history\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n total, runs = orch.get_run_history(job_id, page=page, page_size=page_size)\n return {\"items\": runs, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_history\n\n\n# #region get_run_status [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get status and statistics for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/runs/{run_id}\")\nasync def get_run_status(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get status and statistics for a translation run.\"\"\"\n log.reason(\"Get run status\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_status(run_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_status\n\n\n# #region get_run_records [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get paginated records for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/runs/{run_id}/records\")\nasync def get_run_records(\n run_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n status: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get paginated records for a translation run.\"\"\"\n log.reason(\"Get run records\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_records(run_id, page=page, page_size=page_size, status_filter=status)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_records\n\n\n# ============================================================\n# Batches\n# ============================================================\n\n# #region get_batches [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get batches for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n@router.get(\"/runs/{run_id}/batches\")\nasync def get_batches(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get batches for a translation run.\"\"\"\n log.reason(\"Get batches\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationBatch\n batches = (\n db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .order_by(TranslationBatch.batch_index.asc())\n .all()\n )\n return [\n {\n \"id\": b.id,\n \"run_id\": b.run_id,\n \"batch_index\": b.batch_index,\n \"status\": b.status,\n \"total_records\": b.total_records or 0,\n \"successful_records\": b.successful_records or 0,\n \"failed_records\": b.failed_records or 0,\n \"started_at\": b.started_at.isoformat() if b.started_at else None,\n \"completed_at\": b.completed_at.isoformat() if b.completed_at else None,\n \"created_at\": b.created_at.isoformat() if b.created_at else None,\n }\n for b in batches\n ]\n except Exception as e:\n log.explore(\"Get batches error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_batches\n\n# #endregion TranslateRunRoutesModule\n" + "body": "# #region TranslateRunRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search, execution, history]\n# @BRIEF Translation Run execution, history, status, records and batches routes.\n# @LAYER: API\n\nfrom datetime import UTC, datetime\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.config_manager import ConfigManager\nfrom ....core.database import SessionLocal, get_db\nfrom ....core.logger import logger\nfrom ....dependencies import get_config_manager, get_current_user, has_permission\nfrom ....plugins.translate.orchestrator import TranslationOrchestrator\nfrom ....schemas.auth import User\nfrom ._helpers import _run_to_response\nfrom ._router import router\n\n# ============================================================\n# Translation Run / Execute\n# ============================================================\n\n# #region run_translation [TYPE Function]\n# @BRIEF Execute a translation job (trigger a run).\n# @PRE: User has translate.job.execute permission.\n# @POST: Returns the created translation run.\n@router.post(\"/jobs/{job_id}/run\", status_code=status.HTTP_201_CREATED)\nasync def run_translation(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Execute a translation job (trigger a run).\"\"\"\n logger.reason(f\"run_translation — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.start_run(job_id=job_id, is_scheduled=False)\n # The request-scoped db session will be closed after this handler returns.\n # The background thread must use its OWN session to avoid operating on a\n # closed or expunged session.\n import threading\n\n def _background_execute():\n bg_db = SessionLocal()\n try:\n bg_orch = TranslationOrchestrator(\n bg_db, config_manager,\n current_user.username if current_user else None,\n )\n # Re-fetch the run within the background session to get a fresh,\n # attached object\n from ....models.translate import TranslationRun as TRModel\n bg_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()\n if bg_run is None:\n logger.explore(\n \"Background execute: run not found\",\n extra={\"src\": \"translate_routes\", \"run_id\": run.id},\n )\n return\n\n # execute_run internally calls self.db.commit()\n bg_orch.execute_run(bg_run)\n\n # ----- VERIFICATION -----\n # After execute_run the run object from bg_db is detached after\n # commit. Re-query to verify the status was persisted in the\n # database and is a terminal state.\n check_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()\n if check_run and check_run.status in (\"COMPLETED\", \"FAILED\", \"CANCELLED\"):\n logger.reason(\n \"Background execute verified\",\n extra={\n \"src\": \"translate_routes\",\n \"run_id\": run.id,\n \"status\": check_run.status,\n },\n )\n else:\n # execute_run appeared to succeed but the run is still in a\n # non-terminal state — manually fail it so the frontend\n # polling can detect the terminal state and stop spinning.\n actual_status = check_run.status if check_run else \"NOT_FOUND\"\n logger.explore(\n \"Background execute: run not in terminal state after commit\",\n extra={\n \"src\": \"translate_routes\",\n \"run_id\": run.id,\n \"status\": actual_status,\n },\n )\n if check_run:\n check_run.status = \"FAILED\"\n check_run.error_message = (\n \"Background execution did not reach terminal state \"\n f\"(was: {actual_status})\"\n )\n bg_db.commit()\n\n except Exception as bg_err:\n logger.explore(\n \"Background execute failed\",\n extra={\n \"src\": \"translate_routes\",\n \"run_id\": run.id,\n \"error\": str(bg_err),\n },\n )\n # Mark the run as FAILED so the frontend polling can detect a\n # terminal state and stop spinning.\n try:\n fb_db = SessionLocal()\n try:\n fb_run = fb_db.query(TRModel).filter(TRModel.id == run.id).first()\n if fb_run:\n fb_run.status = \"FAILED\"\n fb_run.error_message = f\"Background execute error: {bg_err}\"\n fb_run.completed_at = datetime.now(UTC)\n fb_db.commit()\n logger.reason(\n \"Background execute: run marked FAILED\",\n extra={\"src\": \"translate_routes\", \"run_id\": run.id},\n )\n finally:\n fb_db.close()\n except Exception as fb_err:\n logger.explore(\n \"Background execute: unable to mark run FAILED\",\n extra={\n \"src\": \"translate_routes\",\n \"run_id\": run.id,\n \"error\": str(fb_err),\n },\n )\n finally:\n bg_db.close()\n\n threading.Thread(\n target=_background_execute,\n daemon=True,\n ).start()\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"run_translation failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Run failed: {e}\")\n# #endregion run_translation\n\n\n# #region retry_run [TYPE Function]\n# @BRIEF Retry failed batches in a translation run.\n# @PRE: User has translate.job.execute permission.\n# @POST: Returns the updated translation run.\n@router.post(\"/runs/{run_id}/retry\")\nasync def retry_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry failed batches in a translation run.\"\"\"\n logger.reason(f\"retry_run — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_failed_batches(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"retry_run failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry failed: {e}\")\n# #endregion retry_run\n\n\n# #region retry_insert [TYPE Function]\n# @BRIEF Retry the SQL insert phase for a completed run.\n# @PRE: User has translate.job.execute permission.\n# @POST: Returns the updated run.\n@router.post(\"/runs/{run_id}/retry-insert\")\nasync def retry_insert(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry the SQL insert phase for a completed run.\"\"\"\n logger.reason(f\"retry_insert — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_insert(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"retry_insert failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry insert failed: {e}\")\n# #endregion retry_insert\n\n\n# #region cancel_run [TYPE Function]\n# @BRIEF Cancel a running translation.\n# @PRE: User has translate.job.execute permission.\n# @POST: Run is cancelled.\n@router.post(\"/runs/{run_id}/cancel\")\nasync def cancel_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Cancel a running translation.\"\"\"\n logger.reason(f\"cancel_run — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.cancel_run(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion cancel_run\n\n\n# #region get_run_history [TYPE Function]\n# @BRIEF Get run history for a translation job.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns list of runs.\n@router.get(\"/jobs/{job_id}/runs\")\nasync def get_run_history(\n job_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get run history for a translation job.\"\"\"\n logger.reason(f\"get_run_history — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n total, runs = orch.get_run_history(job_id, page=page, page_size=page_size)\n return {\"items\": runs, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_history\n\n\n# #region get_run_status [TYPE Function]\n# @BRIEF Get status and statistics for a translation run.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns run details with statistics.\n@router.get(\"/runs/{run_id}\")\nasync def get_run_status(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get status and statistics for a translation run.\"\"\"\n logger.reason(f\"get_run_status — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_status(run_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_status\n\n\n# #region get_run_records [TYPE Function]\n# @BRIEF Get paginated records for a translation run.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns paginated records.\n@router.get(\"/runs/{run_id}/records\")\nasync def get_run_records(\n run_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n status: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get paginated records for a translation run.\"\"\"\n logger.reason(f\"get_run_records — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_records(run_id, page=page, page_size=page_size, status_filter=status)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_records\n\n\n# ============================================================\n# Batches\n# ============================================================\n\n# #region get_batches [TYPE Function]\n# @BRIEF Get batches for a translation run.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns list of batches.\n@router.get(\"/runs/{run_id}/batches\")\nasync def get_batches(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get batches for a translation run.\"\"\"\n logger.reason(f\"get_batches — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n from ....models.translate import TranslationBatch\n batches = (\n db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .order_by(TranslationBatch.batch_index.asc())\n .all()\n )\n return [\n {\n \"id\": b.id,\n \"run_id\": b.run_id,\n \"batch_index\": b.batch_index,\n \"status\": b.status,\n \"total_records\": b.total_records or 0,\n \"successful_records\": b.successful_records or 0,\n \"failed_records\": b.failed_records or 0,\n \"started_at\": b.started_at.isoformat() if b.started_at else None,\n \"completed_at\": b.completed_at.isoformat() if b.completed_at else None,\n \"created_at\": b.created_at.isoformat() if b.created_at else None,\n }\n for b in batches\n ]\n except Exception as e:\n logger.explore(\"get_batches failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_batches\n\n# #endregion TranslateRunRoutesModule\n" }, { "contract_id": "run_translation", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_routes.py", - "start_line": 35, - "end_line": 84, - "tier": "TIER_2", - "complexity": 4, + "start_line": 23, + "end_line": 149, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Translation run created and started in background. Run object returned.", - "PRE": "User has translate.job.execute permission. Job exists and preview is accepted.", - "PURPOSE": "Execute a translation job (trigger a run).", - "SIDE_EFFECT": "Creates TranslationRun record in DB; spawns background thread for translation execution." + "POST": "Returns the created translation run.", + "PRE": "User has translate.job.execute permission.", + "PURPOSE": "Execute a translation job (trigger a run)." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "run_translation", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region run_translation [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Execute a translation job (trigger a run).\n# @PRE User has translate.job.execute permission. Job exists and preview is accepted.\n# @POST Translation run created and started in background. Run object returned.\n# @SIDE_EFFECT Creates TranslationRun record in DB; spawns background thread for translation execution.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/jobs/{job_id}/run\", status_code=status.HTTP_201_CREATED)\nasync def run_translation(\n job_id: str,\n full_translation: bool = Query(False, description=\"If True, fetch ALL rows from Superset dataset instead of preview-only rows\"),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Execute a translation job (trigger a run).\n\n By default runs translation on preview-approved rows only.\n Set full_translation=true to fetch ALL rows from the Superset source dataset.\n \"\"\"\n log.reason(\"Run translation\", payload={\"job_id\": job_id, \"user\": current_user.username, \"full_translation\": full_translation})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.start_run(job_id=job_id, is_scheduled=False)\n # Execute asynchronously in background with a FRESH session.\n # The request-scoped session from Depends(get_db) is closed after the handler returns,\n # so the background thread must create its own session to avoid \"This transaction is closed\".\n import threading\n\n def _execute_background():\n thread_db = SessionLocal()\n try:\n thread_orch = TranslationOrchestrator(thread_db, config_manager, current_user.username)\n # Reload run from DB with the new session (the passed run object is detached)\n thread_run = thread_db.query(TranslationRun).filter(TranslationRun.id == run.id).first()\n if thread_run:\n thread_orch.execute_run(thread_run, full_translation=full_translation)\n except Exception as e:\n log.explore(\"Background execution error\", error=str(e))\n finally:\n thread_db.close()\n\n threading.Thread(target=_execute_background, daemon=True).start()\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Run translation error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Run failed: {e}\")\n# #endregion run_translation\n" + "body": "# #region run_translation [TYPE Function]\n# @BRIEF Execute a translation job (trigger a run).\n# @PRE: User has translate.job.execute permission.\n# @POST: Returns the created translation run.\n@router.post(\"/jobs/{job_id}/run\", status_code=status.HTTP_201_CREATED)\nasync def run_translation(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Execute a translation job (trigger a run).\"\"\"\n logger.reason(f\"run_translation — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.start_run(job_id=job_id, is_scheduled=False)\n # The request-scoped db session will be closed after this handler returns.\n # The background thread must use its OWN session to avoid operating on a\n # closed or expunged session.\n import threading\n\n def _background_execute():\n bg_db = SessionLocal()\n try:\n bg_orch = TranslationOrchestrator(\n bg_db, config_manager,\n current_user.username if current_user else None,\n )\n # Re-fetch the run within the background session to get a fresh,\n # attached object\n from ....models.translate import TranslationRun as TRModel\n bg_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()\n if bg_run is None:\n logger.explore(\n \"Background execute: run not found\",\n extra={\"src\": \"translate_routes\", \"run_id\": run.id},\n )\n return\n\n # execute_run internally calls self.db.commit()\n bg_orch.execute_run(bg_run)\n\n # ----- VERIFICATION -----\n # After execute_run the run object from bg_db is detached after\n # commit. Re-query to verify the status was persisted in the\n # database and is a terminal state.\n check_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()\n if check_run and check_run.status in (\"COMPLETED\", \"FAILED\", \"CANCELLED\"):\n logger.reason(\n \"Background execute verified\",\n extra={\n \"src\": \"translate_routes\",\n \"run_id\": run.id,\n \"status\": check_run.status,\n },\n )\n else:\n # execute_run appeared to succeed but the run is still in a\n # non-terminal state — manually fail it so the frontend\n # polling can detect the terminal state and stop spinning.\n actual_status = check_run.status if check_run else \"NOT_FOUND\"\n logger.explore(\n \"Background execute: run not in terminal state after commit\",\n extra={\n \"src\": \"translate_routes\",\n \"run_id\": run.id,\n \"status\": actual_status,\n },\n )\n if check_run:\n check_run.status = \"FAILED\"\n check_run.error_message = (\n \"Background execution did not reach terminal state \"\n f\"(was: {actual_status})\"\n )\n bg_db.commit()\n\n except Exception as bg_err:\n logger.explore(\n \"Background execute failed\",\n extra={\n \"src\": \"translate_routes\",\n \"run_id\": run.id,\n \"error\": str(bg_err),\n },\n )\n # Mark the run as FAILED so the frontend polling can detect a\n # terminal state and stop spinning.\n try:\n fb_db = SessionLocal()\n try:\n fb_run = fb_db.query(TRModel).filter(TRModel.id == run.id).first()\n if fb_run:\n fb_run.status = \"FAILED\"\n fb_run.error_message = f\"Background execute error: {bg_err}\"\n fb_run.completed_at = datetime.now(UTC)\n fb_db.commit()\n logger.reason(\n \"Background execute: run marked FAILED\",\n extra={\"src\": \"translate_routes\", \"run_id\": run.id},\n )\n finally:\n fb_db.close()\n except Exception as fb_err:\n logger.explore(\n \"Background execute: unable to mark run FAILED\",\n extra={\n \"src\": \"translate_routes\",\n \"run_id\": run.id,\n \"error\": str(fb_err),\n },\n )\n finally:\n bg_db.close()\n\n threading.Thread(\n target=_background_execute,\n daemon=True,\n ).start()\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"run_translation failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Run failed: {e}\")\n# #endregion run_translation\n" }, { "contract_id": "retry_run", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_routes.py", - "start_line": 87, - "end_line": 112, - "tier": "TIER_2", - "complexity": 4, + "start_line": 152, + "end_line": 175, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Failed batches re-executed. Updated run returned.", - "PRE": "User has translate.job.execute permission. Run exists and has failed batches.", - "PURPOSE": "Retry failed batches in a translation run.", - "SIDE_EFFECT": "Updates TranslationRun and TranslationBatch records in DB." + "POST": "Returns the updated translation run.", + "PRE": "User has translate.job.execute permission.", + "PURPOSE": "Retry failed batches in a translation run." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "retry_run", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region retry_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Retry failed batches in a translation run.\n# @PRE User has translate.job.execute permission. Run exists and has failed batches.\n# @POST Failed batches re-executed. Updated run returned.\n# @SIDE_EFFECT Updates TranslationRun and TranslationBatch records in DB.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/retry\")\nasync def retry_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry failed batches in a translation run.\"\"\"\n log.reason(\"Retry run\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_failed_batches(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Retry run error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry failed: {e}\")\n# #endregion retry_run\n" + "body": "# #region retry_run [TYPE Function]\n# @BRIEF Retry failed batches in a translation run.\n# @PRE: User has translate.job.execute permission.\n# @POST: Returns the updated translation run.\n@router.post(\"/runs/{run_id}/retry\")\nasync def retry_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry failed batches in a translation run.\"\"\"\n logger.reason(f\"retry_run — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_failed_batches(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"retry_run failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry failed: {e}\")\n# #endregion retry_run\n" }, { "contract_id": "retry_insert", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_routes.py", - "start_line": 115, - "end_line": 140, - "tier": "TIER_2", - "complexity": 4, + "start_line": 178, + "end_line": 201, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "SQL insert phase re-executed. Updated run returned.", - "PRE": "User has translate.job.execute permission. Run completed with insert_status FAILED.", - "PURPOSE": "Retry the SQL insert phase for a completed run.", - "SIDE_EFFECT": "Re-executes SQL insert into Superset; updates TranslationRun insert_status." + "POST": "Returns the updated run.", + "PRE": "User has translate.job.execute permission.", + "PURPOSE": "Retry the SQL insert phase for a completed run." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "retry_insert", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region retry_insert [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Retry the SQL insert phase for a completed run.\n# @PRE User has translate.job.execute permission. Run completed with insert_status FAILED.\n# @POST SQL insert phase re-executed. Updated run returned.\n# @SIDE_EFFECT Re-executes SQL insert into Superset; updates TranslationRun insert_status.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/retry-insert\")\nasync def retry_insert(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry the SQL insert phase for a completed run.\"\"\"\n log.reason(\"Retry insert\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_insert(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n log.explore(\"Retry insert error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry insert failed: {e}\")\n# #endregion retry_insert\n" + "body": "# #region retry_insert [TYPE Function]\n# @BRIEF Retry the SQL insert phase for a completed run.\n# @PRE: User has translate.job.execute permission.\n# @POST: Returns the updated run.\n@router.post(\"/runs/{run_id}/retry-insert\")\nasync def retry_insert(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Retry the SQL insert phase for a completed run.\"\"\"\n logger.reason(f\"retry_insert — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.retry_insert(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n except Exception as e:\n logger.explore(\"retry_insert failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f\"Retry insert failed: {e}\")\n# #endregion retry_insert\n" }, { "contract_id": "cancel_run", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_routes.py", - "start_line": 143, - "end_line": 165, - "tier": "TIER_2", - "complexity": 4, + "start_line": 204, + "end_line": 224, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Run is cancelled. Updated run returned.", - "PRE": "User has translate.job.execute permission. Run is in RUNNING state.", - "PURPOSE": "Cancel a running translation.", - "SIDE_EFFECT": "Updates TranslationRun status to CANCELLED in DB." + "POST": "Run is cancelled.", + "PRE": "User has translate.job.execute permission.", + "PURPOSE": "Cancel a running translation." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "cancel_run", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region cancel_run [C:4] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Cancel a running translation.\n# @PRE User has translate.job.execute permission. Run is in RUNNING state.\n# @POST Run is cancelled. Updated run returned.\n# @SIDE_EFFECT Updates TranslationRun status to CANCELLED in DB.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.post(\"/runs/{run_id}/cancel\")\nasync def cancel_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Cancel a running translation.\"\"\"\n log.reason(\"Cancel run\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.cancel_run(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion cancel_run\n" + "body": "# #region cancel_run [TYPE Function]\n# @BRIEF Cancel a running translation.\n# @PRE: User has translate.job.execute permission.\n# @POST: Run is cancelled.\n@router.post(\"/runs/{run_id}/cancel\")\nasync def cancel_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"EXECUTE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Cancel a running translation.\"\"\"\n logger.reason(f\"cancel_run — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n run = orch.cancel_run(run_id)\n return _run_to_response(run)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion cancel_run\n" }, { "contract_id": "get_run_history", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_routes.py", - "start_line": 168, - "end_line": 189, - "tier": "TIER_2", - "complexity": 3, + "start_line": 227, + "end_line": 249, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns list of runs.", + "PRE": "User has translate.history.view permission.", "PURPOSE": "Get run history for a translation job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_run_history", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_run_history [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get run history for a translation job.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/jobs/{job_id}/runs\")\nasync def get_run_history(\n job_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get run history for a translation job.\"\"\"\n log.reason(\"Get run history\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n total, runs = orch.get_run_history(job_id, page=page, page_size=page_size)\n return {\"items\": runs, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_history\n" + "body": "# #region get_run_history [TYPE Function]\n# @BRIEF Get run history for a translation job.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns list of runs.\n@router.get(\"/jobs/{job_id}/runs\")\nasync def get_run_history(\n job_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get run history for a translation job.\"\"\"\n logger.reason(f\"get_run_history — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n total, runs = orch.get_run_history(job_id, page=page, page_size=page_size)\n return {\"items\": runs, \"total\": total, \"page\": page, \"page_size\": page_size}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_history\n" }, { "contract_id": "get_run_status", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_routes.py", - "start_line": 192, - "end_line": 210, - "tier": "TIER_2", - "complexity": 3, + "start_line": 252, + "end_line": 271, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns run details with statistics.", + "PRE": "User has translate.history.view permission.", "PURPOSE": "Get status and statistics for a translation run." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_run_status", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_run_status [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get status and statistics for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/runs/{run_id}\")\nasync def get_run_status(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get status and statistics for a translation run.\"\"\"\n log.reason(\"Get run status\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_status(run_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_status\n" + "body": "# #region get_run_status [TYPE Function]\n# @BRIEF Get status and statistics for a translation run.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns run details with statistics.\n@router.get(\"/runs/{run_id}\")\nasync def get_run_status(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get status and statistics for a translation run.\"\"\"\n logger.reason(f\"get_run_status — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_status(run_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_status\n" }, { "contract_id": "get_run_records", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_routes.py", - "start_line": 213, - "end_line": 234, - "tier": "TIER_2", - "complexity": 3, + "start_line": 274, + "end_line": 296, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns paginated records.", + "PRE": "User has translate.history.view permission.", "PURPOSE": "Get paginated records for a translation run." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_run_records", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_run_records [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get paginated records for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n@router.get(\"/runs/{run_id}/records\")\nasync def get_run_records(\n run_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n status: Optional[str] = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get paginated records for a translation run.\"\"\"\n log.reason(\"Get run records\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_records(run_id, page=page, page_size=page_size, status_filter=status)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_records\n" + "body": "# #region get_run_records [TYPE Function]\n# @BRIEF Get paginated records for a translation run.\n# @PRE: User has translate.history.view permission.\n# @POST: Returns paginated records.\n@router.get(\"/runs/{run_id}/records\")\nasync def get_run_records(\n run_id: str,\n page: int = Query(1, ge=1),\n page_size: int = Query(50, ge=1, le=500),\n status: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.history\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get paginated records for a translation run.\"\"\"\n logger.reason(f\"get_run_records — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n orch = TranslationOrchestrator(db, config_manager, current_user.username)\n return orch.get_run_records(run_id, page=page, page_size=page_size, status_filter=status)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_run_records\n" }, { "contract_id": "get_batches", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_run_routes.py", - "start_line": 241, - "end_line": 279, - "tier": "TIER_2", - "complexity": 3, + "start_line": 303, + "end_line": 342, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, + "POST": "Returns list of batches.", + "PRE": "User has translate.job.view permission.", "PURPOSE": "Get batches for a translation run." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_batches", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationBatch", - "target_ref": "[TranslationBatch]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_batches [C:3] [TYPE Function] [SEMANTICS api,translate,runs]\n# @BRIEF Get batches for a translation run.\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n@router.get(\"/runs/{run_id}/batches\")\nasync def get_batches(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get batches for a translation run.\"\"\"\n log.reason(\"Get batches\", payload={\"run_id\": run_id, \"user\": current_user.username})\n try:\n from ....models.translate import TranslationBatch\n batches = (\n db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .order_by(TranslationBatch.batch_index.asc())\n .all()\n )\n return [\n {\n \"id\": b.id,\n \"run_id\": b.run_id,\n \"batch_index\": b.batch_index,\n \"status\": b.status,\n \"total_records\": b.total_records or 0,\n \"successful_records\": b.successful_records or 0,\n \"failed_records\": b.failed_records or 0,\n \"started_at\": b.started_at.isoformat() if b.started_at else None,\n \"completed_at\": b.completed_at.isoformat() if b.completed_at else None,\n \"created_at\": b.created_at.isoformat() if b.created_at else None,\n }\n for b in batches\n ]\n except Exception as e:\n log.explore(\"Get batches error\", error=str(e))\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_batches\n" + "body": "# #region get_batches [TYPE Function]\n# @BRIEF Get batches for a translation run.\n# @PRE: User has translate.job.view permission.\n# @POST: Returns list of batches.\n@router.get(\"/runs/{run_id}/batches\")\nasync def get_batches(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.job\", \"VIEW\")),\n db: Session = Depends(get_db),\n):\n \"\"\"Get batches for a translation run.\"\"\"\n logger.reason(f\"get_batches — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n from ....models.translate import TranslationBatch\n batches = (\n db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .order_by(TranslationBatch.batch_index.asc())\n .all()\n )\n return [\n {\n \"id\": b.id,\n \"run_id\": b.run_id,\n \"batch_index\": b.batch_index,\n \"status\": b.status,\n \"total_records\": b.total_records or 0,\n \"successful_records\": b.successful_records or 0,\n \"failed_records\": b.failed_records or 0,\n \"started_at\": b.started_at.isoformat() if b.started_at else None,\n \"completed_at\": b.completed_at.isoformat() if b.completed_at else None,\n \"created_at\": b.created_at.isoformat() if b.created_at else None,\n }\n for b in batches\n ]\n except Exception as e:\n logger.explore(\"get_batches failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion get_batches\n" }, { "contract_id": "TranslateScheduleRoutesModule", "contract_type": "Module", "file_path": "backend/src/api/routes/translate/_schedule_routes.py", "start_line": 1, - "end_line": 248, + "end_line": 237, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": 4, - "LAYER": "UI", - "POST": "Schedule CRUD operations completed. APScheduler jobs registered/unregistered.", - "PRE": "ConfigManager and DB session initialized. User authenticated with translate.schedule permissions.", - "PURPOSE": "Translation Schedule management routes.", - "SIDE_EFFECT": "Creates/updates/deletes TranslationSchedule records in DB; registers/removes jobs in APScheduler." + "COMPLEXITY": 3, + "LAYER": "API", + "PURPOSE": "Translation Schedule management routes." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "TranslateScheduleRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationScheduler", - "target_ref": "[TranslationScheduler]" - }, - { - "source_id": "TranslateScheduleRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "ConfigManager", - "target_ref": "[ConfigManager]" - }, - { - "source_id": "TranslateScheduleRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_current_user", - "target_ref": "[get_current_user]" - }, - { - "source_id": "TranslateScheduleRoutesModule", - "relation_type": "DEPENDS_ON", - "target_id": "get_db", - "target_ref": "[get_db]" + "code": "missing_required_tag", + "tag": "RELATION", + "message": "@RELATION is required for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslateScheduleRoutesModule [C:4] [TYPE Module] [SEMANTICS api,routes,translate,schedule]\n# @BRIEF Translation Schedule management routes.\n# @LAYER UI\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_current_user]\n# @RELATION DEPENDS_ON -> [get_db]\n# @PRE ConfigManager and DB session initialized. User authenticated with translate.schedule permissions.\n# @POST Schedule CRUD operations completed. APScheduler jobs registered/unregistered.\n# @SIDE_EFFECT Creates/updates/deletes TranslationSchedule records in DB; registers/removes jobs in APScheduler.\n\nfrom fastapi import APIRouter, Depends, HTTPException, status, Query\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\n\nfrom ....core.database import get_db\nfrom ....core.cot_logger import MarkerLogger\nfrom ....schemas.auth import User\n\nlog = MarkerLogger(\"TranslateScheduleRoutes\")\nfrom ....dependencies import get_current_user, has_permission, get_config_manager\nfrom ....core.config_manager import ConfigManager\nfrom ....plugins.translate.scheduler import TranslationScheduler\nfrom ....schemas.translate import ScheduleConfig, ScheduleResponse\n\nfrom ._router import router\n\n\n# ============================================================\n# Schedule\n# ============================================================\n\n# #region get_schedule [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Get the schedule configuration for a translation job.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.get(\"/jobs/{job_id}/schedule\")\nasync def get_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get schedule for a translation job.\"\"\"\n log.reason(\"Get schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"next_run_at\": schedule.next_run_at.isoformat() if schedule.next_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_schedule\n\n\n# #region set_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Set or update the schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Job exists.\n# @POST Schedule created or updated. APScheduler job registered.\n# @SIDE_EFFECT Creates/updates TranslationSchedule record in DB; registers job in APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.put(\"/jobs/{job_id}/schedule\")\nasync def set_schedule(\n job_id: str,\n payload: ScheduleConfig,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Set or update schedule for a translation job.\"\"\"\n log.reason(\"Set schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n # Check if schedule already exists\n from ....models.translate import TranslationSchedule\n existing = db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if existing:\n schedule = scheduler.update_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n )\n else:\n schedule = scheduler.create_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n )\n # Register with APScheduler via SchedulerService\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n )\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion set_schedule\n\n\n# #region enable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Enable a schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is enabled. APScheduler job registered.\n# @SIDE_EFFECT Updates TranslationSchedule is_active to True in DB; registers job in APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.post(\"/jobs/{job_id}/schedule/enable\")\nasync def enable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Enable a schedule for a translation job.\"\"\"\n log.reason(\"Enable schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.set_schedule_active(job_id, is_active=True)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n )\n return {\"status\": \"enabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion enable_schedule\n\n\n# #region disable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Disable a schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is disabled. APScheduler job removed.\n# @SIDE_EFFECT Updates TranslationSchedule is_active to False in DB; removes job from APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.post(\"/jobs/{job_id}/schedule/disable\")\nasync def disable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Disable a schedule for a translation job.\"\"\"\n log.reason(\"Disable schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.set_schedule_active(job_id, is_active=False)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return {\"status\": \"disabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion disable_schedule\n\n\n# #region delete_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Delete the schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is removed. APScheduler job removed.\n# @SIDE_EFFECT Deletes TranslationSchedule record from DB; removes job from APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.delete(\"/jobs/{job_id}/schedule\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete schedule for a translation job.\"\"\"\n log.reason(f\"Delete schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.delete_schedule(job_id)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_schedule\n\n\n# #region get_next_executions [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Preview next N execution times for a job's schedule.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.get(\"/jobs/{job_id}/schedule/next-executions\")\nasync def get_next_executions(\n job_id: str,\n n: int = Query(3, ge=1, le=10),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview next N executions for a job's schedule.\"\"\"\n log.reason(f\"Get next executions\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n next_times = TranslationScheduler.get_next_executions(\n schedule.cron_expression, schedule.timezone, n=n\n )\n return {\n \"job_id\": job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"next_executions\": next_times,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_next_executions\n\n# #endregion TranslateScheduleRoutesModule\n" + "body": "# #region TranslateScheduleRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, schedule, search]\n# @BRIEF Translation Schedule management routes.\n# @LAYER: API\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.config_manager import ConfigManager\nfrom ....core.database import get_db\nfrom ....core.logger import logger\nfrom ....dependencies import get_config_manager, get_current_user, has_permission\nfrom ....plugins.translate.scheduler import TranslationScheduler\nfrom ....schemas.auth import User\nfrom ....schemas.translate import ScheduleConfig\nfrom ._router import router\n\n# ============================================================\n# Schedule\n# ============================================================\n\n# #region get_schedule [TYPE Function]\n# @BRIEF Get the schedule for a translation job.\n# @PRE: User has translate.schedule.view permission.\n# @POST: Returns the schedule configuration.\n@router.get(\"/jobs/{job_id}/schedule\")\nasync def get_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get schedule for a translation job.\"\"\"\n logger.reason(f\"get_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"execution_mode\": schedule.execution_mode,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"next_run_at\": schedule.next_run_at.isoformat() if schedule.next_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_schedule\n\n\n# #region set_schedule [TYPE Function]\n# @BRIEF Set or update the schedule for a translation job.\n# @PRE: User has translate.schedule.manage permission.\n# @POST: Schedule is created or updated.\n@router.put(\"/jobs/{job_id}/schedule\")\nasync def set_schedule(\n job_id: str,\n payload: ScheduleConfig,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Set or update schedule for a translation job.\"\"\"\n logger.reason(f\"set_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n # Check if schedule already exists\n from ....models.translate import TranslationSchedule\n existing = db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n execution_mode = getattr(payload, 'execution_mode', 'full')\n if existing:\n schedule = scheduler.update_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone_str=payload.timezone,\n is_active=payload.is_active,\n execution_mode=execution_mode,\n )\n else:\n schedule = scheduler.create_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n execution_mode=execution_mode,\n )\n # Register with APScheduler via SchedulerService\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n execution_mode=schedule.execution_mode,\n )\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"execution_mode\": schedule.execution_mode,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion set_schedule\n\n\n# #region enable_schedule [TYPE Function]\n# @BRIEF Enable a schedule for a translation job.\n# @PRE: User has translate.schedule.manage permission.\n# @POST: Schedule is enabled.\n@router.post(\"/jobs/{job_id}/schedule/enable\")\nasync def enable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Enable a schedule for a translation job.\"\"\"\n logger.reason(f\"enable_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.set_schedule_active(job_id, is_active=True)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n execution_mode=schedule.execution_mode,\n )\n return {\"status\": \"enabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion enable_schedule\n\n\n# #region disable_schedule [TYPE Function]\n# @BRIEF Disable a schedule for a translation job.\n# @PRE: User has translate.schedule.manage permission.\n# @POST: Schedule is disabled.\n@router.post(\"/jobs/{job_id}/schedule/disable\")\nasync def disable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Disable a schedule for a translation job.\"\"\"\n logger.reason(f\"disable_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.set_schedule_active(job_id, is_active=False)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return {\"status\": \"disabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion disable_schedule\n\n\n# #region delete_schedule [TYPE Function]\n# @BRIEF Delete the schedule for a translation job.\n# @PRE: User has translate.schedule.manage permission.\n# @POST: Schedule is removed.\n@router.delete(\"/jobs/{job_id}/schedule\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete schedule for a translation job.\"\"\"\n logger.reason(f\"delete_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.delete_schedule(job_id)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_schedule\n\n\n# #region get_next_executions [TYPE Function]\n# @BRIEF Preview next N executions for a job's schedule.\n# @PRE: User has translate.schedule.view permission.\n# @POST: Returns next execution times.\n@router.get(\"/jobs/{job_id}/schedule/next-executions\")\nasync def get_next_executions(\n job_id: str,\n n: int = Query(3, ge=1, le=10),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview next N executions for a job's schedule.\"\"\"\n logger.reason(f\"get_next_executions — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n next_times = TranslationScheduler.get_next_executions(\n schedule.cron_expression, schedule.timezone, n=n\n )\n return {\n \"job_id\": job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"next_executions\": next_times,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_next_executions\n\n# #endregion TranslateScheduleRoutesModule\n" }, { "contract_id": "get_schedule", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_schedule_routes.py", - "start_line": 33, - "end_line": 63, - "tier": "TIER_2", - "complexity": 3, + "start_line": 21, + "end_line": 53, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Get the schedule configuration for a translation job." + "POST": "Returns the schedule configuration.", + "PRE": "User has translate.schedule.view permission.", + "PURPOSE": "Get the schedule for a translation job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_schedule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationScheduler", - "target_ref": "[TranslationScheduler]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_schedule [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Get the schedule configuration for a translation job.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.get(\"/jobs/{job_id}/schedule\")\nasync def get_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get schedule for a translation job.\"\"\"\n log.reason(\"Get schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"next_run_at\": schedule.next_run_at.isoformat() if schedule.next_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_schedule\n" + "body": "# #region get_schedule [TYPE Function]\n# @BRIEF Get the schedule for a translation job.\n# @PRE: User has translate.schedule.view permission.\n# @POST: Returns the schedule configuration.\n@router.get(\"/jobs/{job_id}/schedule\")\nasync def get_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Get schedule for a translation job.\"\"\"\n logger.reason(f\"get_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"execution_mode\": schedule.execution_mode,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"next_run_at\": schedule.next_run_at.isoformat() if schedule.next_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_schedule\n" }, { "contract_id": "set_schedule", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_schedule_routes.py", - "start_line": 66, - "end_line": 126, - "tier": "TIER_2", - "complexity": 4, + "start_line": 56, + "end_line": 119, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Schedule created or updated. APScheduler job registered.", - "PRE": "User has translate.schedule.manage permission. Job exists.", - "PURPOSE": "Set or update the schedule for a translation job.", - "SIDE_EFFECT": "Creates/updates TranslationSchedule record in DB; registers job in APScheduler." + "POST": "Schedule is created or updated.", + "PRE": "User has translate.schedule.manage permission.", + "PURPOSE": "Set or update the schedule for a translation job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "set_schedule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationScheduler", - "target_ref": "[TranslationScheduler]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region set_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Set or update the schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Job exists.\n# @POST Schedule created or updated. APScheduler job registered.\n# @SIDE_EFFECT Creates/updates TranslationSchedule record in DB; registers job in APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.put(\"/jobs/{job_id}/schedule\")\nasync def set_schedule(\n job_id: str,\n payload: ScheduleConfig,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Set or update schedule for a translation job.\"\"\"\n log.reason(\"Set schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n # Check if schedule already exists\n from ....models.translate import TranslationSchedule\n existing = db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if existing:\n schedule = scheduler.update_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n )\n else:\n schedule = scheduler.create_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n )\n # Register with APScheduler via SchedulerService\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n )\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion set_schedule\n" + "body": "# #region set_schedule [TYPE Function]\n# @BRIEF Set or update the schedule for a translation job.\n# @PRE: User has translate.schedule.manage permission.\n# @POST: Schedule is created or updated.\n@router.put(\"/jobs/{job_id}/schedule\")\nasync def set_schedule(\n job_id: str,\n payload: ScheduleConfig,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Set or update schedule for a translation job.\"\"\"\n logger.reason(f\"set_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n # Check if schedule already exists\n from ....models.translate import TranslationSchedule\n existing = db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n execution_mode = getattr(payload, 'execution_mode', 'full')\n if existing:\n schedule = scheduler.update_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone_str=payload.timezone,\n is_active=payload.is_active,\n execution_mode=execution_mode,\n )\n else:\n schedule = scheduler.create_schedule(\n job_id,\n cron_expression=payload.cron_expression,\n timezone=payload.timezone,\n is_active=payload.is_active,\n execution_mode=execution_mode,\n )\n # Register with APScheduler via SchedulerService\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n execution_mode=schedule.execution_mode,\n )\n return {\n \"id\": schedule.id,\n \"job_id\": schedule.job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n \"execution_mode\": schedule.execution_mode,\n \"last_run_at\": schedule.last_run_at.isoformat() if schedule.last_run_at else None,\n \"created_by\": schedule.created_by,\n \"created_at\": schedule.created_at.isoformat() if schedule.created_at else None,\n \"updated_at\": schedule.updated_at.isoformat() if schedule.updated_at else None,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion set_schedule\n" }, { "contract_id": "enable_schedule", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_schedule_routes.py", - "start_line": 129, - "end_line": 159, - "tier": "TIER_2", - "complexity": 4, + "start_line": 122, + "end_line": 151, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Schedule is enabled. APScheduler job registered.", - "PRE": "User has translate.schedule.manage permission. Schedule exists.", - "PURPOSE": "Enable a schedule for a translation job.", - "SIDE_EFFECT": "Updates TranslationSchedule is_active to True in DB; registers job in APScheduler." + "POST": "Schedule is enabled.", + "PRE": "User has translate.schedule.manage permission.", + "PURPOSE": "Enable a schedule for a translation job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "enable_schedule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationScheduler", - "target_ref": "[TranslationScheduler]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region enable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Enable a schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is enabled. APScheduler job registered.\n# @SIDE_EFFECT Updates TranslationSchedule is_active to True in DB; registers job in APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.post(\"/jobs/{job_id}/schedule/enable\")\nasync def enable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Enable a schedule for a translation job.\"\"\"\n log.reason(\"Enable schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.set_schedule_active(job_id, is_active=True)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n )\n return {\"status\": \"enabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion enable_schedule\n" + "body": "# #region enable_schedule [TYPE Function]\n# @BRIEF Enable a schedule for a translation job.\n# @PRE: User has translate.schedule.manage permission.\n# @POST: Schedule is enabled.\n@router.post(\"/jobs/{job_id}/schedule/enable\")\nasync def enable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Enable a schedule for a translation job.\"\"\"\n logger.reason(f\"enable_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.set_schedule_active(job_id, is_active=True)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.add_translation_job(\n schedule_id=schedule.id,\n job_id=job_id,\n cron_expression=schedule.cron_expression,\n timezone=schedule.timezone,\n execution_mode=schedule.execution_mode,\n )\n return {\"status\": \"enabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion enable_schedule\n" }, { "contract_id": "disable_schedule", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_schedule_routes.py", - "start_line": 162, - "end_line": 187, - "tier": "TIER_2", - "complexity": 4, + "start_line": 154, + "end_line": 177, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Schedule is disabled. APScheduler job removed.", - "PRE": "User has translate.schedule.manage permission. Schedule exists.", - "PURPOSE": "Disable a schedule for a translation job.", - "SIDE_EFFECT": "Updates TranslationSchedule is_active to False in DB; removes job from APScheduler." + "POST": "Schedule is disabled.", + "PRE": "User has translate.schedule.manage permission.", + "PURPOSE": "Disable a schedule for a translation job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "disable_schedule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationScheduler", - "target_ref": "[TranslationScheduler]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region disable_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Disable a schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is disabled. APScheduler job removed.\n# @SIDE_EFFECT Updates TranslationSchedule is_active to False in DB; removes job from APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.post(\"/jobs/{job_id}/schedule/disable\")\nasync def disable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Disable a schedule for a translation job.\"\"\"\n log.reason(\"Disable schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.set_schedule_active(job_id, is_active=False)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return {\"status\": \"disabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion disable_schedule\n" + "body": "# #region disable_schedule [TYPE Function]\n# @BRIEF Disable a schedule for a translation job.\n# @PRE: User has translate.schedule.manage permission.\n# @POST: Schedule is disabled.\n@router.post(\"/jobs/{job_id}/schedule/disable\")\nasync def disable_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Disable a schedule for a translation job.\"\"\"\n logger.reason(f\"disable_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.set_schedule_active(job_id, is_active=False)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return {\"status\": \"disabled\", \"job_id\": job_id}\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion disable_schedule\n" }, { "contract_id": "delete_schedule", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_schedule_routes.py", - "start_line": 190, - "end_line": 215, - "tier": "TIER_2", - "complexity": 4, + "start_line": 180, + "end_line": 203, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, - "POST": "Schedule is removed. APScheduler job removed.", - "PRE": "User has translate.schedule.manage permission. Schedule exists.", - "PURPOSE": "Delete the schedule for a translation job.", - "SIDE_EFFECT": "Deletes TranslationSchedule record from DB; removes job from APScheduler." + "POST": "Schedule is removed.", + "PRE": "User has translate.schedule.manage permission.", + "PURPOSE": "Delete the schedule for a translation job." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "delete_schedule", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationScheduler", - "target_ref": "[TranslationScheduler]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region delete_schedule [C:4] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Delete the schedule for a translation job.\n# @PRE User has translate.schedule.manage permission. Schedule exists.\n# @POST Schedule is removed. APScheduler job removed.\n# @SIDE_EFFECT Deletes TranslationSchedule record from DB; removes job from APScheduler.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.delete(\"/jobs/{job_id}/schedule\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete schedule for a translation job.\"\"\"\n log.reason(f\"Delete schedule\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.delete_schedule(job_id)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_schedule\n" + "body": "# #region delete_schedule [TYPE Function]\n# @BRIEF Delete the schedule for a translation job.\n# @PRE: User has translate.schedule.manage permission.\n# @POST: Schedule is removed.\n@router.delete(\"/jobs/{job_id}/schedule\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_schedule(\n job_id: str,\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"MANAGE\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Delete schedule for a translation job.\"\"\"\n logger.reason(f\"delete_schedule — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n scheduler.delete_schedule(job_id)\n from ....dependencies import get_scheduler_service\n sched_svc = get_scheduler_service()\n sched_svc.remove_translation_job(schedule_id=job_id)\n return None\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_schedule\n" }, { "contract_id": "get_next_executions", "contract_type": "Function", "file_path": "backend/src/api/routes/translate/_schedule_routes.py", - "start_line": 218, - "end_line": 246, - "tier": "TIER_2", - "complexity": 3, + "start_line": 206, + "end_line": 235, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Preview next N execution times for a job's schedule." + "POST": "Returns next execution times.", + "PRE": "User has translate.schedule.view permission.", + "PURPOSE": "Preview next N executions for a job's schedule." }, - "relations": [ + "relations": [], + "schema_warnings": [ { - "source_id": "get_next_executions", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationScheduler", - "target_ref": "[TranslationScheduler]" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region get_next_executions [C:3] [TYPE Function] [SEMANTICS api,translate,schedule]\n# @BRIEF Preview next N execution times for a job's schedule.\n# @RELATION DEPENDS_ON -> [TranslationScheduler]\n@router.get(\"/jobs/{job_id}/schedule/next-executions\")\nasync def get_next_executions(\n job_id: str,\n n: int = Query(3, ge=1, le=10),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview next N executions for a job's schedule.\"\"\"\n log.reason(f\"Get next executions\", payload={\"job_id\": job_id, \"user\": current_user.username})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n next_times = TranslationScheduler.get_next_executions(\n schedule.cron_expression, schedule.timezone, n=n\n )\n return {\n \"job_id\": job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"next_executions\": next_times,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_next_executions\n" + "body": "# #region get_next_executions [TYPE Function]\n# @BRIEF Preview next N executions for a job's schedule.\n# @PRE: User has translate.schedule.view permission.\n# @POST: Returns next execution times.\n@router.get(\"/jobs/{job_id}/schedule/next-executions\")\nasync def get_next_executions(\n job_id: str,\n n: int = Query(3, ge=1, le=10),\n current_user: User = Depends(get_current_user),\n _ = Depends(has_permission(\"translate.schedule\", \"VIEW\")),\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n \"\"\"Preview next N executions for a job's schedule.\"\"\"\n logger.reason(f\"get_next_executions — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\n try:\n scheduler = TranslationScheduler(db, config_manager, current_user.username)\n schedule = scheduler.get_schedule(job_id)\n next_times = TranslationScheduler.get_next_executions(\n schedule.cron_expression, schedule.timezone, n=n\n )\n return {\n \"job_id\": job_id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"next_executions\": next_times,\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_next_executions\n" }, { "contract_id": "AppModule", "contract_type": "Module", "file_path": "backend/src/app.py", "start_line": 1, - "end_line": 514, + "end_line": 434, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "[HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]", "INVARIANT": "All WebSocket connections must be properly cleaned up on disconnect.", "LAYER": "UI (API)", "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.", - "SEMANTICS": [ - "app", - "main", - "entrypoint", - "fastapi" - ], "SIDE_EFFECT": "Starts background scheduler and binds network ports for HTTP/WS traffic." }, "relations": [ { "source_id": "AppModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AppDependencies", "target_ref": "[AppDependencies]" }, { "source_id": "AppModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ApiRoutesModule", "target_ref": "[ApiRoutesModule]" } @@ -26572,64 +25173,22 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:AppModule:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: app, main, entrypoint, fastapi\n# @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.\n# @LAYER: UI (API)\n# @RELATION: [DEPENDS_ON] ->[AppDependencies]\n# @RELATION: [DEPENDS_ON] ->[ApiRoutesModule]\n# @INVARIANT: Only one FastAPI app instance exists per process.\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\n\nfrom fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException\nfrom starlette.middleware.sessions import SessionMiddleware\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.staticfiles import StaticFiles\nfrom fastapi.responses import FileResponse\nimport asyncio\n\nfrom .dependencies import get_task_manager, get_scheduler_service\nfrom .core.encryption_key import ensure_encryption_key\nfrom .core.utils.network import NetworkError\nfrom .core.logger import logger, belief_scope\nfrom .core.cot_logger import MarkerLogger\n\nstartup_log = MarkerLogger(\"Startup\")\nfrom .core.database import AuthSessionLocal\nfrom .core.auth.security import get_password_hash\nfrom .models.auth import User, Role\nfrom .api.routes import (\n plugins,\n tasks,\n settings,\n environments,\n mappings,\n migration,\n connections,\n git,\n storage,\n admin,\n llm,\n dashboards,\n datasets,\n reports,\n assistant,\n clean_release,\n clean_release_v2,\n profile,\n health,\n dataset_review,\n translate,\n)\nfrom .api import auth\n\n# [DEF:FastAPI_App:Global]\n# @COMPLEXITY: 3\n# @SEMANTICS: app, fastapi, instance, route-registry\n# @PURPOSE: 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# [/DEF:FastAPI_App:Global]\n\n\n# [DEF:ensure_initial_admin_user:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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\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\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\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\n\n# [/DEF:ensure_initial_admin_user:Function]\n\n\n# [DEF:startup_event:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 startup_log.reason(\"Enter startup event\")\n ensure_encryption_key()\n ensure_initial_admin_user()\n scheduler = get_scheduler_service()\n startup_log.reflect(\"Application startup completed\")\n scheduler.start()\n\n\n# [/DEF:startup_event:Function]\n\n\n# [DEF:shutdown_event:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 with belief_scope(\"shutdown_event\"):\n scheduler = get_scheduler_service()\n scheduler.stop()\n\n\n# [/DEF:shutdown_event:Function]\n\n# [DEF:app_middleware:Block]\n# @PURPOSE: Configure application-wide middleware (Session, CORS, TraceContext).\n# Configure Session Middleware (required by Authlib for OAuth2 flow)\nfrom .core.auth.config import auth_config\n\napp.add_middleware(SessionMiddleware, secret_key=auth_config.SECRET_KEY)\n\n# Configure CORS\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"], # Adjust this in production\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\n# Configure Trace Context Middleware (seeds trace_id per request)\nfrom .core.middleware.trace import TraceContextMiddleware\n\napp.add_middleware(TraceContextMiddleware)\n# [/DEF:app_middleware:Block]\n\n\n# [DEF:network_error_handler:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Global exception handler for NetworkError.\n# @PRE: request is a FastAPI Request object.\n# @POST: Returns 503 HTTP Exception.\n# @PARAM: request (Request) - The incoming request object.\n# @PARAM: exc (NetworkError) - The exception instance.\neh_log = MarkerLogger(\"ExceptionHandler\")\n\n\n@app.exception_handler(NetworkError)\nasync def network_error_handler(request: Request, exc: NetworkError):\n with belief_scope(\"network_error_handler\"):\n eh_log.explore(\"Unhandled NetworkError in request\", error=str(exc), payload={\"url\": str(request.url)})\n return HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n\n\n# [/DEF:network_error_handler:Function]\n\n\n# [DEF:log_requests:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: request (Request) - The incoming request object.\n# @PARAM: call_next (Callable) - The next middleware or route handler.\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\n if not is_polling:\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n\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\n\n# [/DEF:log_requests:Function]\n\n# [DEF:API_Routes:Block]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:API_Routes:Block]\n\n\n# [DEF:api.include_routers:Action]\n# @COMPLEXITY: 1\n# @PURPOSE: Registers all API routers with the FastAPI application.\n# @LAYER: API\n# @SEMANTICS: routes, registration, api\n# [/DEF:api.include_routers:Action]\n\n\n# [DEF:websocket_endpoint:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: 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\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 with belief_scope(\"websocket_endpoint\", f\"task_id={task_id}\"):\n await websocket.accept()\n\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\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\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\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\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\n return True\n\n try:\n logger.reason(\n \"Starting task log stream replay and live forwarding\",\n extra={\"task_id\": task_id},\n )\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\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\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\n while True:\n log_entry = await queue.get()\n\n if not matches_filters(log_entry):\n continue\n\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\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\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\n\n# [/DEF:websocket_endpoint:Function]\n\n# [DEF:StaticFiles:Mount]\n# @COMPLEXITY: 1\n# @SEMANTICS: static, frontend, spa\n# @PURPOSE: 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\n # [DEF:serve_spa:Function]\n # @COMPLEXITY: 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\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\n # [/DEF:serve_spa:Function]\nelse:\n # [DEF:read_root:Function]\n # @COMPLEXITY: 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\n # [/DEF:read_root:Function]\n# [/DEF:StaticFiles:Mount]\n# [/DEF:AppModule:Module]\n" + "anchor_syntax": "region", + "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 -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @INVARIANT: Only one FastAPI app instance exists per process.\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]\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" }, { "contract_id": "FastAPI_App", "contract_type": "Global", "file_path": "backend/src/app.py", - "start_line": 63, - "end_line": 74, + "start_line": 58, + "end_line": 67, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Canonical FastAPI application instance for route, middleware, and websocket registration.", - "SEMANTICS": [ - "app", - "fastapi", - "instance", - "route-registry" - ] + "COMPLEXITY": 3, + "PURPOSE": "Canonical FastAPI application instance for route, middleware, and websocket registration." }, "relations": [ { @@ -26699,29 +25258,6 @@ "contract_type": "Global" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Global'", - "detail": { - "actual_type": "Global", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Global' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Global" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -26732,19 +25268,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FastAPI_App:Global]\n# @COMPLEXITY: 3\n# @SEMANTICS: app, fastapi, instance, route-registry\n# @PURPOSE: 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# [/DEF:FastAPI_App:Global]\n" + "anchor_syntax": "region", + "body": "# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry]\n# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION BINDS_TO -> [API_Routes]\napp = FastAPI(\n title=\"Superset Tools API\",\n description=\"API for managing Superset automation tools and plugins.\",\n version=\"1.0.0\",\n)\n# #endregion FastAPI_App\n" }, { "contract_id": "ensure_initial_admin_user", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 77, - "end_line": 130, + "start_line": 68, + "end_line": 115, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Ensures initial admin user exists when bootstrap env flags are enabled." }, "relations": [ @@ -26756,19 +25292,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ensure_initial_admin_user:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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\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\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\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\n\n# [/DEF:ensure_initial_admin_user:Function]\n" + "anchor_syntax": "region", + "body": "# #region ensure_initial_admin_user [C:3] [TYPE Function]\n# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled.\n# @RELATION DEPENDS_ON -> AuthRepository\ndef ensure_initial_admin_user() -> None:\n raw_flag = os.getenv(\"INITIAL_ADMIN_CREATE\", \"false\").strip().lower()\n if raw_flag not in {\"1\", \"true\", \"yes\", \"on\"}:\n return\n username = os.getenv(\"INITIAL_ADMIN_USERNAME\", \"\").strip()\n password = os.getenv(\"INITIAL_ADMIN_PASSWORD\", \"\").strip()\n if not username or not password:\n logger.warning(\n \"INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap.\"\n )\n return\n 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" }, { "contract_id": "startup_event", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 133, - "end_line": 150, + "start_line": 116, + "end_line": 130, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Scheduler is started.", "PRE": "None.", "PURPOSE": "Handles application startup tasks, such as starting the scheduler." @@ -26776,7 +25312,7 @@ "relations": [ { "source_id": "startup_event", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "AppDependencies", "target_ref": "[AppDependencies]" } @@ -26799,39 +25335,21 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:startup_event:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 startup_log.reason(\"Enter startup event\")\n ensure_encryption_key()\n ensure_initial_admin_user()\n scheduler = get_scheduler_service()\n startup_log.reflect(\"Application startup completed\")\n scheduler.start()\n\n\n# [/DEF:startup_event:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "shutdown_event", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 153, - "end_line": 167, + "start_line": 131, + "end_line": 143, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Scheduler is stopped.", "PRE": "None.", "PURPOSE": "Handles application shutdown tasks, such as stopping the scheduler." @@ -26839,7 +25357,7 @@ "relations": [ { "source_id": "shutdown_event", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "AppDependencies", "target_ref": "[AppDependencies]" } @@ -26862,39 +25380,21 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:shutdown_event:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 with belief_scope(\"shutdown_event\"):\n scheduler = get_scheduler_service()\n scheduler.stop()\n\n\n# [/DEF:shutdown_event:Function]\n" + "anchor_syntax": "region", + "body": "# #region shutdown_event [C:3] [TYPE Function]\n# @BRIEF Handles application shutdown tasks, such as stopping the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE: None.\n# @POST: Scheduler is stopped.\n# Shutdown event\n@app.on_event(\"shutdown\")\nasync def shutdown_event():\n seed_trace_id()\n with belief_scope(\"shutdown_event\"):\n scheduler = get_scheduler_service()\n scheduler.stop()\n# #endregion shutdown_event\n" }, { "contract_id": "app_middleware", "contract_type": "Block", "file_path": "backend/src/app.py", - "start_line": 169, - "end_line": 189, + "start_line": 144, + "end_line": 158, "tier": "TIER_1", "complexity": 1, "metadata": { - "PURPOSE": "Configure application-wide middleware (Session, CORS, TraceContext)." + "PURPOSE": "Configure application-wide middleware (Session, CORS)." }, "relations": [], "schema_warnings": [ @@ -26908,32 +25408,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:app_middleware:Block]\n# @PURPOSE: Configure application-wide middleware (Session, CORS, TraceContext).\n# Configure Session Middleware (required by Authlib for OAuth2 flow)\nfrom .core.auth.config import auth_config\n\napp.add_middleware(SessionMiddleware, secret_key=auth_config.SECRET_KEY)\n\n# Configure CORS\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"], # Adjust this in production\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\n# Configure Trace Context Middleware (seeds trace_id per request)\nfrom .core.middleware.trace import TraceContextMiddleware\n\napp.add_middleware(TraceContextMiddleware)\n# [/DEF:app_middleware:Block]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "network_error_handler", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 192, - "end_line": 212, + "start_line": 159, + "end_line": 171, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PARAM": "exc (NetworkError) - The exception instance.", + "COMPLEXITY": 1, "POST": "Returns 503 HTTP Exception.", "PRE": "request is a FastAPI Request object.", "PURPOSE": "Global exception handler for NetworkError." }, "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", @@ -26962,20 +25455,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:network_error_handler:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Global exception handler for NetworkError.\n# @PRE: request is a FastAPI Request object.\n# @POST: Returns 503 HTTP Exception.\n# @PARAM: request (Request) - The incoming request object.\n# @PARAM: exc (NetworkError) - The exception instance.\neh_log = MarkerLogger(\"ExceptionHandler\")\n\n\n@app.exception_handler(NetworkError)\nasync def network_error_handler(request: Request, exc: NetworkError):\n with belief_scope(\"network_error_handler\"):\n eh_log.explore(\"Unhandled NetworkError in request\", error=str(exc), payload={\"url\": str(request.url)})\n return HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n\n\n# [/DEF:network_error_handler:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "log_requests", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 215, - "end_line": 247, + "start_line": 172, + "end_line": 197, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "call_next (Callable) - The next middleware or route handler.", + "COMPLEXITY": 3, "POST": "Logs request and response details.", "PRE": "request is a FastAPI Request object.", "PURPOSE": "Middleware to log incoming HTTP requests and their response status." @@ -26983,18 +25475,12 @@ "relations": [ { "source_id": "log_requests", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LoggerModule", "target_ref": "[LoggerModule]" } ], "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", @@ -27012,39 +25498,21 @@ "actual_complexity": 3, "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:log_requests:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @PARAM: request (Request) - The incoming request object.\n# @PARAM: call_next (Callable) - The next middleware or route handler.\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\n if not is_polling:\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n\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\n\n# [/DEF:log_requests:Function]\n" + "anchor_syntax": "region", + "body": "# #region log_requests [C:3] [TYPE Function]\n# @BRIEF Middleware to log incoming HTTP requests and their response status.\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE: request is a FastAPI Request object.\n# @POST: Logs request and response details.\n@app.middleware(\"http\")\nasync def log_requests(request: Request, call_next):\n with belief_scope(\"log_requests\"):\n # Avoid spamming logs for polling endpoints\n is_polling = request.url.path.endswith(\"/api/tasks\") and request.method == \"GET\"\n if not is_polling:\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n try:\n response = await call_next(request)\n if not is_polling:\n logger.info(\n f\"Response status: {response.status_code} for {request.url.path}\"\n )\n return response\n except NetworkError as e:\n logger.error(f\"Network error caught in middleware: {e}\")\n raise HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion log_requests\n" }, { "contract_id": "API_Routes", "contract_type": "Block", "file_path": "backend/src/app.py", - "start_line": 249, - "end_line": 288, + "start_line": 203, + "end_line": 241, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Register all FastAPI route groups exposed by the application entrypoint." }, "relations": [ @@ -27116,26 +25584,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:API_Routes:Block]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:API_Routes:Block]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "api.include_routers", "contract_type": "Action", "file_path": "backend/src/app.py", - "start_line": 291, - "end_line": 296, + "start_line": 242, + "end_line": 245, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "LAYER": "API", - "PURPOSE": "Registers all API routers with the FastAPI application.", - "SEMANTICS": [ - "routes", - "registration", - "api" - ] + "PURPOSE": "Registers all API routers with the FastAPI application." }, "relations": [], "schema_warnings": [ @@ -27213,44 +25676,21 @@ "actual_complexity": 1, "contract_type": "Action" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Action'", - "detail": { - "actual_type": "Action", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Action' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Action" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:api.include_routers:Action]\n# @COMPLEXITY: 1\n# @PURPOSE: Registers all API routers with the FastAPI application.\n# @LAYER: API\n# @SEMANTICS: routes, registration, api\n# [/DEF:api.include_routers:Action]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "websocket_endpoint", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 299, - "end_line": 460, + "start_line": 246, + "end_line": 389, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "[task_id: str, source: str, level: str] -> [JSON log entry objects]", "INVARIANT": "Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects.", "POST": "WebSocket connection is managed and logs are streamed until disconnect.", @@ -27263,13 +25703,13 @@ "relations": [ { "source_id": "websocket_endpoint", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "TaskManagerPackage", "target_ref": "[TaskManagerPackage]" }, { "source_id": "websocket_endpoint", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LoggerModule", "target_ref": "[LoggerModule]" } @@ -27312,63 +25752,22 @@ "actual_complexity": 5, "contract_type": "Function" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:websocket_endpoint:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: 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\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 with belief_scope(\"websocket_endpoint\", f\"task_id={task_id}\"):\n await websocket.accept()\n\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\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\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\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\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\n return True\n\n try:\n logger.reason(\n \"Starting task log stream replay and live forwarding\",\n extra={\"task_id\": task_id},\n )\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\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\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\n while True:\n log_entry = await queue.get()\n\n if not matches_filters(log_entry):\n continue\n\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\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\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\n\n# [/DEF:websocket_endpoint:Function]\n" + "anchor_syntax": "region", + "body": "# #region websocket_endpoint [C:5] [TYPE Function]\n# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @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" }, { "contract_id": "StaticFiles", "contract_type": "Mount", "file_path": "backend/src/app.py", - "start_line": 462, - "end_line": 513, + "start_line": 390, + "end_line": 433, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Mounts the frontend build directory to serve static assets.", - "SEMANTICS": [ - "static", - "frontend", - "spa" - ] + "COMPLEXITY": 1, + "PURPOSE": "Mounts the frontend build directory to serve static assets." }, "relations": [], "schema_warnings": [ @@ -27424,127 +25823,10 @@ "actual_complexity": 1, "contract_type": "Mount" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Mount'", - "detail": { - "actual_type": "Mount", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Mount' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Mount" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:StaticFiles:Mount]\n# @COMPLEXITY: 1\n# @SEMANTICS: static, frontend, spa\n# @PURPOSE: 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\n # [DEF:serve_spa:Function]\n # @COMPLEXITY: 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\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\n # [/DEF:serve_spa:Function]\nelse:\n # [DEF:read_root:Function]\n # @COMPLEXITY: 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\n # [/DEF:read_root:Function]\n# [/DEF:StaticFiles:Mount]\n" - }, - { - "contract_id": "serve_spa", - "contract_type": "Function", - "file_path": "backend/src/app.py", - "start_line": 472, - "end_line": 498, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns the requested file or index.html.", - "PRE": "frontend_path exists.", - "PURPOSE": "Serves the SPA frontend for any path not matched by API routes." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:serve_spa:Function]\n # @COMPLEXITY: 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\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\n # [/DEF:serve_spa:Function]\n" - }, - { - "contract_id": "read_root", - "contract_type": "Function", - "file_path": "backend/src/app.py", - "start_line": 500, - "end_line": 512, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns a JSON message indicating API status.", - "PRE": "None.", - "PURPOSE": "A simple root endpoint to confirm that the API is running when frontend is missing." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:read_root:Function]\n # @COMPLEXITY: 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\n # [/DEF:read_root:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "src.core", @@ -27587,15 +25869,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:src.core:Package]\n# @PURPOSE: Backend core services and infrastructure package root.\n# [/DEF:src.core:Package]\n" + "anchor_syntax": "region", + "body": "# #region src.core [TYPE Package] [SEMANTICS core, package, init]\n# @BRIEF Backend core services and infrastructure package root.\n# #endregion src.core\n" }, { "contract_id": "test_get_payload_preserves_legacy_sections", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_config_manager_compat.py", - "start_line": 14, - "end_line": 28, + "start_line": 11, + "end_line": 21, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27620,15 +25902,48 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_payload_preserves_legacy_sections:Function]\n# @RELATION: BINDS_TO -> TestConfigManagerCompat\n# @PURPOSE: Ensure get_payload merges typed config into raw payload without dropping legacy sections.\ndef test_get_payload_preserves_legacy_sections():\n manager = ConfigManager.__new__(ConfigManager)\n manager.raw_payload = {\"notifications\": {\"smtp\": {\"host\": \"mail.local\"}}}\n manager.config = AppConfig(environments=[], settings=GlobalSettings())\n\n payload = manager.get_payload()\n\n assert payload[\"settings\"][\"migration_sync_cron\"] == \"0 2 * * *\"\n assert payload[\"notifications\"][\"smtp\"][\"host\"] == \"mail.local\"\n\n\n# [/DEF:test_get_payload_preserves_legacy_sections:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_payload_preserves_legacy_sections [TYPE Function]\n# @RELATION BINDS_TO -> TestConfigManagerCompat\n# @BRIEF Ensure get_payload merges typed config into raw payload without dropping legacy sections.\ndef test_get_payload_preserves_legacy_sections():\n manager = ConfigManager.__new__(ConfigManager)\n manager.raw_payload = {\"notifications\": {\"smtp\": {\"host\": \"mail.local\"}}}\n manager.config = AppConfig(environments=[], settings=GlobalSettings())\n payload = manager.get_payload()\n assert payload[\"settings\"][\"migration_sync_cron\"] == \"0 2 * * *\"\n assert payload[\"notifications\"][\"smtp\"][\"host\"] == \"mail.local\"\n# #endregion test_get_payload_preserves_legacy_sections\n" + }, + { + "contract_id": "test_save_config_accepts_raw_payload_and_keeps_extras", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_config_manager_compat.py", + "start_line": 22, + "end_line": 43, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Ensure save_config accepts raw dict payload, refreshes typed config, and preserves extra sections." + }, + "relations": [ + { + "source_id": "test_save_config_accepts_raw_payload_and_keeps_extras", + "relation_type": "BINDS_TO", + "target_id": "TestConfigManagerCompat", + "target_ref": "TestConfigManagerCompat" + } + ], + "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", + "body": "# #region test_save_config_accepts_raw_payload_and_keeps_extras [TYPE Function]\n# @RELATION BINDS_TO -> TestConfigManagerCompat\n# @BRIEF Ensure save_config accepts raw dict payload, refreshes typed config, and preserves extra sections.\ndef test_save_config_accepts_raw_payload_and_keeps_extras(monkeypatch):\n manager = ConfigManager.__new__(ConfigManager)\n manager.raw_payload = {}\n manager.config = AppConfig(environments=[], settings=GlobalSettings())\n persisted = {}\n def _capture_save(config, session=None):\n persisted[\"payload\"] = manager.get_payload()\n monkeypatch.setattr(manager, \"_save_config_to_db\", _capture_save)\n manager.save_config(\n {\n \"environments\": [],\n \"settings\": GlobalSettings().model_dump(),\n \"notifications\": {\"telegram\": {\"bot_token\": \"secret\"}},\n }\n )\n assert manager.raw_payload[\"notifications\"][\"telegram\"][\"bot_token\"] == \"secret\"\n assert manager.config.settings.migration_sync_cron == \"0 2 * * *\"\n assert persisted[\"payload\"][\"notifications\"][\"telegram\"][\"bot_token\"] == \"secret\"\n# #endregion test_save_config_accepts_raw_payload_and_keeps_extras\n" }, { "contract_id": "test_save_config_syncs_environment_records_for_fk_backed_flows", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_config_manager_compat.py", - "start_line": 59, - "end_line": 128, + "start_line": 44, + "end_line": 101, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27653,121 +25968,48 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_save_config_syncs_environment_records_for_fk_backed_flows:Function]\n# @RELATION: BINDS_TO -> TestConfigManagerCompat\n# @PURPOSE: Ensure saving config mirrors typed environments into relational records required by FK-backed session persistence.\ndef test_save_config_syncs_environment_records_for_fk_backed_flows():\n manager = ConfigManager.__new__(ConfigManager)\n manager.raw_payload = {}\n manager.config = AppConfig(environments=[], settings=GlobalSettings())\n\n added_records = []\n deleted_records = []\n existing_record = SimpleNamespace(\n id=\"legacy-env\",\n name=\"Legacy\",\n url=\"http://legacy.local\",\n credentials_id=\"legacy-user\",\n )\n\n # [DEF:_FakeQuery:Class]\n # @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]\n # @COMPLEXITY: 1\n # @PURPOSE: Minimal query stub returning hardcoded existing environment record list for sync tests.\n # @INVARIANT: all() always returns [existing_record]; no parameterization or filtering.\n class _FakeQuery:\n def all(self):\n return [existing_record]\n\n # [/DEF:_FakeQuery:Class]\n\n # [DEF:_FakeSession:Class]\n # @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]\n # @COMPLEXITY: 1\n # @PURPOSE: Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions.\n # @INVARIANT: query() always returns _FakeQuery; no real DB interaction.\n class _FakeSession:\n def query(self, model):\n return _FakeQuery()\n\n def add(self, value):\n added_records.append(value)\n\n def delete(self, value):\n deleted_records.append(value)\n\n # [/DEF:_FakeSession:Class]\n\n session = _FakeSession()\n config = AppConfig(\n environments=[\n Environment(\n id=\"dev\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n ],\n settings=GlobalSettings(),\n )\n\n manager._sync_environment_records(session, config)\n\n assert len(added_records) == 1\n assert added_records[0].id == \"dev\"\n assert added_records[0].name == \"DEV\"\n assert added_records[0].url == \"http://superset.local\"\n assert added_records[0].credentials_id == \"demo\"\n assert deleted_records == [existing_record]\n\n\n# [/DEF:test_save_config_syncs_environment_records_for_fk_backed_flows:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_save_config_syncs_environment_records_for_fk_backed_flows [TYPE Function]\n# @RELATION BINDS_TO -> TestConfigManagerCompat\n# @BRIEF Ensure saving config mirrors typed environments into relational records required by FK-backed session persistence.\n# Deletion of stale records is tested separately in test_save_config_syncs_deletions_to_persistence.\ndef test_save_config_syncs_environment_records_for_fk_backed_flows():\n manager = ConfigManager.__new__(ConfigManager)\n manager.raw_payload = {}\n manager.config = AppConfig(environments=[], settings=GlobalSettings())\n added_records = []\n deleted_records = []\n existing_record = SimpleNamespace(\n id=\"legacy-env\",\n name=\"Legacy\",\n url=\"http://legacy.local\",\n credentials_id=\"legacy-user\",\n )\n # #region _FakeQuery [TYPE Class] [C:1]\n # @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]\n # @PURPOSE: Minimal query stub returning hardcoded existing environment record list for sync tests.\n # @INVARIANT: all() always returns [existing_record]; no parameterization or filtering.\n class _FakeQuery:\n def all(self):\n return [existing_record]\n # #endregion _FakeQuery\n # #region _FakeSession [TYPE Class] [C:1]\n # @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]\n # @PURPOSE: Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions.\n # @INVARIANT: query() always returns _FakeQuery; no real DB interaction.\n class _FakeSession:\n def query(self, model):\n return _FakeQuery()\n def add(self, value):\n added_records.append(value)\n def delete(self, value):\n deleted_records.append(value)\n # #endregion _FakeSession\n session = _FakeSession()\n config = AppConfig(\n environments=[\n Environment(\n id=\"dev\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n ],\n settings=GlobalSettings(),\n )\n manager._sync_environment_records(session, config)\n assert len(added_records) == 1\n assert added_records[0].id == \"dev\"\n assert added_records[0].name == \"DEV\"\n assert added_records[0].url == \"http://superset.local\"\n assert added_records[0].credentials_id == \"demo\"\n # Sync does NOT delete; deletion is in _delete_stale_environment_records\n assert len(deleted_records) == 0\n# #endregion test_save_config_syncs_environment_records_for_fk_backed_flows\n" }, { - "contract_id": "_FakeQuery", - "contract_type": "Class", + "contract_id": "test_save_config_syncs_deletions_to_persistence", + "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_config_manager_compat.py", - "start_line": 76, - "end_line": 85, + "start_line": 102, + "end_line": 160, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "all() always returns [existing_record]; no parameterization or filtering.", - "PURPOSE": "Minimal query stub returning hardcoded existing environment record list for sync tests." + "PURPOSE": "Ensure stale environment records are deleted during save (in _delete_stale_environment_records)" }, "relations": [ { - "source_id": "_FakeQuery", + "source_id": "test_save_config_syncs_deletions_to_persistence", "relation_type": "BINDS_TO", - "target_id": "test_save_config_syncs_environment_records_for_fk_backed_flows", - "target_ref": "[test_save_config_syncs_environment_records_for_fk_backed_flows]" + "target_id": "TestConfigManagerCompat", + "target_ref": "TestConfigManagerCompat" } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", + "message": "@RELATION is forbidden for contract type 'Function' at C2", "detail": { - "actual_complexity": 1, - "contract_type": "Class" + "actual_complexity": 2, + "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": " # [DEF:_FakeQuery:Class]\n # @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]\n # @COMPLEXITY: 1\n # @PURPOSE: Minimal query stub returning hardcoded existing environment record list for sync tests.\n # @INVARIANT: all() always returns [existing_record]; no parameterization or filtering.\n class _FakeQuery:\n def all(self):\n return [existing_record]\n\n # [/DEF:_FakeQuery:Class]\n" - }, - { - "contract_id": "_FakeSession", - "contract_type": "Class", - "file_path": "backend/src/core/__tests__/test_config_manager_compat.py", - "start_line": 87, - "end_line": 102, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "query() always returns _FakeQuery; no real DB interaction.", - "PURPOSE": "Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions." - }, - "relations": [ - { - "source_id": "_FakeSession", - "relation_type": "BINDS_TO", - "target_id": "test_save_config_syncs_environment_records_for_fk_backed_flows", - "target_ref": "[test_save_config_syncs_environment_records_for_fk_backed_flows]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_FakeSession:Class]\n # @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]\n # @COMPLEXITY: 1\n # @PURPOSE: Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions.\n # @INVARIANT: query() always returns _FakeQuery; no real DB interaction.\n class _FakeSession:\n def query(self, model):\n return _FakeQuery()\n\n def add(self, value):\n added_records.append(value)\n\n def delete(self, value):\n deleted_records.append(value)\n\n # [/DEF:_FakeSession:Class]\n" + "anchor_syntax": "region", + "body": "# #region test_save_config_syncs_deletions_to_persistence [TYPE Function]\n# @RELATION BINDS_TO -> TestConfigManagerCompat\n# @BRIEF Ensure stale environment records are deleted during save (in _delete_stale_environment_records)\n# and NOT during _load_config → _sync_environment_records (which would violate FK constraints\n# from task_records, database_mappings, etc.).\ndef test_save_config_syncs_deletions_to_persistence():\n manager = ConfigManager.__new__(ConfigManager)\n manager.raw_payload = {}\n manager.config = AppConfig(environments=[], settings=GlobalSettings())\n added_records = []\n deleted_records = []\n existing_record = SimpleNamespace(\n id=\"legacy-env\",\n name=\"Legacy\",\n url=\"http://legacy.local\",\n credentials_id=\"legacy-user\",\n )\n # #region _FakeQueryDel [TYPE Class] [C:1]\n # @RELATION: BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]\n # @PURPOSE: Minimal query stub for deletion test.\n class _FakeQuery:\n def all(self):\n return [existing_record]\n # #endregion _FakeQueryDel\n # #region _FakeSessionDel [TYPE Class] [C:1]\n # @RELATION: BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]\n # @PURPOSE: Minimal session stub that captures add/delete for deletion assertions.\n class _FakeSession:\n def query(self, model):\n return _FakeQuery()\n def add(self, value):\n added_records.append(value)\n def delete(self, value):\n deleted_records.append(value)\n # #endregion _FakeSessionDel\n session = _FakeSession()\n config = AppConfig(\n environments=[\n Environment(\n id=\"dev\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n ],\n settings=GlobalSettings(),\n )\n # Step 1: sync creates/updates (no deletion)\n manager._sync_environment_records(session, config)\n assert len(added_records) == 1\n assert added_records[0].id == \"dev\"\n assert len(deleted_records) == 0\n # Step 2: stale deletion removes envs not in the configured set\n manager._delete_stale_environment_records(session, config)\n assert len(deleted_records) == 1\n assert deleted_records[0] == existing_record\n assert deleted_records[0].id == \"legacy-env\"\n# #endregion test_save_config_syncs_deletions_to_persistence\n" }, { "contract_id": "test_load_config_syncs_environment_records_from_existing_db_payload", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_config_manager_compat.py", - "start_line": 131, - "end_line": 193, + "start_line": 161, + "end_line": 212, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27792,15 +26034,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_load_config_syncs_environment_records_from_existing_db_payload:Function]\n# @RELATION: BINDS_TO -> TestConfigManagerCompat\n# @PURPOSE: Ensure loading an existing DB-backed config also mirrors environment rows required by FK-backed runtime flows.\ndef test_load_config_syncs_environment_records_from_existing_db_payload(monkeypatch):\n manager = ConfigManager.__new__(ConfigManager)\n manager.config_path = None\n manager.raw_payload = {}\n manager.config = AppConfig(environments=[], settings=GlobalSettings())\n\n sync_calls = []\n closed = {\"value\": False}\n committed = {\"value\": False}\n\n # [DEF:_FakeSession:Class]\n # @RELATION: BINDS_TO -> [test_load_config_syncs_environment_records_from_existing_db_payload]\n # @COMPLEXITY: 1\n # @PURPOSE: Minimal session stub tracking commit/close signals for config load lifecycle assertions.\n # @INVARIANT: No query or add semantics; only lifecycle signal tracking.\n class _FakeSession:\n def commit(self):\n committed[\"value\"] = True\n\n def close(self):\n closed[\"value\"] = True\n\n # [/DEF:_FakeSession:Class]\n\n fake_session = _FakeSession()\n fake_record = SimpleNamespace(\n id=\"global\",\n payload={\n \"environments\": [\n {\n \"id\": \"dev\",\n \"name\": \"DEV\",\n \"url\": \"http://superset.local\",\n \"username\": \"demo\",\n \"password\": \"secret\",\n }\n ],\n \"settings\": GlobalSettings().model_dump(),\n },\n )\n\n monkeypatch.setattr(\"src.core.config_manager.SessionLocal\", lambda: fake_session)\n monkeypatch.setattr(manager, \"_get_record\", lambda session: fake_record)\n monkeypatch.setattr(\n manager,\n \"_sync_environment_records\",\n lambda session, config: sync_calls.append((session, config)),\n )\n\n config = manager._load_config()\n\n assert config.environments[0].id == \"dev\"\n assert len(sync_calls) == 1\n assert sync_calls[0][0] is fake_session\n assert sync_calls[0][1].environments[0].id == \"dev\"\n assert committed[\"value\"] is True\n assert closed[\"value\"] is True\n\n\n# [/DEF:test_load_config_syncs_environment_records_from_existing_db_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_load_config_syncs_environment_records_from_existing_db_payload [TYPE Function]\n# @RELATION BINDS_TO -> TestConfigManagerCompat\n# @BRIEF Ensure loading an existing DB-backed config also mirrors environment rows required by FK-backed runtime flows.\ndef test_load_config_syncs_environment_records_from_existing_db_payload(monkeypatch):\n manager = ConfigManager.__new__(ConfigManager)\n manager.config_path = None\n manager.raw_payload = {}\n manager.config = AppConfig(environments=[], settings=GlobalSettings())\n sync_calls = []\n closed = {\"value\": False}\n committed = {\"value\": False}\n # #region _FakeSession [TYPE Class] [C:1]\n # @RELATION: BINDS_TO -> [test_load_config_syncs_environment_records_from_existing_db_payload]\n # @PURPOSE: Minimal session stub tracking commit/close signals for config load lifecycle assertions.\n # @INVARIANT: No query or add semantics; only lifecycle signal tracking.\n class _FakeSession:\n def commit(self):\n committed[\"value\"] = True\n def close(self):\n closed[\"value\"] = True\n # #endregion _FakeSession\n fake_session = _FakeSession()\n fake_record = SimpleNamespace(\n id=\"global\",\n payload={\n \"environments\": [\n {\n \"id\": \"dev\",\n \"name\": \"DEV\",\n \"url\": \"http://superset.local\",\n \"username\": \"demo\",\n \"password\": \"secret\",\n }\n ],\n \"settings\": GlobalSettings().model_dump(),\n },\n )\n monkeypatch.setattr(\"src.core.config_manager.SessionLocal\", lambda: fake_session)\n monkeypatch.setattr(manager, \"_get_record\", lambda session: fake_record)\n monkeypatch.setattr(\n manager,\n \"_sync_environment_records\",\n lambda session, config: sync_calls.append((session, config)),\n )\n config = manager._load_config()\n assert config.environments[0].id == \"dev\"\n assert len(sync_calls) == 1\n assert sync_calls[0][0] is fake_session\n assert sync_calls[0][1].environments[0].id == \"dev\"\n assert committed[\"value\"] is True\n assert closed[\"value\"] is True\n# #endregion test_load_config_syncs_environment_records_from_existing_db_payload\n" }, { "contract_id": "_make_environment", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_native_filters.py", - "start_line": 31, - "end_line": 43, + "start_line": 25, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -27823,15 +26065,543 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_environment:Function]\n# @RELATION: BINDS_TO -> NativeFilterExtractionTests\ndef _make_environment() -> Environment:\n return Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n\n\n# [/DEF:_make_environment:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_environment [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\ndef _make_environment() -> Environment:\n return Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n# #endregion _make_environment\n" + }, + { + "contract_id": "test_extract_native_filters_from_permalink", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 36, + "end_line": 78, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Extract native filters from a permalink key." + }, + "relations": [ + { + "source_id": "test_extract_native_filters_from_permalink", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_extract_native_filters_from_permalink [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Extract native filters from a permalink key.\ndef test_extract_native_filters_from_permalink():\n client = SupersetClient(_make_environment())\n client.get_dashboard_permalink_state = MagicMock(\n return_value={\n \"result\": {\n \"state\": {\n \"dataMask\": {\n \"filter_country\": {\n \"extraFormData\": {\n \"filters\": [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n },\n \"filterState\": {\"value\": [\"DE\", \"FR\"]},\n \"ownState\": {},\n },\n \"filter_date\": {\n \"extraFormData\": {\"time_range\": \"2020-01-01 : 2024-12-31\"},\n \"filterState\": {\"value\": \"2020-01-01 : 2024-12-31\"},\n \"ownState\": {},\n },\n },\n \"activeTabs\": [\"tab1\", \"tab2\"],\n \"anchor\": \"SECTION1\",\n \"chartStates\": {\"chart_1\": {}},\n }\n }\n }\n )\n result = client.extract_native_filters_from_permalink(\"test-permalink-key\")\n assert result[\"permalink_key\"] == \"test-permalink-key\"\n assert \"dataMask\" in result\n assert \"filter_country\" in result[\"dataMask\"]\n assert \"filter_date\" in result[\"dataMask\"]\n assert result[\"dataMask\"][\"filter_country\"][\"extraFormData\"][\"filters\"][0][\n \"val\"\n ] == [\"DE\", \"FR\"]\n assert result[\"activeTabs\"] == [\"tab1\", \"tab2\"]\n assert result[\"anchor\"] == \"SECTION1\"\n# #endregion test_extract_native_filters_from_permalink\n" + }, + { + "contract_id": "test_extract_native_filters_from_permalink_direct_response", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 79, + "end_line": 100, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Handle permalink response without result wrapper." + }, + "relations": [ + { + "source_id": "test_extract_native_filters_from_permalink_direct_response", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_extract_native_filters_from_permalink_direct_response [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle permalink response without result wrapper.\ndef test_extract_native_filters_from_permalink_direct_response():\n client = SupersetClient(_make_environment())\n client.get_dashboard_permalink_state = MagicMock(\n return_value={\n \"state\": {\n \"dataMask\": {\n \"filter_1\": {\n \"extraFormData\": {\"filters\": []},\n \"filterState\": {},\n \"ownState\": {},\n }\n }\n }\n }\n )\n result = client.extract_native_filters_from_permalink(\"direct-key\")\n assert result[\"permalink_key\"] == \"direct-key\"\n assert \"filter_1\" in result[\"dataMask\"]\n# #endregion test_extract_native_filters_from_permalink_direct_response\n" + }, + { + "contract_id": "test_extract_native_filters_from_key", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 101, + "end_line": 133, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Extract native filters from a native_filters_key." + }, + "relations": [ + { + "source_id": "test_extract_native_filters_from_key", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_extract_native_filters_from_key [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Extract native filters from a native_filters_key.\ndef test_extract_native_filters_from_key():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": json.dumps(\n {\n \"filter_region\": {\n \"id\": \"filter_region\",\n \"extraFormData\": {\n \"filters\": [\n {\"col\": \"region\", \"op\": \"IN\", \"val\": [\"EMEA\"]}\n ]\n },\n \"filterState\": {\"value\": [\"EMEA\"]},\n }\n }\n )\n }\n }\n )\n result = client.extract_native_filters_from_key(123, \"filter-state-key\")\n assert result[\"dashboard_id\"] == 123\n assert result[\"filter_state_key\"] == \"filter-state-key\"\n assert \"dataMask\" in result\n assert \"filter_region\" in result[\"dataMask\"]\n assert result[\"dataMask\"][\"filter_region\"][\"extraFormData\"][\"filters\"][0][\n \"val\"\n ] == [\"EMEA\"]\n# #endregion test_extract_native_filters_from_key\n" + }, + { + "contract_id": "test_extract_native_filters_from_key_single_filter", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 134, + "end_line": 161, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Handle single filter format in native filter state." + }, + "relations": [ + { + "source_id": "test_extract_native_filters_from_key_single_filter", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_extract_native_filters_from_key_single_filter [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle single filter format in native filter state.\ndef test_extract_native_filters_from_key_single_filter():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": json.dumps(\n {\n \"id\": \"single_filter\",\n \"extraFormData\": {\n \"filters\": [{\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"}]\n },\n \"filterState\": {\"value\": \"active\"},\n }\n )\n }\n }\n )\n result = client.extract_native_filters_from_key(456, \"single-key\")\n assert \"dataMask\" in result\n assert \"single_filter\" in result[\"dataMask\"]\n assert (\n result[\"dataMask\"][\"single_filter\"][\"extraFormData\"][\"filters\"][0][\"col\"]\n == \"status\"\n )\n# #endregion test_extract_native_filters_from_key_single_filter\n" + }, + { + "contract_id": "test_extract_native_filters_from_key_dict_value", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 162, + "end_line": 182, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Handle filter state value as dict instead of JSON string." + }, + "relations": [ + { + "source_id": "test_extract_native_filters_from_key_dict_value", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_extract_native_filters_from_key_dict_value [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Handle filter state value as dict instead of JSON string.\ndef test_extract_native_filters_from_key_dict_value():\n client = SupersetClient(_make_environment())\n client.get_native_filter_state = MagicMock(\n return_value={\n \"result\": {\n \"value\": {\n \"filter_id\": {\n \"extraFormData\": {\"filters\": []},\n \"filterState\": {},\n }\n }\n }\n }\n )\n result = client.extract_native_filters_from_key(789, \"dict-key\")\n assert \"dataMask\" in result\n assert \"filter_id\" in result[\"dataMask\"]\n# #endregion test_extract_native_filters_from_key_dict_value\n" + }, + { + "contract_id": "test_parse_dashboard_url_for_filters_permalink", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 183, + "end_line": 196, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Parse permalink URL format." + }, + "relations": [ + { + "source_id": "test_parse_dashboard_url_for_filters_permalink", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_parse_dashboard_url_for_filters_permalink [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse permalink URL format.\ndef test_parse_dashboard_url_for_filters_permalink():\n client = SupersetClient(_make_environment())\n client.extract_native_filters_from_permalink = MagicMock(\n return_value={\"dataMask\": {\"f1\": {}}, \"permalink_key\": \"abc123\"}\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/superset/dashboard/p/abc123/\"\n )\n assert result[\"filter_type\"] == \"permalink\"\n assert result[\"filters\"][\"dataMask\"][\"f1\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_permalink\n" + }, + { + "contract_id": "test_parse_dashboard_url_for_filters_native_key", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 197, + "end_line": 215, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Parse native_filters_key URL format with numeric dashboard ID." + }, + "relations": [ + { + "source_id": "test_parse_dashboard_url_for_filters_native_key", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_parse_dashboard_url_for_filters_native_key [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters_key URL format with numeric dashboard ID.\ndef test_parse_dashboard_url_for_filters_native_key():\n client = SupersetClient(_make_environment())\n client.extract_native_filters_from_key = MagicMock(\n return_value={\n \"dataMask\": {\"f2\": {}},\n \"dashboard_id\": 42,\n \"filter_state_key\": \"xyz\",\n }\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/42/?native_filters_key=xyz\"\n )\n assert result[\"filter_type\"] == \"native_filters_key\"\n assert result[\"dashboard_id\"] == 42\n assert result[\"filters\"][\"dataMask\"][\"f2\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_native_key\n" + }, + { + "contract_id": "test_parse_dashboard_url_for_filters_native_key_slug", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 216, + "end_line": 242, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Parse native_filters_key URL format when dashboard reference is a slug, not a numeric ID." + }, + "relations": [ + { + "source_id": "test_parse_dashboard_url_for_filters_native_key_slug", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_parse_dashboard_url_for_filters_native_key_slug [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters_key URL format when dashboard reference is a slug, not a numeric ID.\ndef test_parse_dashboard_url_for_filters_native_key_slug():\n client = SupersetClient(_make_environment())\n # Simulate slug resolution: get_dashboard returns the dashboard with numeric ID\n client.get_dashboard = MagicMock(\n return_value={\n \"result\": {\"id\": 99, \"dashboard_title\": \"COVID Dashboard\", \"slug\": \"covid\"}\n }\n )\n client.extract_native_filters_from_key = MagicMock(\n return_value={\n \"dataMask\": {\"f_slug\": {}},\n \"dashboard_id\": 99,\n \"filter_state_key\": \"abc123\",\n }\n )\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/superset/dashboard/covid/?native_filters_key=abc123\"\n )\n assert result[\"filter_type\"] == \"native_filters_key\"\n assert result[\"dashboard_id\"] == 99\n assert result[\"filters\"][\"dataMask\"][\"f_slug\"] == {}\n client.get_dashboard.assert_called_once_with(\"covid\")\n client.extract_native_filters_from_key.assert_called_once_with(99, \"abc123\")\n# #endregion test_parse_dashboard_url_for_filters_native_key_slug\n" + }, + { + "contract_id": "test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 243, + "end_line": 254, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Gracefully handle slug resolution failure for native_filters_key URL." + }, + "relations": [ + { + "source_id": "test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Gracefully handle slug resolution failure for native_filters_key URL.\ndef test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails():\n client = SupersetClient(_make_environment())\n client.get_dashboard = MagicMock(side_effect=Exception(\"Not found\"))\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/unknownslug/?native_filters_key=key1\"\n )\n assert result[\"filter_type\"] is None\n assert result[\"dashboard_id\"] is None\n# #endregion test_parse_dashboard_url_for_filters_native_key_slug_resolution_fails\n" + }, + { + "contract_id": "test_parse_dashboard_url_for_filters_native_filters_direct", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 255, + "end_line": 266, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Parse native_filters direct query param." + }, + "relations": [ + { + "source_id": "test_parse_dashboard_url_for_filters_native_filters_direct", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_parse_dashboard_url_for_filters_native_filters_direct [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Parse native_filters direct query param.\ndef test_parse_dashboard_url_for_filters_native_filters_direct():\n client = SupersetClient(_make_environment())\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/1/?native_filters=\"\n + json.dumps({\"filter_1\": {\"col\": \"x\", \"op\": \"==\", \"val\": \"y\"}})\n )\n assert result[\"filter_type\"] == \"native_filters\"\n assert \"dataMask\" in result[\"filters\"]\n# #endregion test_parse_dashboard_url_for_filters_native_filters_direct\n" + }, + { + "contract_id": "test_parse_dashboard_url_for_filters_no_filters", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 267, + "end_line": 277, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Return empty result when no filters present." + }, + "relations": [ + { + "source_id": "test_parse_dashboard_url_for_filters_no_filters", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_parse_dashboard_url_for_filters_no_filters [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Return empty result when no filters present.\ndef test_parse_dashboard_url_for_filters_no_filters():\n client = SupersetClient(_make_environment())\n result = client.parse_dashboard_url_for_filters(\n \"http://superset.local/dashboard/1/\"\n )\n assert result[\"filter_type\"] is None\n assert result[\"filters\"] == {}\n# #endregion test_parse_dashboard_url_for_filters_no_filters\n" + }, + { + "contract_id": "test_extra_form_data_merge", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 278, + "end_line": 304, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Test ExtraFormDataMerge correctly merges dictionaries." + }, + "relations": [ + { + "source_id": "test_extra_form_data_merge", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_extra_form_data_merge [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ExtraFormDataMerge correctly merges dictionaries.\ndef test_extra_form_data_merge():\n merger = ExtraFormDataMerge()\n original = {\n \"filters\": [{\"col\": \"a\", \"op\": \"IN\", \"val\": [1, 2]}],\n \"time_range\": \"2020-01-01 : 2021-01-01\",\n \"extras\": {\"where\": \"x > 0\"},\n }\n new = {\n \"filters\": [{\"col\": \"b\", \"op\": \"==\", \"val\": \"test\"}],\n \"time_range\": \"2022-01-01 : 2023-01-01\",\n \"columns\": [\"col1\", \"col2\"],\n }\n result = merger.merge(original, new)\n # Filters should be appended\n assert len(result[\"filters\"]) == 2\n assert result[\"filters\"][0][\"col\"] == \"a\"\n assert result[\"filters\"][1][\"col\"] == \"b\"\n # Time range should be overridden\n assert result[\"time_range\"] == \"2022-01-01 : 2023-01-01\"\n # Extras should remain\n assert result[\"extras\"] == {\"where\": \"x > 0\"}\n # New columns should be added\n assert result[\"columns\"] == [\"col1\", \"col2\"]\n# #endregion test_extra_form_data_merge\n" + }, + { + "contract_id": "test_filter_state_model", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 305, + "end_line": 317, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Test FilterState Pydantic model." + }, + "relations": [ + { + "source_id": "test_filter_state_model", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_filter_state_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test FilterState Pydantic model.\ndef test_filter_state_model():\n state = FilterState(\n extraFormData={\"filters\": [{\"col\": \"x\", \"op\": \"==\", \"val\": \"y\"}]},\n filterState={\"value\": \"y\"},\n ownState={\"selectedValues\": [\"y\"]},\n )\n assert state.extraFormData[\"filters\"][0][\"col\"] == \"x\"\n assert state.filterState[\"value\"] == \"y\"\n assert state.ownState[\"selectedValues\"] == [\"y\"]\n# #endregion test_filter_state_model\n" + }, + { + "contract_id": "test_parsed_native_filters_model", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 318, + "end_line": 331, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Test ParsedNativeFilters Pydantic model." + }, + "relations": [ + { + "source_id": "test_parsed_native_filters_model", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_parsed_native_filters_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ParsedNativeFilters Pydantic model.\ndef test_parsed_native_filters_model():\n filters = ParsedNativeFilters(\n dataMask={\"f1\": {\"extraFormData\": {}, \"filterState\": {}}},\n filter_type=\"permalink\",\n dashboard_id=\"42\",\n permalink_key=\"abc\",\n )\n assert filters.has_filters() is True\n assert filters.get_filter_count() == 1\n assert filters.filter_type == \"permalink\"\n# #endregion test_parsed_native_filters_model\n" + }, + { + "contract_id": "test_parsed_native_filters_empty", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 332, + "end_line": 339, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Test ParsedNativeFilters with no filters." + }, + "relations": [ + { + "source_id": "test_parsed_native_filters_empty", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_parsed_native_filters_empty [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test ParsedNativeFilters with no filters.\ndef test_parsed_native_filters_empty():\n filters = ParsedNativeFilters()\n assert filters.has_filters() is False\n assert filters.get_filter_count() == 0\n# #endregion test_parsed_native_filters_empty\n" + }, + { + "contract_id": "test_native_filter_data_mask_model", + "contract_type": "Function", + "file_path": "backend/src/core/__tests__/test_native_filters.py", + "start_line": 340, + "end_line": 355, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Test NativeFilterDataMask model." + }, + "relations": [ + { + "source_id": "test_native_filter_data_mask_model", + "relation_type": "BINDS_TO", + "target_id": "NativeFilterExtractionTests", + "target_ref": "NativeFilterExtractionTests" + } + ], + "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", + "body": "# #region test_native_filter_data_mask_model [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Test NativeFilterDataMask model.\ndef test_native_filter_data_mask_model():\n data_mask = NativeFilterDataMask(\n filters={\n \"filter_1\": FilterState(extraFormData={\"filters\": []}, filterState={}),\n \"filter_2\": FilterState(\n extraFormData={\"time_range\": \"...\"}, filterState={}\n ),\n }\n )\n assert data_mask.get_filter_ids() == [\"filter_1\", \"filter_2\"]\n assert data_mask.get_extra_form_data(\"filter_1\") == {\"filters\": []}\n assert data_mask.get_extra_form_data(\"nonexistent\") == {}\n# #endregion test_native_filter_data_mask_model\n" }, { "contract_id": "test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_native_filters.py", - "start_line": 463, - "end_line": 527, + "start_line": 356, + "end_line": 415, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27856,15 +26626,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names:Function]\n# @RELATION: BINDS_TO -> NativeFilterExtractionTests\n# @PURPOSE: Reconcile raw native filter ids from state to canonical metadata filter names.\ndef test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n }\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"display_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"raw_value\": [\"DE\", \"FR\"],\n \"normalized_value\": {\n \"filter_clauses\": [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"filter_state\",\n },\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n\n result = extractor.recover_imported_filters(parsed_context)\n\n assert len(result) == 1\n assert result[0][\"filter_name\"] == \"Country\"\n assert result[0][\"display_name\"] == \"Country\"\n assert result[0][\"raw_value\"] == [\"DE\", \"FR\"]\n assert result[0][\"source\"] == \"superset_native_filters_key\"\n assert result[0][\"normalized_value\"] == {\n \"filter_clauses\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"filter_state\",\n }\n\n\n# [/DEF:test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Reconcile raw native filter ids from state to canonical metadata filter names.\ndef test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n }\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"display_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"raw_value\": [\"DE\", \"FR\"],\n \"normalized_value\": {\n \"filter_clauses\": [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"filter_state\",\n },\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 1\n assert result[0][\"filter_name\"] == \"Country\"\n assert result[0][\"display_name\"] == \"Country\"\n assert result[0][\"raw_value\"] == [\"DE\", \"FR\"]\n assert result[0][\"source\"] == \"superset_native_filters_key\"\n assert result[0][\"normalized_value\"] == {\n \"filter_clauses\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}]\n },\n \"value_origin\": \"filter_state\",\n }\n# #endregion test_recover_imported_filters_reconciles_raw_native_filter_ids_to_metadata_names\n" }, { "contract_id": "test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_native_filters.py", - "start_line": 530, - "end_line": 585, + "start_line": 416, + "end_line": 466, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27889,15 +26659,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter:Function]\n# @RELATION: BINDS_TO -> NativeFilterExtractionTests\n# @PURPOSE: Collapse raw-id state entries and metadata entries into one canonical filter.\ndef test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n },\n {\n \"id\": \"NATIVE_FILTER-vv123\",\n \"name\": \"Region\",\n \"label\": \"Region\",\n },\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"display_name\": \"Country\",\n \"raw_value\": [\"DE\", \"FR\"],\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n\n result = extractor.recover_imported_filters(parsed_context)\n\n assert len(result) == 2\n country_filter = next(item for item in result if item[\"filter_name\"] == \"Country\")\n region_filter = next(item for item in result if item[\"filter_name\"] == \"Region\")\n assert country_filter[\"raw_value\"] == [\"DE\", \"FR\"]\n assert country_filter[\"recovery_status\"] == \"recovered\"\n assert region_filter[\"raw_value\"] is None\n assert region_filter[\"recovery_status\"] == \"partial\"\n\n\n# [/DEF:test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Collapse raw-id state entries and metadata entries into one canonical filter.\ndef test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n },\n {\n \"id\": \"NATIVE_FILTER-vv123\",\n \"name\": \"Region\",\n \"label\": \"Region\",\n },\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"NATIVE_FILTER-EWNH3M70z\",\n \"display_name\": \"Country\",\n \"raw_value\": [\"DE\", \"FR\"],\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 2\n country_filter = next(item for item in result if item[\"filter_name\"] == \"Country\")\n region_filter = next(item for item in result if item[\"filter_name\"] == \"Region\")\n assert country_filter[\"raw_value\"] == [\"DE\", \"FR\"]\n assert country_filter[\"recovery_status\"] == \"recovered\"\n assert region_filter[\"raw_value\"] is None\n assert region_filter[\"recovery_status\"] == \"partial\"\n# #endregion test_recover_imported_filters_collapses_state_and_metadata_duplicates_into_one_canonical_filter\n" }, { "contract_id": "test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_native_filters.py", - "start_line": 588, - "end_line": 642, + "start_line": 467, + "end_line": 516, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27922,15 +26692,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids:Function]\n# @RELATION: BINDS_TO -> NativeFilterExtractionTests\n# @PURPOSE: Preserve unmatched raw native filter ids as fallback diagnostics when metadata mapping is unavailable.\ndef test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n }\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"UNKNOWN_NATIVE_FILTER\",\n \"display_name\": \"UNKNOWN_NATIVE_FILTER\",\n \"raw_value\": [\"orphan\"],\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n\n result = extractor.recover_imported_filters(parsed_context)\n\n assert len(result) == 2\n assert any(\n item[\"filter_name\"] == \"Country\" and item[\"recovery_status\"] == \"partial\"\n for item in result\n )\n assert any(\n item[\"filter_name\"] == \"UNKNOWN_NATIVE_FILTER\"\n and item[\"raw_value\"] == [\"orphan\"]\n and item[\"source\"] == \"superset_native_filters_key\"\n for item in result\n )\n\n\n# [/DEF:test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Preserve unmatched raw native filter ids as fallback diagnostics when metadata mapping is unavailable.\ndef test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids():\n client = MagicMock()\n client.get_dashboard.return_value = {\n \"result\": {\n \"json_metadata\": json.dumps(\n {\n \"native_filter_configuration\": [\n {\n \"id\": \"NATIVE_FILTER-EWNH3M70z\",\n \"name\": \"Country\",\n \"label\": \"Country\",\n }\n ]\n }\n )\n }\n }\n extractor = SupersetContextExtractor(_make_environment(), client=client)\n parsed_context = SupersetParsedContext(\n source_url=\"http://superset.local/dashboard/42/?native_filters_key=abc\",\n dataset_ref=\"dataset:42\",\n dashboard_id=42,\n imported_filters=[\n {\n \"filter_name\": \"UNKNOWN_NATIVE_FILTER\",\n \"display_name\": \"UNKNOWN_NATIVE_FILTER\",\n \"raw_value\": [\"orphan\"],\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ],\n )\n result = extractor.recover_imported_filters(parsed_context)\n assert len(result) == 2\n assert any(\n item[\"filter_name\"] == \"Country\" and item[\"recovery_status\"] == \"partial\"\n for item in result\n )\n assert any(\n item[\"filter_name\"] == \"UNKNOWN_NATIVE_FILTER\"\n and item[\"raw_value\"] == [\"orphan\"]\n and item[\"source\"] == \"superset_native_filters_key\"\n for item in result\n )\n# #endregion test_recover_imported_filters_preserves_unmatched_raw_native_filter_ids\n" }, { "contract_id": "test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_native_filters.py", - "start_line": 645, - "end_line": 687, + "start_line": 517, + "end_line": 555, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27955,15 +26725,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview:Function]\n# @RELATION: BINDS_TO -> NativeFilterExtractionTests\n# @PURPOSE: Recovered native filter state should preserve exact Superset clause payload and time extras for preview compilation.\ndef test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview():\n extractor = SupersetContextExtractor(_make_environment(), client=MagicMock())\n\n imported_filters = extractor._extract_imported_filters(\n {\n \"native_filter_state\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"NATIVE_FILTER-1\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n \"extraFormData\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"time_range\": \"Last month\",\n },\n }\n }\n }\n )\n\n assert imported_filters == [\n {\n \"filter_name\": \"NATIVE_FILTER-1\",\n \"raw_value\": [\"DE\"],\n \"display_name\": \"Country\",\n \"normalized_value\": {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"time_range\": \"Last month\",\n },\n \"value_origin\": \"filter_state\",\n },\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ]\n\n\n# [/DEF:test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Recovered native filter state should preserve exact Superset clause payload and time extras for preview compilation.\ndef test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview():\n extractor = SupersetContextExtractor(_make_environment(), client=MagicMock())\n imported_filters = extractor._extract_imported_filters(\n {\n \"native_filter_state\": {\n \"NATIVE_FILTER-1\": {\n \"id\": \"NATIVE_FILTER-1\",\n \"filterState\": {\"label\": \"Country\", \"value\": [\"DE\"]},\n \"extraFormData\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"time_range\": \"Last month\",\n },\n }\n }\n }\n )\n assert imported_filters == [\n {\n \"filter_name\": \"NATIVE_FILTER-1\",\n \"raw_value\": [\"DE\"],\n \"display_name\": \"Country\",\n \"normalized_value\": {\n \"filter_clauses\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}],\n \"time_range\": \"Last month\",\n },\n \"value_origin\": \"filter_state\",\n },\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\",\n \"requires_confirmation\": False,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n ]\n# #endregion test_extract_imported_filters_preserves_clause_level_native_filter_payload_for_preview\n" }, { "contract_id": "test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_native_filters.py", - "start_line": 690, - "end_line": 708, + "start_line": 556, + "end_line": 571, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27988,67 +26758,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values:Function]\n# @RELATION: BINDS_TO -> NativeFilterExtractionTests\n# @PURPOSE: Assistant-facing imported-filter payload should redact likely PII while preserving structure metadata.\ndef test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values():\n result = sanitize_imported_filter_for_assistant(\n {\n \"filter_name\": \"customer_email\",\n \"raw_value\": [\"alice@example.com\", \"1234567890\"],\n \"normalized_value\": {\"filter_clauses\": []},\n \"source\": \"manual\",\n }\n )\n\n assert result[\"raw_value\"] == [\"***@example.com\", \"********90\"]\n assert result[\"raw_value_masked\"] is True\n assert result[\"normalized_value\"] == {\"filter_clauses\": []}\n\n\n# [/DEF:test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values:Function]\n" - }, - { - "contract_id": "SupersetPreviewPipelineTests", - "contract_type": "Module", - "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 1, - "end_line": 542, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Domain", - "PURPOSE": "Verify explicit chart-data preview compilation and ensure non-dashboard 404 errors remain generic across sync and async clients.", - "SEMANTICS": [ - "tests", - "superset", - "preview", - "chart_data", - "network", - "404-mapping" - ] - }, - "relations": [ - { - "source_id": "SupersetPreviewPipelineTests", - "relation_type": "[BINDS_TO]", - "target_id": "AsyncNetworkModule", - "target_ref": "[AsyncNetworkModule]" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [BINDS_TO] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[BINDS_TO]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetPreviewPipelineTests:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, superset, preview, chart_data, network, 404-mapping\n# @PURPOSE: Verify explicit chart-data preview compilation and ensure non-dashboard 404 errors remain generic across sync and async clients.\n# @LAYER: Domain\n# @RELATION: [BINDS_TO] ->[AsyncNetworkModule]\n\nimport json\nfrom unittest.mock import MagicMock\n\nimport httpx\nimport pytest\nimport requests\n\nfrom src.core.config_models import Environment\nfrom src.core.superset_client import SupersetClient\nfrom src.core.utils.async_network import AsyncAPIClient\nfrom src.core.utils.network import APIClient, DashboardNotFoundError, SupersetAPIError\n\n\n# [DEF:_make_environment:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\ndef _make_environment() -> Environment:\n return Environment(\n id=\"env-1\",\n name=\"DEV\",\n url=\"http://superset.local\",\n username=\"demo\",\n password=\"secret\",\n )\n\n\n# [/DEF:_make_environment:Function]\n\n\n# [DEF:_make_requests_http_error:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\ndef _make_requests_http_error(\n status_code: int, url: str\n) -> requests.exceptions.HTTPError:\n response = requests.Response()\n response.status_code = status_code\n response.url = url\n response._content = b'{\"message\":\"not found\"}'\n request = requests.Request(\"GET\", url).prepare()\n response.request = request\n return requests.exceptions.HTTPError(response=response, request=request)\n\n\n# [/DEF:_make_requests_http_error:Function]\n\n\n# [DEF:_make_httpx_status_error:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\ndef _make_httpx_status_error(status_code: int, url: str) -> httpx.HTTPStatusError:\n request = httpx.Request(\"GET\", url)\n response = httpx.Response(\n status_code=status_code, request=request, text='{\"message\":\"not found\"}'\n )\n return httpx.HTTPStatusError(\"upstream error\", request=request, response=response)\n\n\n# [/DEF:_make_httpx_status_error:Function]\n\n\n# [DEF:test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Superset preview compilation should prefer the legacy form_data transport inferred from browser traffic before falling back to chart-data.\ndef test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy():\n client = SupersetClient(_make_environment())\n client.get_dataset = MagicMock(\n return_value={\n \"result\": {\n \"id\": 42,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"result_format\": \"json\",\n \"result_type\": \"full\",\n }\n }\n )\n client.network = MagicMock()\n client.network.request.return_value = {\n \"result\": {\n \"query\": \"SELECT count(*) FROM public.sales WHERE country IN ('DE')\",\n }\n }\n\n result = client.compile_dataset_preview(\n dataset_id=42,\n template_params={\"country\": \"DE\"},\n effective_filters=[{\"filter_name\": \"country\", \"effective_value\": [\"DE\"]}],\n )\n\n assert (\n result[\"compiled_sql\"]\n == \"SELECT count(*) FROM public.sales WHERE country IN ('DE')\"\n )\n client.network.request.assert_called_once()\n request_call = client.network.request.call_args\n assert request_call.kwargs[\"method\"] == \"POST\"\n assert request_call.kwargs[\"endpoint\"] == \"/explore_json/form_data\"\n assert request_call.kwargs[\"params\"] is not None\n assert request_call.kwargs[\"params\"].keys() == {\"form_data\"}\n\n legacy_form_data = json.loads(request_call.kwargs[\"params\"][\"form_data\"])\n assert \"datasource\" not in legacy_form_data\n assert legacy_form_data[\"datasource_id\"] == 42\n assert legacy_form_data[\"datasource_type\"] == \"table\"\n assert legacy_form_data[\"extra_filters\"] == [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ]\n assert legacy_form_data[\"extra_form_data\"] == {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n }\n assert legacy_form_data[\"url_params\"] == {\"country\": \"DE\"}\n assert legacy_form_data[\"result_type\"] == \"query\"\n assert legacy_form_data[\"result_format\"] == \"json\"\n assert legacy_form_data[\"force\"] is True\n\n assert result[\"endpoint\"] == \"/explore_json/form_data\"\n assert result[\"endpoint_kind\"] == \"legacy_explore_form_data\"\n assert result[\"dataset_id\"] == 42\n assert result[\"response_diagnostics\"] == [\n {\"source\": \"query\", \"has_query\": False},\n {\"source\": \"sql\", \"has_query\": False},\n {\"source\": \"compiled_sql\", \"has_query\": False},\n {\"source\": \"result.query\", \"has_query\": True},\n ]\n assert result[\"legacy_form_data\"][\"extra_filters\"] == [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ]\n assert result[\"query_context\"][\"datasource\"] == {\"id\": 42, \"type\": \"table\"}\n assert result[\"strategy_attempts\"] == [\n {\n \"endpoint\": \"/explore_json/form_data\",\n \"endpoint_kind\": \"legacy_explore_form_data\",\n \"request_transport\": \"query_param_form_data\",\n \"contains_root_datasource\": False,\n \"contains_form_datasource\": False,\n \"contains_query_object_datasource\": False,\n \"request_param_keys\": [\"form_data\"],\n \"request_payload_keys\": [],\n \"success\": True,\n }\n ]\n\n\n# [/DEF:test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy:Function]\n\n\n# [DEF:test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Superset preview compilation should fall back to chart-data when legacy form_data strategies are rejected.\ndef test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures():\n client = SupersetClient(_make_environment())\n client.get_dataset = MagicMock(\n return_value={\n \"result\": {\n \"id\": 42,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n }\n }\n )\n client.network = MagicMock()\n client.network.request.side_effect = [\n SupersetAPIError(\"legacy explore failed\"),\n SupersetAPIError(\"legacy data failed\"),\n {\n \"result\": [\n {\n \"query\": \"SELECT count(*) FROM public.sales\",\n }\n ]\n },\n ]\n\n result = client.compile_dataset_preview(dataset_id=42)\n\n assert client.network.request.call_count == 3\n first_call = client.network.request.call_args_list[0]\n second_call = client.network.request.call_args_list[1]\n third_call = client.network.request.call_args_list[2]\n assert first_call.kwargs[\"endpoint\"] == \"/explore_json/form_data\"\n assert second_call.kwargs[\"endpoint\"] == \"/data\"\n assert third_call.kwargs[\"endpoint\"] == \"/chart/data\"\n assert third_call.kwargs[\"headers\"] == {\"Content-Type\": \"application/json\"}\n\n first_legacy_form_data = json.loads(first_call.kwargs[\"params\"][\"form_data\"])\n second_legacy_form_data = json.loads(second_call.kwargs[\"params\"][\"form_data\"])\n assert \"datasource\" not in first_legacy_form_data\n assert \"datasource\" not in second_legacy_form_data\n\n query_context = json.loads(third_call.kwargs[\"data\"])\n assert query_context[\"datasource\"] == {\"id\": 42, \"type\": \"table\"}\n assert result[\"endpoint\"] == \"/chart/data\"\n assert result[\"endpoint_kind\"] == \"v1_chart_data\"\n assert len(result[\"strategy_attempts\"]) == 3\n assert result[\"strategy_attempts\"][0][\"endpoint\"] == \"/explore_json/form_data\"\n assert result[\"strategy_attempts\"][0][\"endpoint_kind\"] == \"legacy_explore_form_data\"\n assert (\n result[\"strategy_attempts\"][0][\"request_transport\"] == \"query_param_form_data\"\n )\n assert result[\"strategy_attempts\"][0][\"contains_root_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"contains_form_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"contains_query_object_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"request_param_keys\"] == [\"form_data\"]\n assert result[\"strategy_attempts\"][0][\"request_payload_keys\"] == []\n assert result[\"strategy_attempts\"][0][\"success\"] is False\n assert \"legacy explore failed\" in result[\"strategy_attempts\"][0][\"error\"]\n\n assert result[\"strategy_attempts\"][1][\"endpoint\"] == \"/data\"\n assert result[\"strategy_attempts\"][1][\"endpoint_kind\"] == \"legacy_data_form_data\"\n assert (\n result[\"strategy_attempts\"][1][\"request_transport\"] == \"query_param_form_data\"\n )\n assert result[\"strategy_attempts\"][1][\"contains_root_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"contains_form_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"contains_query_object_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"request_param_keys\"] == [\"form_data\"]\n assert result[\"strategy_attempts\"][1][\"request_payload_keys\"] == []\n assert result[\"strategy_attempts\"][1][\"success\"] is False\n assert \"legacy data failed\" in result[\"strategy_attempts\"][1][\"error\"]\n\n assert result[\"strategy_attempts\"][2] == {\n \"endpoint\": \"/chart/data\",\n \"endpoint_kind\": \"v1_chart_data\",\n \"request_transport\": \"json_body\",\n \"contains_root_datasource\": True,\n \"contains_form_datasource\": False,\n \"contains_query_object_datasource\": False,\n \"request_param_keys\": [],\n \"request_payload_keys\": [\n \"datasource\",\n \"force\",\n \"form_data\",\n \"queries\",\n \"result_format\",\n \"result_type\",\n ],\n \"success\": True,\n }\n\n\n# [/DEF:test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures:Function]\n\n\n# [DEF:test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Preview query context should mirror chart-style filter transport so recovered native filters reach Superset compilation.\ndef test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data():\n client = SupersetClient(_make_environment())\n\n query_context = client.build_dataset_preview_query_context(\n dataset_id=7,\n dataset_record={\n \"id\": 7,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 7, \"type\": \"table\"},\n \"default_time_range\": \"Last year\",\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[\n {\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n },\n {\"filter_name\": \"status\", \"effective_value\": \"active\"},\n ],\n )\n\n assert query_context[\"force\"] is True\n assert query_context[\"result_type\"] == \"query\"\n assert query_context[\"datasource\"] == {\"id\": 7, \"type\": \"table\"}\n assert \"datasource\" not in query_context[\"queries\"][0]\n assert query_context[\"queries\"][0][\"result_type\"] == \"query\"\n assert query_context[\"queries\"][0][\"filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ]\n assert query_context[\"form_data\"][\"datasource\"] == \"7__table\"\n assert query_context[\"form_data\"][\"datasource_id\"] == 7\n assert query_context[\"form_data\"][\"datasource_type\"] == \"table\"\n assert query_context[\"form_data\"][\"extra_filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ]\n assert query_context[\"form_data\"][\"extra_form_data\"] == {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ],\n \"time_range\": \"Last year\",\n }\n assert query_context[\"form_data\"][\"url_params\"] == {\"country\": \"DE\"}\n\n\n# [/DEF:test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data:Function]\n\n\n# [DEF:test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Preview query context should merge dataset template params for parity with real dataset definitions while preserving explicit session overrides.\ndef test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values():\n client = SupersetClient(_make_environment())\n\n query_context = client.build_dataset_preview_query_context(\n dataset_id=8,\n dataset_record={\n \"id\": 8,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 8, \"type\": \"table\"},\n \"template_params\": json.dumps({\"region\": \"EMEA\", \"country\": \"FR\"}),\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[],\n )\n\n assert query_context[\"queries\"][0][\"url_params\"] == {\n \"region\": \"EMEA\",\n \"country\": \"DE\",\n }\n assert query_context[\"form_data\"][\"url_params\"] == {\n \"region\": \"EMEA\",\n \"country\": \"DE\",\n }\n\n\n# [/DEF:test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values:Function]\n\n\n# [DEF:test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Preview query context should preserve time-range native filter extras even when dataset defaults differ.\ndef test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload():\n client = SupersetClient(_make_environment())\n\n query_context = client.build_dataset_preview_query_context(\n dataset_id=9,\n dataset_record={\n \"id\": 9,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 9, \"type\": \"table\"},\n \"default_time_range\": \"Last year\",\n },\n template_params={},\n effective_filters=[\n {\n \"filter_name\": \"Order Date\",\n \"display_name\": \"Order Date\",\n \"effective_value\": \"2020-01-01 : 2020-12-31\",\n \"normalized_filter_payload\": {\n \"filter_clauses\": [],\n \"extra_form_data\": {\"time_range\": \"2020-01-01 : 2020-12-31\"},\n \"value_origin\": \"extra_form_data.time_range\",\n },\n }\n ],\n )\n\n assert query_context[\"queries\"][0][\"time_range\"] == \"2020-01-01 : 2020-12-31\"\n assert query_context[\"form_data\"][\"extra_form_data\"] == {\n \"time_range\": \"2020-01-01 : 2020-12-31\"\n }\n assert query_context[\"queries\"][0][\"filters\"] == []\n\n\n# [/DEF:test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload:Function]\n\n\n# [DEF:test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Legacy preview form_data should preserve recovered native filter clauses in browser-style fields without duplicating datasource for QueryObjectFactory.\ndef test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses():\n client = SupersetClient(_make_environment())\n\n legacy_form_data = client.build_dataset_preview_legacy_form_data(\n dataset_id=11,\n dataset_record={\n \"id\": 11,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 11, \"type\": \"table\"},\n \"default_time_range\": \"No filter\",\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[\n {\n \"filter_name\": \"Country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\", \"FR\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"time_range\": \"Last quarter\",\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n }\n ],\n )\n\n assert \"datasource\" not in legacy_form_data\n assert legacy_form_data[\"datasource_id\"] == 11\n assert legacy_form_data[\"datasource_type\"] == \"table\"\n assert legacy_form_data[\"extra_filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n assert legacy_form_data[\"extra_form_data\"] == {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"time_range\": \"Last quarter\",\n }\n assert legacy_form_data[\"time_range\"] == \"Last quarter\"\n assert legacy_form_data[\"url_params\"] == {\"country\": \"DE\"}\n assert legacy_form_data[\"result_type\"] == \"query\"\n\n\n# [/DEF:test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses:Function]\n\n\n# [DEF:test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Sync network client should reserve dashboard-not-found translation for dashboard endpoints only.\ndef test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic():\n client = APIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n\n with pytest.raises(SupersetAPIError) as exc_info:\n client._handle_http_error(\n _make_requests_http_error(404, \"http://superset.local/api/v1/chart/data\"),\n \"/chart/data\",\n )\n\n assert not isinstance(exc_info.value, DashboardNotFoundError)\n assert \"API resource not found at endpoint '/chart/data'\" in str(exc_info.value)\n\n\n# [/DEF:test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function]\n\n\n# [DEF:test_sync_network_404_mapping_translates_dashboard_endpoints:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Sync network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors.\ndef test_sync_network_404_mapping_translates_dashboard_endpoints():\n client = APIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n\n with pytest.raises(DashboardNotFoundError) as exc_info:\n client._handle_http_error(\n _make_requests_http_error(404, \"http://superset.local/api/v1/dashboard/10\"),\n \"/dashboard/10\",\n )\n\n assert \"Dashboard '/dashboard/10' Dashboard not found\" in str(exc_info.value)\n\n\n# [/DEF:test_sync_network_404_mapping_translates_dashboard_endpoints:Function]\n\n\n# [DEF:test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Async network client should reserve dashboard-not-found translation for dashboard endpoints only.\n@pytest.mark.asyncio\nasync def test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic():\n client = AsyncAPIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n\n try:\n with pytest.raises(SupersetAPIError) as exc_info:\n client._handle_http_error(\n _make_httpx_status_error(\n 404, \"http://superset.local/api/v1/chart/data\"\n ),\n \"/chart/data\",\n )\n\n assert not isinstance(exc_info.value, DashboardNotFoundError)\n assert \"API resource not found at endpoint '/chart/data'\" in str(exc_info.value)\n finally:\n await client.aclose()\n\n\n# [/DEF:test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function]\n\n\n# [DEF:test_async_network_404_mapping_translates_dashboard_endpoints:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Async network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors.\n@pytest.mark.asyncio\nasync def test_async_network_404_mapping_translates_dashboard_endpoints():\n client = AsyncAPIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n\n try:\n with pytest.raises(DashboardNotFoundError) as exc_info:\n client._handle_http_error(\n _make_httpx_status_error(\n 404, \"http://superset.local/api/v1/dashboard/10\"\n ),\n \"/dashboard/10\",\n )\n\n assert \"Dashboard '/dashboard/10' Dashboard not found\" in str(exc_info.value)\n finally:\n await client.aclose()\n\n\n# [/DEF:test_async_network_404_mapping_translates_dashboard_endpoints:Function]\n\n\n# [/DEF:SupersetPreviewPipelineTests:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values [TYPE Function]\n# @RELATION BINDS_TO -> NativeFilterExtractionTests\n# @BRIEF Assistant-facing imported-filter payload should redact likely PII while preserving structure metadata.\ndef test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values():\n result = sanitize_imported_filter_for_assistant(\n {\n \"filter_name\": \"customer_email\",\n \"raw_value\": [\"alice@example.com\", \"1234567890\"],\n \"normalized_value\": {\"filter_clauses\": []},\n \"source\": \"manual\",\n }\n )\n assert result[\"raw_value\"] == [\"***@example.com\", \"********90\"]\n assert result[\"raw_value_masked\"] is True\n assert result[\"normalized_value\"] == {\"filter_clauses\": []}\n# #endregion test_sanitize_imported_filter_for_assistant_masks_sensitive_raw_values\n" }, { "contract_id": "_make_requests_http_error", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 36, - "end_line": 50, + "start_line": 29, + "end_line": 41, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -28071,15 +26789,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_requests_http_error:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\ndef _make_requests_http_error(\n status_code: int, url: str\n) -> requests.exceptions.HTTPError:\n response = requests.Response()\n response.status_code = status_code\n response.url = url\n response._content = b'{\"message\":\"not found\"}'\n request = requests.Request(\"GET\", url).prepare()\n response.request = request\n return requests.exceptions.HTTPError(response=response, request=request)\n\n\n# [/DEF:_make_requests_http_error:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_requests_http_error [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\ndef _make_requests_http_error(\n status_code: int, url: str\n) -> requests.exceptions.HTTPError:\n response = requests.Response()\n response.status_code = status_code\n response.url = url\n response._content = b'{\"message\":\"not found\"}'\n request = requests.Request(\"GET\", url).prepare()\n response.request = request\n return requests.exceptions.HTTPError(response=response, request=request)\n# #endregion _make_requests_http_error\n" }, { "contract_id": "_make_httpx_status_error", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 53, - "end_line": 63, + "start_line": 42, + "end_line": 50, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -28102,15 +26820,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_make_httpx_status_error:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\ndef _make_httpx_status_error(status_code: int, url: str) -> httpx.HTTPStatusError:\n request = httpx.Request(\"GET\", url)\n response = httpx.Response(\n status_code=status_code, request=request, text='{\"message\":\"not found\"}'\n )\n return httpx.HTTPStatusError(\"upstream error\", request=request, response=response)\n\n\n# [/DEF:_make_httpx_status_error:Function]\n" + "anchor_syntax": "region", + "body": "# #region _make_httpx_status_error [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\ndef _make_httpx_status_error(status_code: int, url: str) -> httpx.HTTPStatusError:\n request = httpx.Request(\"GET\", url)\n response = httpx.Response(\n status_code=status_code, request=request, text='{\"message\":\"not found\"}'\n )\n return httpx.HTTPStatusError(\"upstream error\", request=request, response=response)\n# #endregion _make_httpx_status_error\n" }, { "contract_id": "test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 66, - "end_line": 149, + "start_line": 51, + "end_line": 128, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28135,15 +26853,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Superset preview compilation should prefer the legacy form_data transport inferred from browser traffic before falling back to chart-data.\ndef test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy():\n client = SupersetClient(_make_environment())\n client.get_dataset = MagicMock(\n return_value={\n \"result\": {\n \"id\": 42,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"result_format\": \"json\",\n \"result_type\": \"full\",\n }\n }\n )\n client.network = MagicMock()\n client.network.request.return_value = {\n \"result\": {\n \"query\": \"SELECT count(*) FROM public.sales WHERE country IN ('DE')\",\n }\n }\n\n result = client.compile_dataset_preview(\n dataset_id=42,\n template_params={\"country\": \"DE\"},\n effective_filters=[{\"filter_name\": \"country\", \"effective_value\": [\"DE\"]}],\n )\n\n assert (\n result[\"compiled_sql\"]\n == \"SELECT count(*) FROM public.sales WHERE country IN ('DE')\"\n )\n client.network.request.assert_called_once()\n request_call = client.network.request.call_args\n assert request_call.kwargs[\"method\"] == \"POST\"\n assert request_call.kwargs[\"endpoint\"] == \"/explore_json/form_data\"\n assert request_call.kwargs[\"params\"] is not None\n assert request_call.kwargs[\"params\"].keys() == {\"form_data\"}\n\n legacy_form_data = json.loads(request_call.kwargs[\"params\"][\"form_data\"])\n assert \"datasource\" not in legacy_form_data\n assert legacy_form_data[\"datasource_id\"] == 42\n assert legacy_form_data[\"datasource_type\"] == \"table\"\n assert legacy_form_data[\"extra_filters\"] == [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ]\n assert legacy_form_data[\"extra_form_data\"] == {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n }\n assert legacy_form_data[\"url_params\"] == {\"country\": \"DE\"}\n assert legacy_form_data[\"result_type\"] == \"query\"\n assert legacy_form_data[\"result_format\"] == \"json\"\n assert legacy_form_data[\"force\"] is True\n\n assert result[\"endpoint\"] == \"/explore_json/form_data\"\n assert result[\"endpoint_kind\"] == \"legacy_explore_form_data\"\n assert result[\"dataset_id\"] == 42\n assert result[\"response_diagnostics\"] == [\n {\"source\": \"query\", \"has_query\": False},\n {\"source\": \"sql\", \"has_query\": False},\n {\"source\": \"compiled_sql\", \"has_query\": False},\n {\"source\": \"result.query\", \"has_query\": True},\n ]\n assert result[\"legacy_form_data\"][\"extra_filters\"] == [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ]\n assert result[\"query_context\"][\"datasource\"] == {\"id\": 42, \"type\": \"table\"}\n assert result[\"strategy_attempts\"] == [\n {\n \"endpoint\": \"/explore_json/form_data\",\n \"endpoint_kind\": \"legacy_explore_form_data\",\n \"request_transport\": \"query_param_form_data\",\n \"contains_root_datasource\": False,\n \"contains_form_datasource\": False,\n \"contains_query_object_datasource\": False,\n \"request_param_keys\": [\"form_data\"],\n \"request_payload_keys\": [],\n \"success\": True,\n }\n ]\n\n\n# [/DEF:test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Superset preview compilation should prefer the legacy form_data transport inferred from browser traffic before falling back to chart-data.\ndef test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy():\n client = SupersetClient(_make_environment())\n client.get_dataset = MagicMock(\n return_value={\n \"result\": {\n \"id\": 42,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"result_format\": \"json\",\n \"result_type\": \"full\",\n }\n }\n )\n client.network = MagicMock()\n client.network.request.return_value = {\n \"result\": {\n \"query\": \"SELECT count(*) FROM public.sales WHERE country IN ('DE')\",\n }\n }\n result = client.compile_dataset_preview(\n dataset_id=42,\n template_params={\"country\": \"DE\"},\n effective_filters=[{\"filter_name\": \"country\", \"effective_value\": [\"DE\"]}],\n )\n assert (\n result[\"compiled_sql\"]\n == \"SELECT count(*) FROM public.sales WHERE country IN ('DE')\"\n )\n client.network.request.assert_called_once()\n request_call = client.network.request.call_args\n assert request_call.kwargs[\"method\"] == \"POST\"\n assert request_call.kwargs[\"endpoint\"] == \"/explore_json/form_data\"\n assert request_call.kwargs[\"params\"] is not None\n assert request_call.kwargs[\"params\"].keys() == {\"form_data\"}\n legacy_form_data = json.loads(request_call.kwargs[\"params\"][\"form_data\"])\n assert \"datasource\" not in legacy_form_data\n assert legacy_form_data[\"datasource_id\"] == 42\n assert legacy_form_data[\"datasource_type\"] == \"table\"\n assert legacy_form_data[\"extra_filters\"] == [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ]\n assert legacy_form_data[\"extra_form_data\"] == {\n \"filters\": [{\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n }\n assert legacy_form_data[\"url_params\"] == {\"country\": \"DE\"}\n assert legacy_form_data[\"result_type\"] == \"query\"\n assert legacy_form_data[\"result_format\"] == \"json\"\n assert legacy_form_data[\"force\"] is True\n assert result[\"endpoint\"] == \"/explore_json/form_data\"\n assert result[\"endpoint_kind\"] == \"legacy_explore_form_data\"\n assert result[\"dataset_id\"] == 42\n assert result[\"response_diagnostics\"] == [\n {\"source\": \"query\", \"has_query\": False},\n {\"source\": \"sql\", \"has_query\": False},\n {\"source\": \"compiled_sql\", \"has_query\": False},\n {\"source\": \"result.query\", \"has_query\": True},\n ]\n assert result[\"legacy_form_data\"][\"extra_filters\"] == [\n {\"col\": \"country\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ]\n assert result[\"query_context\"][\"datasource\"] == {\"id\": 42, \"type\": \"table\"}\n assert result[\"strategy_attempts\"] == [\n {\n \"endpoint\": \"/explore_json/form_data\",\n \"endpoint_kind\": \"legacy_explore_form_data\",\n \"request_transport\": \"query_param_form_data\",\n \"contains_root_datasource\": False,\n \"contains_form_datasource\": False,\n \"contains_query_object_datasource\": False,\n \"request_param_keys\": [\"form_data\"],\n \"request_payload_keys\": [],\n \"success\": True,\n }\n ]\n# #endregion test_compile_dataset_preview_prefers_legacy_explore_form_data_strategy\n" }, { "contract_id": "test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 152, - "end_line": 246, + "start_line": 129, + "end_line": 215, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28168,15 +26886,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Superset preview compilation should fall back to chart-data when legacy form_data strategies are rejected.\ndef test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures():\n client = SupersetClient(_make_environment())\n client.get_dataset = MagicMock(\n return_value={\n \"result\": {\n \"id\": 42,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n }\n }\n )\n client.network = MagicMock()\n client.network.request.side_effect = [\n SupersetAPIError(\"legacy explore failed\"),\n SupersetAPIError(\"legacy data failed\"),\n {\n \"result\": [\n {\n \"query\": \"SELECT count(*) FROM public.sales\",\n }\n ]\n },\n ]\n\n result = client.compile_dataset_preview(dataset_id=42)\n\n assert client.network.request.call_count == 3\n first_call = client.network.request.call_args_list[0]\n second_call = client.network.request.call_args_list[1]\n third_call = client.network.request.call_args_list[2]\n assert first_call.kwargs[\"endpoint\"] == \"/explore_json/form_data\"\n assert second_call.kwargs[\"endpoint\"] == \"/data\"\n assert third_call.kwargs[\"endpoint\"] == \"/chart/data\"\n assert third_call.kwargs[\"headers\"] == {\"Content-Type\": \"application/json\"}\n\n first_legacy_form_data = json.loads(first_call.kwargs[\"params\"][\"form_data\"])\n second_legacy_form_data = json.loads(second_call.kwargs[\"params\"][\"form_data\"])\n assert \"datasource\" not in first_legacy_form_data\n assert \"datasource\" not in second_legacy_form_data\n\n query_context = json.loads(third_call.kwargs[\"data\"])\n assert query_context[\"datasource\"] == {\"id\": 42, \"type\": \"table\"}\n assert result[\"endpoint\"] == \"/chart/data\"\n assert result[\"endpoint_kind\"] == \"v1_chart_data\"\n assert len(result[\"strategy_attempts\"]) == 3\n assert result[\"strategy_attempts\"][0][\"endpoint\"] == \"/explore_json/form_data\"\n assert result[\"strategy_attempts\"][0][\"endpoint_kind\"] == \"legacy_explore_form_data\"\n assert (\n result[\"strategy_attempts\"][0][\"request_transport\"] == \"query_param_form_data\"\n )\n assert result[\"strategy_attempts\"][0][\"contains_root_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"contains_form_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"contains_query_object_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"request_param_keys\"] == [\"form_data\"]\n assert result[\"strategy_attempts\"][0][\"request_payload_keys\"] == []\n assert result[\"strategy_attempts\"][0][\"success\"] is False\n assert \"legacy explore failed\" in result[\"strategy_attempts\"][0][\"error\"]\n\n assert result[\"strategy_attempts\"][1][\"endpoint\"] == \"/data\"\n assert result[\"strategy_attempts\"][1][\"endpoint_kind\"] == \"legacy_data_form_data\"\n assert (\n result[\"strategy_attempts\"][1][\"request_transport\"] == \"query_param_form_data\"\n )\n assert result[\"strategy_attempts\"][1][\"contains_root_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"contains_form_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"contains_query_object_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"request_param_keys\"] == [\"form_data\"]\n assert result[\"strategy_attempts\"][1][\"request_payload_keys\"] == []\n assert result[\"strategy_attempts\"][1][\"success\"] is False\n assert \"legacy data failed\" in result[\"strategy_attempts\"][1][\"error\"]\n\n assert result[\"strategy_attempts\"][2] == {\n \"endpoint\": \"/chart/data\",\n \"endpoint_kind\": \"v1_chart_data\",\n \"request_transport\": \"json_body\",\n \"contains_root_datasource\": True,\n \"contains_form_datasource\": False,\n \"contains_query_object_datasource\": False,\n \"request_param_keys\": [],\n \"request_payload_keys\": [\n \"datasource\",\n \"force\",\n \"form_data\",\n \"queries\",\n \"result_format\",\n \"result_type\",\n ],\n \"success\": True,\n }\n\n\n# [/DEF:test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Superset preview compilation should fall back to chart-data when legacy form_data strategies are rejected.\ndef test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures():\n client = SupersetClient(_make_environment())\n client.get_dataset = MagicMock(\n return_value={\n \"result\": {\n \"id\": 42,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n }\n }\n )\n client.network = MagicMock()\n client.network.request.side_effect = [\n SupersetAPIError(\"legacy explore failed\"),\n SupersetAPIError(\"legacy data failed\"),\n {\n \"result\": [\n {\n \"query\": \"SELECT count(*) FROM public.sales\",\n }\n ]\n },\n ]\n result = client.compile_dataset_preview(dataset_id=42)\n assert client.network.request.call_count == 3\n first_call = client.network.request.call_args_list[0]\n second_call = client.network.request.call_args_list[1]\n third_call = client.network.request.call_args_list[2]\n assert first_call.kwargs[\"endpoint\"] == \"/explore_json/form_data\"\n assert second_call.kwargs[\"endpoint\"] == \"/data\"\n assert third_call.kwargs[\"endpoint\"] == \"/chart/data\"\n assert third_call.kwargs[\"headers\"] == {\"Content-Type\": \"application/json\"}\n first_legacy_form_data = json.loads(first_call.kwargs[\"params\"][\"form_data\"])\n second_legacy_form_data = json.loads(second_call.kwargs[\"params\"][\"form_data\"])\n assert \"datasource\" not in first_legacy_form_data\n assert \"datasource\" not in second_legacy_form_data\n query_context = json.loads(third_call.kwargs[\"data\"])\n assert query_context[\"datasource\"] == {\"id\": 42, \"type\": \"table\"}\n assert result[\"endpoint\"] == \"/chart/data\"\n assert result[\"endpoint_kind\"] == \"v1_chart_data\"\n assert len(result[\"strategy_attempts\"]) == 3\n assert result[\"strategy_attempts\"][0][\"endpoint\"] == \"/explore_json/form_data\"\n assert result[\"strategy_attempts\"][0][\"endpoint_kind\"] == \"legacy_explore_form_data\"\n assert (\n result[\"strategy_attempts\"][0][\"request_transport\"] == \"query_param_form_data\"\n )\n assert result[\"strategy_attempts\"][0][\"contains_root_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"contains_form_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"contains_query_object_datasource\"] is False\n assert result[\"strategy_attempts\"][0][\"request_param_keys\"] == [\"form_data\"]\n assert result[\"strategy_attempts\"][0][\"request_payload_keys\"] == []\n assert result[\"strategy_attempts\"][0][\"success\"] is False\n assert \"legacy explore failed\" in result[\"strategy_attempts\"][0][\"error\"]\n assert result[\"strategy_attempts\"][1][\"endpoint\"] == \"/data\"\n assert result[\"strategy_attempts\"][1][\"endpoint_kind\"] == \"legacy_data_form_data\"\n assert (\n result[\"strategy_attempts\"][1][\"request_transport\"] == \"query_param_form_data\"\n )\n assert result[\"strategy_attempts\"][1][\"contains_root_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"contains_form_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"contains_query_object_datasource\"] is False\n assert result[\"strategy_attempts\"][1][\"request_param_keys\"] == [\"form_data\"]\n assert result[\"strategy_attempts\"][1][\"request_payload_keys\"] == []\n assert result[\"strategy_attempts\"][1][\"success\"] is False\n assert \"legacy data failed\" in result[\"strategy_attempts\"][1][\"error\"]\n assert result[\"strategy_attempts\"][2] == {\n \"endpoint\": \"/chart/data\",\n \"endpoint_kind\": \"v1_chart_data\",\n \"request_transport\": \"json_body\",\n \"contains_root_datasource\": True,\n \"contains_form_datasource\": False,\n \"contains_query_object_datasource\": False,\n \"request_param_keys\": [],\n \"request_payload_keys\": [\n \"datasource\",\n \"force\",\n \"form_data\",\n \"queries\",\n \"result_format\",\n \"result_type\",\n ],\n \"success\": True,\n }\n# #endregion test_compile_dataset_preview_falls_back_to_chart_data_after_legacy_failures\n" }, { "contract_id": "test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 249, - "end_line": 309, + "start_line": 216, + "end_line": 272, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28201,15 +26919,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Preview query context should mirror chart-style filter transport so recovered native filters reach Superset compilation.\ndef test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data():\n client = SupersetClient(_make_environment())\n\n query_context = client.build_dataset_preview_query_context(\n dataset_id=7,\n dataset_record={\n \"id\": 7,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 7, \"type\": \"table\"},\n \"default_time_range\": \"Last year\",\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[\n {\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n },\n {\"filter_name\": \"status\", \"effective_value\": \"active\"},\n ],\n )\n\n assert query_context[\"force\"] is True\n assert query_context[\"result_type\"] == \"query\"\n assert query_context[\"datasource\"] == {\"id\": 7, \"type\": \"table\"}\n assert \"datasource\" not in query_context[\"queries\"][0]\n assert query_context[\"queries\"][0][\"result_type\"] == \"query\"\n assert query_context[\"queries\"][0][\"filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ]\n assert query_context[\"form_data\"][\"datasource\"] == \"7__table\"\n assert query_context[\"form_data\"][\"datasource_id\"] == 7\n assert query_context[\"form_data\"][\"datasource_type\"] == \"table\"\n assert query_context[\"form_data\"][\"extra_filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ]\n assert query_context[\"form_data\"][\"extra_form_data\"] == {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ],\n \"time_range\": \"Last year\",\n }\n assert query_context[\"form_data\"][\"url_params\"] == {\"country\": \"DE\"}\n\n\n# [/DEF:test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Preview query context should mirror chart-style filter transport so recovered native filters reach Superset compilation.\ndef test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data():\n client = SupersetClient(_make_environment())\n query_context = client.build_dataset_preview_query_context(\n dataset_id=7,\n dataset_record={\n \"id\": 7,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 7, \"type\": \"table\"},\n \"default_time_range\": \"Last year\",\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[\n {\n \"filter_name\": \"country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]}]\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n },\n {\"filter_name\": \"status\", \"effective_value\": \"active\"},\n ],\n )\n assert query_context[\"force\"] is True\n assert query_context[\"result_type\"] == \"query\"\n assert query_context[\"datasource\"] == {\"id\": 7, \"type\": \"table\"}\n assert \"datasource\" not in query_context[\"queries\"][0]\n assert query_context[\"queries\"][0][\"result_type\"] == \"query\"\n assert query_context[\"queries\"][0][\"filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ]\n assert query_context[\"form_data\"][\"datasource\"] == \"7__table\"\n assert query_context[\"form_data\"][\"datasource_id\"] == 7\n assert query_context[\"form_data\"][\"datasource_type\"] == \"table\"\n assert query_context[\"form_data\"][\"extra_filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ]\n assert query_context[\"form_data\"][\"extra_form_data\"] == {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\"]},\n {\"col\": \"status\", \"op\": \"==\", \"val\": \"active\"},\n ],\n \"time_range\": \"Last year\",\n }\n assert query_context[\"form_data\"][\"url_params\"] == {\"country\": \"DE\"}\n# #endregion test_build_dataset_preview_query_context_places_recovered_filters_in_chart_style_form_data\n" }, { "contract_id": "test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 312, - "end_line": 340, + "start_line": 273, + "end_line": 297, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28234,15 +26952,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Preview query context should merge dataset template params for parity with real dataset definitions while preserving explicit session overrides.\ndef test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values():\n client = SupersetClient(_make_environment())\n\n query_context = client.build_dataset_preview_query_context(\n dataset_id=8,\n dataset_record={\n \"id\": 8,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 8, \"type\": \"table\"},\n \"template_params\": json.dumps({\"region\": \"EMEA\", \"country\": \"FR\"}),\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[],\n )\n\n assert query_context[\"queries\"][0][\"url_params\"] == {\n \"region\": \"EMEA\",\n \"country\": \"DE\",\n }\n assert query_context[\"form_data\"][\"url_params\"] == {\n \"region\": \"EMEA\",\n \"country\": \"DE\",\n }\n\n\n# [/DEF:test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Preview query context should merge dataset template params for parity with real dataset definitions while preserving explicit session overrides.\ndef test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values():\n client = SupersetClient(_make_environment())\n query_context = client.build_dataset_preview_query_context(\n dataset_id=8,\n dataset_record={\n \"id\": 8,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 8, \"type\": \"table\"},\n \"template_params\": json.dumps({\"region\": \"EMEA\", \"country\": \"FR\"}),\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[],\n )\n assert query_context[\"queries\"][0][\"url_params\"] == {\n \"region\": \"EMEA\",\n \"country\": \"DE\",\n }\n assert query_context[\"form_data\"][\"url_params\"] == {\n \"region\": \"EMEA\",\n \"country\": \"DE\",\n }\n# #endregion test_build_dataset_preview_query_context_merges_dataset_template_params_and_preserves_user_values\n" }, { "contract_id": "test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 343, - "end_line": 379, + "start_line": 298, + "end_line": 330, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28267,15 +26985,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Preview query context should preserve time-range native filter extras even when dataset defaults differ.\ndef test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload():\n client = SupersetClient(_make_environment())\n\n query_context = client.build_dataset_preview_query_context(\n dataset_id=9,\n dataset_record={\n \"id\": 9,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 9, \"type\": \"table\"},\n \"default_time_range\": \"Last year\",\n },\n template_params={},\n effective_filters=[\n {\n \"filter_name\": \"Order Date\",\n \"display_name\": \"Order Date\",\n \"effective_value\": \"2020-01-01 : 2020-12-31\",\n \"normalized_filter_payload\": {\n \"filter_clauses\": [],\n \"extra_form_data\": {\"time_range\": \"2020-01-01 : 2020-12-31\"},\n \"value_origin\": \"extra_form_data.time_range\",\n },\n }\n ],\n )\n\n assert query_context[\"queries\"][0][\"time_range\"] == \"2020-01-01 : 2020-12-31\"\n assert query_context[\"form_data\"][\"extra_form_data\"] == {\n \"time_range\": \"2020-01-01 : 2020-12-31\"\n }\n assert query_context[\"queries\"][0][\"filters\"] == []\n\n\n# [/DEF:test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Preview query context should preserve time-range native filter extras even when dataset defaults differ.\ndef test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload():\n client = SupersetClient(_make_environment())\n query_context = client.build_dataset_preview_query_context(\n dataset_id=9,\n dataset_record={\n \"id\": 9,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 9, \"type\": \"table\"},\n \"default_time_range\": \"Last year\",\n },\n template_params={},\n effective_filters=[\n {\n \"filter_name\": \"Order Date\",\n \"display_name\": \"Order Date\",\n \"effective_value\": \"2020-01-01 : 2020-12-31\",\n \"normalized_filter_payload\": {\n \"filter_clauses\": [],\n \"extra_form_data\": {\"time_range\": \"2020-01-01 : 2020-12-31\"},\n \"value_origin\": \"extra_form_data.time_range\",\n },\n }\n ],\n )\n assert query_context[\"queries\"][0][\"time_range\"] == \"2020-01-01 : 2020-12-31\"\n assert query_context[\"form_data\"][\"extra_form_data\"] == {\n \"time_range\": \"2020-01-01 : 2020-12-31\"\n }\n assert query_context[\"queries\"][0][\"filters\"] == []\n# #endregion test_build_dataset_preview_query_context_preserves_time_range_from_native_filter_payload\n" }, { "contract_id": "test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 382, - "end_line": 433, + "start_line": 331, + "end_line": 378, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28300,15 +27018,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Legacy preview form_data should preserve recovered native filter clauses in browser-style fields without duplicating datasource for QueryObjectFactory.\ndef test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses():\n client = SupersetClient(_make_environment())\n\n legacy_form_data = client.build_dataset_preview_legacy_form_data(\n dataset_id=11,\n dataset_record={\n \"id\": 11,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 11, \"type\": \"table\"},\n \"default_time_range\": \"No filter\",\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[\n {\n \"filter_name\": \"Country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\", \"FR\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"time_range\": \"Last quarter\",\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n }\n ],\n )\n\n assert \"datasource\" not in legacy_form_data\n assert legacy_form_data[\"datasource_id\"] == 11\n assert legacy_form_data[\"datasource_type\"] == \"table\"\n assert legacy_form_data[\"extra_filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n assert legacy_form_data[\"extra_form_data\"] == {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"time_range\": \"Last quarter\",\n }\n assert legacy_form_data[\"time_range\"] == \"Last quarter\"\n assert legacy_form_data[\"url_params\"] == {\"country\": \"DE\"}\n assert legacy_form_data[\"result_type\"] == \"query\"\n\n\n# [/DEF:test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Legacy preview form_data should preserve recovered native filter clauses in browser-style fields without duplicating datasource for QueryObjectFactory.\ndef test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses():\n client = SupersetClient(_make_environment())\n legacy_form_data = client.build_dataset_preview_legacy_form_data(\n dataset_id=11,\n dataset_record={\n \"id\": 11,\n \"schema\": \"public\",\n \"datasource\": {\"id\": 11, \"type\": \"table\"},\n \"default_time_range\": \"No filter\",\n },\n template_params={\"country\": \"DE\"},\n effective_filters=[\n {\n \"filter_name\": \"Country\",\n \"display_name\": \"Country\",\n \"effective_value\": [\"DE\", \"FR\"],\n \"normalized_filter_payload\": {\n \"filter_clauses\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"extra_form_data\": {\n \"filters\": [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ],\n \"time_range\": \"Last quarter\",\n },\n \"value_origin\": \"extra_form_data.filters\",\n },\n }\n ],\n )\n assert \"datasource\" not in legacy_form_data\n assert legacy_form_data[\"datasource_id\"] == 11\n assert legacy_form_data[\"datasource_type\"] == \"table\"\n assert legacy_form_data[\"extra_filters\"] == [\n {\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}\n ]\n assert legacy_form_data[\"extra_form_data\"] == {\n \"filters\": [{\"col\": \"country_code\", \"op\": \"IN\", \"val\": [\"DE\", \"FR\"]}],\n \"time_range\": \"Last quarter\",\n }\n assert legacy_form_data[\"time_range\"] == \"Last quarter\"\n assert legacy_form_data[\"url_params\"] == {\"country\": \"DE\"}\n assert legacy_form_data[\"result_type\"] == \"query\"\n# #endregion test_build_dataset_preview_legacy_form_data_preserves_native_filter_clauses\n" }, { "contract_id": "test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 436, - "end_line": 457, + "start_line": 379, + "end_line": 396, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28333,15 +27051,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Sync network client should reserve dashboard-not-found translation for dashboard endpoints only.\ndef test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic():\n client = APIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n\n with pytest.raises(SupersetAPIError) as exc_info:\n client._handle_http_error(\n _make_requests_http_error(404, \"http://superset.local/api/v1/chart/data\"),\n \"/chart/data\",\n )\n\n assert not isinstance(exc_info.value, DashboardNotFoundError)\n assert \"API resource not found at endpoint '/chart/data'\" in str(exc_info.value)\n\n\n# [/DEF:test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Sync network client should reserve dashboard-not-found translation for dashboard endpoints only.\ndef test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic():\n client = APIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n with pytest.raises(SupersetAPIError) as exc_info:\n client._handle_http_error(\n _make_requests_http_error(404, \"http://superset.local/api/v1/chart/data\"),\n \"/chart/data\",\n )\n assert not isinstance(exc_info.value, DashboardNotFoundError)\n assert \"API resource not found at endpoint '/chart/data'\" in str(exc_info.value)\n# #endregion test_sync_network_404_mapping_keeps_non_dashboard_endpoints_generic\n" }, { "contract_id": "test_sync_network_404_mapping_translates_dashboard_endpoints", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 460, - "end_line": 480, + "start_line": 397, + "end_line": 413, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28366,15 +27084,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_sync_network_404_mapping_translates_dashboard_endpoints:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Sync network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors.\ndef test_sync_network_404_mapping_translates_dashboard_endpoints():\n client = APIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n\n with pytest.raises(DashboardNotFoundError) as exc_info:\n client._handle_http_error(\n _make_requests_http_error(404, \"http://superset.local/api/v1/dashboard/10\"),\n \"/dashboard/10\",\n )\n\n assert \"Dashboard '/dashboard/10' Dashboard not found\" in str(exc_info.value)\n\n\n# [/DEF:test_sync_network_404_mapping_translates_dashboard_endpoints:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_sync_network_404_mapping_translates_dashboard_endpoints [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Sync network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors.\ndef test_sync_network_404_mapping_translates_dashboard_endpoints():\n client = APIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n with pytest.raises(DashboardNotFoundError) as exc_info:\n client._handle_http_error(\n _make_requests_http_error(404, \"http://superset.local/api/v1/dashboard/10\"),\n \"/dashboard/10\",\n )\n assert \"Dashboard '/dashboard/10' Dashboard not found\" in str(exc_info.value)\n# #endregion test_sync_network_404_mapping_translates_dashboard_endpoints\n" }, { "contract_id": "test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 483, - "end_line": 510, + "start_line": 414, + "end_line": 437, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28399,15 +27117,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Async network client should reserve dashboard-not-found translation for dashboard endpoints only.\n@pytest.mark.asyncio\nasync def test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic():\n client = AsyncAPIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n\n try:\n with pytest.raises(SupersetAPIError) as exc_info:\n client._handle_http_error(\n _make_httpx_status_error(\n 404, \"http://superset.local/api/v1/chart/data\"\n ),\n \"/chart/data\",\n )\n\n assert not isinstance(exc_info.value, DashboardNotFoundError)\n assert \"API resource not found at endpoint '/chart/data'\" in str(exc_info.value)\n finally:\n await client.aclose()\n\n\n# [/DEF:test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Async network client should reserve dashboard-not-found translation for dashboard endpoints only.\n@pytest.mark.asyncio\nasync def test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic():\n client = AsyncAPIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n try:\n with pytest.raises(SupersetAPIError) as exc_info:\n client._handle_http_error(\n _make_httpx_status_error(\n 404, \"http://superset.local/api/v1/chart/data\"\n ),\n \"/chart/data\",\n )\n assert not isinstance(exc_info.value, DashboardNotFoundError)\n assert \"API resource not found at endpoint '/chart/data'\" in str(exc_info.value)\n finally:\n await client.aclose()\n# #endregion test_async_network_404_mapping_keeps_non_dashboard_endpoints_generic\n" }, { "contract_id": "test_async_network_404_mapping_translates_dashboard_endpoints", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_preview_pipeline.py", - "start_line": 513, - "end_line": 539, + "start_line": 438, + "end_line": 460, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28432,111 +27150,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_async_network_404_mapping_translates_dashboard_endpoints:Function]\n# @RELATION: BINDS_TO -> SupersetPreviewPipelineTests\n# @PURPOSE: Async network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors.\n@pytest.mark.asyncio\nasync def test_async_network_404_mapping_translates_dashboard_endpoints():\n client = AsyncAPIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n\n try:\n with pytest.raises(DashboardNotFoundError) as exc_info:\n client._handle_http_error(\n _make_httpx_status_error(\n 404, \"http://superset.local/api/v1/dashboard/10\"\n ),\n \"/dashboard/10\",\n )\n\n assert \"Dashboard '/dashboard/10' Dashboard not found\" in str(exc_info.value)\n finally:\n await client.aclose()\n\n\n# [/DEF:test_async_network_404_mapping_translates_dashboard_endpoints:Function]\n" - }, - { - "contract_id": "TestSupersetProfileLookup", - "contract_type": "Module", - "file_path": "backend/src/core/__tests__/test_superset_profile_lookup.py", - "start_line": 1, - "end_line": 153, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Domain", - "PURPOSE": "Verifies Superset profile lookup adapter payload normalization and fallback error precedence.", - "SEMANTICS": [ - "tests", - "superset", - "profile", - "lookup", - "fallback", - "sorting" - ] - }, - "relations": [ - { - "source_id": "TestSupersetProfileLookup", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestSupersetProfileLookup:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, superset, profile, lookup, fallback, sorting\n# @PURPOSE: Verifies Superset profile lookup adapter payload normalization and fallback error precedence.\n# @LAYER: Domain\n\n# [SECTION: IMPORTS]\nimport json\nimport sys\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional\n\nimport pytest\n\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.superset_profile_lookup import SupersetAccountLookupAdapter\nfrom src.core.utils.network import AuthenticationError, SupersetAPIError\n# [/SECTION]\n\n\n# [DEF:_RecordingNetworkClient:Class]\n# @RELATION: BINDS_TO -> TestSupersetProfileLookup\n# @COMPLEXITY: 2\n# @PURPOSE: Records request payloads and returns scripted responses for deterministic adapter tests.\n# @INVARIANT: Each request consumes one scripted response in call order and persists call metadata.\nclass _RecordingNetworkClient:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes scripted network responses.\n # @PRE: scripted_responses is ordered per expected request sequence.\n # @POST: Instance stores response script and captures subsequent request calls.\n def __init__(self, scripted_responses: List[Any]):\n self._scripted_responses = scripted_responses\n self.calls: List[Dict[str, Any]] = []\n\n # [/DEF:__init__:Function]\n\n # [DEF:request:Function]\n # @PURPOSE: Mimics APIClient.request while capturing call arguments.\n # @PRE: method and endpoint are provided.\n # @POST: Returns scripted response or raises scripted exception.\n def request(\n self,\n method: str,\n endpoint: str,\n params: Optional[Dict[str, Any]] = None,\n **kwargs,\n ) -> Dict[str, Any]:\n self.calls.append(\n {\n \"method\": method,\n \"endpoint\": endpoint,\n \"params\": params or {},\n }\n )\n index = len(self.calls) - 1\n response = self._scripted_responses[index]\n if isinstance(response, Exception):\n raise response\n return response\n\n # [/DEF:request:Function]\n\n\n# [/DEF:_RecordingNetworkClient:Class]\n\n\n# [DEF:test_get_users_page_sends_lowercase_order_direction:Function]\n# @RELATION: BINDS_TO -> TestSupersetProfileLookup\n# @PURPOSE: Ensures adapter sends lowercase order_direction compatible with Superset rison schema.\n# @PRE: Adapter is initialized with recording network client.\n# @POST: First request query payload contains order_direction='asc' for asc sort.\ndef test_get_users_page_sends_lowercase_order_direction():\n client = _RecordingNetworkClient(\n scripted_responses=[{\"result\": [{\"username\": \"admin\"}], \"count\": 1}]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n\n adapter.get_users_page(\n search=\"admin\",\n page_index=0,\n page_size=20,\n sort_column=\"username\",\n sort_order=\"asc\",\n )\n\n sent_query = json.loads(client.calls[0][\"params\"][\"q\"])\n assert sent_query[\"order_direction\"] == \"asc\"\n\n\n# [/DEF:test_get_users_page_sends_lowercase_order_direction:Function]\n\n\n# [DEF:test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error:Function]\n# @RELATION: BINDS_TO -> TestSupersetProfileLookup\n# @PURPOSE: Ensures fallback auth error does not mask primary schema/query failure.\n# @PRE: Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError.\n# @POST: Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause.\ndef test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error():\n client = _RecordingNetworkClient(\n scripted_responses=[\n SupersetAPIError(\"API Error 400: bad rison schema\"),\n AuthenticationError(),\n ]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n\n with pytest.raises(SupersetAPIError) as exc_info:\n adapter.get_users_page(sort_order=\"asc\")\n\n assert \"API Error 400\" in str(exc_info.value)\n assert not isinstance(exc_info.value, AuthenticationError)\n\n\n# [/DEF:test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error:Function]\n\n\n# [DEF:test_get_users_page_uses_fallback_endpoint_when_primary_fails:Function]\n# @RELATION: BINDS_TO -> TestSupersetProfileLookup\n# @PURPOSE: Verifies adapter retries second users endpoint and succeeds when fallback is healthy.\n# @PRE: Primary endpoint fails; fallback returns valid users payload.\n# @POST: Result status is success and both endpoints were attempted in order.\ndef test_get_users_page_uses_fallback_endpoint_when_primary_fails():\n client = _RecordingNetworkClient(\n scripted_responses=[\n SupersetAPIError(\"Primary endpoint failed\"),\n {\"result\": [{\"username\": \"admin\"}], \"count\": 1},\n ]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n\n result = adapter.get_users_page()\n\n assert result[\"status\"] == \"success\"\n assert [call[\"endpoint\"] for call in client.calls] == [\n \"/security/users/\",\n \"/security/users\",\n ]\n\n\n# [/DEF:test_get_users_page_uses_fallback_endpoint_when_primary_fails:Function]\n\n\n# [/DEF:TestSupersetProfileLookup:Module]\n" - }, - { - "contract_id": "_RecordingNetworkClient", - "contract_type": "Class", - "file_path": "backend/src/core/__tests__/test_superset_profile_lookup.py", - "start_line": 25, - "end_line": 68, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "INVARIANT": "Each request consumes one scripted response in call order and persists call metadata.", - "PURPOSE": "Records request payloads and returns scripted responses for deterministic adapter tests." - }, - "relations": [ - { - "source_id": "_RecordingNetworkClient", - "relation_type": "BINDS_TO", - "target_id": "TestSupersetProfileLookup", - "target_ref": "TestSupersetProfileLookup" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_RecordingNetworkClient:Class]\n# @RELATION: BINDS_TO -> TestSupersetProfileLookup\n# @COMPLEXITY: 2\n# @PURPOSE: Records request payloads and returns scripted responses for deterministic adapter tests.\n# @INVARIANT: Each request consumes one scripted response in call order and persists call metadata.\nclass _RecordingNetworkClient:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes scripted network responses.\n # @PRE: scripted_responses is ordered per expected request sequence.\n # @POST: Instance stores response script and captures subsequent request calls.\n def __init__(self, scripted_responses: List[Any]):\n self._scripted_responses = scripted_responses\n self.calls: List[Dict[str, Any]] = []\n\n # [/DEF:__init__:Function]\n\n # [DEF:request:Function]\n # @PURPOSE: Mimics APIClient.request while capturing call arguments.\n # @PRE: method and endpoint are provided.\n # @POST: Returns scripted response or raises scripted exception.\n def request(\n self,\n method: str,\n endpoint: str,\n params: Optional[Dict[str, Any]] = None,\n **kwargs,\n ) -> Dict[str, Any]:\n self.calls.append(\n {\n \"method\": method,\n \"endpoint\": endpoint,\n \"params\": params or {},\n }\n )\n index = len(self.calls) - 1\n response = self._scripted_responses[index]\n if isinstance(response, Exception):\n raise response\n return response\n\n # [/DEF:request:Function]\n\n\n# [/DEF:_RecordingNetworkClient:Class]\n" + "anchor_syntax": "region", + "body": "# #region test_async_network_404_mapping_translates_dashboard_endpoints [TYPE Function]\n# @RELATION BINDS_TO -> SupersetPreviewPipelineTests\n# @BRIEF Async network client should still translate dashboard endpoint 404 responses into dashboard-not-found errors.\n@pytest.mark.asyncio\nasync def test_async_network_404_mapping_translates_dashboard_endpoints():\n client = AsyncAPIClient(\n config={\n \"base_url\": \"http://superset.local\",\n \"auth\": {\"username\": \"demo\", \"password\": \"secret\"},\n }\n )\n try:\n with pytest.raises(DashboardNotFoundError) as exc_info:\n client._handle_http_error(\n _make_httpx_status_error(\n 404, \"http://superset.local/api/v1/dashboard/10\"\n ),\n \"/dashboard/10\",\n )\n assert \"Dashboard '/dashboard/10' Dashboard not found\" in str(exc_info.value)\n finally:\n await client.aclose()\n# #endregion test_async_network_404_mapping_translates_dashboard_endpoints\n" }, { "contract_id": "test_get_users_page_sends_lowercase_order_direction", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_profile_lookup.py", - "start_line": 71, - "end_line": 96, + "start_line": 59, + "end_line": 80, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28581,15 +27203,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_users_page_sends_lowercase_order_direction:Function]\n# @RELATION: BINDS_TO -> TestSupersetProfileLookup\n# @PURPOSE: Ensures adapter sends lowercase order_direction compatible with Superset rison schema.\n# @PRE: Adapter is initialized with recording network client.\n# @POST: First request query payload contains order_direction='asc' for asc sort.\ndef test_get_users_page_sends_lowercase_order_direction():\n client = _RecordingNetworkClient(\n scripted_responses=[{\"result\": [{\"username\": \"admin\"}], \"count\": 1}]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n\n adapter.get_users_page(\n search=\"admin\",\n page_index=0,\n page_size=20,\n sort_column=\"username\",\n sort_order=\"asc\",\n )\n\n sent_query = json.loads(client.calls[0][\"params\"][\"q\"])\n assert sent_query[\"order_direction\"] == \"asc\"\n\n\n# [/DEF:test_get_users_page_sends_lowercase_order_direction:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_users_page_sends_lowercase_order_direction [TYPE Function]\n# @RELATION BINDS_TO -> TestSupersetProfileLookup\n# @BRIEF Ensures adapter sends lowercase order_direction compatible with Superset rison schema.\n# @PRE: Adapter is initialized with recording network client.\n# @POST: First request query payload contains order_direction='asc' for asc sort.\ndef test_get_users_page_sends_lowercase_order_direction():\n client = _RecordingNetworkClient(\n scripted_responses=[{\"result\": [{\"username\": \"admin\"}], \"count\": 1}]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n adapter.get_users_page(\n search=\"admin\",\n page_index=0,\n page_size=20,\n sort_column=\"username\",\n sort_order=\"asc\",\n )\n sent_query = json.loads(client.calls[0][\"params\"][\"q\"])\n assert sent_query[\"order_direction\"] == \"asc\"\n# #endregion test_get_users_page_sends_lowercase_order_direction\n" }, { "contract_id": "test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_profile_lookup.py", - "start_line": 99, - "end_line": 122, + "start_line": 81, + "end_line": 100, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28634,15 +27256,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error:Function]\n# @RELATION: BINDS_TO -> TestSupersetProfileLookup\n# @PURPOSE: Ensures fallback auth error does not mask primary schema/query failure.\n# @PRE: Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError.\n# @POST: Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause.\ndef test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error():\n client = _RecordingNetworkClient(\n scripted_responses=[\n SupersetAPIError(\"API Error 400: bad rison schema\"),\n AuthenticationError(),\n ]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n\n with pytest.raises(SupersetAPIError) as exc_info:\n adapter.get_users_page(sort_order=\"asc\")\n\n assert \"API Error 400\" in str(exc_info.value)\n assert not isinstance(exc_info.value, AuthenticationError)\n\n\n# [/DEF:test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error [TYPE Function]\n# @RELATION BINDS_TO -> TestSupersetProfileLookup\n# @BRIEF Ensures fallback auth error does not mask primary schema/query failure.\n# @PRE: Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError.\n# @POST: Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause.\ndef test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error():\n client = _RecordingNetworkClient(\n scripted_responses=[\n SupersetAPIError(\"API Error 400: bad rison schema\"),\n AuthenticationError(),\n ]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n with pytest.raises(SupersetAPIError) as exc_info:\n adapter.get_users_page(sort_order=\"asc\")\n assert \"API Error 400\" in str(exc_info.value)\n assert not isinstance(exc_info.value, AuthenticationError)\n# #endregion test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error\n" }, { "contract_id": "test_get_users_page_uses_fallback_endpoint_when_primary_fails", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_superset_profile_lookup.py", - "start_line": 125, - "end_line": 150, + "start_line": 101, + "end_line": 122, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28687,58 +27309,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_get_users_page_uses_fallback_endpoint_when_primary_fails:Function]\n# @RELATION: BINDS_TO -> TestSupersetProfileLookup\n# @PURPOSE: Verifies adapter retries second users endpoint and succeeds when fallback is healthy.\n# @PRE: Primary endpoint fails; fallback returns valid users payload.\n# @POST: Result status is success and both endpoints were attempted in order.\ndef test_get_users_page_uses_fallback_endpoint_when_primary_fails():\n client = _RecordingNetworkClient(\n scripted_responses=[\n SupersetAPIError(\"Primary endpoint failed\"),\n {\"result\": [{\"username\": \"admin\"}], \"count\": 1},\n ]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n\n result = adapter.get_users_page()\n\n assert result[\"status\"] == \"success\"\n assert [call[\"endpoint\"] for call in client.calls] == [\n \"/security/users/\",\n \"/security/users\",\n ]\n\n\n# [/DEF:test_get_users_page_uses_fallback_endpoint_when_primary_fails:Function]\n" - }, - { - "contract_id": "test_throttled_scheduler", - "contract_type": "Module", - "file_path": "backend/src/core/__tests__/test_throttled_scheduler.py", - "start_line": 5, - "end_line": 153, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Unit tests for ThrottledSchedulerConfigurator distribution logic." - }, - "relations": [ - { - "source_id": "test_throttled_scheduler", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_throttled_scheduler:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for ThrottledSchedulerConfigurator distribution logic.\n\n\n# [DEF:test_calculate_schedule_even_distribution:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate even spacing across a two-hour scheduling window for three tasks.\ndef test_calculate_schedule_even_distribution():\n \"\"\"\n @TEST_SCENARIO: 3 tasks in a 2-hour window should be spaced 1 hour apart.\n \"\"\"\n start = time(1, 0)\n end = time(3, 0)\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n assert schedule[1] == datetime(2024, 1, 1, 2, 0)\n assert schedule[2] == datetime(2024, 1, 1, 3, 0)\n\n\n# [/DEF:test_calculate_schedule_even_distribution:Function]\n\n\n# [DEF:test_calculate_schedule_midnight_crossing:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate scheduler correctly rolls timestamps into the next day across midnight.\ndef test_calculate_schedule_midnight_crossing():\n \"\"\"\n @TEST_SCENARIO: Window from 23:00 to 01:00 (next day).\n \"\"\"\n start = time(23, 0)\n end = time(1, 0)\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 23, 0)\n assert schedule[1] == datetime(2024, 1, 2, 0, 0)\n assert schedule[2] == datetime(2024, 1, 2, 1, 0)\n\n\n# [/DEF:test_calculate_schedule_midnight_crossing:Function]\n\n\n# [DEF:test_calculate_schedule_single_task:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate single-task schedule returns only the window start timestamp.\ndef test_calculate_schedule_single_task():\n \"\"\"\n @TEST_SCENARIO: Single task should be scheduled at start time.\n \"\"\"\n start = time(1, 0)\n end = time(2, 0)\n dashboards = [\"d1\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 1\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n\n\n# [/DEF:test_calculate_schedule_single_task:Function]\n\n\n# [DEF:test_calculate_schedule_empty_list:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate empty dashboard list produces an empty schedule.\ndef test_calculate_schedule_empty_list():\n \"\"\"\n @TEST_SCENARIO: Empty dashboard list returns empty schedule.\n \"\"\"\n start = time(1, 0)\n end = time(2, 0)\n dashboards = []\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert schedule == []\n\n\n# [/DEF:test_calculate_schedule_empty_list:Function]\n\n\n# [DEF:test_calculate_schedule_zero_window:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate zero-length window schedules all tasks at identical start timestamp.\ndef test_calculate_schedule_zero_window():\n \"\"\"\n @TEST_SCENARIO: Window start == end. All tasks at start time.\n \"\"\"\n start = time(1, 0)\n end = time(1, 0)\n dashboards = [\"d1\", \"d2\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 2\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n assert schedule[1] == datetime(2024, 1, 1, 1, 0)\n\n\n# [/DEF:test_calculate_schedule_zero_window:Function]\n\n\n# [DEF:test_calculate_schedule_very_small_window:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate sub-second interpolation when task count exceeds near-zero window granularity.\ndef test_calculate_schedule_very_small_window():\n \"\"\"\n @TEST_SCENARIO: Window smaller than number of tasks (in seconds).\n \"\"\"\n start = time(1, 0, 0)\n end = time(1, 0, 1) # 1 second window\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 1, 0, 0)\n assert schedule[1] == datetime(2024, 1, 1, 1, 0, 0, 500000) # 0.5s\n assert schedule[2] == datetime(2024, 1, 1, 1, 0, 1)\n\n\n# [/DEF:test_calculate_schedule_very_small_window:Function]\n# [/DEF:test_throttled_scheduler:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_get_users_page_uses_fallback_endpoint_when_primary_fails [TYPE Function]\n# @RELATION BINDS_TO -> TestSupersetProfileLookup\n# @BRIEF Verifies adapter retries second users endpoint and succeeds when fallback is healthy.\n# @PRE: Primary endpoint fails; fallback returns valid users payload.\n# @POST: Result status is success and both endpoints were attempted in order.\ndef test_get_users_page_uses_fallback_endpoint_when_primary_fails():\n client = _RecordingNetworkClient(\n scripted_responses=[\n SupersetAPIError(\"Primary endpoint failed\"),\n {\"result\": [{\"username\": \"admin\"}], \"count\": 1},\n ]\n )\n adapter = SupersetAccountLookupAdapter(\n network_client=client, environment_id=\"ss-dev\"\n )\n result = adapter.get_users_page()\n assert result[\"status\"] == \"success\"\n assert [call[\"endpoint\"] for call in client.calls] == [\n \"/security/users/\",\n \"/security/users\",\n ]\n# #endregion test_get_users_page_uses_fallback_endpoint_when_primary_fails\n" }, { "contract_id": "test_calculate_schedule_even_distribution", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_throttled_scheduler.py", - "start_line": 11, - "end_line": 33, + "start_line": 9, + "end_line": 27, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28763,15 +27342,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_calculate_schedule_even_distribution:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate even spacing across a two-hour scheduling window for three tasks.\ndef test_calculate_schedule_even_distribution():\n \"\"\"\n @TEST_SCENARIO: 3 tasks in a 2-hour window should be spaced 1 hour apart.\n \"\"\"\n start = time(1, 0)\n end = time(3, 0)\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n assert schedule[1] == datetime(2024, 1, 1, 2, 0)\n assert schedule[2] == datetime(2024, 1, 1, 3, 0)\n\n\n# [/DEF:test_calculate_schedule_even_distribution:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_calculate_schedule_even_distribution [TYPE Function]\n# @RELATION BINDS_TO -> test_throttled_scheduler\n# @BRIEF Validate even spacing across a two-hour scheduling window for three tasks.\ndef test_calculate_schedule_even_distribution():\n \"\"\"\n @TEST_SCENARIO: 3 tasks in a 2-hour window should be spaced 1 hour apart.\n \"\"\"\n start = time(1, 0)\n end = time(3, 0)\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n assert schedule[1] == datetime(2024, 1, 1, 2, 0)\n assert schedule[2] == datetime(2024, 1, 1, 3, 0)\n# #endregion test_calculate_schedule_even_distribution\n" }, { "contract_id": "test_calculate_schedule_midnight_crossing", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_throttled_scheduler.py", - "start_line": 36, - "end_line": 58, + "start_line": 28, + "end_line": 46, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28796,15 +27375,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_calculate_schedule_midnight_crossing:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate scheduler correctly rolls timestamps into the next day across midnight.\ndef test_calculate_schedule_midnight_crossing():\n \"\"\"\n @TEST_SCENARIO: Window from 23:00 to 01:00 (next day).\n \"\"\"\n start = time(23, 0)\n end = time(1, 0)\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 23, 0)\n assert schedule[1] == datetime(2024, 1, 2, 0, 0)\n assert schedule[2] == datetime(2024, 1, 2, 1, 0)\n\n\n# [/DEF:test_calculate_schedule_midnight_crossing:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_calculate_schedule_midnight_crossing [TYPE Function]\n# @RELATION BINDS_TO -> test_throttled_scheduler\n# @BRIEF Validate scheduler correctly rolls timestamps into the next day across midnight.\ndef test_calculate_schedule_midnight_crossing():\n \"\"\"\n @TEST_SCENARIO: Window from 23:00 to 01:00 (next day).\n \"\"\"\n start = time(23, 0)\n end = time(1, 0)\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 23, 0)\n assert schedule[1] == datetime(2024, 1, 2, 0, 0)\n assert schedule[2] == datetime(2024, 1, 2, 1, 0)\n# #endregion test_calculate_schedule_midnight_crossing\n" }, { "contract_id": "test_calculate_schedule_single_task", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_throttled_scheduler.py", - "start_line": 61, - "end_line": 81, + "start_line": 47, + "end_line": 63, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28829,15 +27408,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_calculate_schedule_single_task:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate single-task schedule returns only the window start timestamp.\ndef test_calculate_schedule_single_task():\n \"\"\"\n @TEST_SCENARIO: Single task should be scheduled at start time.\n \"\"\"\n start = time(1, 0)\n end = time(2, 0)\n dashboards = [\"d1\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 1\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n\n\n# [/DEF:test_calculate_schedule_single_task:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_calculate_schedule_single_task [TYPE Function]\n# @RELATION BINDS_TO -> test_throttled_scheduler\n# @BRIEF Validate single-task schedule returns only the window start timestamp.\ndef test_calculate_schedule_single_task():\n \"\"\"\n @TEST_SCENARIO: Single task should be scheduled at start time.\n \"\"\"\n start = time(1, 0)\n end = time(2, 0)\n dashboards = [\"d1\"]\n today = date(2024, 1, 1)\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n assert len(schedule) == 1\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n# #endregion test_calculate_schedule_single_task\n" }, { "contract_id": "test_calculate_schedule_empty_list", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_throttled_scheduler.py", - "start_line": 84, - "end_line": 103, + "start_line": 64, + "end_line": 79, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28862,15 +27441,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_calculate_schedule_empty_list:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate empty dashboard list produces an empty schedule.\ndef test_calculate_schedule_empty_list():\n \"\"\"\n @TEST_SCENARIO: Empty dashboard list returns empty schedule.\n \"\"\"\n start = time(1, 0)\n end = time(2, 0)\n dashboards = []\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert schedule == []\n\n\n# [/DEF:test_calculate_schedule_empty_list:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_calculate_schedule_empty_list [TYPE Function]\n# @RELATION BINDS_TO -> test_throttled_scheduler\n# @BRIEF Validate empty dashboard list produces an empty schedule.\ndef test_calculate_schedule_empty_list():\n \"\"\"\n @TEST_SCENARIO: Empty dashboard list returns empty schedule.\n \"\"\"\n start = time(1, 0)\n end = time(2, 0)\n dashboards = []\n today = date(2024, 1, 1)\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n assert schedule == []\n# #endregion test_calculate_schedule_empty_list\n" }, { "contract_id": "test_calculate_schedule_zero_window", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_throttled_scheduler.py", - "start_line": 106, - "end_line": 127, + "start_line": 80, + "end_line": 97, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28895,15 +27474,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_calculate_schedule_zero_window:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate zero-length window schedules all tasks at identical start timestamp.\ndef test_calculate_schedule_zero_window():\n \"\"\"\n @TEST_SCENARIO: Window start == end. All tasks at start time.\n \"\"\"\n start = time(1, 0)\n end = time(1, 0)\n dashboards = [\"d1\", \"d2\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 2\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n assert schedule[1] == datetime(2024, 1, 1, 1, 0)\n\n\n# [/DEF:test_calculate_schedule_zero_window:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_calculate_schedule_zero_window [TYPE Function]\n# @RELATION BINDS_TO -> test_throttled_scheduler\n# @BRIEF Validate zero-length window schedules all tasks at identical start timestamp.\ndef test_calculate_schedule_zero_window():\n \"\"\"\n @TEST_SCENARIO: Window start == end. All tasks at start time.\n \"\"\"\n start = time(1, 0)\n end = time(1, 0)\n dashboards = [\"d1\", \"d2\"]\n today = date(2024, 1, 1)\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n assert len(schedule) == 2\n assert schedule[0] == datetime(2024, 1, 1, 1, 0)\n assert schedule[1] == datetime(2024, 1, 1, 1, 0)\n# #endregion test_calculate_schedule_zero_window\n" }, { "contract_id": "test_calculate_schedule_very_small_window", "contract_type": "Function", "file_path": "backend/src/core/__tests__/test_throttled_scheduler.py", - "start_line": 130, - "end_line": 152, + "start_line": 98, + "end_line": 116, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -28928,29 +27507,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_calculate_schedule_very_small_window:Function]\n# @RELATION: BINDS_TO -> test_throttled_scheduler\n# @PURPOSE: Validate sub-second interpolation when task count exceeds near-zero window granularity.\ndef test_calculate_schedule_very_small_window():\n \"\"\"\n @TEST_SCENARIO: Window smaller than number of tasks (in seconds).\n \"\"\"\n start = time(1, 0, 0)\n end = time(1, 0, 1) # 1 second window\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 1, 0, 0)\n assert schedule[1] == datetime(2024, 1, 1, 1, 0, 0, 500000) # 0.5s\n assert schedule[2] == datetime(2024, 1, 1, 1, 0, 1)\n\n\n# [/DEF:test_calculate_schedule_very_small_window:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_calculate_schedule_very_small_window [TYPE Function]\n# @RELATION BINDS_TO -> test_throttled_scheduler\n# @BRIEF Validate sub-second interpolation when task count exceeds near-zero window granularity.\ndef test_calculate_schedule_very_small_window():\n \"\"\"\n @TEST_SCENARIO: Window smaller than number of tasks (in seconds).\n \"\"\"\n start = time(1, 0, 0)\n end = time(1, 0, 1) # 1 second window\n dashboards = [\"d1\", \"d2\", \"d3\"]\n today = date(2024, 1, 1)\n schedule = ThrottledSchedulerConfigurator.calculate_schedule(\n start, end, dashboards, today\n )\n assert len(schedule) == 3\n assert schedule[0] == datetime(2024, 1, 1, 1, 0, 0)\n assert schedule[1] == datetime(2024, 1, 1, 1, 0, 0, 500000) # 0.5s\n assert schedule[2] == datetime(2024, 1, 1, 1, 0, 1)\n# #endregion test_calculate_schedule_very_small_window\n" }, { "contract_id": "AsyncSupersetClientModule", "contract_type": "Module", "file_path": "backend/src/core/async_superset_client.py", "start_line": 1, - "end_line": 684, + "end_line": 616, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Core", - "PURPOSE": "Parse a Superset dashboard URL and extract native filter state asynchronously.", - "SEMANTICS": [ - "superset", - "async", - "client", - "httpx", - "dashboards", - "datasets" - ] + "PURPOSE": "Parse a Superset dashboard URL and extract native filter state asynchronously." }, "relations": [], "schema_warnings": [ @@ -28964,800 +27535,44 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AsyncSupersetClientModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: superset, async, client, httpx, dashboards, datasets\n# @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n# @LAYER: Core\n\n\n# [SECTION: IMPORTS]\nimport asyncio\nimport json\nimport re\nfrom typing import Any, Dict, List, Optional, Tuple, cast\n\nfrom .config_models import Environment\nfrom .logger import logger as app_logger, belief_scope\nfrom .superset_client import SupersetClient\nfrom .utils.async_network import AsyncAPIClient\n# [/SECTION]\n\n\n# [DEF:AsyncSupersetClient:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Async sibling of SupersetClient for dashboard read paths.\n# @RELATION: [INHERITS] ->[SupersetClient]\n# @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]\n# @RELATION: [CALLS] ->[AsyncAPIClient.request]\nclass AsyncSupersetClient(SupersetClient):\n # [DEF:AsyncSupersetClientInit:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE: env is valid Environment instance.\n # @POST: Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT: Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n\n # [/DEF:AsyncSupersetClientInit:Function]\n\n # [DEF:AsyncSupersetClientClose:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Close async transport resources.\n # @POST: Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT: Closes network sockets.\n # @RELATION: [CALLS] ->[AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n\n # [/DEF:AsyncSupersetClientClose:Function]\n\n # [DEF:get_dashboards_page_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST: Returns total count and page result list.\n # @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION: [CALLS] -> [AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: Optional[Dict] = None\n ) -> Tuple[int, List[Dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n\n response_json = cast(\n Dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n\n # [/DEF:get_dashboards_page_async:Function]\n\n # [DEF:get_dashboard_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST: Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_dashboard_async:Function]\n\n # [DEF:get_chart_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST: Returns raw chart payload from Superset API.\n # @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> Dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_chart_async:Function]\n\n # [DEF:get_dashboard_detail_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST: Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_async]\n # @RELATION: [CALLS] ->[get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n\n charts: List[Dict] = []\n datasets: List[Dict] = []\n\n def extract_dataset_id_from_form_data(\n form_data: Optional[Dict],\n ) -> Optional[int]:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n pass\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n pass\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n\n # [/DEF:get_dashboard_detail_async:Function]\n\n # [DEF:get_dashboard_permalink_state_async:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST: Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_dashboard_permalink_state_async:Function]\n\n # [DEF:get_native_filter_state_async:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST: Returns native filter state payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(Dict, response)\n\n # [/DEF:get_native_filter_state_async:Function]\n\n # [DEF:extract_native_filters_from_permalink_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST: Returns extracted dataMask with filter states.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n\n # [/DEF:extract_native_filters_from_permalink_async:Function]\n\n # [DEF:extract_native_filters_from_key_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST: Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n\n extracted_filters = {}\n\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n\n # [/DEF:extract_native_filters_from_key_async:Function]\n\n # [DEF:parse_dashboard_url_for_filters_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST: Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT: Input[url: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async]\n # @RELATION: [CALLS] ->[extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n pass\n\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n pass\n\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n\n return result\n\n # [/DEF:parse_dashboard_url_for_filters_async:Function]\n\n\n# [/DEF:AsyncSupersetClient:Class]\n\n# [/DEF:AsyncSupersetClientModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region AsyncSupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, transform, dashboard, filter, async-superset-client]\n# @BRIEF Parse a Superset dashboard URL and extract native filter state asynchronously.\n# @LAYER: Core\nimport asyncio\nimport json\nimport re\nfrom typing import Any, cast\n\nfrom .config_models import Environment\nfrom .logger import belief_scope\nfrom .logger import logger as app_logger\nfrom .superset_client import SupersetClient\nfrom .utils.async_network import AsyncAPIClient\n\n\n# #region AsyncSupersetClient [C:3] [TYPE Class]\n# @BRIEF Async sibling of SupersetClient for dashboard read paths.\n# @RELATION INHERITS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AsyncAPIClient]\n# @RELATION CALLS -> [AsyncAPIClient.request]\nclass AsyncSupersetClient(SupersetClient):\n # #region AsyncSupersetClientInit [TYPE Function] [C:3]\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE: env is valid Environment instance.\n # @POST: Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT: Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n # #endregion AsyncSupersetClientInit\n # #region AsyncSupersetClientClose [TYPE Function] [C:3]\n # @PURPOSE: Close async transport resources.\n # @POST: Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT: Closes network sockets.\n # @RELATION: [CALLS] ->[AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n # #endregion AsyncSupersetClientClose\n # #region get_dashboards_page_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST: Returns total count and page result list.\n # @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION: [CALLS] -> [AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: dict | None = None\n ) -> tuple[int, list[dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n response_json = cast(\n dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n # #endregion get_dashboards_page_async\n # #region get_dashboard_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST: Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_async\n # #region get_chart_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST: Returns raw chart payload from Superset API.\n # @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(dict, response)\n # #endregion get_chart_async\n # #region get_dashboard_detail_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST: Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_async]\n # @RELATION: [CALLS] ->[get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n charts: list[dict] = []\n datasets: list[dict] = []\n def extract_dataset_id_from_form_data(\n form_data: dict | None,\n ) -> int | None:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n pass\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n pass\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n # #endregion get_dashboard_detail_async\n # #region get_dashboard_permalink_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST: Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_permalink_state_async\n # #region get_native_filter_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST: Returns native filter state payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(dict, response)\n # #endregion get_native_filter_state_async\n # #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST: Returns extracted dataMask with filter states.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n # #endregion extract_native_filters_from_permalink_async\n # #region extract_native_filters_from_key_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST: Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n extracted_filters = {}\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n # #endregion extract_native_filters_from_key_async\n # #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST: Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT: Input[url: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async]\n # @RELATION: [CALLS] ->[extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n pass\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n pass\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n return result\n # #endregion parse_dashboard_url_for_filters_async\n# #endregion AsyncSupersetClient\n# #endregion AsyncSupersetClientModule\n" }, { "contract_id": "AsyncSupersetClient", "contract_type": "Class", "file_path": "backend/src/core/async_superset_client.py", - "start_line": 21, - "end_line": 682, + "start_line": 16, + "end_line": 615, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Async sibling of SupersetClient for dashboard read paths." }, "relations": [ { "source_id": "AsyncSupersetClient", - "relation_type": "[INHERITS]", + "relation_type": "INHERITS", "target_id": "SupersetClient", "target_ref": "[SupersetClient]" }, { "source_id": "AsyncSupersetClient", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AsyncAPIClient", "target_ref": "[AsyncAPIClient]" }, { "source_id": "AsyncSupersetClient", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "AsyncAPIClient.request", "target_ref": "[AsyncAPIClient.request]" } ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [INHERITS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[INHERITS]" - } - }, - { - "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]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:AsyncSupersetClient:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Async sibling of SupersetClient for dashboard read paths.\n# @RELATION: [INHERITS] ->[SupersetClient]\n# @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]\n# @RELATION: [CALLS] ->[AsyncAPIClient.request]\nclass AsyncSupersetClient(SupersetClient):\n # [DEF:AsyncSupersetClientInit:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE: env is valid Environment instance.\n # @POST: Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT: Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n\n # [/DEF:AsyncSupersetClientInit:Function]\n\n # [DEF:AsyncSupersetClientClose:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Close async transport resources.\n # @POST: Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT: Closes network sockets.\n # @RELATION: [CALLS] ->[AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n\n # [/DEF:AsyncSupersetClientClose:Function]\n\n # [DEF:get_dashboards_page_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST: Returns total count and page result list.\n # @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION: [CALLS] -> [AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: Optional[Dict] = None\n ) -> Tuple[int, List[Dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n\n response_json = cast(\n Dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n\n # [/DEF:get_dashboards_page_async:Function]\n\n # [DEF:get_dashboard_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST: Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_dashboard_async:Function]\n\n # [DEF:get_chart_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST: Returns raw chart payload from Superset API.\n # @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> Dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_chart_async:Function]\n\n # [DEF:get_dashboard_detail_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST: Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_async]\n # @RELATION: [CALLS] ->[get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n\n charts: List[Dict] = []\n datasets: List[Dict] = []\n\n def extract_dataset_id_from_form_data(\n form_data: Optional[Dict],\n ) -> Optional[int]:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n pass\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n pass\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n\n # [/DEF:get_dashboard_detail_async:Function]\n\n # [DEF:get_dashboard_permalink_state_async:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST: Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_dashboard_permalink_state_async:Function]\n\n # [DEF:get_native_filter_state_async:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST: Returns native filter state payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(Dict, response)\n\n # [/DEF:get_native_filter_state_async:Function]\n\n # [DEF:extract_native_filters_from_permalink_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST: Returns extracted dataMask with filter states.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n\n # [/DEF:extract_native_filters_from_permalink_async:Function]\n\n # [DEF:extract_native_filters_from_key_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST: Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n\n extracted_filters = {}\n\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n\n # [/DEF:extract_native_filters_from_key_async:Function]\n\n # [DEF:parse_dashboard_url_for_filters_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST: Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT: Input[url: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async]\n # @RELATION: [CALLS] ->[extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n pass\n\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n pass\n\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n\n return result\n\n # [/DEF:parse_dashboard_url_for_filters_async:Function]\n\n\n# [/DEF:AsyncSupersetClient:Class]\n" - }, - { - "contract_id": "AsyncSupersetClientInit", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 28, - "end_line": 50, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[Environment] -> self.network[AsyncAPIClient]", - "POST": "Client uses async network transport and inherited projection helpers.", - "PRE": "env is valid Environment instance.", - "PURPOSE": "Initialize async Superset client with AsyncAPIClient transport." - }, - "relations": [ - { - "source_id": "AsyncSupersetClientInit", - "relation_type": "[DEPENDS_ON]", - "target_id": "AsyncAPIClient", - "target_ref": "[AsyncAPIClient]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncSupersetClientInit:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE: env is valid Environment instance.\n # @POST: Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT: Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n\n # [/DEF:AsyncSupersetClientInit:Function]\n" - }, - { - "contract_id": "AsyncSupersetClientClose", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 52, - "end_line": 61, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Underlying AsyncAPIClient is closed.", - "PURPOSE": "Close async transport resources.", - "SIDE_EFFECT": "Closes network sockets." - }, - "relations": [ - { - "source_id": "AsyncSupersetClientClose", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient.aclose", - "target_ref": "[AsyncAPIClient.aclose]" - } - ], - "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": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncSupersetClientClose:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Close async transport resources.\n # @POST: Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT: Closes network sockets.\n # @RELATION: [CALLS] ->[AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n\n # [/DEF:AsyncSupersetClientClose:Function]\n" - }, - { - "contract_id": "get_dashboards_page_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 63, - "end_line": 100, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]", - "POST": "Returns total count and page result list.", - "PURPOSE": "Fetch one dashboards page asynchronously." - }, - "relations": [ - { - "source_id": "get_dashboards_page_async", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient.request", - "target_ref": "[AsyncAPIClient.request]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_dashboards_page_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST: Returns total count and page result list.\n # @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION: [CALLS] -> [AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: Optional[Dict] = None\n ) -> Tuple[int, List[Dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n\n response_json = cast(\n Dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n\n # [/DEF:get_dashboards_page_async:Function]\n" - }, - { - "contract_id": "get_dashboard_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 102, - "end_line": 117, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[dashboard_id: int] -> Output[Dict]", - "POST": "Returns raw dashboard payload from Superset API.", - "PURPOSE": "Fetch one dashboard payload asynchronously." - }, - "relations": [ - { - "source_id": "get_dashboard_async", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient.request", - "target_ref": "[AsyncAPIClient.request]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_dashboard_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST: Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_dashboard_async:Function]\n" - }, - { - "contract_id": "get_chart_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 119, - "end_line": 132, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[chart_id: int] -> Output[Dict]", - "POST": "Returns raw chart payload from Superset API.", - "PURPOSE": "Fetch one chart payload asynchronously." - }, - "relations": [ - { - "source_id": "get_chart_async", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient.request", - "target_ref": "[AsyncAPIClient.request]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_chart_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST: Returns raw chart payload from Superset API.\n # @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> Dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_chart_async:Function]\n" - }, - { - "contract_id": "get_dashboard_detail_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 134, - "end_line": 430, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[dashboard_id: int] -> Output[Dict]", - "POST": "Returns dashboard detail payload for overview page.", - "PURPOSE": "Fetch dashboard detail asynchronously with concurrent charts/datasets requests." - }, - "relations": [ - { - "source_id": "get_dashboard_detail_async", - "relation_type": "[CALLS]", - "target_id": "get_dashboard_async", - "target_ref": "[get_dashboard_async]" - }, - { - "source_id": "get_dashboard_detail_async", - "relation_type": "[CALLS]", - "target_id": "get_chart_async", - "target_ref": "[get_chart_async]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_dashboard_detail_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST: Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_async]\n # @RELATION: [CALLS] ->[get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n\n charts: List[Dict] = []\n datasets: List[Dict] = []\n\n def extract_dataset_id_from_form_data(\n form_data: Optional[Dict],\n ) -> Optional[int]:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n pass\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n pass\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n\n # [/DEF:get_dashboard_detail_async:Function]\n" - }, - { - "contract_id": "get_dashboard_permalink_state_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 432, - "end_line": 447, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "DATA_CONTRACT": "Input[permalink_key: str] -> Output[Dict]", - "POST": "Returns dashboard permalink state payload from Superset API.", - "PURPOSE": "Fetch stored dashboard permalink state asynchronously." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "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": "def", - "body": " # [DEF:get_dashboard_permalink_state_async:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST: Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(Dict, response)\n\n # [/DEF:get_dashboard_permalink_state_async:Function]\n" - }, - { - "contract_id": "get_native_filter_state_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 449, - "end_line": 467, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "DATA_CONTRACT": "Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]", - "POST": "Returns native filter state payload from Superset API.", - "PURPOSE": "Fetch stored native filter state asynchronously." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - }, - { - "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": "def", - "body": " # [DEF:get_native_filter_state_async:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST: Returns native filter state payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(Dict, response)\n\n # [/DEF:get_native_filter_state_async:Function]\n" - }, - { - "contract_id": "extract_native_filters_from_permalink_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 469, - "end_line": 508, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[permalink_key: str] -> Output[Dict]", - "POST": "Returns extracted dataMask with filter states.", - "PURPOSE": "Extract native filters dataMask from a permalink key asynchronously." - }, - "relations": [ - { - "source_id": "extract_native_filters_from_permalink_async", - "relation_type": "[CALLS]", - "target_id": "get_dashboard_permalink_state_async", - "target_ref": "[get_dashboard_permalink_state_async]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:extract_native_filters_from_permalink_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST: Returns extracted dataMask with filter states.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n\n # [/DEF:extract_native_filters_from_permalink_async:Function]\n" - }, - { - "contract_id": "extract_native_filters_from_key_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 510, - "end_line": 569, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]", - "POST": "Returns extracted filter state with extraFormData.", - "PURPOSE": "Extract native filters from a native_filters_key URL parameter asynchronously." - }, - "relations": [ - { - "source_id": "extract_native_filters_from_key_async", - "relation_type": "[CALLS]", - "target_id": "get_native_filter_state_async", - "target_ref": "[get_native_filter_state_async]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:extract_native_filters_from_key_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST: Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n\n extracted_filters = {}\n\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n\n # [/DEF:extract_native_filters_from_key_async:Function]\n" - }, - { - "contract_id": "parse_dashboard_url_for_filters_async", - "contract_type": "Function", - "file_path": "backend/src/core/async_superset_client.py", - "start_line": 571, - "end_line": 679, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[url: str] -> Output[Dict]", - "POST": "Returns extracted filter state or empty dict if no filters found.", - "PURPOSE": "Parse a Superset dashboard URL and extract native filter state asynchronously." - }, - "relations": [ - { - "source_id": "parse_dashboard_url_for_filters_async", - "relation_type": "[CALLS]", - "target_id": "extract_native_filters_from_permalink_async", - "target_ref": "[extract_native_filters_from_permalink_async]" - }, - { - "source_id": "parse_dashboard_url_for_filters_async", - "relation_type": "[CALLS]", - "target_id": "extract_native_filters_from_key_async", - "target_ref": "[extract_native_filters_from_key_async]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:parse_dashboard_url_for_filters_async:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST: Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT: Input[url: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async]\n # @RELATION: [CALLS] ->[extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> Dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n pass\n\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n pass\n\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n\n return result\n\n # [/DEF:parse_dashboard_url_for_filters_async:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region AsyncSupersetClient [C:3] [TYPE Class]\n# @BRIEF Async sibling of SupersetClient for dashboard read paths.\n# @RELATION INHERITS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AsyncAPIClient]\n# @RELATION CALLS -> [AsyncAPIClient.request]\nclass AsyncSupersetClient(SupersetClient):\n # #region AsyncSupersetClientInit [TYPE Function] [C:3]\n # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.\n # @PRE: env is valid Environment instance.\n # @POST: Client uses async network transport and inherited projection helpers.\n # @DATA_CONTRACT: Input[Environment] -> self.network[AsyncAPIClient]\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]\n def __init__(self, env: Environment):\n self.env = env\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = AsyncAPIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport = False\n # #endregion AsyncSupersetClientInit\n # #region AsyncSupersetClientClose [TYPE Function] [C:3]\n # @PURPOSE: Close async transport resources.\n # @POST: Underlying AsyncAPIClient is closed.\n # @SIDE_EFFECT: Closes network sockets.\n # @RELATION: [CALLS] ->[AsyncAPIClient.aclose]\n async def aclose(self) -> None:\n await self.network.aclose()\n # #endregion AsyncSupersetClientClose\n # #region get_dashboards_page_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboards page asynchronously.\n # @POST: Returns total count and page result list.\n # @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]\n # @RELATION: [CALLS] -> [AsyncAPIClient.request]\n async def get_dashboards_page_async(\n self, query: dict | None = None\n ) -> tuple[int, list[dict]]:\n with belief_scope(\"AsyncSupersetClient.get_dashboards_page_async\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\",\n \"id\",\n \"url\",\n \"changed_on_utc\",\n \"dashboard_title\",\n \"published\",\n \"created_by\",\n \"changed_by\",\n \"changed_by_name\",\n \"owners\",\n ]\n response_json = cast(\n dict[str, Any],\n await self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n # #endregion get_dashboards_page_async\n # #region get_dashboard_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one dashboard payload asynchronously.\n # @POST: Returns raw dashboard payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_dashboard_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_async\", f\"id={dashboard_id}\"\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_async\n # #region get_chart_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch one chart payload asynchronously.\n # @POST: Returns raw chart payload from Superset API.\n # @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[AsyncAPIClient.request]\n async def get_chart_async(self, chart_id: int) -> dict:\n with belief_scope(\"AsyncSupersetClient.get_chart_async\", f\"id={chart_id}\"):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/chart/{chart_id}\"\n )\n return cast(dict, response)\n # #endregion get_chart_async\n # #region get_dashboard_detail_async [TYPE Function] [C:3]\n # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.\n # @POST: Returns dashboard detail payload for overview page.\n # @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_async]\n # @RELATION: [CALLS] ->[get_chart_async]\n async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_detail_async\", f\"id={dashboard_id}\"\n ):\n dashboard_response = await self.get_dashboard_async(dashboard_id)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n charts: list[dict] = []\n datasets: list[dict] = []\n def extract_dataset_id_from_form_data(\n form_data: dict | None,\n ) -> int | None:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n chart_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/charts\",\n )\n dataset_task = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/datasets\",\n )\n charts_response, datasets_response = await asyncio.gather(\n chart_task,\n dataset_task,\n return_exceptions=True,\n )\n if not isinstance(charts_response, Exception):\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n ),\n \"dataset_id\": int(dataset_id)\n if dataset_id is not None\n else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict)\n else None\n )\n or \"Chart\",\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s\",\n charts_response,\n )\n if not isinstance(datasets_response, Exception):\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_obj.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n else:\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s\",\n datasets_response,\n )\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n pass\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n pass\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n fallback_chart_tasks = [\n self.get_chart_async(int(chart_id))\n for chart_id in sorted(chart_ids_from_position)\n ]\n fallback_chart_responses = await asyncio.gather(\n *fallback_chart_tasks,\n return_exceptions=True,\n )\n for chart_id, chart_response in zip(\n sorted(chart_ids_from_position), fallback_chart_responses\n ):\n if isinstance(chart_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id,\n chart_response,\n )\n continue\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append(\n {\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\")\n or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\")\n or \"Chart\",\n }\n )\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids = sorted(\n int(item)\n for item in dataset_ids_from_charts\n if item not in known_dataset_ids\n )\n if missing_dataset_ids:\n dataset_fetch_tasks = [\n self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n for dataset_id in missing_dataset_ids\n ]\n dataset_fetch_responses = await asyncio.gather(\n *dataset_fetch_tasks,\n return_exceptions=True,\n )\n for dataset_id, dataset_response in zip(\n missing_dataset_ids, dataset_fetch_responses\n ):\n if isinstance(dataset_response, Exception):\n app_logger.warning(\n \"[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s\",\n dataset_id,\n dataset_response,\n )\n continue\n dataset_data = (\n dataset_response.get(\"result\", dataset_response)\n if isinstance(dataset_response, dict)\n else {}\n )\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict)\n else None\n )\n table_name = (\n dataset_data.get(\"table_name\")\n or dataset_data.get(\"datasource_name\")\n or dataset_data.get(\"name\")\n or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\" {schema}.{table_name}\" if schema else table_name\n datasets.append(\n {\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name\n or dataset_data.get(\"database_name\")\n or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n }\n )\n return {\n \"id\": int(dashboard_data.get(\"id\") or dashboard_id),\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\")\n or f\"Dashboard {dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\"),\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": charts,\n \"datasets\": datasets,\n \"chart_count\": len(charts),\n \"dataset_count\": len(datasets),\n }\n # #endregion get_dashboard_detail_async\n # #region get_dashboard_permalink_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored dashboard permalink state asynchronously.\n # @POST: Returns dashboard permalink state payload from Superset API.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_dashboard_permalink_state_async\",\n f\"key={permalink_key}\",\n ):\n response = await self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(dict, response)\n # #endregion get_dashboard_permalink_state_async\n # #region get_native_filter_state_async [TYPE Function] [C:2]\n # @PURPOSE: Fetch stored native filter state asynchronously.\n # @POST: Returns native filter state payload from Superset API.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n async def get_native_filter_state_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.get_native_filter_state_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = await self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(dict, response)\n # #endregion get_native_filter_state_async\n # #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.\n # @POST: Returns extracted dataMask with filter states.\n # @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_dashboard_permalink_state_async]\n async def extract_native_filters_from_permalink_async(\n self, permalink_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_permalink_async\",\n f\"key={permalink_key}\",\n ):\n permalink_response = await self.get_dashboard_permalink_state_async(\n permalink_key\n )\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n # #endregion extract_native_filters_from_permalink_async\n # #region extract_native_filters_from_key_async [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.\n # @POST: Returns extracted filter state with extraFormData.\n # @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[get_native_filter_state_async]\n async def extract_native_filters_from_key_async(\n self, dashboard_id: int, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.extract_native_filters_from_key_async\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = await self.get_native_filter_state_async(\n dashboard_id, filter_state_key\n )\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n extracted_filters = {}\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n # #endregion extract_native_filters_from_key_async\n # #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.\n # @POST: Returns extracted filter state or empty dict if no filters found.\n # @DATA_CONTRACT: Input[url: str] -> Output[Dict]\n # @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async]\n # @RELATION: [CALLS] ->[extract_native_filters_from_key_async]\n async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:\n with belief_scope(\n \"AsyncSupersetClient.parse_dashboard_url_for_filters_async\", f\"url={url}\"\n ):\n import urllib.parse\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n # Check for permalink URL: /dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = (\n await self.extract_native_filters_from_permalink_async(\n permalink_key\n )\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n pass\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n pass\n if dashboard_ref:\n # Resolve slug to numeric ID — the filter_state API requires a numeric ID\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = await self.get_dashboard_async(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n if resolved_id is not None:\n filter_data = await self.extract_native_filters_from_key_async(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n return result\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n return result\n # #endregion parse_dashboard_url_for_filters_async\n# #endregion AsyncSupersetClient\n" }, { "contract_id": "AuthPackage", @@ -29800,15 +27615,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthPackage:Package]\n# @PURPOSE: Authentication and authorization package root.\n# [/DEF:AuthPackage:Package]\n" + "anchor_syntax": "region", + "body": "# #region AuthPackage [TYPE Package] [SEMANTICS auth, package, init]\n# @BRIEF Authentication and authorization package root.\n# #endregion AuthPackage\n" }, { "contract_id": "test_create_user", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 61, - "end_line": 83, + "start_line": 46, + "end_line": 64, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -29833,15 +27648,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_create_user:Function]\n# @PURPOSE: Verifies that a persisted user can be retrieved with intact credential hash.\n# @RELATION: BINDS_TO -> test_auth\ndef test_create_user(auth_repo):\n \"\"\"Test user creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"testuser\")\n assert retrieved_user is not None\n assert retrieved_user.username == \"testuser\"\n assert retrieved_user.email == \"test@example.com\"\n assert verify_password(\"testpassword123\", retrieved_user.password_hash)\n\n\n# [/DEF:test_create_user:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_create_user [TYPE Function]\n# @BRIEF Verifies that a persisted user can be retrieved with intact credential hash.\n# @RELATION BINDS_TO -> test_auth\ndef test_create_user(auth_repo):\n \"\"\"Test user creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n retrieved_user = auth_repo.get_user_by_username(\"testuser\")\n assert retrieved_user is not None\n assert retrieved_user.username == \"testuser\"\n assert retrieved_user.email == \"test@example.com\"\n assert verify_password(\"testpassword123\", retrieved_user.password_hash)\n# #endregion test_create_user\n" }, { "contract_id": "test_authenticate_user", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 86, - "end_line": 115, + "start_line": 65, + "end_line": 88, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -29866,15 +27681,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_authenticate_user:Function]\n# @PURPOSE: Validates authentication outcomes for valid, wrong-password, and unknown-user cases.\n# @RELATION: BINDS_TO -> test_auth\ndef test_authenticate_user(auth_service, auth_repo):\n \"\"\"Test user authentication with valid and invalid credentials\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n # Test valid credentials\n authenticated_user = auth_service.authenticate_user(\"testuser\", \"testpassword123\")\n assert authenticated_user is not None\n assert authenticated_user.username == \"testuser\"\n\n # Test invalid password\n invalid_user = auth_service.authenticate_user(\"testuser\", \"wrongpassword\")\n assert invalid_user is None\n\n # Test invalid username\n invalid_user = auth_service.authenticate_user(\"nonexistent\", \"testpassword123\")\n assert invalid_user is None\n\n\n# [/DEF:test_authenticate_user:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_authenticate_user [TYPE Function]\n# @BRIEF Validates authentication outcomes for valid, wrong-password, and unknown-user cases.\n# @RELATION BINDS_TO -> test_auth\ndef test_authenticate_user(auth_service, auth_repo):\n \"\"\"Test user authentication with valid and invalid credentials\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n # Test valid credentials\n authenticated_user = auth_service.authenticate_user(\"testuser\", \"testpassword123\")\n assert authenticated_user is not None\n assert authenticated_user.username == \"testuser\"\n # Test invalid password\n invalid_user = auth_service.authenticate_user(\"testuser\", \"wrongpassword\")\n assert invalid_user is None\n # Test invalid username\n invalid_user = auth_service.authenticate_user(\"nonexistent\", \"testpassword123\")\n assert invalid_user is None\n# #endregion test_authenticate_user\n" }, { "contract_id": "test_create_session", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 118, - "end_line": 140, + "start_line": 89, + "end_line": 107, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -29899,15 +27714,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_create_session:Function]\n# @PURPOSE: Ensures session creation returns bearer token payload fields.\n# @RELATION: BINDS_TO -> test_auth\ndef test_create_session(auth_service, auth_repo):\n \"\"\"Test session token creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n session = auth_service.create_session(user)\n assert \"access_token\" in session\n assert \"token_type\" in session\n assert session[\"token_type\"] == \"bearer\"\n assert len(session[\"access_token\"]) > 0\n\n\n# [/DEF:test_create_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_create_session [TYPE Function]\n# @BRIEF Ensures session creation returns bearer token payload fields.\n# @RELATION BINDS_TO -> test_auth\ndef test_create_session(auth_service, auth_repo):\n \"\"\"Test session token creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n session = auth_service.create_session(user)\n assert \"access_token\" in session\n assert \"token_type\" in session\n assert session[\"token_type\"] == \"bearer\"\n assert len(session[\"access_token\"]) > 0\n# #endregion test_create_session\n" }, { "contract_id": "test_role_permission_association", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 143, - "end_line": 166, + "start_line": 108, + "end_line": 125, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -29932,15 +27747,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_role_permission_association:Function]\n# @PURPOSE: Confirms role-permission many-to-many assignments persist and reload correctly.\n# @RELATION: BINDS_TO -> test_auth\ndef test_role_permission_association(auth_repo):\n \"\"\"Test role and permission association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n perm1 = Permission(resource=\"admin:users\", action=\"READ\")\n perm2 = Permission(resource=\"admin:users\", action=\"WRITE\")\n\n role.permissions.extend([perm1, perm2])\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n retrieved_role = auth_repo.get_role_by_name(\"Admin\")\n assert retrieved_role is not None\n assert len(retrieved_role.permissions) == 2\n\n permissions = [f\"{p.resource}:{p.action}\" for p in retrieved_role.permissions]\n assert \"admin:users:READ\" in permissions\n assert \"admin:users:WRITE\" in permissions\n\n\n# [/DEF:test_role_permission_association:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_role_permission_association [TYPE Function]\n# @BRIEF Confirms role-permission many-to-many assignments persist and reload correctly.\n# @RELATION BINDS_TO -> test_auth\ndef test_role_permission_association(auth_repo):\n \"\"\"Test role and permission association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n perm1 = Permission(resource=\"admin:users\", action=\"READ\")\n perm2 = Permission(resource=\"admin:users\", action=\"WRITE\")\n role.permissions.extend([perm1, perm2])\n auth_repo.db.add(role)\n auth_repo.db.commit()\n retrieved_role = auth_repo.get_role_by_name(\"Admin\")\n assert retrieved_role is not None\n assert len(retrieved_role.permissions) == 2\n permissions = [f\"{p.resource}:{p.action}\" for p in retrieved_role.permissions]\n assert \"admin:users:READ\" in permissions\n assert \"admin:users:WRITE\" in permissions\n# #endregion test_role_permission_association\n" }, { "contract_id": "test_user_role_association", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 169, - "end_line": 194, + "start_line": 126, + "end_line": 146, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -29965,15 +27780,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_user_role_association:Function]\n# @PURPOSE: Confirms user-role assignment persists and is queryable from repository reads.\n# @RELATION: BINDS_TO -> test_auth\ndef test_user_role_association(auth_repo):\n \"\"\"Test user and role association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n user = User(\n username=\"adminuser\",\n email=\"admin@example.com\",\n password_hash=get_password_hash(\"adminpass123\"),\n auth_source=\"LOCAL\",\n )\n\n user.roles.append(role)\n\n auth_repo.db.add(role)\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"adminuser\")\n assert retrieved_user is not None\n assert len(retrieved_user.roles) == 1\n assert retrieved_user.roles[0].name == \"Admin\"\n\n\n# [/DEF:test_user_role_association:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_user_role_association [TYPE Function]\n# @BRIEF Confirms user-role assignment persists and is queryable from repository reads.\n# @RELATION BINDS_TO -> test_auth\ndef test_user_role_association(auth_repo):\n \"\"\"Test user and role association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n user = User(\n username=\"adminuser\",\n email=\"admin@example.com\",\n password_hash=get_password_hash(\"adminpass123\"),\n auth_source=\"LOCAL\",\n )\n user.roles.append(role)\n auth_repo.db.add(role)\n auth_repo.db.add(user)\n auth_repo.db.commit()\n retrieved_user = auth_repo.get_user_by_username(\"adminuser\")\n assert retrieved_user is not None\n assert len(retrieved_user.roles) == 1\n assert retrieved_user.roles[0].name == \"Admin\"\n# #endregion test_user_role_association\n" }, { "contract_id": "test_ad_group_mapping", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 197, - "end_line": 221, + "start_line": 147, + "end_line": 165, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -29998,15 +27813,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_ad_group_mapping:Function]\n# @PURPOSE: Verifies AD group mapping rows persist and reference the expected role.\n# @RELATION: BINDS_TO -> test_auth\ndef test_ad_group_mapping(auth_repo):\n \"\"\"Test AD group mapping\"\"\"\n role = Role(name=\"ADFS_Admin\", description=\"ADFS administrators\")\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\ADFS_Admins\", role_id=role.id)\n\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n\n retrieved_mapping = (\n auth_repo.db.query(ADGroupMapping)\n .filter_by(ad_group=\"DOMAIN\\\\ADFS_Admins\")\n .first()\n )\n assert retrieved_mapping is not None\n assert retrieved_mapping.role_id == role.id\n\n\n# [/DEF:test_ad_group_mapping:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_ad_group_mapping [TYPE Function]\n# @BRIEF Verifies AD group mapping rows persist and reference the expected role.\n# @RELATION BINDS_TO -> test_auth\ndef test_ad_group_mapping(auth_repo):\n \"\"\"Test AD group mapping\"\"\"\n role = Role(name=\"ADFS_Admin\", description=\"ADFS administrators\")\n auth_repo.db.add(role)\n auth_repo.db.commit()\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\ADFS_Admins\", role_id=role.id)\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n retrieved_mapping = (\n auth_repo.db.query(ADGroupMapping)\n .filter_by(ad_group=\"DOMAIN\\\\ADFS_Admins\")\n .first()\n )\n assert retrieved_mapping is not None\n assert retrieved_mapping.role_id == role.id\n# #endregion test_ad_group_mapping\n" }, { "contract_id": "test_authenticate_user_updates_last_login", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 224, - "end_line": 245, + "start_line": 166, + "end_line": 183, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -30031,15 +27846,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_authenticate_user_updates_last_login:Function]\n# @PURPOSE: Verifies successful authentication updates last_login audit field.\n# @RELATION: BINDS_TO -> test_auth\ndef test_authenticate_user_updates_last_login(auth_service, auth_repo):\n \"\"\"@SIDE_EFFECT: authenticate_user updates last_login timestamp on success.\"\"\"\n user = User(\n username=\"loginuser\",\n email=\"login@example.com\",\n password_hash=get_password_hash(\"mypassword\"),\n auth_source=\"LOCAL\",\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n assert user.last_login is None\n\n authenticated = auth_service.authenticate_user(\"loginuser\", \"mypassword\")\n assert authenticated is not None\n assert authenticated.last_login is not None\n\n\n# [/DEF:test_authenticate_user_updates_last_login:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_authenticate_user_updates_last_login [TYPE Function]\n# @BRIEF Verifies successful authentication updates last_login audit field.\n# @RELATION BINDS_TO -> test_auth\ndef test_authenticate_user_updates_last_login(auth_service, auth_repo):\n \"\"\"@SIDE_EFFECT: authenticate_user updates last_login timestamp on success.\"\"\"\n user = User(\n username=\"loginuser\",\n email=\"login@example.com\",\n password_hash=get_password_hash(\"mypassword\"),\n auth_source=\"LOCAL\",\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n assert user.last_login is None\n authenticated = auth_service.authenticate_user(\"loginuser\", \"mypassword\")\n assert authenticated is not None\n assert authenticated.last_login is not None\n# #endregion test_authenticate_user_updates_last_login\n" }, { "contract_id": "test_authenticate_inactive_user", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 248, - "end_line": 267, + "start_line": 184, + "end_line": 200, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -30064,15 +27879,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_authenticate_inactive_user:Function]\n# @PURPOSE: Verifies inactive accounts are rejected during password authentication.\n# @RELATION: BINDS_TO -> test_auth\ndef test_authenticate_inactive_user(auth_service, auth_repo):\n \"\"\"@PRE: User with is_active=False should not authenticate.\"\"\"\n user = User(\n username=\"inactive_user\",\n email=\"inactive@example.com\",\n password_hash=get_password_hash(\"testpass\"),\n auth_source=\"LOCAL\",\n is_active=False,\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n result = auth_service.authenticate_user(\"inactive_user\", \"testpass\")\n assert result is None\n\n\n# [/DEF:test_authenticate_inactive_user:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_authenticate_inactive_user [TYPE Function]\n# @BRIEF Verifies inactive accounts are rejected during password authentication.\n# @RELATION BINDS_TO -> test_auth\ndef test_authenticate_inactive_user(auth_service, auth_repo):\n \"\"\"@PRE: User with is_active=False should not authenticate.\"\"\"\n user = User(\n username=\"inactive_user\",\n email=\"inactive@example.com\",\n password_hash=get_password_hash(\"testpass\"),\n auth_source=\"LOCAL\",\n is_active=False,\n )\n auth_repo.db.add(user)\n auth_repo.db.commit()\n result = auth_service.authenticate_user(\"inactive_user\", \"testpass\")\n assert result is None\n# #endregion test_authenticate_inactive_user\n" }, { "contract_id": "test_verify_password_empty_hash", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 270, - "end_line": 279, + "start_line": 201, + "end_line": 208, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -30097,15 +27912,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_verify_password_empty_hash:Function]\n# @PURPOSE: Verifies password verification safely rejects empty or null password hashes.\n# @RELATION: BINDS_TO -> test_auth\ndef test_verify_password_empty_hash():\n \"\"\"@PRE: verify_password with empty/None hash returns False.\"\"\"\n assert verify_password(\"anypassword\", \"\") is False\n assert verify_password(\"anypassword\", None) is False\n\n\n# [/DEF:test_verify_password_empty_hash:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_verify_password_empty_hash [TYPE Function]\n# @BRIEF Verifies password verification safely rejects empty or null password hashes.\n# @RELATION BINDS_TO -> test_auth\ndef test_verify_password_empty_hash():\n \"\"\"@PRE: verify_password with empty/None hash returns False.\"\"\"\n assert verify_password(\"anypassword\", \"\") is False\n assert verify_password(\"anypassword\", None) is False\n# #endregion test_verify_password_empty_hash\n" }, { "contract_id": "test_provision_adfs_user_new", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 282, - "end_line": 311, + "start_line": 209, + "end_line": 233, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -30130,15 +27945,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_provision_adfs_user_new:Function]\n# @PURPOSE: Verifies JIT provisioning creates a new ADFS user and maps group-derived roles.\n# @RELATION: BINDS_TO -> test_auth\ndef test_provision_adfs_user_new(auth_service, auth_repo):\n \"\"\"@POST: provision_adfs_user creates a new ADFS user with correct roles.\"\"\"\n # Set up a role and AD group mapping\n role = Role(name=\"ADFS_Viewer\", description=\"ADFS viewer role\")\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\Viewers\", role_id=role.id)\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n\n user_info = {\n \"upn\": \"newadfsuser@domain.com\",\n \"email\": \"newadfsuser@domain.com\",\n \"groups\": [\"DOMAIN\\\\Viewers\"],\n }\n\n user = auth_service.provision_adfs_user(user_info)\n assert user is not None\n assert user.username == \"newadfsuser@domain.com\"\n assert user.auth_source == \"ADFS\"\n assert user.is_active is True\n assert len(user.roles) == 1\n assert user.roles[0].name == \"ADFS_Viewer\"\n\n\n# [/DEF:test_provision_adfs_user_new:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_provision_adfs_user_new [TYPE Function]\n# @BRIEF Verifies JIT provisioning creates a new ADFS user and maps group-derived roles.\n# @RELATION BINDS_TO -> test_auth\ndef test_provision_adfs_user_new(auth_service, auth_repo):\n \"\"\"@POST: provision_adfs_user creates a new ADFS user with correct roles.\"\"\"\n # Set up a role and AD group mapping\n role = Role(name=\"ADFS_Viewer\", description=\"ADFS viewer role\")\n auth_repo.db.add(role)\n auth_repo.db.commit()\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\Viewers\", role_id=role.id)\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n user_info = {\n \"upn\": \"newadfsuser@domain.com\",\n \"email\": \"newadfsuser@domain.com\",\n \"groups\": [\"DOMAIN\\\\Viewers\"],\n }\n user = auth_service.provision_adfs_user(user_info)\n assert user is not None\n assert user.username == \"newadfsuser@domain.com\"\n assert user.auth_source == \"ADFS\"\n assert user.is_active is True\n assert len(user.roles) == 1\n assert user.roles[0].name == \"ADFS_Viewer\"\n# #endregion test_provision_adfs_user_new\n" }, { "contract_id": "test_provision_adfs_user_existing", "contract_type": "Function", "file_path": "backend/src/core/auth/__tests__/test_auth.py", - "start_line": 314, - "end_line": 342, + "start_line": 234, + "end_line": 258, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -30163,29 +27978,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_provision_adfs_user_existing:Function]\n# @PURPOSE: Verifies JIT provisioning reuses existing ADFS user and refreshes role assignments.\n# @RELATION: BINDS_TO -> test_auth\ndef test_provision_adfs_user_existing(auth_service, auth_repo):\n \"\"\"@POST: provision_adfs_user updates roles for existing user.\"\"\"\n # Create existing user\n existing = User(\n username=\"existingadfs@domain.com\",\n email=\"existingadfs@domain.com\",\n auth_source=\"ADFS\",\n is_active=True,\n )\n auth_repo.db.add(existing)\n auth_repo.db.commit()\n\n user_info = {\n \"upn\": \"existingadfs@domain.com\",\n \"email\": \"existingadfs@domain.com\",\n \"groups\": [],\n }\n\n user = auth_service.provision_adfs_user(user_info)\n assert user is not None\n assert user.username == \"existingadfs@domain.com\"\n assert len(user.roles) == 0 # No matching group mappings\n\n\n# [/DEF:test_auth:Module]\n# [/DEF:test_provision_adfs_user_existing:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_provision_adfs_user_existing [TYPE Function]\n# @BRIEF Verifies JIT provisioning reuses existing ADFS user and refreshes role assignments.\n# @RELATION BINDS_TO -> test_auth\ndef test_provision_adfs_user_existing(auth_service, auth_repo):\n \"\"\"@POST: provision_adfs_user updates roles for existing user.\"\"\"\n # Create existing user\n existing = User(\n username=\"existingadfs@domain.com\",\n email=\"existingadfs@domain.com\",\n auth_source=\"ADFS\",\n is_active=True,\n )\n auth_repo.db.add(existing)\n auth_repo.db.commit()\n user_info = {\n \"upn\": \"existingadfs@domain.com\",\n \"email\": \"existingadfs@domain.com\",\n \"groups\": [],\n }\n user = auth_service.provision_adfs_user(user_info)\n assert user is not None\n assert user.username == \"existingadfs@domain.com\"\n assert len(user.roles) == 0 # No matching group mappings\n# #endregion test_auth\n# #endregion test_provision_adfs_user_existing\n" }, { "contract_id": "AuthConfigModule", "contract_type": "Module", "file_path": "backend/src/core/auth/config.py", "start_line": 1, - "end_line": 50, + "end_line": 47, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "All sensitive configuration must have defaults or be loaded from environment.", "LAYER": "Core", - "PURPOSE": "Centralized configuration for authentication and authorization.", - "SEMANTICS": [ - "auth", - "config", - "settings", - "jwt", - "adfs" - ] + "PURPOSE": "Centralized configuration for authentication and authorization." }, "relations": [ { @@ -30215,15 +28023,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthConfigModule:Module]\n#\n# @SEMANTICS: auth, config, settings, jwt, adfs\n# @PURPOSE: Centralized configuration for authentication and authorization.\n# @COMPLEXITY: 2\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> pydantic\n#\n# @INVARIANT: All sensitive configuration must have defaults or be loaded from environment.\n\n# [SECTION: IMPORTS]\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\n# [/SECTION]\n\n# [DEF:AuthConfig:Class]\n# @PURPOSE: Holds authentication-related settings.\n# @PRE: Environment variables may be provided via .env file.\n# @POST: Returns a configuration object with validated settings.\n# @RELATION: INHERITS -> pydantic_settings.BaseSettings\nclass AuthConfig(BaseSettings):\n # JWT Settings\n SECRET_KEY: str = Field(default=\"super-secret-key-change-in-production\", env=\"AUTH_SECRET_KEY\")\n ALGORITHM: str = \"HS256\"\n ACCESS_TOKEN_EXPIRE_MINUTES: int = 480\n REFRESH_TOKEN_EXPIRE_DAYS: int = 7\n\n # Database Settings\n AUTH_DATABASE_URL: str = Field(\n default=\"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n env=\"AUTH_DATABASE_URL\",\n )\n\n # ADFS Settings\n ADFS_CLIENT_ID: str = Field(default=\"\", env=\"ADFS_CLIENT_ID\")\n ADFS_CLIENT_SECRET: str = Field(default=\"\", env=\"ADFS_CLIENT_SECRET\")\n ADFS_METADATA_URL: str = Field(default=\"\", env=\"ADFS_METADATA_URL\")\n \n class Config:\n env_file = \".env\"\n extra = \"ignore\"\n# [/DEF:AuthConfig:Class]\n\n# [DEF:auth_config:Variable]\n# @PURPOSE: Singleton instance of AuthConfig.\n# @RELATION: DEPENDS_ON -> AuthConfig\nauth_config = AuthConfig()\n# [/DEF:auth_config:Variable]\n\n# [/DEF:AuthConfigModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region AuthConfigModule [C:2] [TYPE Module] [SEMANTICS pydantic, auth, auth-config, config]\n#\n# @BRIEF Centralized configuration for authentication and authorization.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> pydantic\n#\n# @INVARIANT: All sensitive configuration must have defaults or be loaded from environment.\n\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\n\n\n# #region AuthConfig [TYPE Class]\n# @BRIEF Holds authentication-related settings.\n# @PRE: Environment variables may be provided via .env file.\n# @POST: Returns a configuration object with validated settings.\n# @RELATION INHERITS -> pydantic_settings.BaseSettings\nclass AuthConfig(BaseSettings):\n # JWT Settings\n SECRET_KEY: str = Field(default=\"super-secret-key-change-in-production\", env=\"AUTH_SECRET_KEY\")\n ALGORITHM: str = \"HS256\"\n ACCESS_TOKEN_EXPIRE_MINUTES: int = 480\n REFRESH_TOKEN_EXPIRE_DAYS: int = 7\n\n # Database Settings\n AUTH_DATABASE_URL: str = Field(\n default=\"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n env=\"AUTH_DATABASE_URL\",\n )\n\n # ADFS Settings\n ADFS_CLIENT_ID: str = Field(default=\"\", env=\"ADFS_CLIENT_ID\")\n ADFS_CLIENT_SECRET: str = Field(default=\"\", env=\"ADFS_CLIENT_SECRET\")\n ADFS_METADATA_URL: str = Field(default=\"\", env=\"ADFS_METADATA_URL\")\n\n class Config:\n env_file = \".env\"\n extra = \"ignore\"\n# #endregion AuthConfig\n\n# #region auth_config [TYPE Variable]\n# @BRIEF Singleton instance of AuthConfig.\n# @RELATION DEPENDS_ON -> AuthConfig\nauth_config = AuthConfig()\n# #endregion auth_config\n\n# #endregion AuthConfigModule\n" }, { "contract_id": "AuthConfig", "contract_type": "Class", "file_path": "backend/src/core/auth/config.py", - "start_line": 16, - "end_line": 42, + "start_line": 13, + "end_line": 39, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30277,15 +28085,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthConfig:Class]\n# @PURPOSE: Holds authentication-related settings.\n# @PRE: Environment variables may be provided via .env file.\n# @POST: Returns a configuration object with validated settings.\n# @RELATION: INHERITS -> pydantic_settings.BaseSettings\nclass AuthConfig(BaseSettings):\n # JWT Settings\n SECRET_KEY: str = Field(default=\"super-secret-key-change-in-production\", env=\"AUTH_SECRET_KEY\")\n ALGORITHM: str = \"HS256\"\n ACCESS_TOKEN_EXPIRE_MINUTES: int = 480\n REFRESH_TOKEN_EXPIRE_DAYS: int = 7\n\n # Database Settings\n AUTH_DATABASE_URL: str = Field(\n default=\"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n env=\"AUTH_DATABASE_URL\",\n )\n\n # ADFS Settings\n ADFS_CLIENT_ID: str = Field(default=\"\", env=\"ADFS_CLIENT_ID\")\n ADFS_CLIENT_SECRET: str = Field(default=\"\", env=\"ADFS_CLIENT_SECRET\")\n ADFS_METADATA_URL: str = Field(default=\"\", env=\"ADFS_METADATA_URL\")\n \n class Config:\n env_file = \".env\"\n extra = \"ignore\"\n# [/DEF:AuthConfig:Class]\n" + "anchor_syntax": "region", + "body": "# #region AuthConfig [TYPE Class]\n# @BRIEF Holds authentication-related settings.\n# @PRE: Environment variables may be provided via .env file.\n# @POST: Returns a configuration object with validated settings.\n# @RELATION INHERITS -> pydantic_settings.BaseSettings\nclass AuthConfig(BaseSettings):\n # JWT Settings\n SECRET_KEY: str = Field(default=\"super-secret-key-change-in-production\", env=\"AUTH_SECRET_KEY\")\n ALGORITHM: str = \"HS256\"\n ACCESS_TOKEN_EXPIRE_MINUTES: int = 480\n REFRESH_TOKEN_EXPIRE_DAYS: int = 7\n\n # Database Settings\n AUTH_DATABASE_URL: str = Field(\n default=\"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n env=\"AUTH_DATABASE_URL\",\n )\n\n # ADFS Settings\n ADFS_CLIENT_ID: str = Field(default=\"\", env=\"ADFS_CLIENT_ID\")\n ADFS_CLIENT_SECRET: str = Field(default=\"\", env=\"ADFS_CLIENT_SECRET\")\n ADFS_METADATA_URL: str = Field(default=\"\", env=\"ADFS_METADATA_URL\")\n\n class Config:\n env_file = \".env\"\n extra = \"ignore\"\n# #endregion AuthConfig\n" }, { "contract_id": "auth_config", "contract_type": "Variable", "file_path": "backend/src/core/auth/config.py", - "start_line": 44, - "end_line": 48, + "start_line": 41, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30337,28 +28145,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:auth_config:Variable]\n# @PURPOSE: Singleton instance of AuthConfig.\n# @RELATION: DEPENDS_ON -> AuthConfig\nauth_config = AuthConfig()\n# [/DEF:auth_config:Variable]\n" + "anchor_syntax": "region", + "body": "# #region auth_config [TYPE Variable]\n# @BRIEF Singleton instance of AuthConfig.\n# @RELATION DEPENDS_ON -> AuthConfig\nauth_config = AuthConfig()\n# #endregion auth_config\n" }, { "contract_id": "AuthJwtModule", "contract_type": "Module", "file_path": "backend/src/core/auth/jwt.py", "start_line": 1, - "end_line": 69, + "end_line": 60, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Tokens must include expiration time and user identifier.", "LAYER": "Core", - "PURPOSE": "JWT token generation and validation logic.", - "SEMANTICS": [ - "jwt", - "token", - "session", - "auth" - ] + "PURPOSE": "JWT token generation and validation logic." }, "relations": [ { @@ -30403,23 +28205,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthJwtModule:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: jwt, token, session, auth\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom datetime import datetime, timedelta\nfrom typing import Optional\nfrom jose import jwt\nfrom .config import auth_config\nfrom ..logger import belief_scope\n# [/SECTION]\n\n\n# [DEF:create_access_token:Function]\n# @PURPOSE: 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#\n# @PARAM: data (dict) - Payload data for the token.\n# @PARAM: expires_delta (Optional[timedelta]) - Custom expiration time.\n# @RETURN: str - The encoded JWT.\ndef create_access_token(data: dict, expires_delta: Optional[timedelta] = 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# [/DEF:create_access_token:Function]\n\n\n# [DEF:decode_token:Function]\n# @PURPOSE: 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#\n# @PARAM: token (str) - The JWT to decode.\n# @RETURN: dict - The decoded payload.\n# @THROW: jose.JWTError - If token is invalid or expired.\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# [/DEF:decode_token:Function]\n\n# [/DEF:AuthJwtModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "create_access_token", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 21, - "end_line": 47, + "start_line": 18, + "end_line": 41, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "expires_delta (Optional[timedelta]) - Custom expiration time.", "POST": "Returns a signed JWT string.", "PRE": "data dict contains 'sub' (user_id) and optional 'scopes' (roles).", - "PURPOSE": "Generates a new JWT access token.", - "RETURN": "str - The encoded JWT." + "PURPOSE": "Generates a new JWT access token." }, "relations": [ { @@ -30430,12 +28230,6 @@ } ], "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", @@ -30463,12 +28257,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -30479,24 +28267,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:create_access_token:Function]\n# @PURPOSE: 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#\n# @PARAM: data (dict) - Payload data for the token.\n# @PARAM: expires_delta (Optional[timedelta]) - Custom expiration time.\n# @RETURN: str - The encoded JWT.\ndef create_access_token(data: dict, expires_delta: Optional[timedelta] = 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# [/DEF:create_access_token:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "decode_token", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 50, - "end_line": 67, + "start_line": 44, + "end_line": 58, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "token (str) - The JWT to decode.", "POST": "Returns the decoded payload if valid.", "PRE": "token is a signed JWT string.", - "PURPOSE": "Decodes and validates a JWT token.", - "RETURN": "dict - The decoded payload.", - "THROW": "jose.JWTError - If token is invalid or expired." + "PURPOSE": "Decodes and validates a JWT token." }, "relations": [ { @@ -30507,12 +28292,6 @@ } ], "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", @@ -30540,18 +28319,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -30562,28 +28329,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:decode_token:Function]\n# @PURPOSE: 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#\n# @PARAM: token (str) - The JWT to decode.\n# @RETURN: dict - The decoded payload.\n# @THROW: jose.JWTError - If token is invalid or expired.\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# [/DEF:decode_token:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "AuthLoggerModule", "contract_type": "Module", "file_path": "backend/src/core/auth/logger.py", "start_line": 1, - "end_line": 33, + "end_line": 28, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Must not log sensitive data like passwords or full tokens.", "LAYER": "Core", - "PURPOSE": "Audit logging for security-related events.", - "SEMANTICS": [ - "auth", - "logger", - "audit", - "security" - ] + "PURPOSE": "Audit logging for security-related events." }, "relations": [ { @@ -30622,19 +28383,18 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthLoggerModule:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: auth, logger, audit, security\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom ..logger import logger, belief_scope\nfrom datetime import datetime\n# [/SECTION]\n\n# [DEF:log_security_event:Function]\n# @PURPOSE: 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\n# @PARAM: event_type (str) - Type of event (e.g., LOGIN_SUCCESS, PERMISSION_DENIED).\n# @PARAM: username (str) - The user involved in the event.\n# @PARAM: details (dict) - Additional non-sensitive metadata.\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# [/DEF:log_security_event:Function]\n\n# [/DEF:AuthLoggerModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "log_security_event", "contract_type": "Function", "file_path": "backend/src/core/auth/logger.py", - "start_line": 16, - "end_line": 31, + "start_line": 14, + "end_line": 26, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "details (dict) - Additional non-sensitive metadata.", "POST": "Security event is written to the application log.", "PRE": "event_type and username are strings.", "PURPOSE": "Logs a security-related event for audit trails." @@ -30648,12 +28408,6 @@ } ], "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", @@ -30709,28 +28463,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:log_security_event:Function]\n# @PURPOSE: 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\n# @PARAM: event_type (str) - Type of event (e.g., LOGIN_SUCCESS, PERMISSION_DENIED).\n# @PARAM: username (str) - The user involved in the event.\n# @PARAM: details (dict) - Additional non-sensitive metadata.\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# [/DEF:log_security_event:Function]\n" + "anchor_syntax": "region", + "body": "# #region log_security_event [TYPE Function]\n# @BRIEF Logs a security-related event for audit trails.\n# @PRE: event_type and username are strings.\n# @POST: Security event is written to the application log.\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" }, { "contract_id": "AuthOauthModule", "contract_type": "Module", "file_path": "backend/src/core/auth/oauth.py", "start_line": 1, - "end_line": 56, + "end_line": 52, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "Must use secure OIDC flows.", "LAYER": "Core", - "PURPOSE": "ADFS OIDC configuration and client using Authlib.", - "SEMANTICS": [ - "auth", - "oauth", - "oidc", - "adfs" - ] + "PURPOSE": "ADFS OIDC configuration and client using Authlib." }, "relations": [ { @@ -30784,15 +28532,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthOauthModule:Module]\n#\n# @SEMANTICS: auth, oauth, oidc, adfs\n# @PURPOSE: ADFS OIDC configuration and client using Authlib.\n# @COMPLEXITY: 2\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> authlib\n# @RELATION: USES -> auth_config\n#\n# @INVARIANT: Must use secure OIDC flows.\n\n# [SECTION: IMPORTS]\nfrom authlib.integrations.starlette_client import OAuth\nfrom .config import auth_config\n# [/SECTION]\n\n# [DEF:oauth:Variable]\n# @PURPOSE: Global Authlib OAuth registry.\n# @RELATION: DEPENDS_ON -> OAuth\noauth = OAuth()\n# [/DEF:oauth:Variable]\n\n# [DEF:register_adfs:Function]\n# @PURPOSE: Registers the ADFS OIDC client.\n# @PRE: ADFS configuration is provided in auth_config.\n# @POST: ADFS client is registered in oauth registry.\n# @RELATION: USES -> oauth\n# @RELATION: USES -> auth_config\ndef register_adfs():\n if auth_config.ADFS_CLIENT_ID:\n oauth.register(\n name='adfs',\n client_id=auth_config.ADFS_CLIENT_ID,\n client_secret=auth_config.ADFS_CLIENT_SECRET,\n server_metadata_url=auth_config.ADFS_METADATA_URL,\n client_kwargs={\n 'scope': 'openid email profile groups'\n }\n )\n# [/DEF:register_adfs:Function]\n\n# [DEF:is_adfs_configured:Function]\n# @PURPOSE: Checks if ADFS is properly configured.\n# @PRE: None.\n# @POST: Returns True if ADFS client is registered, False otherwise.\n# @RELATION: USES -> oauth\n# @RETURN: bool - Configuration status.\ndef is_adfs_configured() -> bool:\n \"\"\"Check if ADFS OAuth client is registered.\"\"\"\n return 'adfs' in oauth._registry\n# [/DEF:is_adfs_configured:Function]\n\n# Initial registration\nregister_adfs()\n\n# [/DEF:AuthOauthModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region AuthOauthModule [C:2] [TYPE Module] [SEMANTICS auth, oauth, oidc, adfs, authlib]\n#\n# @BRIEF ADFS OIDC configuration and client using Authlib.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> authlib\n# @RELATION USES -> auth_config\n#\n# @INVARIANT: Must use secure OIDC flows.\n\nfrom authlib.integrations.starlette_client import OAuth\n\nfrom .config import auth_config\n\n# #region oauth [TYPE Variable]\n# @BRIEF Global Authlib OAuth registry.\n# @RELATION DEPENDS_ON -> OAuth\noauth = OAuth()\n# #endregion oauth\n\n# #region register_adfs [TYPE Function]\n# @BRIEF Registers the ADFS OIDC client.\n# @PRE: ADFS configuration is provided in auth_config.\n# @POST: ADFS client is registered in oauth registry.\n# @RELATION USES -> oauth\n# @RELATION USES -> auth_config\ndef register_adfs():\n if auth_config.ADFS_CLIENT_ID:\n oauth.register(\n name='adfs',\n client_id=auth_config.ADFS_CLIENT_ID,\n client_secret=auth_config.ADFS_CLIENT_SECRET,\n server_metadata_url=auth_config.ADFS_METADATA_URL,\n client_kwargs={\n 'scope': 'openid email profile groups'\n }\n )\n# #endregion register_adfs\n\n# #region is_adfs_configured [TYPE Function]\n# @BRIEF Checks if ADFS is properly configured.\n# @PRE: None.\n# @POST: Returns True if ADFS client is registered, False otherwise.\n# @RELATION USES -> oauth\ndef is_adfs_configured() -> bool:\n \"\"\"Check if ADFS OAuth client is registered.\"\"\"\n return 'adfs' in oauth._registry\n# #endregion is_adfs_configured\n\n# Initial registration\nregister_adfs()\n\n# #endregion AuthOauthModule\n" }, { "contract_id": "oauth", "contract_type": "Variable", "file_path": "backend/src/core/auth/oauth.py", - "start_line": 17, - "end_line": 21, + "start_line": 14, + "end_line": 18, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30844,15 +28592,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:oauth:Variable]\n# @PURPOSE: Global Authlib OAuth registry.\n# @RELATION: DEPENDS_ON -> OAuth\noauth = OAuth()\n# [/DEF:oauth:Variable]\n" + "anchor_syntax": "region", + "body": "# #region oauth [TYPE Variable]\n# @BRIEF Global Authlib OAuth registry.\n# @RELATION DEPENDS_ON -> OAuth\noauth = OAuth()\n# #endregion oauth\n" }, { "contract_id": "register_adfs", "contract_type": "Function", "file_path": "backend/src/core/auth/oauth.py", - "start_line": 23, - "end_line": 40, + "start_line": 20, + "end_line": 37, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30948,22 +28696,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:register_adfs:Function]\n# @PURPOSE: Registers the ADFS OIDC client.\n# @PRE: ADFS configuration is provided in auth_config.\n# @POST: ADFS client is registered in oauth registry.\n# @RELATION: USES -> oauth\n# @RELATION: USES -> auth_config\ndef register_adfs():\n if auth_config.ADFS_CLIENT_ID:\n oauth.register(\n name='adfs',\n client_id=auth_config.ADFS_CLIENT_ID,\n client_secret=auth_config.ADFS_CLIENT_SECRET,\n server_metadata_url=auth_config.ADFS_METADATA_URL,\n client_kwargs={\n 'scope': 'openid email profile groups'\n }\n )\n# [/DEF:register_adfs:Function]\n" + "anchor_syntax": "region", + "body": "# #region register_adfs [TYPE Function]\n# @BRIEF Registers the ADFS OIDC client.\n# @PRE: ADFS configuration is provided in auth_config.\n# @POST: ADFS client is registered in oauth registry.\n# @RELATION USES -> oauth\n# @RELATION USES -> auth_config\ndef register_adfs():\n if auth_config.ADFS_CLIENT_ID:\n oauth.register(\n name='adfs',\n client_id=auth_config.ADFS_CLIENT_ID,\n client_secret=auth_config.ADFS_CLIENT_SECRET,\n server_metadata_url=auth_config.ADFS_METADATA_URL,\n client_kwargs={\n 'scope': 'openid email profile groups'\n }\n )\n# #endregion register_adfs\n" }, { "contract_id": "is_adfs_configured", "contract_type": "Function", "file_path": "backend/src/core/auth/oauth.py", - "start_line": 42, - "end_line": 51, + "start_line": 39, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Returns True if ADFS client is registered, False otherwise.", "PRE": "None.", - "PURPOSE": "Checks if ADFS is properly configured.", - "RETURN": "bool - Configuration status." + "PURPOSE": "Checks if ADFS is properly configured." }, "relations": [ { @@ -31001,12 +28748,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31035,33 +28776,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:is_adfs_configured:Function]\n# @PURPOSE: Checks if ADFS is properly configured.\n# @PRE: None.\n# @POST: Returns True if ADFS client is registered, False otherwise.\n# @RELATION: USES -> oauth\n# @RETURN: bool - Configuration status.\ndef is_adfs_configured() -> bool:\n \"\"\"Check if ADFS OAuth client is registered.\"\"\"\n return 'adfs' in oauth._registry\n# [/DEF:is_adfs_configured:Function]\n" + "anchor_syntax": "region", + "body": "# #region is_adfs_configured [TYPE Function]\n# @BRIEF Checks if ADFS is properly configured.\n# @PRE: None.\n# @POST: Returns True if ADFS client is registered, False otherwise.\n# @RELATION USES -> oauth\ndef is_adfs_configured() -> bool:\n \"\"\"Check if ADFS OAuth client is registered.\"\"\"\n return 'adfs' in oauth._registry\n# #endregion is_adfs_configured\n" }, { "contract_id": "AuthRepositoryModule", "contract_type": "Module", "file_path": "backend/src/core/auth/repository.py", "start_line": 1, - "end_line": 159, + "end_line": 134, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[sqlalchemy.orm.Session] -> Output[User|Role|Permission|UserDashboardPreference access]", "INVARIANT": "All database read/write operations must execute via the injected SQLAlchemy session boundary.", "LAYER": "Domain", "POST": "Provides valid access to identity data.", "PRE": "Database connection is active.", "PURPOSE": "Data access layer for authentication and user preference entities.", - "SEMANTICS": [ - "auth", - "repository", - "database", - "user", - "role", - "permission" - ], "SIDE_EFFECT": "Executes database read queries through the injected SQLAlchemy session boundary." }, "relations": [ @@ -31104,15 +28837,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthRepositoryModule:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: auth, repository, database, user, role, permission\n# @PURPOSE: Data access layer for authentication and user preference entities.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [AuthModels]\n# @RELATION: DEPENDS_ON -> [ProfileModels]\n# @RELATION: DEPENDS_ON -> [belief_scope]\n# @INVARIANT: All database read/write operations must execute via the injected SQLAlchemy session boundary.\n# @DATA_CONTRACT: Input[sqlalchemy.orm.Session] -> Output[User|Role|Permission|UserDashboardPreference access]\n# @PRE: Database connection is active.\n# @POST: Provides valid access to identity data.\n# @SIDE_EFFECT: Executes database read queries through the injected SQLAlchemy session boundary.\n\n# [SECTION: IMPORTS]\nfrom typing import List, Optional\nfrom sqlalchemy.orm import Session, selectinload\nfrom ...models.auth import Permission, Role, User, ADGroupMapping\nfrom ...models.profile import UserDashboardPreference\nfrom ..logger import belief_scope, logger\n# [/SECTION]\n\n\n# [DEF:AuthRepository:Class]\n# @PURPOSE: Provides low-level CRUD operations for identity and authorization records.\n# @PRE: Database session is bound.\n# @POST: Entity instances returned safely.\n# @SIDE_EFFECT: Performs database reads.\n# @RELATION: DEPENDS_ON -> [AuthModels]\nclass AuthRepository:\n def __init__(self, db: Session):\n self.db = db\n\n # [DEF:get_user_by_id:Function]\n # @PURPOSE: Retrieve user by UUID.\n # @PRE: user_id is a valid UUID string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_id(self, user_id: str) -> Optional[User]:\n with belief_scope(\"AuthRepository.get_user_by_id\"):\n logger.reason(f\"Fetching user by id: {user_id}\")\n result = self.db.query(User).filter(User.id == user_id).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n\n # [/DEF:get_user_by_id:Function]\n\n # [DEF:get_user_by_username:Function]\n # @PURPOSE: Retrieve user by username.\n # @PRE: username is a non-empty string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_username(self, username: str) -> Optional[User]:\n with belief_scope(\"AuthRepository.get_user_by_username\"):\n logger.reason(f\"Fetching user by username: {username}\")\n result = self.db.query(User).filter(User.username == username).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n\n # [/DEF:get_user_by_username:Function]\n\n # [DEF:get_role_by_id:Function]\n # @PURPOSE: Retrieve role by UUID with permissions preloaded.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_role_by_id(self, role_id: str) -> Optional[Role]:\n with belief_scope(\"AuthRepository.get_role_by_id\"):\n return (\n self.db.query(Role)\n .options(selectinload(Role.permissions))\n .filter(Role.id == role_id)\n .first()\n )\n\n # [/DEF:get_role_by_id:Function]\n\n # [DEF:get_role_by_name:Function]\n # @PURPOSE: Retrieve role by unique name.\n # @RELATION: DEPENDS_ON -> [Role]\n def get_role_by_name(self, name: str) -> Optional[Role]:\n with belief_scope(\"AuthRepository.get_role_by_name\"):\n return self.db.query(Role).filter(Role.name == name).first()\n\n # [/DEF:get_role_by_name:Function]\n\n # [DEF:get_permission_by_id:Function]\n # @PURPOSE: Retrieve permission by UUID.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_id(self, permission_id: str) -> Optional[Permission]:\n with belief_scope(\"AuthRepository.get_permission_by_id\"):\n return (\n self.db.query(Permission).filter(Permission.id == permission_id).first()\n )\n\n # [/DEF:get_permission_by_id:Function]\n\n # [DEF:get_permission_by_resource_action:Function]\n # @PURPOSE: Retrieve permission by resource and action tuple.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_resource_action(\n self, resource: str, action: str\n ) -> Optional[Permission]:\n with belief_scope(\"AuthRepository.get_permission_by_resource_action\"):\n return (\n self.db.query(Permission)\n .filter(Permission.resource == resource, Permission.action == action)\n .first()\n )\n\n # [/DEF:get_permission_by_resource_action:Function]\n\n # [DEF:list_permissions:Function]\n # @PURPOSE: List all system permissions.\n # @RELATION: DEPENDS_ON -> [Permission]\n def list_permissions(self) -> List[Permission]:\n with belief_scope(\"AuthRepository.list_permissions\"):\n return self.db.query(Permission).all()\n\n # [/DEF:list_permissions:Function]\n\n # [DEF:get_user_dashboard_preference:Function]\n # @PURPOSE: Retrieve dashboard filters/preferences for a user.\n # @RELATION: DEPENDS_ON -> [UserDashboardPreference]\n def get_user_dashboard_preference(\n self, user_id: str\n ) -> Optional[UserDashboardPreference]:\n with belief_scope(\"AuthRepository.get_user_dashboard_preference\"):\n return (\n self.db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n\n # [/DEF:get_user_dashboard_preference:Function]\n\n # [DEF:get_roles_by_ad_groups:Function]\n # @PURPOSE: Retrieve roles that match a list of AD group names.\n # @PRE: groups is a list of strings representing AD group identifiers.\n # @POST: Returns a list of Role objects mapped to the provided AD groups.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [ADGroupMapping]\n def get_roles_by_ad_groups(self, groups: List[str]) -> List[Role]:\n with belief_scope(\"AuthRepository.get_roles_by_ad_groups\"):\n logger.reason(f\"Fetching roles for AD groups: {groups}\")\n if not groups:\n return []\n return (\n self.db.query(Role)\n .join(ADGroupMapping)\n .filter(ADGroupMapping.ad_group.in_(groups))\n .all()\n )\n\n # [/DEF:get_roles_by_ad_groups:Function]\n\n\n# [/DEF:AuthRepository:Class]\n\n# [/DEF:AuthRepositoryModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region AuthRepositoryModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, search, user, auth-repository]\n# @BRIEF Data access layer for authentication and user preference entities.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [AuthModels]\n# @RELATION DEPENDS_ON -> [ProfileModels]\n# @RELATION DEPENDS_ON -> [belief_scope]\n# @INVARIANT: All database read/write operations must execute via the injected SQLAlchemy session boundary.\n# @DATA_CONTRACT: Input[sqlalchemy.orm.Session] -> Output[User|Role|Permission|UserDashboardPreference access]\n# @PRE: Database connection is active.\n# @POST: Provides valid access to identity data.\n# @SIDE_EFFECT: Executes database read queries through the injected SQLAlchemy session boundary.\n\nfrom sqlalchemy.orm import Session, selectinload\n\nfrom ...models.auth import ADGroupMapping, Permission, Role, User\nfrom ...models.profile import UserDashboardPreference\nfrom ..logger import belief_scope, logger\n\n\n# #region AuthRepository [TYPE Class]\n# @BRIEF Provides low-level CRUD operations for identity and authorization records.\n# @PRE: Database session is bound.\n# @POST: Entity instances returned safely.\n# @SIDE_EFFECT: Performs database reads.\n# @RELATION DEPENDS_ON -> [AuthModels]\nclass AuthRepository:\n def __init__(self, db: Session):\n self.db = db\n # #region get_user_by_id [TYPE Function]\n # @PURPOSE: Retrieve user by UUID.\n # @PRE: user_id is a valid UUID string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_id(self, user_id: str) -> User | None:\n with belief_scope(\"AuthRepository.get_user_by_id\"):\n logger.reason(f\"Fetching user by id: {user_id}\")\n result = self.db.query(User).filter(User.id == user_id).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n # #endregion get_user_by_id\n # #region get_user_by_username [TYPE Function]\n # @PURPOSE: Retrieve user by username.\n # @PRE: username is a non-empty string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_username(self, username: str) -> User | None:\n with belief_scope(\"AuthRepository.get_user_by_username\"):\n logger.reason(f\"Fetching user by username: {username}\")\n result = self.db.query(User).filter(User.username == username).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n # #endregion get_user_by_username\n # #region get_role_by_id [TYPE Function]\n # @PURPOSE: Retrieve role by UUID with permissions preloaded.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_role_by_id(self, role_id: str) -> Role | None:\n with belief_scope(\"AuthRepository.get_role_by_id\"):\n return (\n self.db.query(Role)\n .options(selectinload(Role.permissions))\n .filter(Role.id == role_id)\n .first()\n )\n # #endregion get_role_by_id\n # #region get_role_by_name [TYPE Function]\n # @PURPOSE: Retrieve role by unique name.\n # @RELATION: DEPENDS_ON -> [Role]\n def get_role_by_name(self, name: str) -> Role | None:\n with belief_scope(\"AuthRepository.get_role_by_name\"):\n return self.db.query(Role).filter(Role.name == name).first()\n # #endregion get_role_by_name\n # #region get_permission_by_id [TYPE Function]\n # @PURPOSE: Retrieve permission by UUID.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_id(self, permission_id: str) -> Permission | None:\n with belief_scope(\"AuthRepository.get_permission_by_id\"):\n return (\n self.db.query(Permission).filter(Permission.id == permission_id).first()\n )\n # #endregion get_permission_by_id\n # #region get_permission_by_resource_action [TYPE Function]\n # @PURPOSE: Retrieve permission by resource and action tuple.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_resource_action(\n self, resource: str, action: str\n ) -> Permission | None:\n with belief_scope(\"AuthRepository.get_permission_by_resource_action\"):\n return (\n self.db.query(Permission)\n .filter(Permission.resource == resource, Permission.action == action)\n .first()\n )\n # #endregion get_permission_by_resource_action\n # #region list_permissions [TYPE Function]\n # @PURPOSE: List all system permissions.\n # @RELATION: DEPENDS_ON -> [Permission]\n def list_permissions(self) -> list[Permission]:\n with belief_scope(\"AuthRepository.list_permissions\"):\n return self.db.query(Permission).all()\n # #endregion list_permissions\n # #region get_user_dashboard_preference [TYPE Function]\n # @PURPOSE: Retrieve dashboard filters/preferences for a user.\n # @RELATION: DEPENDS_ON -> [UserDashboardPreference]\n def get_user_dashboard_preference(\n self, user_id: str\n ) -> UserDashboardPreference | None:\n with belief_scope(\"AuthRepository.get_user_dashboard_preference\"):\n return (\n self.db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n # #endregion get_user_dashboard_preference\n # #region get_roles_by_ad_groups [TYPE Function]\n # @PURPOSE: Retrieve roles that match a list of AD group names.\n # @PRE: groups is a list of strings representing AD group identifiers.\n # @POST: Returns a list of Role objects mapped to the provided AD groups.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [ADGroupMapping]\n def get_roles_by_ad_groups(self, groups: list[str]) -> list[Role]:\n with belief_scope(\"AuthRepository.get_roles_by_ad_groups\"):\n logger.reason(f\"Fetching roles for AD groups: {groups}\")\n if not groups:\n return []\n return (\n self.db.query(Role)\n .join(ADGroupMapping)\n .filter(ADGroupMapping.ad_group.in_(groups))\n .all()\n )\n # #endregion get_roles_by_ad_groups\n# #endregion AuthRepository\n# #endregion AuthRepositoryModule\n" }, { "contract_id": "AuthRepository", "contract_type": "Class", "file_path": "backend/src/core/auth/repository.py", - "start_line": 24, - "end_line": 157, + "start_line": 20, + "end_line": 133, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31176,15 +28909,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthRepository:Class]\n# @PURPOSE: Provides low-level CRUD operations for identity and authorization records.\n# @PRE: Database session is bound.\n# @POST: Entity instances returned safely.\n# @SIDE_EFFECT: Performs database reads.\n# @RELATION: DEPENDS_ON -> [AuthModels]\nclass AuthRepository:\n def __init__(self, db: Session):\n self.db = db\n\n # [DEF:get_user_by_id:Function]\n # @PURPOSE: Retrieve user by UUID.\n # @PRE: user_id is a valid UUID string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_id(self, user_id: str) -> Optional[User]:\n with belief_scope(\"AuthRepository.get_user_by_id\"):\n logger.reason(f\"Fetching user by id: {user_id}\")\n result = self.db.query(User).filter(User.id == user_id).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n\n # [/DEF:get_user_by_id:Function]\n\n # [DEF:get_user_by_username:Function]\n # @PURPOSE: Retrieve user by username.\n # @PRE: username is a non-empty string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_username(self, username: str) -> Optional[User]:\n with belief_scope(\"AuthRepository.get_user_by_username\"):\n logger.reason(f\"Fetching user by username: {username}\")\n result = self.db.query(User).filter(User.username == username).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n\n # [/DEF:get_user_by_username:Function]\n\n # [DEF:get_role_by_id:Function]\n # @PURPOSE: Retrieve role by UUID with permissions preloaded.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_role_by_id(self, role_id: str) -> Optional[Role]:\n with belief_scope(\"AuthRepository.get_role_by_id\"):\n return (\n self.db.query(Role)\n .options(selectinload(Role.permissions))\n .filter(Role.id == role_id)\n .first()\n )\n\n # [/DEF:get_role_by_id:Function]\n\n # [DEF:get_role_by_name:Function]\n # @PURPOSE: Retrieve role by unique name.\n # @RELATION: DEPENDS_ON -> [Role]\n def get_role_by_name(self, name: str) -> Optional[Role]:\n with belief_scope(\"AuthRepository.get_role_by_name\"):\n return self.db.query(Role).filter(Role.name == name).first()\n\n # [/DEF:get_role_by_name:Function]\n\n # [DEF:get_permission_by_id:Function]\n # @PURPOSE: Retrieve permission by UUID.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_id(self, permission_id: str) -> Optional[Permission]:\n with belief_scope(\"AuthRepository.get_permission_by_id\"):\n return (\n self.db.query(Permission).filter(Permission.id == permission_id).first()\n )\n\n # [/DEF:get_permission_by_id:Function]\n\n # [DEF:get_permission_by_resource_action:Function]\n # @PURPOSE: Retrieve permission by resource and action tuple.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_resource_action(\n self, resource: str, action: str\n ) -> Optional[Permission]:\n with belief_scope(\"AuthRepository.get_permission_by_resource_action\"):\n return (\n self.db.query(Permission)\n .filter(Permission.resource == resource, Permission.action == action)\n .first()\n )\n\n # [/DEF:get_permission_by_resource_action:Function]\n\n # [DEF:list_permissions:Function]\n # @PURPOSE: List all system permissions.\n # @RELATION: DEPENDS_ON -> [Permission]\n def list_permissions(self) -> List[Permission]:\n with belief_scope(\"AuthRepository.list_permissions\"):\n return self.db.query(Permission).all()\n\n # [/DEF:list_permissions:Function]\n\n # [DEF:get_user_dashboard_preference:Function]\n # @PURPOSE: Retrieve dashboard filters/preferences for a user.\n # @RELATION: DEPENDS_ON -> [UserDashboardPreference]\n def get_user_dashboard_preference(\n self, user_id: str\n ) -> Optional[UserDashboardPreference]:\n with belief_scope(\"AuthRepository.get_user_dashboard_preference\"):\n return (\n self.db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n\n # [/DEF:get_user_dashboard_preference:Function]\n\n # [DEF:get_roles_by_ad_groups:Function]\n # @PURPOSE: Retrieve roles that match a list of AD group names.\n # @PRE: groups is a list of strings representing AD group identifiers.\n # @POST: Returns a list of Role objects mapped to the provided AD groups.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [ADGroupMapping]\n def get_roles_by_ad_groups(self, groups: List[str]) -> List[Role]:\n with belief_scope(\"AuthRepository.get_roles_by_ad_groups\"):\n logger.reason(f\"Fetching roles for AD groups: {groups}\")\n if not groups:\n return []\n return (\n self.db.query(Role)\n .join(ADGroupMapping)\n .filter(ADGroupMapping.ad_group.in_(groups))\n .all()\n )\n\n # [/DEF:get_roles_by_ad_groups:Function]\n\n\n# [/DEF:AuthRepository:Class]\n" + "anchor_syntax": "region", + "body": "# #region AuthRepository [TYPE Class]\n# @BRIEF Provides low-level CRUD operations for identity and authorization records.\n# @PRE: Database session is bound.\n# @POST: Entity instances returned safely.\n# @SIDE_EFFECT: Performs database reads.\n# @RELATION DEPENDS_ON -> [AuthModels]\nclass AuthRepository:\n def __init__(self, db: Session):\n self.db = db\n # #region get_user_by_id [TYPE Function]\n # @PURPOSE: Retrieve user by UUID.\n # @PRE: user_id is a valid UUID string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_id(self, user_id: str) -> User | None:\n with belief_scope(\"AuthRepository.get_user_by_id\"):\n logger.reason(f\"Fetching user by id: {user_id}\")\n result = self.db.query(User).filter(User.id == user_id).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n # #endregion get_user_by_id\n # #region get_user_by_username [TYPE Function]\n # @PURPOSE: Retrieve user by username.\n # @PRE: username is a non-empty string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_username(self, username: str) -> User | None:\n with belief_scope(\"AuthRepository.get_user_by_username\"):\n logger.reason(f\"Fetching user by username: {username}\")\n result = self.db.query(User).filter(User.username == username).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n # #endregion get_user_by_username\n # #region get_role_by_id [TYPE Function]\n # @PURPOSE: Retrieve role by UUID with permissions preloaded.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_role_by_id(self, role_id: str) -> Role | None:\n with belief_scope(\"AuthRepository.get_role_by_id\"):\n return (\n self.db.query(Role)\n .options(selectinload(Role.permissions))\n .filter(Role.id == role_id)\n .first()\n )\n # #endregion get_role_by_id\n # #region get_role_by_name [TYPE Function]\n # @PURPOSE: Retrieve role by unique name.\n # @RELATION: DEPENDS_ON -> [Role]\n def get_role_by_name(self, name: str) -> Role | None:\n with belief_scope(\"AuthRepository.get_role_by_name\"):\n return self.db.query(Role).filter(Role.name == name).first()\n # #endregion get_role_by_name\n # #region get_permission_by_id [TYPE Function]\n # @PURPOSE: Retrieve permission by UUID.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_id(self, permission_id: str) -> Permission | None:\n with belief_scope(\"AuthRepository.get_permission_by_id\"):\n return (\n self.db.query(Permission).filter(Permission.id == permission_id).first()\n )\n # #endregion get_permission_by_id\n # #region get_permission_by_resource_action [TYPE Function]\n # @PURPOSE: Retrieve permission by resource and action tuple.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_resource_action(\n self, resource: str, action: str\n ) -> Permission | None:\n with belief_scope(\"AuthRepository.get_permission_by_resource_action\"):\n return (\n self.db.query(Permission)\n .filter(Permission.resource == resource, Permission.action == action)\n .first()\n )\n # #endregion get_permission_by_resource_action\n # #region list_permissions [TYPE Function]\n # @PURPOSE: List all system permissions.\n # @RELATION: DEPENDS_ON -> [Permission]\n def list_permissions(self) -> list[Permission]:\n with belief_scope(\"AuthRepository.list_permissions\"):\n return self.db.query(Permission).all()\n # #endregion list_permissions\n # #region get_user_dashboard_preference [TYPE Function]\n # @PURPOSE: Retrieve dashboard filters/preferences for a user.\n # @RELATION: DEPENDS_ON -> [UserDashboardPreference]\n def get_user_dashboard_preference(\n self, user_id: str\n ) -> UserDashboardPreference | None:\n with belief_scope(\"AuthRepository.get_user_dashboard_preference\"):\n return (\n self.db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n # #endregion get_user_dashboard_preference\n # #region get_roles_by_ad_groups [TYPE Function]\n # @PURPOSE: Retrieve roles that match a list of AD group names.\n # @PRE: groups is a list of strings representing AD group identifiers.\n # @POST: Returns a list of Role objects mapped to the provided AD groups.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [ADGroupMapping]\n def get_roles_by_ad_groups(self, groups: list[str]) -> list[Role]:\n with belief_scope(\"AuthRepository.get_roles_by_ad_groups\"):\n logger.reason(f\"Fetching roles for AD groups: {groups}\")\n if not groups:\n return []\n return (\n self.db.query(Role)\n .join(ADGroupMapping)\n .filter(ADGroupMapping.ad_group.in_(groups))\n .all()\n )\n # #endregion get_roles_by_ad_groups\n# #endregion AuthRepository\n" }, { "contract_id": "get_user_by_id", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 34, - "end_line": 46, + "start_line": 29, + "end_line": 40, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31238,15 +28971,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_user_by_id:Function]\n # @PURPOSE: Retrieve user by UUID.\n # @PRE: user_id is a valid UUID string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_id(self, user_id: str) -> Optional[User]:\n with belief_scope(\"AuthRepository.get_user_by_id\"):\n logger.reason(f\"Fetching user by id: {user_id}\")\n result = self.db.query(User).filter(User.id == user_id).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n\n # [/DEF:get_user_by_id:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_user_by_id [TYPE Function]\n # @PURPOSE: Retrieve user by UUID.\n # @PRE: user_id is a valid UUID string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_id(self, user_id: str) -> User | None:\n with belief_scope(\"AuthRepository.get_user_by_id\"):\n logger.reason(f\"Fetching user by id: {user_id}\")\n result = self.db.query(User).filter(User.id == user_id).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n # #endregion get_user_by_id\n" }, { "contract_id": "get_user_by_username", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 48, - "end_line": 60, + "start_line": 41, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31300,15 +29033,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_user_by_username:Function]\n # @PURPOSE: Retrieve user by username.\n # @PRE: username is a non-empty string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_username(self, username: str) -> Optional[User]:\n with belief_scope(\"AuthRepository.get_user_by_username\"):\n logger.reason(f\"Fetching user by username: {username}\")\n result = self.db.query(User).filter(User.username == username).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n\n # [/DEF:get_user_by_username:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_user_by_username [TYPE Function]\n # @PURPOSE: Retrieve user by username.\n # @PRE: username is a non-empty string.\n # @POST: Returns User object if found, else None.\n # @RELATION: DEPENDS_ON -> [User]\n def get_user_by_username(self, username: str) -> User | None:\n with belief_scope(\"AuthRepository.get_user_by_username\"):\n logger.reason(f\"Fetching user by username: {username}\")\n result = self.db.query(User).filter(User.username == username).first()\n logger.reflect(f\"User found: {result is not None}\")\n return result\n # #endregion get_user_by_username\n" }, { "contract_id": "get_role_by_id", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 62, - "end_line": 75, + "start_line": 53, + "end_line": 65, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31348,15 +29081,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_role_by_id:Function]\n # @PURPOSE: Retrieve role by UUID with permissions preloaded.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_role_by_id(self, role_id: str) -> Optional[Role]:\n with belief_scope(\"AuthRepository.get_role_by_id\"):\n return (\n self.db.query(Role)\n .options(selectinload(Role.permissions))\n .filter(Role.id == role_id)\n .first()\n )\n\n # [/DEF:get_role_by_id:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_role_by_id [TYPE Function]\n # @PURPOSE: Retrieve role by UUID with permissions preloaded.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_role_by_id(self, role_id: str) -> Role | None:\n with belief_scope(\"AuthRepository.get_role_by_id\"):\n return (\n self.db.query(Role)\n .options(selectinload(Role.permissions))\n .filter(Role.id == role_id)\n .first()\n )\n # #endregion get_role_by_id\n" }, { "contract_id": "get_role_by_name", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 77, - "end_line": 84, + "start_line": 66, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31390,15 +29123,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_role_by_name:Function]\n # @PURPOSE: Retrieve role by unique name.\n # @RELATION: DEPENDS_ON -> [Role]\n def get_role_by_name(self, name: str) -> Optional[Role]:\n with belief_scope(\"AuthRepository.get_role_by_name\"):\n return self.db.query(Role).filter(Role.name == name).first()\n\n # [/DEF:get_role_by_name:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_role_by_name [TYPE Function]\n # @PURPOSE: Retrieve role by unique name.\n # @RELATION: DEPENDS_ON -> [Role]\n def get_role_by_name(self, name: str) -> Role | None:\n with belief_scope(\"AuthRepository.get_role_by_name\"):\n return self.db.query(Role).filter(Role.name == name).first()\n # #endregion get_role_by_name\n" }, { "contract_id": "get_permission_by_id", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 86, - "end_line": 95, + "start_line": 73, + "end_line": 81, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31432,15 +29165,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_permission_by_id:Function]\n # @PURPOSE: Retrieve permission by UUID.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_id(self, permission_id: str) -> Optional[Permission]:\n with belief_scope(\"AuthRepository.get_permission_by_id\"):\n return (\n self.db.query(Permission).filter(Permission.id == permission_id).first()\n )\n\n # [/DEF:get_permission_by_id:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_permission_by_id [TYPE Function]\n # @PURPOSE: Retrieve permission by UUID.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_id(self, permission_id: str) -> Permission | None:\n with belief_scope(\"AuthRepository.get_permission_by_id\"):\n return (\n self.db.query(Permission).filter(Permission.id == permission_id).first()\n )\n # #endregion get_permission_by_id\n" }, { "contract_id": "get_permission_by_resource_action", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 97, - "end_line": 110, + "start_line": 82, + "end_line": 94, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31474,15 +29207,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_permission_by_resource_action:Function]\n # @PURPOSE: Retrieve permission by resource and action tuple.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_resource_action(\n self, resource: str, action: str\n ) -> Optional[Permission]:\n with belief_scope(\"AuthRepository.get_permission_by_resource_action\"):\n return (\n self.db.query(Permission)\n .filter(Permission.resource == resource, Permission.action == action)\n .first()\n )\n\n # [/DEF:get_permission_by_resource_action:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_permission_by_resource_action [TYPE Function]\n # @PURPOSE: Retrieve permission by resource and action tuple.\n # @RELATION: DEPENDS_ON -> [Permission]\n def get_permission_by_resource_action(\n self, resource: str, action: str\n ) -> Permission | None:\n with belief_scope(\"AuthRepository.get_permission_by_resource_action\"):\n return (\n self.db.query(Permission)\n .filter(Permission.resource == resource, Permission.action == action)\n .first()\n )\n # #endregion get_permission_by_resource_action\n" }, { "contract_id": "list_permissions", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 112, - "end_line": 119, + "start_line": 95, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31516,15 +29249,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:list_permissions:Function]\n # @PURPOSE: List all system permissions.\n # @RELATION: DEPENDS_ON -> [Permission]\n def list_permissions(self) -> List[Permission]:\n with belief_scope(\"AuthRepository.list_permissions\"):\n return self.db.query(Permission).all()\n\n # [/DEF:list_permissions:Function]\n" + "anchor_syntax": "region", + "body": " # #region list_permissions [TYPE Function]\n # @PURPOSE: List all system permissions.\n # @RELATION: DEPENDS_ON -> [Permission]\n def list_permissions(self) -> list[Permission]:\n with belief_scope(\"AuthRepository.list_permissions\"):\n return self.db.query(Permission).all()\n # #endregion list_permissions\n" }, { "contract_id": "get_user_dashboard_preference", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 121, - "end_line": 134, + "start_line": 102, + "end_line": 114, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31558,15 +29291,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_user_dashboard_preference:Function]\n # @PURPOSE: Retrieve dashboard filters/preferences for a user.\n # @RELATION: DEPENDS_ON -> [UserDashboardPreference]\n def get_user_dashboard_preference(\n self, user_id: str\n ) -> Optional[UserDashboardPreference]:\n with belief_scope(\"AuthRepository.get_user_dashboard_preference\"):\n return (\n self.db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n\n # [/DEF:get_user_dashboard_preference:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_user_dashboard_preference [TYPE Function]\n # @PURPOSE: Retrieve dashboard filters/preferences for a user.\n # @RELATION: DEPENDS_ON -> [UserDashboardPreference]\n def get_user_dashboard_preference(\n self, user_id: str\n ) -> UserDashboardPreference | None:\n with belief_scope(\"AuthRepository.get_user_dashboard_preference\"):\n return (\n self.db.query(UserDashboardPreference)\n .filter(UserDashboardPreference.user_id == user_id)\n .first()\n )\n # #endregion get_user_dashboard_preference\n" }, { "contract_id": "get_roles_by_ad_groups", "contract_type": "Function", "file_path": "backend/src/core/auth/repository.py", - "start_line": 136, - "end_line": 154, + "start_line": 115, + "end_line": 132, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31626,28 +29359,22 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_roles_by_ad_groups:Function]\n # @PURPOSE: Retrieve roles that match a list of AD group names.\n # @PRE: groups is a list of strings representing AD group identifiers.\n # @POST: Returns a list of Role objects mapped to the provided AD groups.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [ADGroupMapping]\n def get_roles_by_ad_groups(self, groups: List[str]) -> List[Role]:\n with belief_scope(\"AuthRepository.get_roles_by_ad_groups\"):\n logger.reason(f\"Fetching roles for AD groups: {groups}\")\n if not groups:\n return []\n return (\n self.db.query(Role)\n .join(ADGroupMapping)\n .filter(ADGroupMapping.ad_group.in_(groups))\n .all()\n )\n\n # [/DEF:get_roles_by_ad_groups:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_roles_by_ad_groups [TYPE Function]\n # @PURPOSE: Retrieve roles that match a list of AD group names.\n # @PRE: groups is a list of strings representing AD group identifiers.\n # @POST: Returns a list of Role objects mapped to the provided AD groups.\n # @RELATION: DEPENDS_ON -> [Role]\n # @RELATION: DEPENDS_ON -> [ADGroupMapping]\n def get_roles_by_ad_groups(self, groups: list[str]) -> list[Role]:\n with belief_scope(\"AuthRepository.get_roles_by_ad_groups\"):\n logger.reason(f\"Fetching roles for AD groups: {groups}\")\n if not groups:\n return []\n return (\n self.db.query(Role)\n .join(ADGroupMapping)\n .filter(ADGroupMapping.ad_group.in_(groups))\n .all()\n )\n # #endregion get_roles_by_ad_groups\n" }, { "contract_id": "AuthSecurityModule", "contract_type": "Module", "file_path": "backend/src/core/auth/security.py", "start_line": 1, - "end_line": 48, + "end_line": 40, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "Uses bcrypt for hashing with standard work factor.", "LAYER": "Core", - "PURPOSE": "Utility for password hashing and verification using Passlib.", - "SEMANTICS": [ - "security", - "password", - "hashing", - "bcrypt" - ] + "PURPOSE": "Utility for password hashing and verification using Passlib." }, "relations": [ { @@ -31677,23 +29404,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthSecurityModule:Module]\n#\n# @SEMANTICS: security, password, hashing, bcrypt\n# @PURPOSE: Utility for password hashing and verification using Passlib.\n# @COMPLEXITY: 2\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> bcrypt\n#\n# @INVARIANT: Uses bcrypt for hashing with standard work factor.\n\n# [SECTION: IMPORTS]\nimport bcrypt\n# [/SECTION]\n\n# [DEF:verify_password:Function]\n# @PURPOSE: Verifies a plain password against a hashed password.\n# @PRE: plain_password is a string, hashed_password is a bcrypt hash.\n# @POST: Returns True if password matches, False otherwise.\n# @RELATION: DEPENDS_ON -> bcrypt\n#\n# @PARAM: plain_password (str) - The unhashed password.\n# @PARAM: hashed_password (str) - The stored hash.\n# @RETURN: bool - Verification result.\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n if not hashed_password:\n return False\n try:\n return bcrypt.checkpw(\n plain_password.encode(\"utf-8\"),\n hashed_password.encode(\"utf-8\"),\n )\n except Exception:\n return False\n# [/DEF:verify_password:Function]\n\n# [DEF:get_password_hash:Function]\n# @PURPOSE: Generates a bcrypt hash for a plain password.\n# @PRE: password is a string.\n# @POST: Returns a secure bcrypt hash string.\n# @RELATION: DEPENDS_ON -> bcrypt\n#\n# @PARAM: password (str) - The password to hash.\n# @RETURN: str - The generated hash.\ndef get_password_hash(password: str) -> str:\n return bcrypt.hashpw(password.encode(\"utf-8\"), bcrypt.gensalt()).decode(\"utf-8\")\n# [/DEF:get_password_hash:Function]\n\n# [/DEF:AuthSecurityModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region AuthSecurityModule [C:2] [TYPE Module] [SEMANTICS auth, password, hashing, bcrypt, security]\n#\n# @BRIEF Utility for password hashing and verification using Passlib.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> bcrypt\n#\n# @INVARIANT: Uses bcrypt for hashing with standard work factor.\n\nimport bcrypt\n\n\n# #region verify_password [TYPE Function]\n# @BRIEF Verifies a plain password against a hashed password.\n# @PRE: plain_password is a string, hashed_password is a bcrypt hash.\n# @POST: Returns True if password matches, False otherwise.\n# @RELATION DEPENDS_ON -> bcrypt\n#\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n if not hashed_password:\n return False\n try:\n return bcrypt.checkpw(\n plain_password.encode(\"utf-8\"),\n hashed_password.encode(\"utf-8\"),\n )\n except Exception:\n return False\n# #endregion verify_password\n\n# #region get_password_hash [TYPE Function]\n# @BRIEF Generates a bcrypt hash for a plain password.\n# @PRE: password is a string.\n# @POST: Returns a secure bcrypt hash string.\n# @RELATION DEPENDS_ON -> bcrypt\n#\ndef get_password_hash(password: str) -> str:\n return bcrypt.hashpw(password.encode(\"utf-8\"), bcrypt.gensalt()).decode(\"utf-8\")\n# #endregion get_password_hash\n\n# #endregion AuthSecurityModule\n" }, { "contract_id": "verify_password", "contract_type": "Function", "file_path": "backend/src/core/auth/security.py", - "start_line": 15, - "end_line": 34, + "start_line": 12, + "end_line": 28, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "hashed_password (str) - The stored hash.", "POST": "Returns True if password matches, False otherwise.", "PRE": "plain_password is a string, hashed_password is a bcrypt hash.", - "PURPOSE": "Verifies a plain password against a hashed password.", - "RETURN": "bool - Verification result." + "PURPOSE": "Verifies a plain password against a hashed password." }, "relations": [ { @@ -31704,12 +29429,6 @@ } ], "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", @@ -31737,12 +29456,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31753,23 +29466,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:verify_password:Function]\n# @PURPOSE: Verifies a plain password against a hashed password.\n# @PRE: plain_password is a string, hashed_password is a bcrypt hash.\n# @POST: Returns True if password matches, False otherwise.\n# @RELATION: DEPENDS_ON -> bcrypt\n#\n# @PARAM: plain_password (str) - The unhashed password.\n# @PARAM: hashed_password (str) - The stored hash.\n# @RETURN: bool - Verification result.\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n if not hashed_password:\n return False\n try:\n return bcrypt.checkpw(\n plain_password.encode(\"utf-8\"),\n hashed_password.encode(\"utf-8\"),\n )\n except Exception:\n return False\n# [/DEF:verify_password:Function]\n" + "anchor_syntax": "region", + "body": "# #region verify_password [TYPE Function]\n# @BRIEF Verifies a plain password against a hashed password.\n# @PRE: plain_password is a string, hashed_password is a bcrypt hash.\n# @POST: Returns True if password matches, False otherwise.\n# @RELATION DEPENDS_ON -> bcrypt\n#\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n if not hashed_password:\n return False\n try:\n return bcrypt.checkpw(\n plain_password.encode(\"utf-8\"),\n hashed_password.encode(\"utf-8\"),\n )\n except Exception:\n return False\n# #endregion verify_password\n" }, { "contract_id": "get_password_hash", "contract_type": "Function", "file_path": "backend/src/core/auth/security.py", - "start_line": 36, - "end_line": 46, + "start_line": 30, + "end_line": 38, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "password (str) - The password to hash.", "POST": "Returns a secure bcrypt hash string.", "PRE": "password is a string.", - "PURPOSE": "Generates a bcrypt hash for a plain password.", - "RETURN": "str - The generated hash." + "PURPOSE": "Generates a bcrypt hash for a plain password." }, "relations": [ { @@ -31780,12 +29491,6 @@ } ], "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", @@ -31813,12 +29518,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -31829,62 +29528,55 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_password_hash:Function]\n# @PURPOSE: Generates a bcrypt hash for a plain password.\n# @PRE: password is a string.\n# @POST: Returns a secure bcrypt hash string.\n# @RELATION: DEPENDS_ON -> bcrypt\n#\n# @PARAM: password (str) - The password to hash.\n# @RETURN: str - The generated hash.\ndef get_password_hash(password: str) -> str:\n return bcrypt.hashpw(password.encode(\"utf-8\"), bcrypt.gensalt()).decode(\"utf-8\")\n# [/DEF:get_password_hash:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_password_hash [TYPE Function]\n# @BRIEF Generates a bcrypt hash for a plain password.\n# @PRE: password is a string.\n# @POST: Returns a secure bcrypt hash string.\n# @RELATION DEPENDS_ON -> bcrypt\n#\ndef get_password_hash(password: str) -> str:\n return bcrypt.hashpw(password.encode(\"utf-8\"), bcrypt.gensalt()).decode(\"utf-8\")\n# #endregion get_password_hash\n" }, { "contract_id": "ConfigManager", "contract_type": "Module", "file_path": "backend/src/core/config_manager.py", "start_line": 1, - "end_line": 576, + "end_line": 541, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[json, record] -> Model[AppConfig]", "INVARIANT": "Configuration must always be representable by AppConfig and persisted under global record id.", "LAYER": "Domain", "POST": "Configuration is loaded into memory and logger is configured.", "PRE": "Database schema for AppConfigRecord must be initialized.", "PURPOSE": "Manages application configuration persistence in DB with one-time migration from legacy JSON.", - "SEMANTICS": [ - "config", - "manager", - "persistence", - "migration", - "postgresql" - ], "SIDE_EFFECT": "Performs DB I/O and may update global logging level." }, "relations": [ { "source_id": "ConfigManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AppConfig", "target_ref": "[AppConfig]" }, { "source_id": "ConfigManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SessionLocal", "target_ref": "[SessionLocal]" }, { "source_id": "ConfigManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AppConfigRecord", "target_ref": "[AppConfigRecord]" }, { "source_id": "ConfigManager", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "logger", "target_ref": "[logger]" }, { "source_id": "ConfigManager", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "configure_logger", "target_ref": "[configure_logger]" } @@ -31907,107 +29599,17 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ConfigManager:Module]\n#\n# @COMPLEXITY: 5\n# @SEMANTICS: config, manager, persistence, migration, postgresql\n# @PURPOSE: Manages application configuration persistence in DB with one-time migration from legacy JSON.\n# @LAYER: Domain\n# @PRE: Database schema for AppConfigRecord must be initialized.\n# @POST: Configuration is loaded into memory and logger is configured.\n# @SIDE_EFFECT: Performs DB I/O and may update global logging level.\n# @DATA_CONTRACT: Input[json, record] -> Model[AppConfig]\n# @INVARIANT: Configuration must always be representable by AppConfig and persisted under global record id.\n# @RELATION: [DEPENDS_ON] ->[AppConfig]\n# @RELATION: [DEPENDS_ON] ->[SessionLocal]\n# @RELATION: [DEPENDS_ON] ->[AppConfigRecord]\n# @RELATION: [CALLS] ->[logger]\n# @RELATION: [CALLS] ->[configure_logger]\n#\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Any, Optional, List\n\nfrom sqlalchemy.orm import Session\n\nfrom .config_models import AppConfig, Environment, GlobalSettings\nfrom .database import SessionLocal\nfrom ..models.config import AppConfigRecord\nfrom ..models.mapping import Environment as EnvironmentRecord\nfrom .logger import configure_logger, belief_scope\nfrom .cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"ConfigManager\")\n\n# [DEF:ConfigManager:Class]\n# @COMPLEXITY: 5\n# @PURPOSE: Handles application configuration load, validation, mutation, and persistence lifecycle.\n# @PRE: Database is accessible and AppConfigRecord schema is loaded.\n# @POST: Configuration state is synchronized between memory and database.\n# @SIDE_EFFECT: Performs DB I/O, OS path validation, and logger reconfiguration.\nclass ConfigManager:\n # [DEF:__init__:Function]\n # @PURPOSE: Initialize manager state from persisted or migrated configuration.\n # @PRE: config_path is a non-empty string path.\n # @POST: self.config is initialized as AppConfig and logger is configured.\n # @SIDE_EFFECT: Reads config sources and updates logging configuration.\n # @DATA_CONTRACT: Input(str config_path) -> Output(None; self.config: AppConfig)\n def __init__(self, config_path: str = \"config.json\"):\n with belief_scope(\"ConfigManager.__init__\"):\n if not isinstance(config_path, str) or not config_path:\n log.explore(\"Invalid config_path provided\", error=\"config_path must be a non-empty string\", payload={\"path\": config_path})\n raise ValueError(\"config_path must be a non-empty string\")\n\n log.reason(f\"Initializing ConfigManager with legacy path: {config_path}\")\n\n self.config_path = Path(config_path)\n self.raw_payload: dict[str, Any] = {}\n self.config: AppConfig = self._load_config()\n\n configure_logger(self.config.settings.logging)\n\n if not isinstance(self.config, AppConfig):\n log.explore(\"Config loading resulted in invalid type\", error=\"Loaded config is not an AppConfig instance\", payload={\"type\": type(self.config)})\n raise TypeError(\"self.config must be an instance of AppConfig\")\n\n log.reflect(\"ConfigManager initialization complete\")\n\n # [/DEF:__init__:Function]\n\n # [DEF:_apply_features_from_env:Function]\n # @PURPOSE: Read FEATURES__* env vars and apply them to a GlobalSettings features config.\n # @SIDE_EFFECT: Reads os.environ; mutates settings.features in-place.\n # @RATIONALE: Env vars seed the initial defaults. After first bootstrap, DB is source of truth.\n @staticmethod\n def _apply_features_from_env(settings: GlobalSettings) -> None:\n with belief_scope(\"ConfigManager._apply_features_from_env\"):\n dataset_review_env = os.getenv(\"FEATURES__DATASET_REVIEW\")\n if dataset_review_env is not None:\n parsed = dataset_review_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.dataset_review = parsed\n log.reason(\n \"Applied FEATURES__DATASET_REVIEW from env\",\n payload={\"value\": parsed, \"raw\": dataset_review_env},\n )\n\n health_monitor_env = os.getenv(\"FEATURES__HEALTH_MONITOR\")\n if health_monitor_env is not None:\n parsed = health_monitor_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.health_monitor = parsed\n log.reason(\n \"Applied FEATURES__HEALTH_MONITOR from env\",\n payload={\"value\": parsed, \"raw\": health_monitor_env},\n )\n\n # [/DEF:_apply_features_from_env:Function]\n\n # [DEF:_default_config:Function]\n # @PURPOSE: Build default application configuration fallback.\n def _default_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._default_config\"):\n log.reason(\"Building default AppConfig fallback\")\n config = AppConfig(environments=[], settings=GlobalSettings())\n self._apply_features_from_env(config.settings)\n return config\n\n # [/DEF:_default_config:Function]\n\n # [DEF:_sync_raw_payload_from_config:Function]\n # @PURPOSE: Merge typed AppConfig state into raw payload while preserving unsupported legacy sections.\n def _sync_raw_payload_from_config(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._sync_raw_payload_from_config\"):\n typed_payload = self.config.model_dump()\n merged_payload = dict(self.raw_payload or {})\n merged_payload[\"environments\"] = typed_payload.get(\"environments\", [])\n merged_payload[\"settings\"] = typed_payload.get(\"settings\", {})\n self.raw_payload = merged_payload\n log.reason(\n \"Synchronized raw payload from typed config\",\n payload={\n \"environments_count\": len(\n merged_payload.get(\"environments\", []) or []\n ),\n \"has_settings\": \"settings\" in merged_payload,\n \"extra_sections\": sorted(\n key\n for key in merged_payload.keys()\n if key not in {\"environments\", \"settings\"}\n ),\n },\n )\n return merged_payload\n\n # [/DEF:_sync_raw_payload_from_config:Function]\n\n # [DEF:_load_from_legacy_file:Function]\n # @PURPOSE: Load legacy JSON configuration for migration fallback path.\n def _load_from_legacy_file(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._load_from_legacy_file\"):\n if not self.config_path.exists():\n log.reason(\n \"Legacy config file not found; using default payload\",\n payload={\"path\": str(self.config_path)},\n )\n return {}\n\n log.reason(\n \"Loading legacy config file\", payload={\"path\": str(self.config_path)}\n )\n with self.config_path.open(\"r\", encoding=\"utf-8\") as fh:\n payload = json.load(fh)\n\n if not isinstance(payload, dict):\n log.explore(\"Legacy config payload is not a JSON object\", error=f\"Expected dict, got {type(payload).__name__}\", payload={\n \"path\": str(self.config_path),\n \"type\": type(payload).__name__,\n })\n raise ValueError(\"Legacy config payload must be a JSON object\")\n\n log.reason(\n \"Legacy config file loaded successfully\",\n payload={\"path\": str(self.config_path), \"keys\": sorted(payload.keys())},\n )\n return payload\n\n # [/DEF:_load_from_legacy_file:Function]\n\n # [DEF:_get_record:Function]\n # @PURPOSE: Resolve global configuration record from DB.\n def _get_record(self, session: Session) -> Optional[AppConfigRecord]:\n with belief_scope(\"ConfigManager._get_record\"):\n record = (\n session.query(AppConfigRecord)\n .filter(AppConfigRecord.id == \"global\")\n .first()\n )\n log.reason(\n \"Resolved app config record\", payload={\"exists\": record is not None}\n )\n return record\n\n # [/DEF:_get_record:Function]\n\n # [DEF:_load_config:Function]\n # @PURPOSE: Load configuration from DB or perform one-time migration from legacy JSON.\n def _load_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._load_config\"):\n session = SessionLocal()\n try:\n record = self._get_record(session)\n if record and isinstance(record.payload, dict):\n log.reason(\n \"Loading configuration from database\",\n payload={\"record_id\": record.id},\n )\n self.raw_payload = dict(record.payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._sync_environment_records(session, config)\n session.commit()\n log.reason(\n \"Database configuration validated successfully\",\n payload={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n return config\n\n log.reason(\n \"Database configuration record missing; attempting legacy file migration\",\n payload={\"legacy_path\": str(self.config_path)},\n )\n legacy_payload = self._load_from_legacy_file()\n\n if legacy_payload:\n self.raw_payload = dict(legacy_payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._apply_features_from_env(config.settings)\n log.reason(\n \"Legacy payload validated; persisting migrated configuration to database\",\n payload={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n self._save_config_to_db(config, session=session)\n return config\n\n log.reason(\n \"No persisted config found; falling back to default configuration\"\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config, session=session)\n return config\n except (json.JSONDecodeError, TypeError, ValueError) as exc:\n log.explore(\"Recoverable config load failure; falling back to default configuration\", error=str(exc), payload={\"legacy_path\": str(self.config_path)})\n config = self._default_config()\n self.raw_payload = config.model_dump()\n return config\n except Exception as exc:\n log.explore(\"Critical config load failure; re-raising persistence or validation error\", error=str(exc))\n raise\n finally:\n session.close()\n\n # [/DEF:_load_config:Function]\n\n # [DEF:_sync_environment_records:Function]\n # @PURPOSE: Mirror configured environments into the relational environments table used by FK-backed domain models.\n def _sync_environment_records(self, session: Session, config: AppConfig) -> None:\n with belief_scope(\"ConfigManager._sync_environment_records\"):\n configured_envs = list(config.environments or [])\n persisted_records = session.query(EnvironmentRecord).all()\n persisted_by_id = {\n str(record.id or \"\").strip(): record for record in persisted_records\n }\n\n for environment in configured_envs:\n normalized_id = str(environment.id or \"\").strip()\n if not normalized_id:\n continue\n\n display_name = (\n str(environment.name or normalized_id).strip() or normalized_id\n )\n normalized_url = str(environment.url or \"\").strip()\n credentials_id = (\n str(environment.username or \"\").strip() or normalized_id\n )\n\n record = persisted_by_id.get(normalized_id)\n if record is None:\n log.reason(\n \"Creating relational environment record from typed config\",\n payload={\n \"environment_id\": normalized_id,\n \"environment_name\": display_name,\n },\n )\n session.add(\n EnvironmentRecord(\n id=normalized_id,\n name=display_name,\n url=normalized_url,\n credentials_id=credentials_id,\n )\n )\n continue\n\n record.name = display_name\n record.url = normalized_url\n record.credentials_id = credentials_id\n\n # [/DEF:_sync_environment_records:Function]\n\n # [DEF:_save_config_to_db:Function]\n # @PURPOSE: Persist provided AppConfig into the global DB configuration record.\n def _save_config_to_db(\n self, config: AppConfig, session: Optional[Session] = None\n ) -> None:\n with belief_scope(\"ConfigManager._save_config_to_db\"):\n owns_session = session is None\n db = session or SessionLocal()\n try:\n self.config = config\n payload = self._sync_raw_payload_from_config()\n record = self._get_record(db)\n if record is None:\n log.reason(\"Creating new global app config record\")\n record = AppConfigRecord(id=\"global\", payload=payload)\n db.add(record)\n else:\n log.reason(\n \"Updating existing global app config record\",\n payload={\"record_id\": record.id},\n )\n record.payload = payload\n\n self._sync_environment_records(db, config)\n\n db.commit()\n log.reason(\n \"Configuration persisted to database\",\n payload={\n \"environments_count\": len(\n payload.get(\"environments\", []) or []\n ),\n \"payload_keys\": sorted(payload.keys()),\n },\n )\n except Exception:\n db.rollback()\n log.explore(\"Database save failed; transaction rolled back\", error=\"Database transaction rolled back\")\n raise\n finally:\n if owns_session:\n db.close()\n\n # [/DEF:_save_config_to_db:Function]\n\n # [DEF:save:Function]\n # @PURPOSE: Persist current in-memory configuration state.\n def save(self) -> None:\n with belief_scope(\"ConfigManager.save\"):\n log.reason(\"Persisting current in-memory configuration\")\n self._save_config_to_db(self.config)\n\n # [/DEF:save:Function]\n\n # [DEF:get_config:Function]\n # @PURPOSE: Return current in-memory configuration snapshot.\n def get_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager.get_config\"):\n return self.config\n\n # [/DEF:get_config:Function]\n\n # [DEF:get_payload:Function]\n # @PURPOSE: Return full persisted payload including sections outside typed AppConfig schema.\n def get_payload(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager.get_payload\"):\n return self._sync_raw_payload_from_config()\n\n # [/DEF:get_payload:Function]\n\n # [DEF:save_config:Function]\n # @PURPOSE: Persist configuration provided either as typed AppConfig or raw payload dict.\n def save_config(self, config: Any) -> AppConfig:\n with belief_scope(\"ConfigManager.save_config\"):\n if isinstance(config, AppConfig):\n log.reason(\"Saving typed AppConfig payload\")\n self.config = config\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config)\n return self.config\n\n if isinstance(config, dict):\n log.reason(\n \"Saving raw config payload\",\n payload={\"keys\": sorted(config.keys())},\n )\n self.raw_payload = dict(config)\n typed_config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self.config = typed_config\n self._save_config_to_db(typed_config)\n return self.config\n\n log.explore(\"Unsupported config type supplied to save_config\", error=f\"Expected AppConfig or dict, got {type(config).__name__}\", payload={\"type\": type(config).__name__})\n raise TypeError(\"config must be AppConfig or dict\")\n\n # [/DEF:save_config:Function]\n\n # [DEF:update_global_settings:Function]\n # @PURPOSE: Replace global settings and persist the resulting configuration.\n def update_global_settings(self, settings: GlobalSettings) -> AppConfig:\n with belief_scope(\"ConfigManager.update_global_settings\"):\n log.reason(\"Updating global settings\")\n self.config.settings = settings\n self.save()\n return self.config\n\n # [/DEF:update_global_settings:Function]\n\n # [DEF:validate_path:Function]\n # @PURPOSE: Validate that path exists and is writable, creating it when absent.\n def validate_path(self, path: str) -> tuple[bool, str]:\n with belief_scope(\"ConfigManager.validate_path\", f\"path={path}\"):\n try:\n target = Path(path).expanduser()\n target.mkdir(parents=True, exist_ok=True)\n\n if not target.exists():\n return False, f\"Path does not exist: {target}\"\n\n if not target.is_dir():\n return False, f\"Path is not a directory: {target}\"\n\n test_file = target / \".write_test\"\n with test_file.open(\"w\", encoding=\"utf-8\") as fh:\n fh.write(\"ok\")\n test_file.unlink(missing_ok=True)\n\n log.reason(\"Path validation succeeded\", payload={\"path\": str(target)})\n return True, \"OK\"\n except Exception as exc:\n log.explore(\"Path validation failed\", error=str(exc), payload={\"path\": path})\n return False, str(exc)\n\n # [/DEF:validate_path:Function]\n\n # [DEF:get_environments:Function]\n # @PURPOSE: Return all configured environments.\n def get_environments(self) -> List[Environment]:\n with belief_scope(\"ConfigManager.get_environments\"):\n return list(self.config.environments)\n\n # [/DEF:get_environments:Function]\n\n # [DEF:has_environments:Function]\n # @PURPOSE: Check whether at least one environment exists in configuration.\n def has_environments(self) -> bool:\n with belief_scope(\"ConfigManager.has_environments\"):\n return len(self.config.environments) > 0\n\n # [/DEF:has_environments:Function]\n\n # [DEF:get_environment:Function]\n # @PURPOSE: Resolve a configured environment by identifier.\n def get_environment(self, env_id: str) -> Optional[Environment]:\n with belief_scope(\"ConfigManager.get_environment\", f\"env_id={env_id}\"):\n normalized = str(env_id or \"\").strip()\n if not normalized:\n return None\n\n for env in self.config.environments:\n if env.id == normalized or env.name == normalized:\n return env\n return None\n\n # [/DEF:get_environment:Function]\n\n # [DEF:add_environment:Function]\n # @PURPOSE: Upsert environment by id into configuration and persist.\n def add_environment(self, env: Environment) -> AppConfig:\n with belief_scope(\"ConfigManager.add_environment\", f\"env_id={env.id}\"):\n existing_index = next(\n (\n i\n for i, item in enumerate(self.config.environments)\n if item.id == env.id\n ),\n None,\n )\n if env.is_default:\n for item in self.config.environments:\n item.is_default = False\n\n if existing_index is None:\n log.reason(\"Appending new environment\", payload={\"env_id\": env.id})\n self.config.environments.append(env)\n else:\n log.reason(\n \"Replacing existing environment during add\",\n payload={\"env_id\": env.id},\n )\n self.config.environments[existing_index] = env\n\n if len(self.config.environments) == 1 and not any(\n item.is_default for item in self.config.environments\n ):\n self.config.environments[0].is_default = True\n\n self.save()\n return self.config\n\n # [/DEF:add_environment:Function]\n\n # [DEF:update_environment:Function]\n # @PURPOSE: Update existing environment by id and preserve masked password placeholder behavior.\n def update_environment(self, env_id: str, env: Environment) -> bool:\n with belief_scope(\"ConfigManager.update_environment\", f\"env_id={env_id}\"):\n for index, existing in enumerate(self.config.environments):\n if existing.id != env_id:\n continue\n\n update_data = env.model_dump()\n if update_data.get(\"password\") == \"********\":\n update_data[\"password\"] = existing.password\n\n updated = Environment.model_validate(update_data)\n\n if updated.is_default:\n for item in self.config.environments:\n item.is_default = False\n elif existing.is_default and not updated.is_default:\n updated.is_default = True\n\n self.config.environments[index] = updated\n log.reason(\"Environment updated\", payload={\"env_id\": env_id})\n self.save()\n return True\n\n log.explore(\"Environment update skipped; env not found\", error=f\"No matching environment found for id: {env_id}\", payload={\"env_id\": env_id})\n return False\n\n # [/DEF:update_environment:Function]\n\n # [DEF:delete_environment:Function]\n # @PURPOSE: Delete environment by id and persist when deletion occurs.\n def delete_environment(self, env_id: str) -> bool:\n with belief_scope(\"ConfigManager.delete_environment\", f\"env_id={env_id}\"):\n before = len(self.config.environments)\n removed = [env for env in self.config.environments if env.id == env_id]\n self.config.environments = [\n env for env in self.config.environments if env.id != env_id\n ]\n\n if len(self.config.environments) == before:\n log.explore(\"Environment delete skipped; env not found\", error=f\"No matching environment found for id: {env_id}\", payload={\"env_id\": env_id})\n return False\n\n if removed and removed[0].is_default and self.config.environments:\n self.config.environments[0].is_default = True\n\n if self.config.settings.default_environment_id == env_id:\n replacement = next(\n (env.id for env in self.config.environments if env.is_default), None\n )\n self.config.settings.default_environment_id = replacement\n\n log.reason(\n \"Environment deleted\",\n payload={\"env_id\": env_id, \"remaining\": len(self.config.environments)},\n )\n self.save()\n return True\n\n # [/DEF:delete_environment:Function]\n\n\n# [/DEF:ConfigManager:Class]\n# [/DEF:ConfigManager:Module]\n" + "anchor_syntax": "region", + "body": "# #region ConfigManager [C:5] [TYPE Module] [SEMANTICS sqlalchemy, validate, migration, config-manager]\n#\n# @BRIEF Manages application configuration persistence in DB with one-time migration from legacy JSON.\n# @LAYER: Domain\n# @PRE: Database schema for AppConfigRecord must be initialized.\n# @POST: Configuration is loaded into memory and logger is configured.\n# @SIDE_EFFECT: Performs DB I/O and may update global logging level.\n# @DATA_CONTRACT: Input[json, record] -> Model[AppConfig]\n# @INVARIANT: Configuration must always be representable by AppConfig and persisted under global record id.\n# @RELATION DEPENDS_ON -> [AppConfig]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION CALLS -> [logger]\n# @RELATION CALLS -> [configure_logger]\n#\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..models.config import AppConfigRecord\nfrom ..models.mapping import Environment as EnvironmentRecord\nfrom .config_models import AppConfig, Environment, GlobalSettings\nfrom .database import SessionLocal\nfrom .logger import belief_scope, configure_logger, logger\n\n\n# #region ConfigManager [C:5] [TYPE Class]\n# @BRIEF Handles application configuration load, validation, mutation, and persistence lifecycle.\n# @PRE: Database is accessible and AppConfigRecord schema is loaded.\n# @POST: Configuration state is synchronized between memory and database.\n# @SIDE_EFFECT: Performs DB I/O, OS path validation, and logger reconfiguration.\nclass ConfigManager:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initialize manager state from persisted or migrated configuration.\n # @PRE: config_path is a non-empty string path.\n # @POST: self.config is initialized as AppConfig and logger is configured.\n # @SIDE_EFFECT: Reads config sources and updates logging configuration.\n # @DATA_CONTRACT: Input(str config_path) -> Output(None; self.config: AppConfig)\n def __init__(self, config_path: str = \"config.json\"):\n with belief_scope(\"ConfigManager.__init__\"):\n if not isinstance(config_path, str) or not config_path:\n logger.explore(\n \"Invalid config_path provided\", extra={\"path\": config_path}\n )\n raise ValueError(\"config_path must be a non-empty string\")\n logger.reason(f\"Initializing ConfigManager with legacy path: {config_path}\")\n self.config_path = Path(config_path)\n self.raw_payload: dict[str, Any] = {}\n self.config: AppConfig = self._load_config()\n configure_logger(self.config.settings.logging)\n if not isinstance(self.config, AppConfig):\n logger.explore(\n \"Config loading resulted in invalid type\",\n extra={\"type\": type(self.config)},\n )\n raise TypeError(\"self.config must be an instance of AppConfig\")\n logger.reflect(\"ConfigManager initialization complete\")\n # #endregion __init__\n # #region _apply_features_from_env [TYPE Function]\n # @PURPOSE: Read FEATURES__* env vars and apply them to a GlobalSettings features config.\n # @SIDE_EFFECT: Reads os.environ; mutates settings.features in-place.\n # @RATIONALE: Env vars seed the initial defaults. After first bootstrap, DB is source of truth.\n @staticmethod\n def _apply_features_from_env(settings: GlobalSettings) -> None:\n with belief_scope(\"ConfigManager._apply_features_from_env\"):\n dataset_review_env = os.getenv(\"FEATURES__DATASET_REVIEW\")\n if dataset_review_env is not None:\n parsed = dataset_review_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.dataset_review = parsed\n logger.reason(\n \"Applied FEATURES__DATASET_REVIEW from env\",\n extra={\"value\": parsed, \"raw\": dataset_review_env},\n )\n health_monitor_env = os.getenv(\"FEATURES__HEALTH_MONITOR\")\n if health_monitor_env is not None:\n parsed = health_monitor_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.health_monitor = parsed\n logger.reason(\n \"Applied FEATURES__HEALTH_MONITOR from env\",\n extra={\"value\": parsed, \"raw\": health_monitor_env},\n )\n # #endregion _apply_features_from_env\n # #region _default_config [TYPE Function]\n # @PURPOSE: Build default application configuration fallback.\n def _default_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._default_config\"):\n logger.reason(\"Building default AppConfig fallback\")\n config = AppConfig(environments=[], settings=GlobalSettings())\n self._apply_features_from_env(config.settings)\n return config\n # #endregion _default_config\n # #region _sync_raw_payload_from_config [TYPE Function]\n # @PURPOSE: Merge typed AppConfig state into raw payload while preserving unsupported legacy sections.\n def _sync_raw_payload_from_config(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._sync_raw_payload_from_config\"):\n typed_payload = self.config.model_dump()\n merged_payload = dict(self.raw_payload or {})\n merged_payload[\"environments\"] = typed_payload.get(\"environments\", [])\n merged_payload[\"settings\"] = typed_payload.get(\"settings\", {})\n self.raw_payload = merged_payload\n logger.reason(\n \"Synchronized raw payload from typed config\",\n extra={\n \"environments_count\": len(\n merged_payload.get(\"environments\", []) or []\n ),\n \"has_settings\": \"settings\" in merged_payload,\n \"extra_sections\": sorted(\n key\n for key in merged_payload\n if key not in {\"environments\", \"settings\"}\n ),\n },\n )\n return merged_payload\n # #endregion _sync_raw_payload_from_config\n # #region _load_from_legacy_file [TYPE Function]\n # @PURPOSE: Load legacy JSON configuration for migration fallback path.\n def _load_from_legacy_file(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._load_from_legacy_file\"):\n if not self.config_path.exists():\n logger.reason(\n \"Legacy config file not found; using default payload\",\n extra={\"path\": str(self.config_path)},\n )\n return {}\n logger.reason(\n \"Loading legacy config file\", extra={\"path\": str(self.config_path)}\n )\n with self.config_path.open(\"r\", encoding=\"utf-8\") as fh:\n payload = json.load(fh)\n if not isinstance(payload, dict):\n logger.explore(\n \"Legacy config payload is not a JSON object\",\n extra={\n \"path\": str(self.config_path),\n \"type\": type(payload).__name__,\n },\n )\n raise ValueError(\"Legacy config payload must be a JSON object\")\n logger.reason(\n \"Legacy config file loaded successfully\",\n extra={\"path\": str(self.config_path), \"keys\": sorted(payload.keys())},\n )\n return payload\n # #endregion _load_from_legacy_file\n # #region _get_record [TYPE Function]\n # @PURPOSE: Resolve global configuration record from DB.\n def _get_record(self, session: Session) -> AppConfigRecord | None:\n with belief_scope(\"ConfigManager._get_record\"):\n record = (\n session.query(AppConfigRecord)\n .filter(AppConfigRecord.id == \"global\")\n .first()\n )\n logger.reason(\n \"Resolved app config record\", extra={\"exists\": record is not None}\n )\n return record\n # #endregion _get_record\n # #region _load_config [TYPE Function]\n # @PURPOSE: Load configuration from DB or perform one-time migration from legacy JSON.\n def _load_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._load_config\"):\n session = SessionLocal()\n try:\n record = self._get_record(session)\n if record and isinstance(record.payload, dict):\n logger.reason(\n \"Loading configuration from database\",\n extra={\"record_id\": record.id},\n )\n self.raw_payload = dict(record.payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._sync_environment_records(session, config)\n session.commit()\n logger.reason(\n \"Database configuration validated successfully\",\n extra={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n return config\n logger.reason(\n \"Database configuration record missing; attempting legacy file migration\",\n extra={\"legacy_path\": str(self.config_path)},\n )\n legacy_payload = self._load_from_legacy_file()\n if legacy_payload:\n self.raw_payload = dict(legacy_payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._apply_features_from_env(config.settings)\n logger.reason(\n \"Legacy payload validated; persisting migrated configuration to database\",\n extra={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n self._save_config_to_db(config, session=session)\n return config\n logger.reason(\n \"No persisted config found; falling back to default configuration\"\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config, session=session)\n return config\n except (json.JSONDecodeError, TypeError, ValueError) as exc:\n logger.explore(\n \"Recoverable config load failure; falling back to default configuration\",\n extra={\"error\": str(exc), \"legacy_path\": str(self.config_path)},\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n return config\n except Exception as exc:\n logger.explore(\n \"Critical config load failure; re-raising persistence or validation error\",\n extra={\"error\": str(exc)},\n )\n raise\n finally:\n session.close()\n # #endregion _load_config\n # #region _sync_environment_records [TYPE Function]\n # @PURPOSE: Mirror configured environments into the relational environments table used by FK-backed domain models.\n def _sync_environment_records(self, session: Session, config: AppConfig) -> None:\n with belief_scope(\"ConfigManager._sync_environment_records\"):\n configured_envs = list(config.environments or [])\n persisted_records = session.query(EnvironmentRecord).all()\n persisted_by_id = {\n str(record.id or \"\").strip(): record for record in persisted_records\n }\n for environment in configured_envs:\n normalized_id = str(environment.id or \"\").strip()\n if not normalized_id:\n continue\n display_name = (\n str(environment.name or normalized_id).strip() or normalized_id\n )\n normalized_url = str(environment.url or \"\").strip()\n credentials_id = (\n str(environment.username or \"\").strip() or normalized_id\n )\n record = persisted_by_id.get(normalized_id)\n if record is None:\n logger.reason(\n \"Creating relational environment record from typed config\",\n extra={\n \"environment_id\": normalized_id,\n \"environment_name\": display_name,\n },\n )\n session.add(\n EnvironmentRecord(\n id=normalized_id,\n name=display_name,\n url=normalized_url,\n credentials_id=credentials_id,\n )\n )\n continue\n record.name = display_name\n record.url = normalized_url\n record.credentials_id = credentials_id\n # #endregion _sync_environment_records\n # #region _delete_stale_environment_records [TYPE Function]\n # @PURPOSE: Remove persisted environment records that are no longer in the configured environments.\n # @PRE: _sync_environment_records must already have run so the query returns a current view.\n # @POST: Stale Environment rows are deleted via the session (caller must commit).\n # @SIDE_EFFECT: Only safe to call during explicit save (_save_config_to_db), NOT during _load_config,\n # because FK-dependent rows (task_records, database_mappings, etc.) may reference deleted rows.\n def _delete_stale_environment_records(\n self, session: Session, config: AppConfig\n ) -> None:\n with belief_scope(\"ConfigManager._delete_stale_environment_records\"):\n persisted_records = session.query(EnvironmentRecord).all()\n configured_ids = {\n str(env.id or \"\").strip()\n for env in (config.environments or [])\n if str(env.id or \"\").strip()\n }\n for record in persisted_records:\n record_id = str(record.id or \"\").strip()\n if record_id and record_id not in configured_ids:\n logger.reason(\n \"Deleting stale environment record\",\n extra={\"environment_id\": record_id},\n )\n session.delete(record)\n # #endregion _delete_stale_environment_records\n # #region _save_config_to_db [TYPE Function]\n # @PURPOSE: Persist provided AppConfig into the global DB configuration record.\n def _save_config_to_db(\n self, config: AppConfig, session: Session | None = None\n ) -> None:\n with belief_scope(\"ConfigManager._save_config_to_db\"):\n owns_session = session is None\n db = session or SessionLocal()\n try:\n self.config = config\n payload = self._sync_raw_payload_from_config()\n record = self._get_record(db)\n if record is None:\n logger.reason(\"Creating new global app config record\")\n record = AppConfigRecord(id=\"global\", payload=payload)\n db.add(record)\n else:\n logger.reason(\n \"Updating existing global app config record\",\n extra={\"record_id\": record.id},\n )\n record.payload = payload\n self._sync_environment_records(db, config)\n self._delete_stale_environment_records(db, config)\n db.commit()\n logger.reason(\n \"Configuration persisted to database\",\n extra={\n \"environments_count\": len(\n payload.get(\"environments\", []) or []\n ),\n \"payload_keys\": sorted(payload.keys()),\n },\n )\n except Exception:\n db.rollback()\n logger.explore(\"Database save failed; transaction rolled back\")\n raise\n finally:\n if owns_session:\n db.close()\n # #endregion _save_config_to_db\n # #region save [TYPE Function]\n # @PURPOSE: Persist current in-memory configuration state.\n def save(self) -> None:\n with belief_scope(\"ConfigManager.save\"):\n logger.reason(\"Persisting current in-memory configuration\")\n self._save_config_to_db(self.config)\n # #endregion save\n # #region get_config [TYPE Function]\n # @PURPOSE: Return current in-memory configuration snapshot.\n def get_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager.get_config\"):\n return self.config\n # #endregion get_config\n # #region get_payload [TYPE Function]\n # @PURPOSE: Return full persisted payload including sections outside typed AppConfig schema.\n def get_payload(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager.get_payload\"):\n return self._sync_raw_payload_from_config()\n # #endregion get_payload\n # #region save_config [TYPE Function]\n # @PURPOSE: Persist configuration provided either as typed AppConfig or raw payload dict.\n def save_config(self, config: Any) -> AppConfig:\n with belief_scope(\"ConfigManager.save_config\"):\n if isinstance(config, AppConfig):\n logger.reason(\"Saving typed AppConfig payload\")\n self.config = config\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config)\n return self.config\n if isinstance(config, dict):\n logger.reason(\n \"Saving raw config payload\",\n extra={\"keys\": sorted(config.keys())},\n )\n self.raw_payload = dict(config)\n typed_config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self.config = typed_config\n self._save_config_to_db(typed_config)\n return self.config\n logger.explore(\n \"Unsupported config type supplied to save_config\",\n extra={\"type\": type(config).__name__},\n )\n raise TypeError(\"config must be AppConfig or dict\")\n # #endregion save_config\n # #region update_global_settings [TYPE Function]\n # @PURPOSE: Replace global settings and persist the resulting configuration.\n def update_global_settings(self, settings: GlobalSettings) -> AppConfig:\n with belief_scope(\"ConfigManager.update_global_settings\"):\n logger.reason(\"Updating global settings\")\n self.config.settings = settings\n self.save()\n return self.config\n # #endregion update_global_settings\n # #region validate_path [TYPE Function]\n # @PURPOSE: Validate that path exists and is writable, creating it when absent.\n def validate_path(self, path: str) -> tuple[bool, str]:\n with belief_scope(\"ConfigManager.validate_path\", f\"path={path}\"):\n try:\n target = Path(path).expanduser()\n target.mkdir(parents=True, exist_ok=True)\n if not target.exists():\n return False, f\"Path does not exist: {target}\"\n if not target.is_dir():\n return False, f\"Path is not a directory: {target}\"\n test_file = target / \".write_test\"\n with test_file.open(\"w\", encoding=\"utf-8\") as fh:\n fh.write(\"ok\")\n test_file.unlink(missing_ok=True)\n logger.reason(\"Path validation succeeded\", extra={\"path\": str(target)})\n return True, \"OK\"\n except Exception as exc:\n logger.explore(\n \"Path validation failed\", extra={\"path\": path, \"error\": str(exc)}\n )\n return False, str(exc)\n # #endregion validate_path\n # #region get_environments [TYPE Function]\n # @PURPOSE: Return all configured environments.\n def get_environments(self) -> list[Environment]:\n with belief_scope(\"ConfigManager.get_environments\"):\n return list(self.config.environments)\n # #endregion get_environments\n # #region has_environments [TYPE Function]\n # @PURPOSE: Check whether at least one environment exists in configuration.\n def has_environments(self) -> bool:\n with belief_scope(\"ConfigManager.has_environments\"):\n return len(self.config.environments) > 0\n # #endregion has_environments\n # #region get_environment [TYPE Function]\n # @PURPOSE: Resolve a configured environment by identifier.\n def get_environment(self, env_id: str) -> Environment | None:\n with belief_scope(\"ConfigManager.get_environment\", f\"env_id={env_id}\"):\n normalized = str(env_id or \"\").strip()\n if not normalized:\n return None\n for env in self.config.environments:\n if env.id == normalized or env.name == normalized:\n return env\n return None\n # #endregion get_environment\n # #region add_environment [TYPE Function]\n # @PURPOSE: Upsert environment by id into configuration and persist.\n def add_environment(self, env: Environment) -> AppConfig:\n with belief_scope(\"ConfigManager.add_environment\", f\"env_id={env.id}\"):\n existing_index = next(\n (\n i\n for i, item in enumerate(self.config.environments)\n if item.id == env.id\n ),\n None,\n )\n if env.is_default:\n for item in self.config.environments:\n item.is_default = False\n if existing_index is None:\n logger.reason(\"Appending new environment\", extra={\"env_id\": env.id})\n self.config.environments.append(env)\n else:\n logger.reason(\n \"Replacing existing environment during add\",\n extra={\"env_id\": env.id},\n )\n self.config.environments[existing_index] = env\n if len(self.config.environments) == 1 and not any(\n item.is_default for item in self.config.environments\n ):\n self.config.environments[0].is_default = True\n self.save()\n return self.config\n # #endregion add_environment\n # #region update_environment [TYPE Function]\n # @PURPOSE: Update existing environment by id and preserve masked password placeholder behavior.\n def update_environment(self, env_id: str, env: Environment) -> bool:\n with belief_scope(\"ConfigManager.update_environment\", f\"env_id={env_id}\"):\n for index, existing in enumerate(self.config.environments):\n if existing.id != env_id:\n continue\n update_data = env.model_dump()\n if update_data.get(\"password\") == \"********\":\n update_data[\"password\"] = existing.password\n updated = Environment.model_validate(update_data)\n if updated.is_default:\n for item in self.config.environments:\n item.is_default = False\n elif existing.is_default and not updated.is_default:\n updated.is_default = True\n self.config.environments[index] = updated\n logger.reason(\"Environment updated\", extra={\"env_id\": env_id})\n self.save()\n return True\n logger.explore(\n \"Environment update skipped; env not found\", extra={\"env_id\": env_id}\n )\n return False\n # #endregion update_environment\n # #region delete_environment [TYPE Function]\n # @PURPOSE: Delete environment by id and persist when deletion occurs.\n def delete_environment(self, env_id: str) -> bool:\n with belief_scope(\"ConfigManager.delete_environment\", f\"env_id={env_id}\"):\n before = len(self.config.environments)\n removed = [env for env in self.config.environments if env.id == env_id]\n self.config.environments = [\n env for env in self.config.environments if env.id != env_id\n ]\n if len(self.config.environments) == before:\n logger.explore(\n \"Environment delete skipped; env not found\",\n extra={\"env_id\": env_id},\n )\n return False\n if removed and removed[0].is_default and self.config.environments:\n self.config.environments[0].is_default = True\n if self.config.settings.default_environment_id == env_id:\n replacement = next(\n (env.id for env in self.config.environments if env.is_default), None\n )\n self.config.settings.default_environment_id = replacement\n logger.reason(\n \"Environment deleted\",\n extra={\"env_id\": env_id, \"remaining\": len(self.config.environments)},\n )\n self.save()\n return True\n # #endregion delete_environment\n# #endregion ConfigManager\n# #endregion ConfigManager\n" }, { "contract_id": "_apply_features_from_env", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 69, - "end_line": 94, + "start_line": 62, + "end_line": 85, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32045,15 +29647,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_apply_features_from_env:Function]\n # @PURPOSE: Read FEATURES__* env vars and apply them to a GlobalSettings features config.\n # @SIDE_EFFECT: Reads os.environ; mutates settings.features in-place.\n # @RATIONALE: Env vars seed the initial defaults. After first bootstrap, DB is source of truth.\n @staticmethod\n def _apply_features_from_env(settings: GlobalSettings) -> None:\n with belief_scope(\"ConfigManager._apply_features_from_env\"):\n dataset_review_env = os.getenv(\"FEATURES__DATASET_REVIEW\")\n if dataset_review_env is not None:\n parsed = dataset_review_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.dataset_review = parsed\n log.reason(\n \"Applied FEATURES__DATASET_REVIEW from env\",\n payload={\"value\": parsed, \"raw\": dataset_review_env},\n )\n\n health_monitor_env = os.getenv(\"FEATURES__HEALTH_MONITOR\")\n if health_monitor_env is not None:\n parsed = health_monitor_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.health_monitor = parsed\n log.reason(\n \"Applied FEATURES__HEALTH_MONITOR from env\",\n payload={\"value\": parsed, \"raw\": health_monitor_env},\n )\n\n # [/DEF:_apply_features_from_env:Function]\n" + "anchor_syntax": "region", + "body": " # #region _apply_features_from_env [TYPE Function]\n # @PURPOSE: Read FEATURES__* env vars and apply them to a GlobalSettings features config.\n # @SIDE_EFFECT: Reads os.environ; mutates settings.features in-place.\n # @RATIONALE: Env vars seed the initial defaults. After first bootstrap, DB is source of truth.\n @staticmethod\n def _apply_features_from_env(settings: GlobalSettings) -> None:\n with belief_scope(\"ConfigManager._apply_features_from_env\"):\n dataset_review_env = os.getenv(\"FEATURES__DATASET_REVIEW\")\n if dataset_review_env is not None:\n parsed = dataset_review_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.dataset_review = parsed\n logger.reason(\n \"Applied FEATURES__DATASET_REVIEW from env\",\n extra={\"value\": parsed, \"raw\": dataset_review_env},\n )\n health_monitor_env = os.getenv(\"FEATURES__HEALTH_MONITOR\")\n if health_monitor_env is not None:\n parsed = health_monitor_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.health_monitor = parsed\n logger.reason(\n \"Applied FEATURES__HEALTH_MONITOR from env\",\n extra={\"value\": parsed, \"raw\": health_monitor_env},\n )\n # #endregion _apply_features_from_env\n" }, { "contract_id": "_default_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 96, - "end_line": 105, + "start_line": 86, + "end_line": 94, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32071,15 +29673,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_default_config:Function]\n # @PURPOSE: Build default application configuration fallback.\n def _default_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._default_config\"):\n log.reason(\"Building default AppConfig fallback\")\n config = AppConfig(environments=[], settings=GlobalSettings())\n self._apply_features_from_env(config.settings)\n return config\n\n # [/DEF:_default_config:Function]\n" + "anchor_syntax": "region", + "body": " # #region _default_config [TYPE Function]\n # @PURPOSE: Build default application configuration fallback.\n def _default_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._default_config\"):\n logger.reason(\"Building default AppConfig fallback\")\n config = AppConfig(environments=[], settings=GlobalSettings())\n self._apply_features_from_env(config.settings)\n return config\n # #endregion _default_config\n" }, { "contract_id": "_sync_raw_payload_from_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 107, - "end_line": 132, + "start_line": 95, + "end_line": 119, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32097,15 +29699,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_sync_raw_payload_from_config:Function]\n # @PURPOSE: Merge typed AppConfig state into raw payload while preserving unsupported legacy sections.\n def _sync_raw_payload_from_config(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._sync_raw_payload_from_config\"):\n typed_payload = self.config.model_dump()\n merged_payload = dict(self.raw_payload or {})\n merged_payload[\"environments\"] = typed_payload.get(\"environments\", [])\n merged_payload[\"settings\"] = typed_payload.get(\"settings\", {})\n self.raw_payload = merged_payload\n log.reason(\n \"Synchronized raw payload from typed config\",\n payload={\n \"environments_count\": len(\n merged_payload.get(\"environments\", []) or []\n ),\n \"has_settings\": \"settings\" in merged_payload,\n \"extra_sections\": sorted(\n key\n for key in merged_payload.keys()\n if key not in {\"environments\", \"settings\"}\n ),\n },\n )\n return merged_payload\n\n # [/DEF:_sync_raw_payload_from_config:Function]\n" + "anchor_syntax": "region", + "body": " # #region _sync_raw_payload_from_config [TYPE Function]\n # @PURPOSE: Merge typed AppConfig state into raw payload while preserving unsupported legacy sections.\n def _sync_raw_payload_from_config(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._sync_raw_payload_from_config\"):\n typed_payload = self.config.model_dump()\n merged_payload = dict(self.raw_payload or {})\n merged_payload[\"environments\"] = typed_payload.get(\"environments\", [])\n merged_payload[\"settings\"] = typed_payload.get(\"settings\", {})\n self.raw_payload = merged_payload\n logger.reason(\n \"Synchronized raw payload from typed config\",\n extra={\n \"environments_count\": len(\n merged_payload.get(\"environments\", []) or []\n ),\n \"has_settings\": \"settings\" in merged_payload,\n \"extra_sections\": sorted(\n key\n for key in merged_payload\n if key not in {\"environments\", \"settings\"}\n ),\n },\n )\n return merged_payload\n # #endregion _sync_raw_payload_from_config\n" }, { "contract_id": "_load_from_legacy_file", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 134, - "end_line": 164, + "start_line": 120, + "end_line": 149, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32123,15 +29725,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_load_from_legacy_file:Function]\n # @PURPOSE: Load legacy JSON configuration for migration fallback path.\n def _load_from_legacy_file(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._load_from_legacy_file\"):\n if not self.config_path.exists():\n log.reason(\n \"Legacy config file not found; using default payload\",\n payload={\"path\": str(self.config_path)},\n )\n return {}\n\n log.reason(\n \"Loading legacy config file\", payload={\"path\": str(self.config_path)}\n )\n with self.config_path.open(\"r\", encoding=\"utf-8\") as fh:\n payload = json.load(fh)\n\n if not isinstance(payload, dict):\n log.explore(\"Legacy config payload is not a JSON object\", error=f\"Expected dict, got {type(payload).__name__}\", payload={\n \"path\": str(self.config_path),\n \"type\": type(payload).__name__,\n })\n raise ValueError(\"Legacy config payload must be a JSON object\")\n\n log.reason(\n \"Legacy config file loaded successfully\",\n payload={\"path\": str(self.config_path), \"keys\": sorted(payload.keys())},\n )\n return payload\n\n # [/DEF:_load_from_legacy_file:Function]\n" + "anchor_syntax": "region", + "body": " # #region _load_from_legacy_file [TYPE Function]\n # @PURPOSE: Load legacy JSON configuration for migration fallback path.\n def _load_from_legacy_file(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager._load_from_legacy_file\"):\n if not self.config_path.exists():\n logger.reason(\n \"Legacy config file not found; using default payload\",\n extra={\"path\": str(self.config_path)},\n )\n return {}\n logger.reason(\n \"Loading legacy config file\", extra={\"path\": str(self.config_path)}\n )\n with self.config_path.open(\"r\", encoding=\"utf-8\") as fh:\n payload = json.load(fh)\n if not isinstance(payload, dict):\n logger.explore(\n \"Legacy config payload is not a JSON object\",\n extra={\n \"path\": str(self.config_path),\n \"type\": type(payload).__name__,\n },\n )\n raise ValueError(\"Legacy config payload must be a JSON object\")\n logger.reason(\n \"Legacy config file loaded successfully\",\n extra={\"path\": str(self.config_path), \"keys\": sorted(payload.keys())},\n )\n return payload\n # #endregion _load_from_legacy_file\n" }, { "contract_id": "_get_record", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 166, - "end_line": 180, + "start_line": 150, + "end_line": 163, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32149,15 +29751,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_get_record:Function]\n # @PURPOSE: Resolve global configuration record from DB.\n def _get_record(self, session: Session) -> Optional[AppConfigRecord]:\n with belief_scope(\"ConfigManager._get_record\"):\n record = (\n session.query(AppConfigRecord)\n .filter(AppConfigRecord.id == \"global\")\n .first()\n )\n log.reason(\n \"Resolved app config record\", payload={\"exists\": record is not None}\n )\n return record\n\n # [/DEF:_get_record:Function]\n" + "anchor_syntax": "region", + "body": " # #region _get_record [TYPE Function]\n # @PURPOSE: Resolve global configuration record from DB.\n def _get_record(self, session: Session) -> AppConfigRecord | None:\n with belief_scope(\"ConfigManager._get_record\"):\n record = (\n session.query(AppConfigRecord)\n .filter(AppConfigRecord.id == \"global\")\n .first()\n )\n logger.reason(\n \"Resolved app config record\", extra={\"exists\": record is not None}\n )\n return record\n # #endregion _get_record\n" }, { "contract_id": "_load_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 182, - "end_line": 255, + "start_line": 164, + "end_line": 239, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32175,15 +29777,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_load_config:Function]\n # @PURPOSE: Load configuration from DB or perform one-time migration from legacy JSON.\n def _load_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._load_config\"):\n session = SessionLocal()\n try:\n record = self._get_record(session)\n if record and isinstance(record.payload, dict):\n log.reason(\n \"Loading configuration from database\",\n payload={\"record_id\": record.id},\n )\n self.raw_payload = dict(record.payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._sync_environment_records(session, config)\n session.commit()\n log.reason(\n \"Database configuration validated successfully\",\n payload={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n return config\n\n log.reason(\n \"Database configuration record missing; attempting legacy file migration\",\n payload={\"legacy_path\": str(self.config_path)},\n )\n legacy_payload = self._load_from_legacy_file()\n\n if legacy_payload:\n self.raw_payload = dict(legacy_payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._apply_features_from_env(config.settings)\n log.reason(\n \"Legacy payload validated; persisting migrated configuration to database\",\n payload={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n self._save_config_to_db(config, session=session)\n return config\n\n log.reason(\n \"No persisted config found; falling back to default configuration\"\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config, session=session)\n return config\n except (json.JSONDecodeError, TypeError, ValueError) as exc:\n log.explore(\"Recoverable config load failure; falling back to default configuration\", error=str(exc), payload={\"legacy_path\": str(self.config_path)})\n config = self._default_config()\n self.raw_payload = config.model_dump()\n return config\n except Exception as exc:\n log.explore(\"Critical config load failure; re-raising persistence or validation error\", error=str(exc))\n raise\n finally:\n session.close()\n\n # [/DEF:_load_config:Function]\n" + "anchor_syntax": "region", + "body": " # #region _load_config [TYPE Function]\n # @PURPOSE: Load configuration from DB or perform one-time migration from legacy JSON.\n def _load_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._load_config\"):\n session = SessionLocal()\n try:\n record = self._get_record(session)\n if record and isinstance(record.payload, dict):\n logger.reason(\n \"Loading configuration from database\",\n extra={\"record_id\": record.id},\n )\n self.raw_payload = dict(record.payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._sync_environment_records(session, config)\n session.commit()\n logger.reason(\n \"Database configuration validated successfully\",\n extra={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n return config\n logger.reason(\n \"Database configuration record missing; attempting legacy file migration\",\n extra={\"legacy_path\": str(self.config_path)},\n )\n legacy_payload = self._load_from_legacy_file()\n if legacy_payload:\n self.raw_payload = dict(legacy_payload)\n config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self._apply_features_from_env(config.settings)\n logger.reason(\n \"Legacy payload validated; persisting migrated configuration to database\",\n extra={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n self._save_config_to_db(config, session=session)\n return config\n logger.reason(\n \"No persisted config found; falling back to default configuration\"\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config, session=session)\n return config\n except (json.JSONDecodeError, TypeError, ValueError) as exc:\n logger.explore(\n \"Recoverable config load failure; falling back to default configuration\",\n extra={\"error\": str(exc), \"legacy_path\": str(self.config_path)},\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n return config\n except Exception as exc:\n logger.explore(\n \"Critical config load failure; re-raising persistence or validation error\",\n extra={\"error\": str(exc)},\n )\n raise\n finally:\n session.close()\n # #endregion _load_config\n" }, { "contract_id": "_sync_environment_records", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 257, - "end_line": 303, + "start_line": 240, + "end_line": 281, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32201,14 +29803,70 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_sync_environment_records:Function]\n # @PURPOSE: Mirror configured environments into the relational environments table used by FK-backed domain models.\n def _sync_environment_records(self, session: Session, config: AppConfig) -> None:\n with belief_scope(\"ConfigManager._sync_environment_records\"):\n configured_envs = list(config.environments or [])\n persisted_records = session.query(EnvironmentRecord).all()\n persisted_by_id = {\n str(record.id or \"\").strip(): record for record in persisted_records\n }\n\n for environment in configured_envs:\n normalized_id = str(environment.id or \"\").strip()\n if not normalized_id:\n continue\n\n display_name = (\n str(environment.name or normalized_id).strip() or normalized_id\n )\n normalized_url = str(environment.url or \"\").strip()\n credentials_id = (\n str(environment.username or \"\").strip() or normalized_id\n )\n\n record = persisted_by_id.get(normalized_id)\n if record is None:\n log.reason(\n \"Creating relational environment record from typed config\",\n payload={\n \"environment_id\": normalized_id,\n \"environment_name\": display_name,\n },\n )\n session.add(\n EnvironmentRecord(\n id=normalized_id,\n name=display_name,\n url=normalized_url,\n credentials_id=credentials_id,\n )\n )\n continue\n\n record.name = display_name\n record.url = normalized_url\n record.credentials_id = credentials_id\n\n # [/DEF:_sync_environment_records:Function]\n" + "anchor_syntax": "region", + "body": " # #region _sync_environment_records [TYPE Function]\n # @PURPOSE: Mirror configured environments into the relational environments table used by FK-backed domain models.\n def _sync_environment_records(self, session: Session, config: AppConfig) -> None:\n with belief_scope(\"ConfigManager._sync_environment_records\"):\n configured_envs = list(config.environments or [])\n persisted_records = session.query(EnvironmentRecord).all()\n persisted_by_id = {\n str(record.id or \"\").strip(): record for record in persisted_records\n }\n for environment in configured_envs:\n normalized_id = str(environment.id or \"\").strip()\n if not normalized_id:\n continue\n display_name = (\n str(environment.name or normalized_id).strip() or normalized_id\n )\n normalized_url = str(environment.url or \"\").strip()\n credentials_id = (\n str(environment.username or \"\").strip() or normalized_id\n )\n record = persisted_by_id.get(normalized_id)\n if record is None:\n logger.reason(\n \"Creating relational environment record from typed config\",\n extra={\n \"environment_id\": normalized_id,\n \"environment_name\": display_name,\n },\n )\n session.add(\n EnvironmentRecord(\n id=normalized_id,\n name=display_name,\n url=normalized_url,\n credentials_id=credentials_id,\n )\n )\n continue\n record.name = display_name\n record.url = normalized_url\n record.credentials_id = credentials_id\n # #endregion _sync_environment_records\n" + }, + { + "contract_id": "_delete_stale_environment_records", + "contract_type": "Function", + "file_path": "backend/src/core/config_manager.py", + "start_line": 282, + "end_line": 306, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Stale Environment rows are deleted via the session (caller must commit).", + "PRE": "_sync_environment_records must already have run so the query returns a current view.", + "PURPOSE": "Remove persisted environment records that are no longer in the configured environments.", + "SIDE_EFFECT": "Only safe to call during explicit save (_save_config_to_db), NOT during _load_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" + } + }, + { + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "body": " # #region _delete_stale_environment_records [TYPE Function]\n # @PURPOSE: Remove persisted environment records that are no longer in the configured environments.\n # @PRE: _sync_environment_records must already have run so the query returns a current view.\n # @POST: Stale Environment rows are deleted via the session (caller must commit).\n # @SIDE_EFFECT: Only safe to call during explicit save (_save_config_to_db), NOT during _load_config,\n # because FK-dependent rows (task_records, database_mappings, etc.) may reference deleted rows.\n def _delete_stale_environment_records(\n self, session: Session, config: AppConfig\n ) -> None:\n with belief_scope(\"ConfigManager._delete_stale_environment_records\"):\n persisted_records = session.query(EnvironmentRecord).all()\n configured_ids = {\n str(env.id or \"\").strip()\n for env in (config.environments or [])\n if str(env.id or \"\").strip()\n }\n for record in persisted_records:\n record_id = str(record.id or \"\").strip()\n if record_id and record_id not in configured_ids:\n logger.reason(\n \"Deleting stale environment record\",\n extra={\"environment_id\": record_id},\n )\n session.delete(record)\n # #endregion _delete_stale_environment_records\n" }, { "contract_id": "_save_config_to_db", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 305, + "start_line": 307, "end_line": 348, "tier": "TIER_1", "complexity": 1, @@ -32227,15 +29885,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_save_config_to_db:Function]\n # @PURPOSE: Persist provided AppConfig into the global DB configuration record.\n def _save_config_to_db(\n self, config: AppConfig, session: Optional[Session] = None\n ) -> None:\n with belief_scope(\"ConfigManager._save_config_to_db\"):\n owns_session = session is None\n db = session or SessionLocal()\n try:\n self.config = config\n payload = self._sync_raw_payload_from_config()\n record = self._get_record(db)\n if record is None:\n log.reason(\"Creating new global app config record\")\n record = AppConfigRecord(id=\"global\", payload=payload)\n db.add(record)\n else:\n log.reason(\n \"Updating existing global app config record\",\n payload={\"record_id\": record.id},\n )\n record.payload = payload\n\n self._sync_environment_records(db, config)\n\n db.commit()\n log.reason(\n \"Configuration persisted to database\",\n payload={\n \"environments_count\": len(\n payload.get(\"environments\", []) or []\n ),\n \"payload_keys\": sorted(payload.keys()),\n },\n )\n except Exception:\n db.rollback()\n log.explore(\"Database save failed; transaction rolled back\", error=\"Database transaction rolled back\")\n raise\n finally:\n if owns_session:\n db.close()\n\n # [/DEF:_save_config_to_db:Function]\n" + "anchor_syntax": "region", + "body": " # #region _save_config_to_db [TYPE Function]\n # @PURPOSE: Persist provided AppConfig into the global DB configuration record.\n def _save_config_to_db(\n self, config: AppConfig, session: Session | None = None\n ) -> None:\n with belief_scope(\"ConfigManager._save_config_to_db\"):\n owns_session = session is None\n db = session or SessionLocal()\n try:\n self.config = config\n payload = self._sync_raw_payload_from_config()\n record = self._get_record(db)\n if record is None:\n logger.reason(\"Creating new global app config record\")\n record = AppConfigRecord(id=\"global\", payload=payload)\n db.add(record)\n else:\n logger.reason(\n \"Updating existing global app config record\",\n extra={\"record_id\": record.id},\n )\n record.payload = payload\n self._sync_environment_records(db, config)\n self._delete_stale_environment_records(db, config)\n db.commit()\n logger.reason(\n \"Configuration persisted to database\",\n extra={\n \"environments_count\": len(\n payload.get(\"environments\", []) or []\n ),\n \"payload_keys\": sorted(payload.keys()),\n },\n )\n except Exception:\n db.rollback()\n logger.explore(\"Database save failed; transaction rolled back\")\n raise\n finally:\n if owns_session:\n db.close()\n # #endregion _save_config_to_db\n" }, { "contract_id": "save", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 350, - "end_line": 357, + "start_line": 349, + "end_line": 355, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32253,15 +29911,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:save:Function]\n # @PURPOSE: Persist current in-memory configuration state.\n def save(self) -> None:\n with belief_scope(\"ConfigManager.save\"):\n log.reason(\"Persisting current in-memory configuration\")\n self._save_config_to_db(self.config)\n\n # [/DEF:save:Function]\n" + "anchor_syntax": "region", + "body": " # #region save [TYPE Function]\n # @PURPOSE: Persist current in-memory configuration state.\n def save(self) -> None:\n with belief_scope(\"ConfigManager.save\"):\n logger.reason(\"Persisting current in-memory configuration\")\n self._save_config_to_db(self.config)\n # #endregion save\n" }, { "contract_id": "get_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 359, - "end_line": 365, + "start_line": 356, + "end_line": 361, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32279,15 +29937,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_config:Function]\n # @PURPOSE: Return current in-memory configuration snapshot.\n def get_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager.get_config\"):\n return self.config\n\n # [/DEF:get_config:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_config [TYPE Function]\n # @PURPOSE: Return current in-memory configuration snapshot.\n def get_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager.get_config\"):\n return self.config\n # #endregion get_config\n" }, { "contract_id": "get_payload", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 367, - "end_line": 373, + "start_line": 362, + "end_line": 367, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32305,15 +29963,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_payload:Function]\n # @PURPOSE: Return full persisted payload including sections outside typed AppConfig schema.\n def get_payload(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager.get_payload\"):\n return self._sync_raw_payload_from_config()\n\n # [/DEF:get_payload:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_payload [TYPE Function]\n # @PURPOSE: Return full persisted payload including sections outside typed AppConfig schema.\n def get_payload(self) -> dict[str, Any]:\n with belief_scope(\"ConfigManager.get_payload\"):\n return self._sync_raw_payload_from_config()\n # #endregion get_payload\n" }, { "contract_id": "save_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 375, - "end_line": 405, + "start_line": 368, + "end_line": 398, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32331,15 +29989,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:save_config:Function]\n # @PURPOSE: Persist configuration provided either as typed AppConfig or raw payload dict.\n def save_config(self, config: Any) -> AppConfig:\n with belief_scope(\"ConfigManager.save_config\"):\n if isinstance(config, AppConfig):\n log.reason(\"Saving typed AppConfig payload\")\n self.config = config\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config)\n return self.config\n\n if isinstance(config, dict):\n log.reason(\n \"Saving raw config payload\",\n payload={\"keys\": sorted(config.keys())},\n )\n self.raw_payload = dict(config)\n typed_config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self.config = typed_config\n self._save_config_to_db(typed_config)\n return self.config\n\n log.explore(\"Unsupported config type supplied to save_config\", error=f\"Expected AppConfig or dict, got {type(config).__name__}\", payload={\"type\": type(config).__name__})\n raise TypeError(\"config must be AppConfig or dict\")\n\n # [/DEF:save_config:Function]\n" + "anchor_syntax": "region", + "body": " # #region save_config [TYPE Function]\n # @PURPOSE: Persist configuration provided either as typed AppConfig or raw payload dict.\n def save_config(self, config: Any) -> AppConfig:\n with belief_scope(\"ConfigManager.save_config\"):\n if isinstance(config, AppConfig):\n logger.reason(\"Saving typed AppConfig payload\")\n self.config = config\n self.raw_payload = config.model_dump()\n self._save_config_to_db(config)\n return self.config\n if isinstance(config, dict):\n logger.reason(\n \"Saving raw config payload\",\n extra={\"keys\": sorted(config.keys())},\n )\n self.raw_payload = dict(config)\n typed_config = AppConfig.model_validate(\n {\n \"environments\": self.raw_payload.get(\"environments\", []),\n \"settings\": self.raw_payload.get(\"settings\", {}),\n }\n )\n self.config = typed_config\n self._save_config_to_db(typed_config)\n return self.config\n logger.explore(\n \"Unsupported config type supplied to save_config\",\n extra={\"type\": type(config).__name__},\n )\n raise TypeError(\"config must be AppConfig or dict\")\n # #endregion save_config\n" }, { "contract_id": "update_global_settings", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 407, - "end_line": 416, + "start_line": 399, + "end_line": 407, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32357,15 +30015,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:update_global_settings:Function]\n # @PURPOSE: Replace global settings and persist the resulting configuration.\n def update_global_settings(self, settings: GlobalSettings) -> AppConfig:\n with belief_scope(\"ConfigManager.update_global_settings\"):\n log.reason(\"Updating global settings\")\n self.config.settings = settings\n self.save()\n return self.config\n\n # [/DEF:update_global_settings:Function]\n" + "anchor_syntax": "region", + "body": " # #region update_global_settings [TYPE Function]\n # @PURPOSE: Replace global settings and persist the resulting configuration.\n def update_global_settings(self, settings: GlobalSettings) -> AppConfig:\n with belief_scope(\"ConfigManager.update_global_settings\"):\n logger.reason(\"Updating global settings\")\n self.config.settings = settings\n self.save()\n return self.config\n # #endregion update_global_settings\n" }, { "contract_id": "validate_path", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 418, - "end_line": 443, + "start_line": 408, + "end_line": 430, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32383,15 +30041,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:validate_path:Function]\n # @PURPOSE: Validate that path exists and is writable, creating it when absent.\n def validate_path(self, path: str) -> tuple[bool, str]:\n with belief_scope(\"ConfigManager.validate_path\", f\"path={path}\"):\n try:\n target = Path(path).expanduser()\n target.mkdir(parents=True, exist_ok=True)\n\n if not target.exists():\n return False, f\"Path does not exist: {target}\"\n\n if not target.is_dir():\n return False, f\"Path is not a directory: {target}\"\n\n test_file = target / \".write_test\"\n with test_file.open(\"w\", encoding=\"utf-8\") as fh:\n fh.write(\"ok\")\n test_file.unlink(missing_ok=True)\n\n log.reason(\"Path validation succeeded\", payload={\"path\": str(target)})\n return True, \"OK\"\n except Exception as exc:\n log.explore(\"Path validation failed\", error=str(exc), payload={\"path\": path})\n return False, str(exc)\n\n # [/DEF:validate_path:Function]\n" + "anchor_syntax": "region", + "body": " # #region validate_path [TYPE Function]\n # @PURPOSE: Validate that path exists and is writable, creating it when absent.\n def validate_path(self, path: str) -> tuple[bool, str]:\n with belief_scope(\"ConfigManager.validate_path\", f\"path={path}\"):\n try:\n target = Path(path).expanduser()\n target.mkdir(parents=True, exist_ok=True)\n if not target.exists():\n return False, f\"Path does not exist: {target}\"\n if not target.is_dir():\n return False, f\"Path is not a directory: {target}\"\n test_file = target / \".write_test\"\n with test_file.open(\"w\", encoding=\"utf-8\") as fh:\n fh.write(\"ok\")\n test_file.unlink(missing_ok=True)\n logger.reason(\"Path validation succeeded\", extra={\"path\": str(target)})\n return True, \"OK\"\n except Exception as exc:\n logger.explore(\n \"Path validation failed\", extra={\"path\": path, \"error\": str(exc)}\n )\n return False, str(exc)\n # #endregion validate_path\n" }, { "contract_id": "get_environments", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 445, - "end_line": 451, + "start_line": 431, + "end_line": 436, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32409,15 +30067,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_environments:Function]\n # @PURPOSE: Return all configured environments.\n def get_environments(self) -> List[Environment]:\n with belief_scope(\"ConfigManager.get_environments\"):\n return list(self.config.environments)\n\n # [/DEF:get_environments:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_environments [TYPE Function]\n # @PURPOSE: Return all configured environments.\n def get_environments(self) -> list[Environment]:\n with belief_scope(\"ConfigManager.get_environments\"):\n return list(self.config.environments)\n # #endregion get_environments\n" }, { "contract_id": "has_environments", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 453, - "end_line": 459, + "start_line": 437, + "end_line": 442, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32435,15 +30093,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:has_environments:Function]\n # @PURPOSE: Check whether at least one environment exists in configuration.\n def has_environments(self) -> bool:\n with belief_scope(\"ConfigManager.has_environments\"):\n return len(self.config.environments) > 0\n\n # [/DEF:has_environments:Function]\n" + "anchor_syntax": "region", + "body": " # #region has_environments [TYPE Function]\n # @PURPOSE: Check whether at least one environment exists in configuration.\n def has_environments(self) -> bool:\n with belief_scope(\"ConfigManager.has_environments\"):\n return len(self.config.environments) > 0\n # #endregion has_environments\n" }, { "contract_id": "get_environment", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 461, - "end_line": 474, + "start_line": 443, + "end_line": 454, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32461,15 +30119,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:get_environment:Function]\n # @PURPOSE: Resolve a configured environment by identifier.\n def get_environment(self, env_id: str) -> Optional[Environment]:\n with belief_scope(\"ConfigManager.get_environment\", f\"env_id={env_id}\"):\n normalized = str(env_id or \"\").strip()\n if not normalized:\n return None\n\n for env in self.config.environments:\n if env.id == normalized or env.name == normalized:\n return env\n return None\n\n # [/DEF:get_environment:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_environment [TYPE Function]\n # @PURPOSE: Resolve a configured environment by identifier.\n def get_environment(self, env_id: str) -> Environment | None:\n with belief_scope(\"ConfigManager.get_environment\", f\"env_id={env_id}\"):\n normalized = str(env_id or \"\").strip()\n if not normalized:\n return None\n for env in self.config.environments:\n if env.id == normalized or env.name == normalized:\n return env\n return None\n # #endregion get_environment\n" }, { "contract_id": "add_environment", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 476, - "end_line": 510, + "start_line": 455, + "end_line": 485, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32487,15 +30145,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:add_environment:Function]\n # @PURPOSE: Upsert environment by id into configuration and persist.\n def add_environment(self, env: Environment) -> AppConfig:\n with belief_scope(\"ConfigManager.add_environment\", f\"env_id={env.id}\"):\n existing_index = next(\n (\n i\n for i, item in enumerate(self.config.environments)\n if item.id == env.id\n ),\n None,\n )\n if env.is_default:\n for item in self.config.environments:\n item.is_default = False\n\n if existing_index is None:\n log.reason(\"Appending new environment\", payload={\"env_id\": env.id})\n self.config.environments.append(env)\n else:\n log.reason(\n \"Replacing existing environment during add\",\n payload={\"env_id\": env.id},\n )\n self.config.environments[existing_index] = env\n\n if len(self.config.environments) == 1 and not any(\n item.is_default for item in self.config.environments\n ):\n self.config.environments[0].is_default = True\n\n self.save()\n return self.config\n\n # [/DEF:add_environment:Function]\n" + "anchor_syntax": "region", + "body": " # #region add_environment [TYPE Function]\n # @PURPOSE: Upsert environment by id into configuration and persist.\n def add_environment(self, env: Environment) -> AppConfig:\n with belief_scope(\"ConfigManager.add_environment\", f\"env_id={env.id}\"):\n existing_index = next(\n (\n i\n for i, item in enumerate(self.config.environments)\n if item.id == env.id\n ),\n None,\n )\n if env.is_default:\n for item in self.config.environments:\n item.is_default = False\n if existing_index is None:\n logger.reason(\"Appending new environment\", extra={\"env_id\": env.id})\n self.config.environments.append(env)\n else:\n logger.reason(\n \"Replacing existing environment during add\",\n extra={\"env_id\": env.id},\n )\n self.config.environments[existing_index] = env\n if len(self.config.environments) == 1 and not any(\n item.is_default for item in self.config.environments\n ):\n self.config.environments[0].is_default = True\n self.save()\n return self.config\n # #endregion add_environment\n" }, { "contract_id": "update_environment", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 512, - "end_line": 540, + "start_line": 486, + "end_line": 510, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32513,15 +30171,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:update_environment:Function]\n # @PURPOSE: Update existing environment by id and preserve masked password placeholder behavior.\n def update_environment(self, env_id: str, env: Environment) -> bool:\n with belief_scope(\"ConfigManager.update_environment\", f\"env_id={env_id}\"):\n for index, existing in enumerate(self.config.environments):\n if existing.id != env_id:\n continue\n\n update_data = env.model_dump()\n if update_data.get(\"password\") == \"********\":\n update_data[\"password\"] = existing.password\n\n updated = Environment.model_validate(update_data)\n\n if updated.is_default:\n for item in self.config.environments:\n item.is_default = False\n elif existing.is_default and not updated.is_default:\n updated.is_default = True\n\n self.config.environments[index] = updated\n log.reason(\"Environment updated\", payload={\"env_id\": env_id})\n self.save()\n return True\n\n log.explore(\"Environment update skipped; env not found\", error=f\"No matching environment found for id: {env_id}\", payload={\"env_id\": env_id})\n return False\n\n # [/DEF:update_environment:Function]\n" + "anchor_syntax": "region", + "body": " # #region update_environment [TYPE Function]\n # @PURPOSE: Update existing environment by id and preserve masked password placeholder behavior.\n def update_environment(self, env_id: str, env: Environment) -> bool:\n with belief_scope(\"ConfigManager.update_environment\", f\"env_id={env_id}\"):\n for index, existing in enumerate(self.config.environments):\n if existing.id != env_id:\n continue\n update_data = env.model_dump()\n if update_data.get(\"password\") == \"********\":\n update_data[\"password\"] = existing.password\n updated = Environment.model_validate(update_data)\n if updated.is_default:\n for item in self.config.environments:\n item.is_default = False\n elif existing.is_default and not updated.is_default:\n updated.is_default = True\n self.config.environments[index] = updated\n logger.reason(\"Environment updated\", extra={\"env_id\": env_id})\n self.save()\n return True\n logger.explore(\n \"Environment update skipped; env not found\", extra={\"env_id\": env_id}\n )\n return False\n # #endregion update_environment\n" }, { "contract_id": "delete_environment", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 542, - "end_line": 572, + "start_line": 511, + "end_line": 539, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32539,26 +30197,21 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:delete_environment:Function]\n # @PURPOSE: Delete environment by id and persist when deletion occurs.\n def delete_environment(self, env_id: str) -> bool:\n with belief_scope(\"ConfigManager.delete_environment\", f\"env_id={env_id}\"):\n before = len(self.config.environments)\n removed = [env for env in self.config.environments if env.id == env_id]\n self.config.environments = [\n env for env in self.config.environments if env.id != env_id\n ]\n\n if len(self.config.environments) == before:\n log.explore(\"Environment delete skipped; env not found\", error=f\"No matching environment found for id: {env_id}\", payload={\"env_id\": env_id})\n return False\n\n if removed and removed[0].is_default and self.config.environments:\n self.config.environments[0].is_default = True\n\n if self.config.settings.default_environment_id == env_id:\n replacement = next(\n (env.id for env in self.config.environments if env.is_default), None\n )\n self.config.settings.default_environment_id = replacement\n\n log.reason(\n \"Environment deleted\",\n payload={\"env_id\": env_id, \"remaining\": len(self.config.environments)},\n )\n self.save()\n return True\n\n # [/DEF:delete_environment:Function]\n" + "anchor_syntax": "region", + "body": " # #region delete_environment [TYPE Function]\n # @PURPOSE: Delete environment by id and persist when deletion occurs.\n def delete_environment(self, env_id: str) -> bool:\n with belief_scope(\"ConfigManager.delete_environment\", f\"env_id={env_id}\"):\n before = len(self.config.environments)\n removed = [env for env in self.config.environments if env.id == env_id]\n self.config.environments = [\n env for env in self.config.environments if env.id != env_id\n ]\n if len(self.config.environments) == before:\n logger.explore(\n \"Environment delete skipped; env not found\",\n extra={\"env_id\": env_id},\n )\n return False\n if removed and removed[0].is_default and self.config.environments:\n self.config.environments[0].is_default = True\n if self.config.settings.default_environment_id == env_id:\n replacement = next(\n (env.id for env in self.config.environments if env.is_default), None\n )\n self.config.settings.default_environment_id = replacement\n logger.reason(\n \"Environment deleted\",\n extra={\"env_id\": env_id, \"remaining\": len(self.config.environments)},\n )\n self.save()\n return True\n # #endregion delete_environment\n" }, { "contract_id": "ConfigModels", "contract_type": "Module", "file_path": "backend/src/core/config_models.py", "start_line": 1, - "end_line": 132, + "end_line": 130, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Core", - "PURPOSE": "Defines the data models for application configuration using Pydantic.", - "SEMANTICS": [ - "config", - "models", - "pydantic" - ] + "PURPOSE": "Defines the data models for application configuration using Pydantic." }, "relations": [ { @@ -32575,15 +30228,15 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ConfigModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: config, models, pydantic\n# @PURPOSE: Defines the data models for application configuration using Pydantic.\n# @LAYER: Core\n# @RELATION: IMPLEMENTS -> [CoreContracts]\n# @RELATION: IMPLEMENTS -> [ConnectionContracts]\n\nfrom pydantic import BaseModel, Field\nfrom typing import List, Optional\nfrom ..models.storage import StorageConfig\nfrom ..services.llm_prompt_templates import (\n DEFAULT_LLM_ASSISTANT_SETTINGS,\n DEFAULT_LLM_PROMPTS,\n DEFAULT_LLM_PROVIDER_BINDINGS,\n)\n\n\n# [DEF:Schedule:DataClass]\n# @PURPOSE: Represents a backup schedule configuration.\nclass Schedule(BaseModel):\n enabled: bool = False\n cron_expression: str = \"0 0 * * *\" # Default: daily at midnight\n\n\n# [/DEF:Schedule:DataClass]\n\n\n# [DEF:Environment:DataClass]\n# @PURPOSE: Represents a Superset environment configuration.\nclass Environment(BaseModel):\n id: str\n name: str\n url: str\n username: str\n password: str # Will be masked in UI\n stage: str = Field(default=\"DEV\", pattern=\"^(DEV|PREPROD|PROD)$\")\n verify_ssl: bool = True\n timeout: int = 30\n is_default: bool = False\n is_production: bool = False\n backup_schedule: Schedule = Field(default_factory=Schedule)\n\n\n# [/DEF:Environment:DataClass]\n\n\n# [DEF:LoggingConfig:DataClass]\n# @PURPOSE: Defines the configuration for the application's logging system.\nclass LoggingConfig(BaseModel):\n level: str = \"INFO\"\n task_log_level: str = (\n \"INFO\" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)\n )\n file_path: Optional[str] = None\n max_bytes: int = 10 * 1024 * 1024\n backup_count: int = 5\n enable_belief_state: bool = True\n\n\n# [/DEF:LoggingConfig:DataClass]\n\n\n# [DEF:CleanReleaseConfig:DataClass]\n# @PURPOSE: Configuration for clean release compliance subsystem.\nclass CleanReleaseConfig(BaseModel):\n active_policy_id: Optional[str] = None\n active_registry_id: Optional[str] = None\n\n\n# [/DEF:CleanReleaseConfig:DataClass]\n\n\n# [DEF:FeaturesConfig:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Top-level feature flags that toggle entire project features on/off.\n# @RATIONALE: Features are read from environment variables on bootstrap and persisted in DB.\n# DB is source of truth after initial bootstrap; env vars only seed defaults.\nclass FeaturesConfig(BaseModel):\n dataset_review: bool = True\n health_monitor: bool = True\n\n\n# [/DEF:FeaturesConfig:DataClass]\n\n\n# [DEF:GlobalSettings:DataClass]\n# @PURPOSE: Represents global application settings.\nclass GlobalSettings(BaseModel):\n storage: StorageConfig = Field(default_factory=StorageConfig)\n clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)\n default_environment_id: Optional[str] = None\n logging: LoggingConfig = Field(default_factory=LoggingConfig)\n features: FeaturesConfig = Field(default_factory=FeaturesConfig)\n connections: List[dict] = []\n llm: dict = Field(\n default_factory=lambda: {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": dict(DEFAULT_LLM_PROMPTS),\n \"provider_bindings\": dict(DEFAULT_LLM_PROVIDER_BINDINGS),\n **dict(DEFAULT_LLM_ASSISTANT_SETTINGS),\n }\n )\n\n # Task retention settings\n task_retention_days: int = 30\n task_retention_limit: int = 100\n pagination_limit: int = 10\n\n # Migration sync settings\n migration_sync_cron: str = \"0 2 * * *\"\n\n # Dataset Review Feature Flags\n ff_dataset_auto_review: bool = True\n ff_dataset_clarification: bool = True\n ff_dataset_execution: bool = True\n\n\n# [/DEF:GlobalSettings:DataClass]\n\n\n# [DEF:AppConfig:DataClass]\n# @PURPOSE: The root configuration model containing all application settings.\nclass AppConfig(BaseModel):\n environments: List[Environment] = []\n settings: GlobalSettings\n\n\n# [/DEF:AppConfig:DataClass]\n\n# [/DEF:ConfigModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region ConfigModels [C:3] [TYPE Module] [SEMANTICS pydantic, model, schedule]\n# @BRIEF Defines the data models for application configuration using Pydantic.\n# @LAYER: Core\n# @RELATION IMPLEMENTS -> [CoreContracts]\n# @RELATION IMPLEMENTS -> [ConnectionContracts]\n\n\nfrom pydantic import BaseModel, Field\n\nfrom ..models.storage import StorageConfig\nfrom ..services.llm_prompt_templates import (\n DEFAULT_LLM_ASSISTANT_SETTINGS,\n DEFAULT_LLM_PROMPTS,\n DEFAULT_LLM_PROVIDER_BINDINGS,\n)\n\n\n# #region Schedule [TYPE DataClass]\n# @BRIEF Represents a backup schedule configuration.\nclass Schedule(BaseModel):\n enabled: bool = False\n cron_expression: str = \"0 0 * * *\" # Default: daily at midnight\n\n\n# #endregion Schedule\n\n\n# #region Environment [TYPE DataClass]\n# @BRIEF Represents a Superset environment configuration.\nclass Environment(BaseModel):\n id: str\n name: str\n url: str\n username: str\n password: str # Will be masked in UI\n stage: str = Field(default=\"DEV\", pattern=\"^(DEV|PREPROD|PROD)$\")\n verify_ssl: bool = True\n timeout: int = 30\n is_default: bool = False\n is_production: bool = False\n backup_schedule: Schedule = Field(default_factory=Schedule)\n\n\n# #endregion Environment\n\n\n# #region LoggingConfig [TYPE DataClass]\n# @BRIEF Defines the configuration for the application's logging system.\nclass LoggingConfig(BaseModel):\n level: str = \"INFO\"\n task_log_level: str = (\n \"INFO\" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)\n )\n file_path: str | None = None\n max_bytes: int = 10 * 1024 * 1024\n backup_count: int = 5\n enable_belief_state: bool = True\n\n\n# #endregion LoggingConfig\n\n\n# #region CleanReleaseConfig [TYPE DataClass]\n# @BRIEF Configuration for clean release compliance subsystem.\nclass CleanReleaseConfig(BaseModel):\n active_policy_id: str | None = None\n active_registry_id: str | None = None\n\n\n# #endregion CleanReleaseConfig\n\n\n# #region FeaturesConfig [C:1] [TYPE DataClass]\n# @BRIEF Top-level feature flags that toggle entire project features on/off.\n# @RATIONALE: Features are read from environment variables on bootstrap and persisted in DB.\n# DB is source of truth after initial bootstrap; env vars only seed defaults.\nclass FeaturesConfig(BaseModel):\n dataset_review: bool = True\n health_monitor: bool = True\n\n\n# #endregion FeaturesConfig\n\n\n# #region GlobalSettings [TYPE DataClass]\n# @BRIEF Represents global application settings.\nclass GlobalSettings(BaseModel):\n storage: StorageConfig = Field(default_factory=StorageConfig)\n clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)\n default_environment_id: str | None = None\n logging: LoggingConfig = Field(default_factory=LoggingConfig)\n features: FeaturesConfig = Field(default_factory=FeaturesConfig)\n connections: list[dict] = []\n llm: dict = Field(\n default_factory=lambda: {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": dict(DEFAULT_LLM_PROMPTS),\n \"provider_bindings\": dict(DEFAULT_LLM_PROVIDER_BINDINGS),\n **dict(DEFAULT_LLM_ASSISTANT_SETTINGS),\n }\n )\n\n # Task retention settings\n task_retention_days: int = 30\n task_retention_limit: int = 100\n pagination_limit: int = 10\n\n # Migration sync settings\n migration_sync_cron: str = \"0 2 * * *\"\n\n # Dataset Review Feature Flags\n ff_dataset_auto_review: bool = True\n ff_dataset_clarification: bool = True\n ff_dataset_execution: bool = True\n\n\n# #endregion GlobalSettings\n\n\n# #region AppConfig [TYPE DataClass]\n# @BRIEF The root configuration model containing all application settings.\nclass AppConfig(BaseModel):\n environments: list[Environment] = []\n settings: GlobalSettings\n\n\n# #endregion AppConfig\n\n# #endregion ConfigModels\n" }, { "contract_id": "Schedule", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 19, - "end_line": 26, + "start_line": 18, + "end_line": 25, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32619,15 +30272,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:Schedule:DataClass]\n# @PURPOSE: Represents a backup schedule configuration.\nclass Schedule(BaseModel):\n enabled: bool = False\n cron_expression: str = \"0 0 * * *\" # Default: daily at midnight\n\n\n# [/DEF:Schedule:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region Schedule [TYPE DataClass]\n# @BRIEF Represents a backup schedule configuration.\nclass Schedule(BaseModel):\n enabled: bool = False\n cron_expression: str = \"0 0 * * *\" # Default: daily at midnight\n\n\n# #endregion Schedule\n" }, { "contract_id": "Environment", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 29, - "end_line": 45, + "start_line": 28, + "end_line": 44, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32663,15 +30316,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:Environment:DataClass]\n# @PURPOSE: Represents a Superset environment configuration.\nclass Environment(BaseModel):\n id: str\n name: str\n url: str\n username: str\n password: str # Will be masked in UI\n stage: str = Field(default=\"DEV\", pattern=\"^(DEV|PREPROD|PROD)$\")\n verify_ssl: bool = True\n timeout: int = 30\n is_default: bool = False\n is_production: bool = False\n backup_schedule: Schedule = Field(default_factory=Schedule)\n\n\n# [/DEF:Environment:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region Environment [TYPE DataClass]\n# @BRIEF Represents a Superset environment configuration.\nclass Environment(BaseModel):\n id: str\n name: str\n url: str\n username: str\n password: str # Will be masked in UI\n stage: str = Field(default=\"DEV\", pattern=\"^(DEV|PREPROD|PROD)$\")\n verify_ssl: bool = True\n timeout: int = 30\n is_default: bool = False\n is_production: bool = False\n backup_schedule: Schedule = Field(default_factory=Schedule)\n\n\n# #endregion Environment\n" }, { "contract_id": "LoggingConfig", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 48, - "end_line": 61, + "start_line": 47, + "end_line": 60, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32707,15 +30360,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LoggingConfig:DataClass]\n# @PURPOSE: Defines the configuration for the application's logging system.\nclass LoggingConfig(BaseModel):\n level: str = \"INFO\"\n task_log_level: str = (\n \"INFO\" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)\n )\n file_path: Optional[str] = None\n max_bytes: int = 10 * 1024 * 1024\n backup_count: int = 5\n enable_belief_state: bool = True\n\n\n# [/DEF:LoggingConfig:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region LoggingConfig [TYPE DataClass]\n# @BRIEF Defines the configuration for the application's logging system.\nclass LoggingConfig(BaseModel):\n level: str = \"INFO\"\n task_log_level: str = (\n \"INFO\" # Minimum level for task-specific logs (DEBUG, INFO, WARNING, ERROR)\n )\n file_path: str | None = None\n max_bytes: int = 10 * 1024 * 1024\n backup_count: int = 5\n enable_belief_state: bool = True\n\n\n# #endregion LoggingConfig\n" }, { "contract_id": "CleanReleaseConfig", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 64, - "end_line": 71, + "start_line": 63, + "end_line": 70, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32751,19 +30404,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseConfig:DataClass]\n# @PURPOSE: Configuration for clean release compliance subsystem.\nclass CleanReleaseConfig(BaseModel):\n active_policy_id: Optional[str] = None\n active_registry_id: Optional[str] = None\n\n\n# [/DEF:CleanReleaseConfig:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region CleanReleaseConfig [TYPE DataClass]\n# @BRIEF Configuration for clean release compliance subsystem.\nclass CleanReleaseConfig(BaseModel):\n active_policy_id: str | None = None\n active_registry_id: str | None = None\n\n\n# #endregion CleanReleaseConfig\n" }, { "contract_id": "FeaturesConfig", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 74, - "end_line": 84, + "start_line": 73, + "end_line": 82, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Top-level feature flags that toggle entire project features on/off.", "RATIONALE": "Features are read from environment variables on bootstrap and persisted in DB." }, @@ -32850,15 +30503,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FeaturesConfig:DataClass]\n# @COMPLEXITY: 1\n# @PURPOSE: Top-level feature flags that toggle entire project features on/off.\n# @RATIONALE: Features are read from environment variables on bootstrap and persisted in DB.\n# DB is source of truth after initial bootstrap; env vars only seed defaults.\nclass FeaturesConfig(BaseModel):\n dataset_review: bool = True\n health_monitor: bool = True\n\n\n# [/DEF:FeaturesConfig:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region FeaturesConfig [C:1] [TYPE DataClass]\n# @BRIEF Top-level feature flags that toggle entire project features on/off.\n# @RATIONALE: Features are read from environment variables on bootstrap and persisted in DB.\n# DB is source of truth after initial bootstrap; env vars only seed defaults.\nclass FeaturesConfig(BaseModel):\n dataset_review: bool = True\n health_monitor: bool = True\n\n\n# #endregion FeaturesConfig\n" }, { "contract_id": "GlobalSettings", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 87, - "end_line": 120, + "start_line": 85, + "end_line": 118, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32894,15 +30547,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GlobalSettings:DataClass]\n# @PURPOSE: Represents global application settings.\nclass GlobalSettings(BaseModel):\n storage: StorageConfig = Field(default_factory=StorageConfig)\n clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)\n default_environment_id: Optional[str] = None\n logging: LoggingConfig = Field(default_factory=LoggingConfig)\n features: FeaturesConfig = Field(default_factory=FeaturesConfig)\n connections: List[dict] = []\n llm: dict = Field(\n default_factory=lambda: {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": dict(DEFAULT_LLM_PROMPTS),\n \"provider_bindings\": dict(DEFAULT_LLM_PROVIDER_BINDINGS),\n **dict(DEFAULT_LLM_ASSISTANT_SETTINGS),\n }\n )\n\n # Task retention settings\n task_retention_days: int = 30\n task_retention_limit: int = 100\n pagination_limit: int = 10\n\n # Migration sync settings\n migration_sync_cron: str = \"0 2 * * *\"\n\n # Dataset Review Feature Flags\n ff_dataset_auto_review: bool = True\n ff_dataset_clarification: bool = True\n ff_dataset_execution: bool = True\n\n\n# [/DEF:GlobalSettings:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region GlobalSettings [TYPE DataClass]\n# @BRIEF Represents global application settings.\nclass GlobalSettings(BaseModel):\n storage: StorageConfig = Field(default_factory=StorageConfig)\n clean_release: CleanReleaseConfig = Field(default_factory=CleanReleaseConfig)\n default_environment_id: str | None = None\n logging: LoggingConfig = Field(default_factory=LoggingConfig)\n features: FeaturesConfig = Field(default_factory=FeaturesConfig)\n connections: list[dict] = []\n llm: dict = Field(\n default_factory=lambda: {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": dict(DEFAULT_LLM_PROMPTS),\n \"provider_bindings\": dict(DEFAULT_LLM_PROVIDER_BINDINGS),\n **dict(DEFAULT_LLM_ASSISTANT_SETTINGS),\n }\n )\n\n # Task retention settings\n task_retention_days: int = 30\n task_retention_limit: int = 100\n pagination_limit: int = 10\n\n # Migration sync settings\n migration_sync_cron: str = \"0 2 * * *\"\n\n # Dataset Review Feature Flags\n ff_dataset_auto_review: bool = True\n ff_dataset_clarification: bool = True\n ff_dataset_execution: bool = True\n\n\n# #endregion GlobalSettings\n" }, { "contract_id": "AppConfig", "contract_type": "DataClass", "file_path": "backend/src/core/config_models.py", - "start_line": 123, - "end_line": 130, + "start_line": 121, + "end_line": 128, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -32938,28 +30591,20 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AppConfig:DataClass]\n# @PURPOSE: The root configuration model containing all application settings.\nclass AppConfig(BaseModel):\n environments: List[Environment] = []\n settings: GlobalSettings\n\n\n# [/DEF:AppConfig:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region AppConfig [TYPE DataClass]\n# @BRIEF The root configuration model containing all application settings.\nclass AppConfig(BaseModel):\n environments: list[Environment] = []\n settings: GlobalSettings\n\n\n# #endregion AppConfig\n" }, { "contract_id": "CotLoggerModule", "contract_type": "Module", "file_path": "backend/src/core/cot_logger.py", "start_line": 1, - "end_line": 258, + "end_line": 236, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", - "PURPOSE": "Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol.", - "SEMANTICS": [ - "logging", - "cot", - "molecular", - "structured", - "json", - "contextvar" - ] + "COMPLEXITY": 4, + "PURPOSE": "Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol." }, "relations": [], "schema_warnings": [ @@ -33000,26 +30645,20 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CotLoggerModule:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: logging, cot, molecular, structured, json, contextvar\n# @PURPOSE: Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol.\n# Uses ContextVar for trace_id and span_id propagation across async contexts.\n# Provides log(), MarkerLogger, seed_trace_id(), push_span(), pop_span().\n# @LAYER: Core\n# @RELATION: [CALLED_BY] -> [TraceContextMiddleware]\n# @RELATION: [CALLED_BY] -> [All C4+ service and route modules]\n# @PRE: Python 3.7+ (ContextVar available).\n# @POST: JSON log records written to the 'cot' logger at appropriate levels.\n# @SIDE_EFFECT: Writes structured JSON to the 'cot' Python logger.\n# @DATA_CONTRACT: Log call -> Single-line JSON to logging.StreamHandler/file.\n\nimport json\nimport logging\nimport uuid\nfrom contextvars import ContextVar\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, Optional\n\n# [DEF:cot_trace_context:Data]\n# @COMPLEXITY: 1\n# @SEMANTICS: contextvar, trace_id, span_id, propagation\n# @PURPOSE: ContextVars for trace ID and span ID propagation across async boundaries.\n_trace_id: ContextVar[str] = ContextVar(\"_trace_id\", default=\"\")\n_span_id: ContextVar[str] = ContextVar(\"_span_id\", default=\"\")\n# [/DEF:cot_trace_context:Data]\n\n# [DEF:cot_logger_instance:Data]\n# @COMPLEXITY: 1\n# @SEMANTICS: logger, instance\n# @PURPOSE: Dedicated Python logger for all CoT (molecular) log output.\ncot_logger = logging.getLogger(\"cot\")\n# [/DEF:cot_logger_instance:Data]\n\n__all__ = [\n \"seed_trace_id\",\n \"set_trace_id\",\n \"get_trace_id\",\n \"push_span\",\n \"pop_span\",\n \"log\",\n \"MarkerLogger\",\n]\n\n# [DEF:seed_trace_id:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: trace_id, uuid, contextvar, set\n# @BRIEF: Generate a new UUID4 trace_id, set it in ContextVar, and return it.\ndef seed_trace_id() -> str:\n \"\"\"Generate a new UUID4 trace ID and store it in the thread-local ContextVar.\n\n Returns:\n str: The newly generated trace ID.\n \"\"\"\n trace_id = str(uuid.uuid4())\n _trace_id.set(trace_id)\n return trace_id\n\n\n# [/DEF:seed_trace_id:Function]\n\n# [DEF:set_trace_id:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: trace_id, contextvar, set, public\n# @BRIEF: Set an explicit trace_id into the ContextVar (e.g. from an incoming header).\ndef set_trace_id(trace_id: str) -> None:\n \"\"\"Set a specific trace_id into the ContextVar.\n\n This is used by TraceContextMiddleware when an ``X-Trace-ID`` header\n is present, enabling cross-service trace chaining.\n\n Args:\n trace_id: The trace ID to set.\n \"\"\"\n _trace_id.set(trace_id)\n\n\n# [/DEF:set_trace_id:Function]\n\n# [DEF:get_trace_id:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: trace_id, contextvar, get, public\n# @BRIEF: Get the current trace_id from ContextVar.\ndef get_trace_id() -> str:\n \"\"\"Get the current trace_id from the thread-local ContextVar.\n\n Returns:\n str: The current trace ID, or empty string if none set.\n \"\"\"\n return _trace_id.get()\n\n\n# [/DEF:get_trace_id:Function]\n\n# [DEF:push_span:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: span_id, contextvar, stack\n# @BRIEF: Set a new span_id in ContextVar and return the previous span_id for later restoration.\ndef push_span(span: str) -> str:\n \"\"\"Push a new span ID onto the context and return the previous span ID.\n\n Args:\n span: The new span identifier (e.g. function or operation name).\n\n Returns:\n str: The previous span ID, suitable for passing to pop_span().\n \"\"\"\n prev = _span_id.get()\n _span_id.set(span)\n return prev\n\n\n# [/DEF:push_span:Function]\n\n# [DEF:pop_span:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: span_id, contextvar, restore\n# @BRIEF: Restore a previous span_id into the ContextVar.\ndef pop_span(prev: str) -> None:\n \"\"\"Restore a previous span ID into the ContextVar.\n\n Args:\n prev: The span ID to restore (previously returned by push_span).\n \"\"\"\n _span_id.set(prev)\n\n\n# [/DEF:pop_span:Function]\n\n# [DEF:cot_log_function:Function]\n# @COMPLEXITY: 2\n# @SEMANTICS: log, json, structured, marker\n# @BRIEF: Core structured logging function that emits a single-line JSON record.\ndef log(\n src: str,\n marker: str,\n intent: str,\n payload: Optional[Dict[str, Any]] = None,\n error: Optional[str] = None,\n level: Optional[str] = None,\n) -> None:\n \"\"\"Emit a single-line structured JSON log record through the 'cot' logger.\n\n Args:\n src: Qualified function or component name (e.g. 'AuthRepository.get_user').\n marker: Protocol marker — one of 'REASON', 'REFLECT', 'EXPLORE'.\n intent: Short one-line description of the intent or action.\n payload: Optional structured data dict to include in the record.\n error: Required for EXPLORE markers; describes the violated assumption.\n level: Log level override. Auto-inferred from marker if omitted\n (REASON/REFLECT -> INFO, EXPLORE -> WARNING).\n\n Side effects:\n Writes a single-line JSON string to the 'cot' Python logger.\n \"\"\"\n if level is None:\n level = \"WARNING\" if marker == \"EXPLORE\" else \"INFO\"\n\n # Build structured extra data for the LogRecord.\n # CotJsonFormatter will read these attributes to produce the final JSON output,\n # consistent with the main superset_tools_app logger.\n extra: Dict[str, Any] = {\n \"marker\": marker,\n \"intent\": intent,\n \"src\": src,\n }\n\n if payload is not None:\n extra[\"payload\"] = payload\n\n if error is not None:\n extra[\"error\"] = error\n\n log_func = {\n \"WARNING\": cot_logger.warning,\n \"ERROR\": cot_logger.error,\n \"DEBUG\": cot_logger.debug,\n }.get(level, cot_logger.info)\n\n log_func(intent, extra=extra)\n\n\n# [/DEF:cot_log_function:Function]\n\n# [DEF:MarkerLogger:Class]\n# @COMPLEXITY: 2\n# @SEMANTICS: logger, proxy, marker, syntactic-sugar\n# @BRIEF: Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.\nclass MarkerLogger:\n \"\"\"Thin proxy over the cot_logger.log() function.\n\n Usage::\n\n log = MarkerLogger(\"AuthService\")\n log.reason(\"Validating credentials\", payload={\"user_id\": \"...\"})\n log.reflect(\"Validation complete\", payload={\"result\": \"ok\"})\n log.explore(\"Token expired\", error=\"JWT expired, attempting refresh\")\n\n Each method delegates to :func:`log` with the corresponding marker.\n \"\"\"\n\n # [DEF:MarkerLogger.__init__:Function]\n # @BRIEF: Store the module/component name used as the 'src' field in all log calls.\n def __init__(self, module_name: str) -> None:\n \"\"\"Initialise the MarkerLogger with a module or component name.\n\n Args:\n module_name: The value used for the 'src' field (e.g. 'AuthService').\n \"\"\"\n self._module_name = module_name\n\n # [/DEF:MarkerLogger.__init__:Function]\n\n # [DEF:MarkerLogger.reason:Function]\n # @BRIEF: Log a REASON marker — strict deduction, core logic.\n def reason(\n self, intent: str, *, payload: Optional[Dict[str, Any]] = None\n ) -> None:\n \"\"\"Log a REASON marker (INFO level).\"\"\"\n log(self._module_name, \"REASON\", intent, payload=payload)\n\n # [/DEF:MarkerLogger.reason:Function]\n\n # [DEF:MarkerLogger.reflect:Function]\n # @BRIEF: Log a REFLECT marker — self-check, structural validation.\n def reflect(\n self, intent: str, *, payload: Optional[Dict[str, Any]] = None\n ) -> None:\n \"\"\"Log a REFLECT marker (INFO level).\"\"\"\n log(self._module_name, \"REFLECT\", intent, payload=payload)\n\n # [/DEF:MarkerLogger.reflect:Function]\n\n # [DEF:MarkerLogger.explore:Function]\n # @BRIEF: Log an EXPLORE marker — searching, alternatives, violated assumptions.\n def explore(\n self,\n intent: str,\n *,\n payload: Optional[Dict[str, Any]] = None,\n error: Optional[str] = None,\n ) -> None:\n \"\"\"Log an EXPLORE marker (WARNING level).\n\n Args:\n intent: Short description of what was being attempted.\n payload: Optional structured data.\n error: Describes the violated assumption or error context.\n \"\"\"\n log(self._module_name, \"EXPLORE\", intent, payload=payload, error=error)\n\n # [/DEF:MarkerLogger.explore:Function]\n\n\n# [/DEF:MarkerLogger:Class]\n# [/DEF:CotLoggerModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region CotLoggerModule [C:4] [TYPE Module] [SEMANTICS logging, cot, json, trace, structured]\n# @BRIEF Structured JSON logger implementing the molecular CoT (Chain-of-Thought) logging protocol.\n# Uses ContextVar for trace_id and span_id propagation across async contexts.\n# Provides log(), MarkerLogger, seed_trace_id(), push_span(), pop_span().\n# @LAYER: Core\n# @RELATION CALLED_BY -> [TraceContextMiddleware]\n# @RELATION CALLED_BY -> [All C4+ service and route modules]\n# @PRE: Python 3.7+ (ContextVar available).\n# @POST: JSON log records written to the 'cot' logger at appropriate levels.\n# @SIDE_EFFECT: Writes structured JSON to the 'cot' Python logger.\n# @DATA_CONTRACT: Log call -> Single-line JSON to logging.StreamHandler/file.\n\nimport logging\nimport uuid\nfrom contextvars import ContextVar\nfrom typing import Any\n\n# #region cot_trace_context [C:1] [TYPE Data] [SEMANTICS contextvar, trace_id, span_id, propagation]\n# @BRIEF ContextVars for trace ID and span ID propagation across async boundaries.\n_trace_id: ContextVar[str] = ContextVar(\"_trace_id\", default=\"\")\n_span_id: ContextVar[str] = ContextVar(\"_span_id\", default=\"\")\n# #endregion cot_trace_context\n\n# #region cot_logger_instance [C:1] [TYPE Data] [SEMANTICS logger, instance]\n# @BRIEF Dedicated Python logger for all CoT (molecular) log output.\ncot_logger = logging.getLogger(\"cot\")\n# #endregion cot_logger_instance\n\n__all__ = [\n \"MarkerLogger\",\n \"get_trace_id\",\n \"log\",\n \"pop_span\",\n \"push_span\",\n \"seed_trace_id\",\n \"set_trace_id\",\n]\n\n# #region seed_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, uuid, contextvar, set]\n# @BRIEF Generate a new UUID4 trace_id, set it in ContextVar, and return it.\ndef seed_trace_id() -> str:\n \"\"\"Generate a new UUID4 trace ID and store it in the thread-local ContextVar.\n\n Returns:\n str: The newly generated trace ID.\n \"\"\"\n trace_id = str(uuid.uuid4())\n _trace_id.set(trace_id)\n return trace_id\n\n\n# #endregion seed_trace_id\n\n# #region set_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, set, public]\n# @BRIEF Set an explicit trace_id into the ContextVar (e.g. from an incoming header).\ndef set_trace_id(trace_id: str) -> None:\n \"\"\"Set a specific trace_id into the ContextVar.\n\n This is used by TraceContextMiddleware when an ``X-Trace-ID`` header\n is present, enabling cross-service trace chaining.\n\n Args:\n trace_id: The trace ID to set.\n \"\"\"\n _trace_id.set(trace_id)\n\n\n# #endregion set_trace_id\n\n# #region get_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, get, public]\n# @BRIEF Get the current trace_id from ContextVar.\ndef get_trace_id() -> str:\n \"\"\"Get the current trace_id from the thread-local ContextVar.\n\n Returns:\n str: The current trace ID, or empty string if none set.\n \"\"\"\n return _trace_id.get()\n\n\n# #endregion get_trace_id\n\n# #region push_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, stack]\n# @BRIEF Set a new span_id in ContextVar and return the previous span_id for later restoration.\ndef push_span(span: str) -> str:\n \"\"\"Push a new span ID onto the context and return the previous span ID.\n\n Args:\n span: The new span identifier (e.g. function or operation name).\n\n Returns:\n str: The previous span ID, suitable for passing to pop_span().\n \"\"\"\n prev = _span_id.get()\n _span_id.set(span)\n return prev\n\n\n# #endregion push_span\n\n# #region pop_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, restore]\n# @BRIEF Restore a previous span_id into the ContextVar.\ndef pop_span(prev: str) -> None:\n \"\"\"Restore a previous span ID into the ContextVar.\n\n Args:\n prev: The span ID to restore (previously returned by push_span).\n \"\"\"\n _span_id.set(prev)\n\n\n# #endregion pop_span\n\n# #region cot_log_function [C:2] [TYPE Function] [SEMANTICS log, json, structured, marker]\n# @BRIEF Core structured logging function that emits a single-line JSON record.\ndef log(\n src: str,\n marker: str,\n intent: str,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n level: str | None = None,\n) -> None:\n \"\"\"Emit a single-line structured JSON log record through the 'cot' logger.\n\n Args:\n src: Qualified function or component name (e.g. 'AuthRepository.get_user').\n marker: Protocol marker — one of 'REASON', 'REFLECT', 'EXPLORE'.\n intent: Short one-line description of the intent or action.\n payload: Optional structured data dict to include in the record.\n error: Required for EXPLORE markers; describes the violated assumption.\n level: Log level override. Auto-inferred from marker if omitted\n (REASON/REFLECT -> INFO, EXPLORE -> WARNING).\n\n Side effects:\n Writes a single-line JSON string to the 'cot' Python logger.\n \"\"\"\n if level is None:\n level = \"WARNING\" if marker == \"EXPLORE\" else \"INFO\"\n\n # Build structured extra data for the LogRecord.\n # CotJsonFormatter will read these attributes to produce the final JSON output,\n # consistent with the main superset_tools_app logger.\n extra: dict[str, Any] = {\n \"marker\": marker,\n \"intent\": intent,\n \"src\": src,\n }\n\n if payload is not None:\n extra[\"payload\"] = payload\n\n if error is not None:\n extra[\"error\"] = error\n\n log_func = {\n \"WARNING\": cot_logger.warning,\n \"ERROR\": cot_logger.error,\n \"DEBUG\": cot_logger.debug,\n }.get(level, cot_logger.info)\n\n log_func(intent, extra=extra)\n\n\n# #endregion cot_log_function\n\n# #region MarkerLogger [C:2] [TYPE Class] [SEMANTICS logger, proxy, marker, syntactic-sugar]\n# @BRIEF Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.\nclass MarkerLogger:\n \"\"\"Thin proxy over the cot_logger.log() function.\n\n Usage::\n\n log = MarkerLogger(\"AuthService\")\n log.reason(\"Validating credentials\", payload={\"user_id\": \"...\"})\n log.reflect(\"Validation complete\", payload={\"result\": \"ok\"})\n log.explore(\"Token expired\", error=\"JWT expired, attempting refresh\")\n\n Each method delegates to :func:`log` with the corresponding marker.\n \"\"\"\n\n # #region MarkerLogger.__init__ [TYPE Function]\n # @BRIEF Store the module/component name used as the 'src' field in all log calls.\n def __init__(self, module_name: str) -> None:\n \"\"\"Initialise the MarkerLogger with a module or component name.\n\n Args:\n module_name: The value used for the 'src' field (e.g. 'AuthService').\n \"\"\"\n self._module_name = module_name\n\n # #endregion MarkerLogger.__init__\n\n # #region MarkerLogger.reason [TYPE Function]\n # @BRIEF Log a REASON marker — strict deduction, core logic.\n def reason(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REASON marker (INFO level).\"\"\"\n log(self._module_name, \"REASON\", intent, payload=payload)\n\n # #endregion MarkerLogger.reason\n\n # #region MarkerLogger.reflect [TYPE Function]\n # @BRIEF Log a REFLECT marker — self-check, structural validation.\n def reflect(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REFLECT marker (INFO level).\"\"\"\n log(self._module_name, \"REFLECT\", intent, payload=payload)\n\n # #endregion MarkerLogger.reflect\n\n # #region MarkerLogger.explore [TYPE Function]\n # @BRIEF Log an EXPLORE marker — searching, alternatives, violated assumptions.\n def explore(\n self,\n intent: str,\n *,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n ) -> None:\n \"\"\"Log an EXPLORE marker (WARNING level).\n\n Args:\n intent: Short description of what was being attempted.\n payload: Optional structured data.\n error: Describes the violated assumption or error context.\n \"\"\"\n log(self._module_name, \"EXPLORE\", intent, payload=payload, error=error)\n\n # #endregion MarkerLogger.explore\n\n\n# #endregion MarkerLogger\n# #endregion CotLoggerModule\n" }, { "contract_id": "cot_trace_context", "contract_type": "Data", "file_path": "backend/src/core/cot_logger.py", - "start_line": 22, - "end_line": 28, + "start_line": 18, + "end_line": 22, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "ContextVars for trace ID and span ID propagation across async boundaries.", - "SEMANTICS": [ - "contextvar", - "trace_id", - "span_id", - "propagation" - ] + "COMPLEXITY": 1, + "PURPOSE": "ContextVars for trace ID and span ID propagation across async boundaries." }, "relations": [], "schema_warnings": [ @@ -33075,49 +30714,22 @@ "actual_complexity": 1, "contract_type": "Data" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Data'", - "detail": { - "actual_type": "Data", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Data' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Data" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:cot_trace_context:Data]\n# @COMPLEXITY: 1\n# @SEMANTICS: contextvar, trace_id, span_id, propagation\n# @PURPOSE: ContextVars for trace ID and span ID propagation across async boundaries.\n_trace_id: ContextVar[str] = ContextVar(\"_trace_id\", default=\"\")\n_span_id: ContextVar[str] = ContextVar(\"_span_id\", default=\"\")\n# [/DEF:cot_trace_context:Data]\n" + "anchor_syntax": "region", + "body": "# #region cot_trace_context [C:1] [TYPE Data] [SEMANTICS contextvar, trace_id, span_id, propagation]\n# @BRIEF ContextVars for trace ID and span ID propagation across async boundaries.\n_trace_id: ContextVar[str] = ContextVar(\"_trace_id\", default=\"\")\n_span_id: ContextVar[str] = ContextVar(\"_span_id\", default=\"\")\n# #endregion cot_trace_context\n" }, { "contract_id": "cot_logger_instance", "contract_type": "Data", "file_path": "backend/src/core/cot_logger.py", - "start_line": 30, - "end_line": 35, + "start_line": 24, + "end_line": 27, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Dedicated Python logger for all CoT (molecular) log output.", - "SEMANTICS": [ - "logger", - "instance" - ] + "COMPLEXITY": 1, + "PURPOSE": "Dedicated Python logger for all CoT (molecular) log output." }, "relations": [], "schema_warnings": [ @@ -33173,51 +30785,22 @@ "actual_complexity": 1, "contract_type": "Data" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Data'", - "detail": { - "actual_type": "Data", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Data' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Data" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:cot_logger_instance:Data]\n# @COMPLEXITY: 1\n# @SEMANTICS: logger, instance\n# @PURPOSE: Dedicated Python logger for all CoT (molecular) log output.\ncot_logger = logging.getLogger(\"cot\")\n# [/DEF:cot_logger_instance:Data]\n" + "anchor_syntax": "region", + "body": "# #region cot_logger_instance [C:1] [TYPE Data] [SEMANTICS logger, instance]\n# @BRIEF Dedicated Python logger for all CoT (molecular) log output.\ncot_logger = logging.getLogger(\"cot\")\n# #endregion cot_logger_instance\n" }, { "contract_id": "seed_trace_id", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 47, - "end_line": 62, + "start_line": 39, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Generate a new UUID4 trace_id, set it in ContextVar, and return it.", - "SEMANTICS": [ - "trace_id", - "uuid", - "contextvar", - "set" - ] + "COMPLEXITY": 1, + "PURPOSE": "Generate a new UUID4 trace_id, set it in ContextVar, and return it." }, "relations": [], "schema_warnings": [ @@ -33229,51 +30812,22 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "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" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:seed_trace_id:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: trace_id, uuid, contextvar, set\n# @BRIEF: Generate a new UUID4 trace_id, set it in ContextVar, and return it.\ndef seed_trace_id() -> str:\n \"\"\"Generate a new UUID4 trace ID and store it in the thread-local ContextVar.\n\n Returns:\n str: The newly generated trace ID.\n \"\"\"\n trace_id = str(uuid.uuid4())\n _trace_id.set(trace_id)\n return trace_id\n\n\n# [/DEF:seed_trace_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region seed_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, uuid, contextvar, set]\n# @BRIEF Generate a new UUID4 trace_id, set it in ContextVar, and return it.\ndef seed_trace_id() -> str:\n \"\"\"Generate a new UUID4 trace ID and store it in the thread-local ContextVar.\n\n Returns:\n str: The newly generated trace ID.\n \"\"\"\n trace_id = str(uuid.uuid4())\n _trace_id.set(trace_id)\n return trace_id\n\n\n# #endregion seed_trace_id\n" }, { "contract_id": "set_trace_id", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 64, - "end_line": 80, + "start_line": 54, + "end_line": 68, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Set an explicit trace_id into the ContextVar (e.g. from an incoming header).", - "SEMANTICS": [ - "trace_id", - "contextvar", - "set", - "public" - ] + "COMPLEXITY": 1, + "PURPOSE": "Set an explicit trace_id into the ContextVar (e.g. from an incoming header)." }, "relations": [], "schema_warnings": [ @@ -33285,51 +30839,22 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "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" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:set_trace_id:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: trace_id, contextvar, set, public\n# @BRIEF: Set an explicit trace_id into the ContextVar (e.g. from an incoming header).\ndef set_trace_id(trace_id: str) -> None:\n \"\"\"Set a specific trace_id into the ContextVar.\n\n This is used by TraceContextMiddleware when an ``X-Trace-ID`` header\n is present, enabling cross-service trace chaining.\n\n Args:\n trace_id: The trace ID to set.\n \"\"\"\n _trace_id.set(trace_id)\n\n\n# [/DEF:set_trace_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region set_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, set, public]\n# @BRIEF Set an explicit trace_id into the ContextVar (e.g. from an incoming header).\ndef set_trace_id(trace_id: str) -> None:\n \"\"\"Set a specific trace_id into the ContextVar.\n\n This is used by TraceContextMiddleware when an ``X-Trace-ID`` header\n is present, enabling cross-service trace chaining.\n\n Args:\n trace_id: The trace ID to set.\n \"\"\"\n _trace_id.set(trace_id)\n\n\n# #endregion set_trace_id\n" }, { "contract_id": "get_trace_id", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 82, - "end_line": 95, + "start_line": 70, + "end_line": 81, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Get the current trace_id from ContextVar.", - "SEMANTICS": [ - "trace_id", - "contextvar", - "get", - "public" - ] + "COMPLEXITY": 1, + "PURPOSE": "Get the current trace_id from ContextVar." }, "relations": [], "schema_warnings": [ @@ -33341,50 +30866,22 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "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" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_trace_id:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: trace_id, contextvar, get, public\n# @BRIEF: Get the current trace_id from ContextVar.\ndef get_trace_id() -> str:\n \"\"\"Get the current trace_id from the thread-local ContextVar.\n\n Returns:\n str: The current trace ID, or empty string if none set.\n \"\"\"\n return _trace_id.get()\n\n\n# [/DEF:get_trace_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_trace_id [C:1] [TYPE Function] [SEMANTICS trace_id, contextvar, get, public]\n# @BRIEF Get the current trace_id from ContextVar.\ndef get_trace_id() -> str:\n \"\"\"Get the current trace_id from the thread-local ContextVar.\n\n Returns:\n str: The current trace ID, or empty string if none set.\n \"\"\"\n return _trace_id.get()\n\n\n# #endregion get_trace_id\n" }, { "contract_id": "push_span", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 97, - "end_line": 115, + "start_line": 83, + "end_line": 99, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Set a new span_id in ContextVar and return the previous span_id for later restoration.", - "SEMANTICS": [ - "span_id", - "contextvar", - "stack" - ] + "COMPLEXITY": 1, + "PURPOSE": "Set a new span_id in ContextVar and return the previous span_id for later restoration." }, "relations": [], "schema_warnings": [ @@ -33396,50 +30893,22 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "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" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:push_span:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: span_id, contextvar, stack\n# @BRIEF: Set a new span_id in ContextVar and return the previous span_id for later restoration.\ndef push_span(span: str) -> str:\n \"\"\"Push a new span ID onto the context and return the previous span ID.\n\n Args:\n span: The new span identifier (e.g. function or operation name).\n\n Returns:\n str: The previous span ID, suitable for passing to pop_span().\n \"\"\"\n prev = _span_id.get()\n _span_id.set(span)\n return prev\n\n\n# [/DEF:push_span:Function]\n" + "anchor_syntax": "region", + "body": "# #region push_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, stack]\n# @BRIEF Set a new span_id in ContextVar and return the previous span_id for later restoration.\ndef push_span(span: str) -> str:\n \"\"\"Push a new span ID onto the context and return the previous span ID.\n\n Args:\n span: The new span identifier (e.g. function or operation name).\n\n Returns:\n str: The previous span ID, suitable for passing to pop_span().\n \"\"\"\n prev = _span_id.get()\n _span_id.set(span)\n return prev\n\n\n# #endregion push_span\n" }, { "contract_id": "pop_span", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 117, - "end_line": 130, + "start_line": 101, + "end_line": 112, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Restore a previous span_id into the ContextVar.", - "SEMANTICS": [ - "span_id", - "contextvar", - "restore" - ] + "COMPLEXITY": 1, + "PURPOSE": "Restore a previous span_id into the ContextVar." }, "relations": [], "schema_warnings": [ @@ -33451,134 +30920,51 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "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" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:pop_span:Function]\n# @COMPLEXITY: 1\n# @SEMANTICS: span_id, contextvar, restore\n# @BRIEF: Restore a previous span_id into the ContextVar.\ndef pop_span(prev: str) -> None:\n \"\"\"Restore a previous span ID into the ContextVar.\n\n Args:\n prev: The span ID to restore (previously returned by push_span).\n \"\"\"\n _span_id.set(prev)\n\n\n# [/DEF:pop_span:Function]\n" + "anchor_syntax": "region", + "body": "# #region pop_span [C:1] [TYPE Function] [SEMANTICS span_id, contextvar, restore]\n# @BRIEF Restore a previous span_id into the ContextVar.\ndef pop_span(prev: str) -> None:\n \"\"\"Restore a previous span ID into the ContextVar.\n\n Args:\n prev: The span ID to restore (previously returned by push_span).\n \"\"\"\n _span_id.set(prev)\n\n\n# #endregion pop_span\n" }, { "contract_id": "cot_log_function", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 132, - "end_line": 185, + "start_line": 114, + "end_line": 165, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Core structured logging function that emits a single-line JSON record.", - "SEMANTICS": [ - "log", - "json", - "structured", - "marker" - ] + "COMPLEXITY": 2, + "PURPOSE": "Core structured logging function that emits a single-line JSON record." }, "relations": [], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:cot_log_function:Function]\n# @COMPLEXITY: 2\n# @SEMANTICS: log, json, structured, marker\n# @BRIEF: Core structured logging function that emits a single-line JSON record.\ndef log(\n src: str,\n marker: str,\n intent: str,\n payload: Optional[Dict[str, Any]] = None,\n error: Optional[str] = None,\n level: Optional[str] = None,\n) -> None:\n \"\"\"Emit a single-line structured JSON log record through the 'cot' logger.\n\n Args:\n src: Qualified function or component name (e.g. 'AuthRepository.get_user').\n marker: Protocol marker — one of 'REASON', 'REFLECT', 'EXPLORE'.\n intent: Short one-line description of the intent or action.\n payload: Optional structured data dict to include in the record.\n error: Required for EXPLORE markers; describes the violated assumption.\n level: Log level override. Auto-inferred from marker if omitted\n (REASON/REFLECT -> INFO, EXPLORE -> WARNING).\n\n Side effects:\n Writes a single-line JSON string to the 'cot' Python logger.\n \"\"\"\n if level is None:\n level = \"WARNING\" if marker == \"EXPLORE\" else \"INFO\"\n\n # Build structured extra data for the LogRecord.\n # CotJsonFormatter will read these attributes to produce the final JSON output,\n # consistent with the main superset_tools_app logger.\n extra: Dict[str, Any] = {\n \"marker\": marker,\n \"intent\": intent,\n \"src\": src,\n }\n\n if payload is not None:\n extra[\"payload\"] = payload\n\n if error is not None:\n extra[\"error\"] = error\n\n log_func = {\n \"WARNING\": cot_logger.warning,\n \"ERROR\": cot_logger.error,\n \"DEBUG\": cot_logger.debug,\n }.get(level, cot_logger.info)\n\n log_func(intent, extra=extra)\n\n\n# [/DEF:cot_log_function:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region cot_log_function [C:2] [TYPE Function] [SEMANTICS log, json, structured, marker]\n# @BRIEF Core structured logging function that emits a single-line JSON record.\ndef log(\n src: str,\n marker: str,\n intent: str,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n level: str | None = None,\n) -> None:\n \"\"\"Emit a single-line structured JSON log record through the 'cot' logger.\n\n Args:\n src: Qualified function or component name (e.g. 'AuthRepository.get_user').\n marker: Protocol marker — one of 'REASON', 'REFLECT', 'EXPLORE'.\n intent: Short one-line description of the intent or action.\n payload: Optional structured data dict to include in the record.\n error: Required for EXPLORE markers; describes the violated assumption.\n level: Log level override. Auto-inferred from marker if omitted\n (REASON/REFLECT -> INFO, EXPLORE -> WARNING).\n\n Side effects:\n Writes a single-line JSON string to the 'cot' Python logger.\n \"\"\"\n if level is None:\n level = \"WARNING\" if marker == \"EXPLORE\" else \"INFO\"\n\n # Build structured extra data for the LogRecord.\n # CotJsonFormatter will read these attributes to produce the final JSON output,\n # consistent with the main superset_tools_app logger.\n extra: dict[str, Any] = {\n \"marker\": marker,\n \"intent\": intent,\n \"src\": src,\n }\n\n if payload is not None:\n extra[\"payload\"] = payload\n\n if error is not None:\n extra[\"error\"] = error\n\n log_func = {\n \"WARNING\": cot_logger.warning,\n \"ERROR\": cot_logger.error,\n \"DEBUG\": cot_logger.debug,\n }.get(level, cot_logger.info)\n\n log_func(intent, extra=extra)\n\n\n# #endregion cot_log_function\n" }, { "contract_id": "MarkerLogger", "contract_type": "Class", "file_path": "backend/src/core/cot_logger.py", - "start_line": 187, - "end_line": 257, + "start_line": 167, + "end_line": 235, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.", - "SEMANTICS": [ - "logger", - "proxy", - "marker", - "syntactic-sugar" - ] + "COMPLEXITY": 2, + "PURPOSE": "Thin proxy class providing .reason(), .reflect(), .explore() convenience methods." }, "relations": [], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:MarkerLogger:Class]\n# @COMPLEXITY: 2\n# @SEMANTICS: logger, proxy, marker, syntactic-sugar\n# @BRIEF: Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.\nclass MarkerLogger:\n \"\"\"Thin proxy over the cot_logger.log() function.\n\n Usage::\n\n log = MarkerLogger(\"AuthService\")\n log.reason(\"Validating credentials\", payload={\"user_id\": \"...\"})\n log.reflect(\"Validation complete\", payload={\"result\": \"ok\"})\n log.explore(\"Token expired\", error=\"JWT expired, attempting refresh\")\n\n Each method delegates to :func:`log` with the corresponding marker.\n \"\"\"\n\n # [DEF:MarkerLogger.__init__:Function]\n # @BRIEF: Store the module/component name used as the 'src' field in all log calls.\n def __init__(self, module_name: str) -> None:\n \"\"\"Initialise the MarkerLogger with a module or component name.\n\n Args:\n module_name: The value used for the 'src' field (e.g. 'AuthService').\n \"\"\"\n self._module_name = module_name\n\n # [/DEF:MarkerLogger.__init__:Function]\n\n # [DEF:MarkerLogger.reason:Function]\n # @BRIEF: Log a REASON marker — strict deduction, core logic.\n def reason(\n self, intent: str, *, payload: Optional[Dict[str, Any]] = None\n ) -> None:\n \"\"\"Log a REASON marker (INFO level).\"\"\"\n log(self._module_name, \"REASON\", intent, payload=payload)\n\n # [/DEF:MarkerLogger.reason:Function]\n\n # [DEF:MarkerLogger.reflect:Function]\n # @BRIEF: Log a REFLECT marker — self-check, structural validation.\n def reflect(\n self, intent: str, *, payload: Optional[Dict[str, Any]] = None\n ) -> None:\n \"\"\"Log a REFLECT marker (INFO level).\"\"\"\n log(self._module_name, \"REFLECT\", intent, payload=payload)\n\n # [/DEF:MarkerLogger.reflect:Function]\n\n # [DEF:MarkerLogger.explore:Function]\n # @BRIEF: Log an EXPLORE marker — searching, alternatives, violated assumptions.\n def explore(\n self,\n intent: str,\n *,\n payload: Optional[Dict[str, Any]] = None,\n error: Optional[str] = None,\n ) -> None:\n \"\"\"Log an EXPLORE marker (WARNING level).\n\n Args:\n intent: Short description of what was being attempted.\n payload: Optional structured data.\n error: Describes the violated assumption or error context.\n \"\"\"\n log(self._module_name, \"EXPLORE\", intent, payload=payload, error=error)\n\n # [/DEF:MarkerLogger.explore:Function]\n\n\n# [/DEF:MarkerLogger:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region MarkerLogger [C:2] [TYPE Class] [SEMANTICS logger, proxy, marker, syntactic-sugar]\n# @BRIEF Thin proxy class providing .reason(), .reflect(), .explore() convenience methods.\nclass MarkerLogger:\n \"\"\"Thin proxy over the cot_logger.log() function.\n\n Usage::\n\n log = MarkerLogger(\"AuthService\")\n log.reason(\"Validating credentials\", payload={\"user_id\": \"...\"})\n log.reflect(\"Validation complete\", payload={\"result\": \"ok\"})\n log.explore(\"Token expired\", error=\"JWT expired, attempting refresh\")\n\n Each method delegates to :func:`log` with the corresponding marker.\n \"\"\"\n\n # #region MarkerLogger.__init__ [TYPE Function]\n # @BRIEF Store the module/component name used as the 'src' field in all log calls.\n def __init__(self, module_name: str) -> None:\n \"\"\"Initialise the MarkerLogger with a module or component name.\n\n Args:\n module_name: The value used for the 'src' field (e.g. 'AuthService').\n \"\"\"\n self._module_name = module_name\n\n # #endregion MarkerLogger.__init__\n\n # #region MarkerLogger.reason [TYPE Function]\n # @BRIEF Log a REASON marker — strict deduction, core logic.\n def reason(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REASON marker (INFO level).\"\"\"\n log(self._module_name, \"REASON\", intent, payload=payload)\n\n # #endregion MarkerLogger.reason\n\n # #region MarkerLogger.reflect [TYPE Function]\n # @BRIEF Log a REFLECT marker — self-check, structural validation.\n def reflect(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REFLECT marker (INFO level).\"\"\"\n log(self._module_name, \"REFLECT\", intent, payload=payload)\n\n # #endregion MarkerLogger.reflect\n\n # #region MarkerLogger.explore [TYPE Function]\n # @BRIEF Log an EXPLORE marker — searching, alternatives, violated assumptions.\n def explore(\n self,\n intent: str,\n *,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n ) -> None:\n \"\"\"Log an EXPLORE marker (WARNING level).\n\n Args:\n intent: Short description of what was being attempted.\n payload: Optional structured data.\n error: Describes the violated assumption or error context.\n \"\"\"\n log(self._module_name, \"EXPLORE\", intent, payload=payload, error=error)\n\n # #endregion MarkerLogger.explore\n\n\n# #endregion MarkerLogger\n" }, { "contract_id": "MarkerLogger.__init__", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 204, - "end_line": 214, + "start_line": 182, + "end_line": 192, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -33596,15 +30982,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:MarkerLogger.__init__:Function]\n # @BRIEF: Store the module/component name used as the 'src' field in all log calls.\n def __init__(self, module_name: str) -> None:\n \"\"\"Initialise the MarkerLogger with a module or component name.\n\n Args:\n module_name: The value used for the 'src' field (e.g. 'AuthService').\n \"\"\"\n self._module_name = module_name\n\n # [/DEF:MarkerLogger.__init__:Function]\n" + "anchor_syntax": "region", + "body": " # #region MarkerLogger.__init__ [TYPE Function]\n # @BRIEF Store the module/component name used as the 'src' field in all log calls.\n def __init__(self, module_name: str) -> None:\n \"\"\"Initialise the MarkerLogger with a module or component name.\n\n Args:\n module_name: The value used for the 'src' field (e.g. 'AuthService').\n \"\"\"\n self._module_name = module_name\n\n # #endregion MarkerLogger.__init__\n" }, { "contract_id": "MarkerLogger.reason", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 216, - "end_line": 224, + "start_line": 194, + "end_line": 202, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -33622,15 +31008,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:MarkerLogger.reason:Function]\n # @BRIEF: Log a REASON marker — strict deduction, core logic.\n def reason(\n self, intent: str, *, payload: Optional[Dict[str, Any]] = None\n ) -> None:\n \"\"\"Log a REASON marker (INFO level).\"\"\"\n log(self._module_name, \"REASON\", intent, payload=payload)\n\n # [/DEF:MarkerLogger.reason:Function]\n" + "anchor_syntax": "region", + "body": " # #region MarkerLogger.reason [TYPE Function]\n # @BRIEF Log a REASON marker — strict deduction, core logic.\n def reason(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REASON marker (INFO level).\"\"\"\n log(self._module_name, \"REASON\", intent, payload=payload)\n\n # #endregion MarkerLogger.reason\n" }, { "contract_id": "MarkerLogger.reflect", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 226, - "end_line": 234, + "start_line": 204, + "end_line": 212, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -33648,15 +31034,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:MarkerLogger.reflect:Function]\n # @BRIEF: Log a REFLECT marker — self-check, structural validation.\n def reflect(\n self, intent: str, *, payload: Optional[Dict[str, Any]] = None\n ) -> None:\n \"\"\"Log a REFLECT marker (INFO level).\"\"\"\n log(self._module_name, \"REFLECT\", intent, payload=payload)\n\n # [/DEF:MarkerLogger.reflect:Function]\n" + "anchor_syntax": "region", + "body": " # #region MarkerLogger.reflect [TYPE Function]\n # @BRIEF Log a REFLECT marker — self-check, structural validation.\n def reflect(\n self, intent: str, *, payload: dict[str, Any] | None = None\n ) -> None:\n \"\"\"Log a REFLECT marker (INFO level).\"\"\"\n log(self._module_name, \"REFLECT\", intent, payload=payload)\n\n # #endregion MarkerLogger.reflect\n" }, { "contract_id": "MarkerLogger.explore", "contract_type": "Function", "file_path": "backend/src/core/cot_logger.py", - "start_line": 236, - "end_line": 254, + "start_line": 214, + "end_line": 232, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -33674,46 +31060,39 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:MarkerLogger.explore:Function]\n # @BRIEF: Log an EXPLORE marker — searching, alternatives, violated assumptions.\n def explore(\n self,\n intent: str,\n *,\n payload: Optional[Dict[str, Any]] = None,\n error: Optional[str] = None,\n ) -> None:\n \"\"\"Log an EXPLORE marker (WARNING level).\n\n Args:\n intent: Short description of what was being attempted.\n payload: Optional structured data.\n error: Describes the violated assumption or error context.\n \"\"\"\n log(self._module_name, \"EXPLORE\", intent, payload=payload, error=error)\n\n # [/DEF:MarkerLogger.explore:Function]\n" + "anchor_syntax": "region", + "body": " # #region MarkerLogger.explore [TYPE Function]\n # @BRIEF Log an EXPLORE marker — searching, alternatives, violated assumptions.\n def explore(\n self,\n intent: str,\n *,\n payload: dict[str, Any] | None = None,\n error: str | None = None,\n ) -> None:\n \"\"\"Log an EXPLORE marker (WARNING level).\n\n Args:\n intent: Short description of what was being attempted.\n payload: Optional structured data.\n error: Describes the violated assumption or error context.\n \"\"\"\n log(self._module_name, \"EXPLORE\", intent, payload=payload, error=error)\n\n # #endregion MarkerLogger.explore\n" }, { "contract_id": "DatabaseModule", "contract_type": "Module", "file_path": "backend/src/core/database.py", "start_line": 1, - "end_line": 728, + "end_line": 755, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "A single engine instance is used for the entire application.", "LAYER": "Core", - "PURPOSE": "Configures database connection and session management (PostgreSQL-first).", - "SEMANTICS": [ - "database", - "postgresql", - "sqlalchemy", - "session", - "persistence" - ] + "PURPOSE": "Configures database connection and session management (PostgreSQL-first)." }, "relations": [ { "source_id": "DatabaseModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "MappingModels", "target_ref": "[MappingModels]" }, { "source_id": "DatabaseModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "auth_config", "target_ref": "[auth_config]" }, { "source_id": "DatabaseModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ConnectionConfig", "target_ref": "[ConnectionConfig]" } @@ -33727,75 +31106,21 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatabaseModule:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: database, postgresql, sqlalchemy, session, persistence\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom sqlalchemy import create_engine, inspect, text\nfrom sqlalchemy.orm import sessionmaker\nfrom ..models.mapping import Base\nfrom ..models.connection import ConnectionConfig\n\n# Import models to ensure they're registered with Base\nfrom ..models import task as _task_models # noqa: F401\nfrom ..models import auth as _auth_models # noqa: F401\nfrom ..models import config as _config_models # noqa: F401\nfrom ..models import llm as _llm_models # noqa: F401\nfrom ..models import assistant as _assistant_models # noqa: F401\nfrom ..models import profile as _profile_models # noqa: F401\nfrom ..models import clean_release as _clean_release_models # noqa: F401\nfrom ..models import connection as _connection_models # noqa: F401\nfrom ..models import dataset_review as _dataset_review_models # noqa: F401\nfrom .logger import belief_scope, logger\nfrom .auth.config import auth_config\nimport os\nfrom pathlib import Path\n# [/SECTION]\n\n# [DEF:BASE_DIR:Variable]\n# @COMPLEXITY: 1\n# @PURPOSE: Base directory for the backend.\nBASE_DIR = Path(__file__).resolve().parent.parent.parent\n# [/DEF:BASE_DIR:Variable]\n\n# [DEF:DATABASE_URL:Constant]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:DATABASE_URL:Constant]\n\n# [DEF:TASKS_DATABASE_URL:Constant]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:TASKS_DATABASE_URL:Constant]\n\n# [DEF:AUTH_DATABASE_URL:Constant]\n# @COMPLEXITY: 1\n# @PURPOSE: URL for the authentication database.\nAUTH_DATABASE_URL = os.getenv(\"AUTH_DATABASE_URL\", auth_config.AUTH_DATABASE_URL)\n# [/DEF:AUTH_DATABASE_URL:Constant]\n\n\n# [DEF:engine:Variable]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:engine:Variable]\n\n# [DEF:tasks_engine:Variable]\n# @COMPLEXITY: 1\n# @PURPOSE: SQLAlchemy engine for tasks database.\ntasks_engine = _build_engine(TASKS_DATABASE_URL)\n# [/DEF:tasks_engine:Variable]\n\n# [DEF:auth_engine:Variable]\n# @COMPLEXITY: 1\n# @PURPOSE: SQLAlchemy engine for authentication database.\nauth_engine = _build_engine(AUTH_DATABASE_URL)\n# [/DEF:auth_engine:Variable]\n\n# [DEF:SessionLocal:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: A session factory for the main mappings database.\n# @PRE: engine is initialized.\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n# [/DEF:SessionLocal:Class]\n\n# [DEF:TasksSessionLocal:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: A session factory for the tasks execution database.\n# @PRE: tasks_engine is initialized.\nTasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine)\n# [/DEF:TasksSessionLocal:Class]\n\n# [DEF:AuthSessionLocal:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: A session factory for the authentication database.\n# @PRE: auth_engine is initialized.\nAuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine)\n# [/DEF:AuthSessionLocal:Class]\n\n\n# [DEF:_ensure_user_dashboard_preferences_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] Profile preference additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_user_dashboard_preferences_columns:Function]\n\n\n# [DEF:_ensure_user_dashboard_preferences_health_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] Profile health preference additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_user_dashboard_preferences_health_columns:Function]\n\n\n# [DEF:_ensure_llm_validation_results_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] ValidationRecord additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_llm_validation_results_columns:Function]\n\n\n# [DEF:_ensure_git_server_configs_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] GitServerConfig preference additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_git_server_configs_columns:Function]\n\n\n# [DEF:_ensure_auth_users_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] Auth users additive migration failed: %s\",\n migration_error,\n )\n raise\n\n\n# [/DEF:_ensure_auth_users_columns:Function]\n\n\n# [DEF:ensure_connection_configs_table:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] ConnectionConfig table ensure failed: %s\",\n migration_error,\n )\n raise\n\n\n# [/DEF:ensure_connection_configs_table:Function]\n\n\n# [DEF:_ensure_filter_source_enum_values:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] FilterSource enum additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_filter_source_enum_values:Function]\n\n\n# [DEF:_ensure_dataset_review_session_columns:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: 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 raise\n\n\n# [DEF:_ensure_translation_records_columns:Function]\n# @PURPOSE: Add source_data JSON column to translation_records and translation_preview_records.\n# @PRE: bind_engine points to the application database.\n# @POST: source_data column exists on both tables.\n# @SIDE_EFFECT: Executes ALTER TABLE statements.\ndef _ensure_translation_records_columns(bind_engine):\n with belief_scope(\"_ensure_translation_records_columns\"):\n migrations = [\n (\"translation_records\", \"source_data\",\n \"ALTER TABLE translation_records ADD COLUMN source_data JSON\"),\n (\"translation_preview_records\", \"source_data\",\n \"ALTER TABLE translation_preview_records ADD COLUMN source_data JSON\"),\n ]\n for table_name, column_name, alter_sql in migrations:\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n continue\n existing_columns = {\n str(c.get(\"name\") or \"\").strip()\n for c in inspector.get_columns(table_name)\n }\n if column_name not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(text(alter_sql))\n logger.reflect(\n f\"Added {column_name} column to {table_name}\",\n )\n except Exception as migration_error:\n logger.explore(\n f\"Failed to add {column_name} to {table_name}\",\n extra={\"error\": str(migration_error)},\n )\n raise\n# [/DEF:_ensure_translation_records_columns:Function]\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# [/DEF:_ensure_dataset_review_session_columns:Function]\n\n\n# [DEF:init_db:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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]\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_records_columns(engine)\n\n\n# [/DEF:init_db:Function]\n\n\n# [DEF:get_db:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for getting a database session.\n# @PRE: SessionLocal is initialized.\n# @POST: Session is closed after use.\n# @RETURN: Generator[Session, None, None]\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# [/DEF:get_db:Function]\n\n\n# [DEF:get_tasks_db:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for getting a tasks database session.\n# @PRE: TasksSessionLocal is initialized.\n# @POST: Session is closed after use.\n# @RETURN: Generator[Session, None, None]\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# [/DEF:get_tasks_db:Function]\n\n\n# [DEF:get_auth_db:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @RETURN: Generator[Session, None, None]\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# [/DEF:get_auth_db:Function]\n\n# [/DEF:DatabaseModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "BASE_DIR", "contract_type": "Variable", "file_path": "backend/src/core/database.py", - "start_line": 35, - "end_line": 39, + "start_line": 33, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Base directory for the backend." }, "relations": [], @@ -33854,19 +31179,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BASE_DIR:Variable]\n# @COMPLEXITY: 1\n# @PURPOSE: Base directory for the backend.\nBASE_DIR = Path(__file__).resolve().parent.parent.parent\n# [/DEF:BASE_DIR:Variable]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DATABASE_URL", "contract_type": "Constant", "file_path": "backend/src/core/database.py", - "start_line": 41, - "end_line": 49, + "start_line": 38, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "URL for the main application database." }, "relations": [], @@ -33925,19 +31250,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DATABASE_URL:Constant]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:DATABASE_URL:Constant]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "TASKS_DATABASE_URL", "contract_type": "Constant", "file_path": "backend/src/core/database.py", - "start_line": 51, - "end_line": 56, + "start_line": 47, + "end_line": 51, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "URL for the tasks execution database." }, "relations": [], @@ -33996,19 +31321,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TASKS_DATABASE_URL:Constant]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:TASKS_DATABASE_URL:Constant]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "AUTH_DATABASE_URL", "contract_type": "Constant", "file_path": "backend/src/core/database.py", - "start_line": 58, - "end_line": 62, + "start_line": 53, + "end_line": 56, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "URL for the authentication database." }, "relations": [], @@ -34067,19 +31392,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AUTH_DATABASE_URL:Constant]\n# @COMPLEXITY: 1\n# @PURPOSE: URL for the authentication database.\nAUTH_DATABASE_URL = os.getenv(\"AUTH_DATABASE_URL\", auth_config.AUTH_DATABASE_URL)\n# [/DEF:AUTH_DATABASE_URL:Constant]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "engine", "contract_type": "Variable", "file_path": "backend/src/core/database.py", - "start_line": 65, - "end_line": 77, + "start_line": 59, + "end_line": 70, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "SQLAlchemy engine for mappings database.", "SIDE_EFFECT": "Creates database engine and manages connection pool." }, @@ -34163,19 +31488,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:engine:Variable]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:engine:Variable]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "tasks_engine", "contract_type": "Variable", "file_path": "backend/src/core/database.py", - "start_line": 79, - "end_line": 83, + "start_line": 72, + "end_line": 75, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "SQLAlchemy engine for tasks database." }, "relations": [], @@ -34234,19 +31559,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:tasks_engine:Variable]\n# @COMPLEXITY: 1\n# @PURPOSE: SQLAlchemy engine for tasks database.\ntasks_engine = _build_engine(TASKS_DATABASE_URL)\n# [/DEF:tasks_engine:Variable]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "auth_engine", "contract_type": "Variable", "file_path": "backend/src/core/database.py", - "start_line": 85, - "end_line": 89, + "start_line": 77, + "end_line": 80, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "SQLAlchemy engine for authentication database." }, "relations": [], @@ -34305,19 +31630,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:auth_engine:Variable]\n# @COMPLEXITY: 1\n# @PURPOSE: SQLAlchemy engine for authentication database.\nauth_engine = _build_engine(AUTH_DATABASE_URL)\n# [/DEF:auth_engine:Variable]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SessionLocal", "contract_type": "Class", "file_path": "backend/src/core/database.py", - "start_line": 91, - "end_line": 96, + "start_line": 82, + "end_line": 86, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PRE": "engine is initialized.", "PURPOSE": "A session factory for the main mappings database." }, @@ -34342,19 +31667,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionLocal:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: A session factory for the main mappings database.\n# @PRE: engine is initialized.\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n# [/DEF:SessionLocal:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "TasksSessionLocal", "contract_type": "Class", "file_path": "backend/src/core/database.py", - "start_line": 98, - "end_line": 103, + "start_line": 88, + "end_line": 92, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PRE": "tasks_engine is initialized.", "PURPOSE": "A session factory for the tasks execution database." }, @@ -34379,19 +31704,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TasksSessionLocal:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: A session factory for the tasks execution database.\n# @PRE: tasks_engine is initialized.\nTasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine)\n# [/DEF:TasksSessionLocal:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "AuthSessionLocal", "contract_type": "Class", "file_path": "backend/src/core/database.py", - "start_line": 105, - "end_line": 110, + "start_line": 94, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PRE": "auth_engine is initialized.", "PURPOSE": "A session factory for the authentication database." }, @@ -34416,19 +31741,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthSessionLocal:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: A session factory for the authentication database.\n# @PRE: auth_engine is initialized.\nAuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine)\n# [/DEF:AuthSessionLocal:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_ensure_user_dashboard_preferences_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 113, - "end_line": 180, + "start_line": 101, + "end_line": 167, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Missing columns are added without data loss.", "PRE": "bind_engine points to application database where profile table is stored.", "PURPOSE": "Applies additive schema upgrades for user_dashboard_preferences table." @@ -34436,7 +31761,7 @@ "relations": [ { "source_id": "_ensure_user_dashboard_preferences_columns", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "engine", "target_ref": "[engine]" } @@ -34459,125 +31784,69 @@ "actual_complexity": 3, "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_user_dashboard_preferences_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] Profile preference additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_user_dashboard_preferences_columns:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_ensure_user_dashboard_preferences_health_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 183, - "end_line": 227, + "start_line": 170, + "end_line": 213, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Applies additive schema upgrades for user_dashboard_preferences table (health fields)." }, "relations": [ { "source_id": "_ensure_user_dashboard_preferences_health_columns", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "engine", "target_ref": "[engine]" } ], - "schema_warnings": [ - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_user_dashboard_preferences_health_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] Profile health preference additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_user_dashboard_preferences_health_columns:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_ensure_llm_validation_results_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 230, - "end_line": 270, + "start_line": 216, + "end_line": 255, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Applies additive schema upgrades for llm_validation_results table." }, "relations": [ { "source_id": "_ensure_llm_validation_results_columns", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "engine", "target_ref": "[engine]" } ], - "schema_warnings": [ - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_llm_validation_results_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] ValidationRecord additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_llm_validation_results_columns:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_ensure_git_server_configs_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 273, - "end_line": 311, + "start_line": 258, + "end_line": 295, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Missing columns are added without data loss.", "PRE": "bind_engine points to application database.", "PURPOSE": "Applies additive schema upgrades for git_server_configs table." @@ -34585,7 +31854,7 @@ "relations": [ { "source_id": "_ensure_git_server_configs_columns", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "engine", "target_ref": "[engine]" } @@ -34608,39 +31877,21 @@ "actual_complexity": 3, "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_git_server_configs_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] GitServerConfig preference additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_git_server_configs_columns:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_ensure_auth_users_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 314, - "end_line": 374, + "start_line": 298, + "end_line": 357, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Missing columns are added without data loss.", "PRE": "bind_engine points to authentication database.", "PURPOSE": "Applies additive schema upgrades for auth users table." @@ -34648,7 +31899,7 @@ "relations": [ { "source_id": "_ensure_auth_users_columns", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "auth_engine", "target_ref": "[auth_engine]" } @@ -34671,39 +31922,21 @@ "actual_complexity": 3, "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_auth_users_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] Auth users additive migration failed: %s\",\n migration_error,\n )\n raise\n\n\n# [/DEF:_ensure_auth_users_columns:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ensure_connection_configs_table", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 377, - "end_line": 395, + "start_line": 360, + "end_line": 377, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "connection_configs table exists without dropping existing data.", "PRE": "bind_engine points to the application database.", "PURPOSE": "Ensures the external connection registry table exists in the main database." @@ -34711,7 +31944,7 @@ "relations": [ { "source_id": "ensure_connection_configs_table", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ConnectionConfig", "target_ref": "[ConnectionConfig]" } @@ -34734,39 +31967,21 @@ "actual_complexity": 3, "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ensure_connection_configs_table:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] ConnectionConfig table ensure failed: %s\",\n migration_error,\n )\n raise\n\n\n# [/DEF:ensure_connection_configs_table:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_ensure_filter_source_enum_values", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 398, - "end_line": 467, + "start_line": 380, + "end_line": 448, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "New enum values are available without data loss.", "PRE": "bind_engine points to application database with imported_filters table.", "PURPOSE": "Adds missing FilterSource enum values to the PostgreSQL native filtersource type." @@ -34774,7 +31989,7 @@ "relations": [ { "source_id": "_ensure_filter_source_enum_values", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "engine", "target_ref": "[engine]" } @@ -34797,39 +32012,21 @@ "actual_complexity": 3, "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_filter_source_enum_values:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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.warning(\n \"[database][EXPLORE] FilterSource enum additive migration failed: %s\",\n migration_error,\n )\n\n\n# [/DEF:_ensure_filter_source_enum_values:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_ensure_dataset_review_session_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 470, - "end_line": 639, + "start_line": 451, + "end_line": 581, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "POST": "Missing additive columns across legacy dataset review tables are created without removing existing data.", "PRE": "bind_engine points to the application database where dataset review tables are stored.", "PURPOSE": "Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.", @@ -34838,124 +32035,100 @@ "relations": [ { "source_id": "_ensure_dataset_review_session_columns", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "DatasetReviewSession", "target_ref": "[DatasetReviewSession]" }, { "source_id": "_ensure_dataset_review_session_columns", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ImportedFilter", "target_ref": "[ImportedFilter]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_dataset_review_session_columns:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: 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 raise\n\n\n# [DEF:_ensure_translation_records_columns:Function]\n# @PURPOSE: Add source_data JSON column to translation_records and translation_preview_records.\n# @PRE: bind_engine points to the application database.\n# @POST: source_data column exists on both tables.\n# @SIDE_EFFECT: Executes ALTER TABLE statements.\ndef _ensure_translation_records_columns(bind_engine):\n with belief_scope(\"_ensure_translation_records_columns\"):\n migrations = [\n (\"translation_records\", \"source_data\",\n \"ALTER TABLE translation_records ADD COLUMN source_data JSON\"),\n (\"translation_preview_records\", \"source_data\",\n \"ALTER TABLE translation_preview_records ADD COLUMN source_data JSON\"),\n ]\n for table_name, column_name, alter_sql in migrations:\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n continue\n existing_columns = {\n str(c.get(\"name\") or \"\").strip()\n for c in inspector.get_columns(table_name)\n }\n if column_name not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(text(alter_sql))\n logger.reflect(\n f\"Added {column_name} column to {table_name}\",\n )\n except Exception as migration_error:\n logger.explore(\n f\"Failed to add {column_name} to {table_name}\",\n extra={\"error\": str(migration_error)},\n )\n raise\n# [/DEF:_ensure_translation_records_columns:Function]\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# [/DEF:_ensure_dataset_review_session_columns:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region _ensure_dataset_review_session_columns [C:4] [TYPE Function]\n# @BRIEF Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.\n# @PRE: bind_engine points to the application database where dataset review tables are stored.\n# @POST: Missing additive columns across legacy dataset review tables are created without removing existing data.\n# @SIDE_EFFECT: Executes ALTER TABLE statements against dataset review tables in the application database.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\ndef _ensure_translation_jobs_columns(bind_engine):\n with belief_scope(\"_ensure_translation_jobs_columns\"):\n table_name = \"translation_jobs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"environment_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN environment_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added environment_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add environment_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n raise\n\n if \"target_database_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_database_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added target_database_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_database_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n\ndef _ensure_dataset_review_session_columns(bind_engine):\n with belief_scope(\"_ensure_dataset_review_session_columns\"):\n inspector = inspect(bind_engine)\n existing_tables = set(inspector.get_table_names())\n migration_plan = {\n \"dataset_review_sessions\": [\n (\n \"version\",\n \"ALTER TABLE dataset_review_sessions \"\n \"ADD COLUMN version INTEGER NOT NULL DEFAULT 0\",\n )\n ],\n \"imported_filters\": [\n (\n \"raw_value_masked\",\n \"ALTER TABLE imported_filters \"\n \"ADD COLUMN raw_value_masked BOOLEAN NOT NULL DEFAULT FALSE\",\n )\n ],\n }\n\n for table_name, planned_columns in migration_plan.items():\n if table_name not in existing_tables:\n logger.reason(\n \"Dataset review table does not exist yet; skipping additive schema migration\",\n extra={\"table\": table_name},\n )\n continue\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n alter_statements = [\n statement\n for column_name, statement in planned_columns\n if column_name not in existing_columns\n ]\n\n if not alter_statements:\n logger.reason(\n \"Dataset review table schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n continue\n\n logger.reason(\n \"Applying additive dataset review schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reflect(\n \"Dataset review additive schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Dataset review additive schema migration failed\",\n extra={\"table\": table_name, \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_dataset_review_session_columns\n" }, { - "contract_id": "_ensure_translation_records_columns", + "contract_id": "_ensure_translation_schedules_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 529, - "end_line": 563, - "tier": "TIER_1", - "complexity": 1, + "start_line": 584, + "end_line": 622, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "POST": "source_data column exists on both tables.", - "PRE": "bind_engine points to the application database.", - "PURPOSE": "Add source_data JSON column to translation_records and translation_preview_records.", - "SIDE_EFFECT": "Executes ALTER TABLE statements." + "COMPLEXITY": 3, + "POST": "Missing columns are added without data loss.", + "PRE": "bind_engine points to application database.", + "PURPOSE": "Applies additive schema upgrades for translation_schedules table." }, - "relations": [], + "relations": [ + { + "source_id": "_ensure_translation_schedules_columns", + "relation_type": "DEPENDS_ON", + "target_id": "engine", + "target_ref": "[engine]" + } + ], "schema_warnings": [ { "code": "tag_forbidden_by_complexity", "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C1", + "message": "@POST is forbidden for contract type 'Function' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C1", + "message": "@PRE is forbidden for contract type 'Function' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_translation_records_columns:Function]\n# @PURPOSE: Add source_data JSON column to translation_records and translation_preview_records.\n# @PRE: bind_engine points to the application database.\n# @POST: source_data column exists on both tables.\n# @SIDE_EFFECT: Executes ALTER TABLE statements.\ndef _ensure_translation_records_columns(bind_engine):\n with belief_scope(\"_ensure_translation_records_columns\"):\n migrations = [\n (\"translation_records\", \"source_data\",\n \"ALTER TABLE translation_records ADD COLUMN source_data JSON\"),\n (\"translation_preview_records\", \"source_data\",\n \"ALTER TABLE translation_preview_records ADD COLUMN source_data JSON\"),\n ]\n for table_name, column_name, alter_sql in migrations:\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n continue\n existing_columns = {\n str(c.get(\"name\") or \"\").strip()\n for c in inspector.get_columns(table_name)\n }\n if column_name not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(text(alter_sql))\n logger.reflect(\n f\"Added {column_name} column to {table_name}\",\n )\n except Exception as migration_error:\n logger.explore(\n f\"Failed to add {column_name} to {table_name}\",\n extra={\"error\": str(migration_error)},\n )\n raise\n# [/DEF:_ensure_translation_records_columns:Function]\n" + "anchor_syntax": "region", + "body": "# #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" + }, + { + "contract_id": "_ensure_dictionary_entries_columns", + "contract_type": "Function", + "file_path": "backend/src/core/database.py", + "start_line": 625, + "end_line": 671, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Additive migration for dictionary_entries origin tracking columns." + }, + "relations": [ + { + "source_id": "_ensure_dictionary_entries_columns", + "relation_type": "DEPENDS_ON", + "target_id": "engine", + "target_ref": "[engine]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "init_db", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 642, - "end_line": 668, + "start_line": 674, + "end_line": 701, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Database tables created in all databases.", "PRE": "engine, tasks_engine and auth_engine are initialized.", "PURPOSE": "Initializes the database by creating all tables.", @@ -34964,21 +32137,227 @@ "relations": [ { "source_id": "init_db", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "ensure_connection_configs_table", "target_ref": "[ensure_connection_configs_table]" }, { "source_id": "init_db", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "_ensure_filter_source_enum_values", "target_ref": "[_ensure_filter_source_enum_values]" }, { "source_id": "init_db", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "_ensure_dataset_review_session_columns", "target_ref": "[_ensure_dataset_review_session_columns]" + }, + { + "source_id": "init_db", + "relation_type": "CALLS", + "target_id": "_ensure_dictionary_entries_columns", + "target_ref": "[_ensure_dictionary_entries_columns]" + } + ], + "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" + } + } + ], + "anchor_syntax": "region", + "body": "# #region init_db [C:3] [TYPE Function]\n# @BRIEF Initializes the database by creating all tables.\n# @PRE: engine, tasks_engine and auth_engine are initialized.\n# @POST: Database tables created in all databases.\n# @SIDE_EFFECT: Creates physical database files if they don't exist.\n# @RELATION CALLS -> [ensure_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" + }, + { + "contract_id": "get_db", + "contract_type": "Function", + "file_path": "backend/src/core/database.py", + "start_line": 704, + "end_line": 718, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Session is closed after use.", + "PRE": "SessionLocal is initialized.", + "PURPOSE": "Dependency for getting a database session." + }, + "relations": [ + { + "source_id": "get_db", + "relation_type": "DEPENDS_ON", + "target_id": "SessionLocal", + "target_ref": "[SessionLocal]" + } + ], + "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", + "body": "# #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" + }, + { + "contract_id": "get_tasks_db", + "contract_type": "Function", + "file_path": "backend/src/core/database.py", + "start_line": 721, + "end_line": 735, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Session is closed after use.", + "PRE": "TasksSessionLocal is initialized.", + "PURPOSE": "Dependency for getting a tasks database session." + }, + "relations": [ + { + "source_id": "get_tasks_db", + "relation_type": "DEPENDS_ON", + "target_id": "TasksSessionLocal", + "target_ref": "[TasksSessionLocal]" + } + ], + "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", + "body": "# #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" + }, + { + "contract_id": "get_auth_db", + "contract_type": "Function", + "file_path": "backend/src/core/database.py", + "start_line": 738, + "end_line": 753, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "DATA_CONTRACT": "None -> Output[sqlalchemy.orm.Session]", + "POST": "Session is closed after use.", + "PRE": "AuthSessionLocal is initialized.", + "PURPOSE": "Dependency for getting an authentication database session." + }, + "relations": [ + { + "source_id": "get_auth_db", + "relation_type": "DEPENDS_ON", + "target_id": "AuthSessionLocal", + "target_ref": "[AuthSessionLocal]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "DATA_CONTRACT", + "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Function" + } + }, + { + "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", + "body": "# #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" + }, + { + "contract_id": "_ensure_translation_jobs_columns", + "contract_type": "Function", + "file_path": "backend/src/core/database.py.bak", + "start_line": 470, + "end_line": 527, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": "3", + "POST": "Missing additive columns (environment_id, target_database_id) across translation_jobs are created without removing existing data.", + "PRE": "bind_engine points to the application database where translation_jobs table is stored.", + "PURPOSE": "Apply additive schema upgrades for translation_jobs table.", + "SIDE_EFFECT": "Executes ALTER TABLE statements against translation_jobs table." + }, + "relations": [ + { + "source_id": "_ensure_translation_jobs_columns", + "relation_type": "[DEPENDS_ON]", + "target_id": "engine", + "target_ref": "[engine]" } ], "schema_warnings": [ @@ -35012,7 +32391,7 @@ { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", + "message": "Predicate [DEPENDS_ON] is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -35024,70 +32403,34 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" + "predicate": "[DEPENDS_ON]" } } ], "anchor_syntax": "def", - "body": "# [DEF:init_db:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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]\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_records_columns(engine)\n\n\n# [/DEF:init_db:Function]\n" + "body": "# [DEF:_ensure_translation_jobs_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Apply additive schema upgrades for translation_jobs table.\n# @PRE: bind_engine points to the application database where translation_jobs table is stored.\n# @POST: Missing additive columns (environment_id, target_database_id) across translation_jobs are created without removing existing data.\n# @SIDE_EFFECT: Executes ALTER TABLE statements against translation_jobs table.\n# @RELATION: [DEPENDS_ON] ->[engine]\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 raise\n\n# [/DEF:_ensure_translation_jobs_columns:Function]\n" }, { - "contract_id": "get_db", + "contract_id": "_ensure_translation_records_columns", "contract_type": "Function", - "file_path": "backend/src/core/database.py", - "start_line": 671, - "end_line": 687, + "file_path": "backend/src/core/database.py.bak", + "start_line": 529, + "end_line": 565, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": "3", - "POST": "Session is closed after use.", - "PRE": "SessionLocal is initialized.", - "PURPOSE": "Dependency for getting a database session.", - "RETURN": "Generator[Session, None, None]" + "POST": "source_data column exists on both tables.", + "PRE": "bind_engine points to the application database.", + "PURPOSE": "Add source_data JSON column to translation_records and translation_preview_records.", + "SIDE_EFFECT": "Executes ALTER TABLE statements." }, "relations": [ { - "source_id": "get_db", + "source_id": "_ensure_translation_records_columns", "relation_type": "[DEPENDS_ON]", - "target_id": "SessionLocal", - "target_ref": "[SessionLocal]" + "target_id": "engine", + "target_ref": "[engine]" } ], "schema_warnings": [ @@ -35110,10 +32453,13 @@ } }, { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null + "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": "invalid_relation_predicate", @@ -35135,181 +32481,24 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:get_db:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for getting a database session.\n# @PRE: SessionLocal is initialized.\n# @POST: Session is closed after use.\n# @RETURN: Generator[Session, None, None]\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# [/DEF:get_db:Function]\n" - }, - { - "contract_id": "get_tasks_db", - "contract_type": "Function", - "file_path": "backend/src/core/database.py", - "start_line": 690, - "end_line": 706, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Session is closed after use.", - "PRE": "TasksSessionLocal is initialized.", - "PURPOSE": "Dependency for getting a tasks database session.", - "RETURN": "Generator[Session, None, None]" - }, - "relations": [ - { - "source_id": "get_tasks_db", - "relation_type": "[DEPENDS_ON]", - "target_id": "TasksSessionLocal", - "target_ref": "[TasksSessionLocal]" - } - ], - "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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:get_tasks_db:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for getting a tasks database session.\n# @PRE: TasksSessionLocal is initialized.\n# @POST: Session is closed after use.\n# @RETURN: Generator[Session, None, None]\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# [/DEF:get_tasks_db:Function]\n" - }, - { - "contract_id": "get_auth_db", - "contract_type": "Function", - "file_path": "backend/src/core/database.py", - "start_line": 709, - "end_line": 726, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "None -> Output[sqlalchemy.orm.Session]", - "POST": "Session is closed after use.", - "PRE": "AuthSessionLocal is initialized.", - "PURPOSE": "Dependency for getting an authentication database session.", - "RETURN": "Generator[Session, None, None]" - }, - "relations": [ - { - "source_id": "get_auth_db", - "relation_type": "[DEPENDS_ON]", - "target_id": "AuthSessionLocal", - "target_ref": "[AuthSessionLocal]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:get_auth_db:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# @RETURN: Generator[Session, None, None]\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# [/DEF:get_auth_db:Function]\n" + "body": "# [DEF:_ensure_translation_records_columns:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Add source_data JSON column to translation_records and translation_preview_records.\n# @PRE: bind_engine points to the application database.\n# @POST: source_data column exists on both tables.\n# @SIDE_EFFECT: Executes ALTER TABLE statements.\n# @RELATION: [DEPENDS_ON] ->[engine]\ndef _ensure_translation_records_columns(bind_engine):\n with belief_scope(\"_ensure_translation_records_columns\"):\n migrations = [\n (\"translation_records\", \"source_data\",\n \"ALTER TABLE translation_records ADD COLUMN source_data JSON\"),\n (\"translation_preview_records\", \"source_data\",\n \"ALTER TABLE translation_preview_records ADD COLUMN source_data JSON\"),\n ]\n for table_name, column_name, alter_sql in migrations:\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n continue\n existing_columns = {\n str(c.get(\"name\") or \"\").strip()\n for c in inspector.get_columns(table_name)\n }\n if column_name not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(text(alter_sql))\n logger.reflect(\n f\"Added {column_name} column to {table_name}\",\n )\n except Exception as migration_error:\n logger.explore(\n f\"Failed to add {column_name} to {table_name}\",\n extra={\"error\": str(migration_error)},\n )\n raise\n# [/DEF:_ensure_translation_records_columns:Function]\n" }, { "contract_id": "EncryptionKeyModule", "contract_type": "Module", "file_path": "backend/src/core/encryption_key.py", "start_line": 1, - "end_line": 62, + "end_line": 60, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[env_file_path] -> Output[encryption_key]", "INVARIANT": "Runtime key resolution never falls back to an ephemeral secret.", "LAYER": "Infra", "POST": "A valid Fernet key is available to runtime services via ENCRYPTION_KEY.", "PRE": "Runtime environment can read process variables and target .env path is writable when key generation is required.", "PURPOSE": "Resolve and persist the Fernet encryption key required by runtime services.", - "SEMANTICS": [ - "encryption", - "key", - "bootstrap", - "environment", - "startup" - ], "SIDE_EFFECT": "May append ENCRYPTION_KEY entry into backend .env file and set process environment variable." }, "relations": [ @@ -35340,15 +32529,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:EncryptionKeyModule:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: encryption, key, bootstrap, environment, startup\n# @PURPOSE: Resolve and persist the Fernet encryption key required by runtime services.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Runtime key resolution never falls back to an ephemeral secret.\n# @PRE: Runtime environment can read process variables and target .env path is writable when key generation is required.\n# @POST: A valid Fernet key is available to runtime services via ENCRYPTION_KEY.\n# @SIDE_EFFECT: May append ENCRYPTION_KEY entry into backend .env file and set process environment variable.\n# @DATA_CONTRACT: Input[env_file_path] -> Output[encryption_key]\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\nfrom cryptography.fernet import Fernet\n\nfrom .logger import logger, belief_scope\n\nDEFAULT_ENV_FILE_PATH = Path(__file__).resolve().parents[2] / \".env\"\n\n\n# [DEF:ensure_encryption_key:Function]\n# @PURPOSE: Ensure backend runtime has a persistent valid Fernet key.\n# @PRE: env_file_path points to a writable backend .env file or ENCRYPTION_KEY exists in process environment.\n# @POST: Returns a valid Fernet key and guarantees it is present in process environment.\n# @SIDE_EFFECT: May create or append backend/.env when key is missing.\ndef ensure_encryption_key(env_file_path: Path = DEFAULT_ENV_FILE_PATH) -> str:\n with belief_scope(\"ensure_encryption_key\", f\"env_file_path={env_file_path}\"):\n existing_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if existing_key:\n Fernet(existing_key.encode())\n logger.reason(\"Using ENCRYPTION_KEY from process environment.\")\n return existing_key\n\n if env_file_path.exists():\n for raw_line in env_file_path.read_text(encoding=\"utf-8\").splitlines():\n if raw_line.startswith(\"ENCRYPTION_KEY=\"):\n persisted_key = raw_line.partition(\"=\")[2].strip()\n if persisted_key:\n Fernet(persisted_key.encode())\n os.environ[\"ENCRYPTION_KEY\"] = persisted_key\n logger.reason(f\"Loaded ENCRYPTION_KEY from {env_file_path}.\")\n return persisted_key\n\n generated_key = Fernet.generate_key().decode()\n with env_file_path.open(\"a\", encoding=\"utf-8\") as env_file:\n if env_file.tell() > 0:\n env_file.write(\"\\n\")\n env_file.write(f\"ENCRYPTION_KEY={generated_key}\\n\")\n\n os.environ[\"ENCRYPTION_KEY\"] = generated_key\n logger.reason(f\"Generated ENCRYPTION_KEY and persisted it to {env_file_path}.\")\n logger.reflect(\"Encryption key is available for runtime services.\")\n return generated_key\n\n\n# [/DEF:ensure_encryption_key:Function]\n\n# [/DEF:EncryptionKeyModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region EncryptionKeyModule [C:5] [TYPE Module] [SEMANTICS encryption, fernet, key, env, secret]\n# @BRIEF Resolve and persist the Fernet encryption key required by runtime services.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Runtime key resolution never falls back to an ephemeral secret.\n# @PRE: Runtime environment can read process variables and target .env path is writable when key generation is required.\n# @POST: A valid Fernet key is available to runtime services via ENCRYPTION_KEY.\n# @SIDE_EFFECT: May append ENCRYPTION_KEY entry into backend .env file and set process environment variable.\n# @DATA_CONTRACT: Input[env_file_path] -> Output[encryption_key]\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\nfrom cryptography.fernet import Fernet\n\nfrom .logger import belief_scope, logger\n\nDEFAULT_ENV_FILE_PATH = Path(__file__).resolve().parents[2] / \".env\"\n\n\n# #region ensure_encryption_key [TYPE Function]\n# @BRIEF Ensure backend runtime has a persistent valid Fernet key.\n# @PRE: env_file_path points to a writable backend .env file or ENCRYPTION_KEY exists in process environment.\n# @POST: Returns a valid Fernet key and guarantees it is present in process environment.\n# @SIDE_EFFECT: May create or append backend/.env when key is missing.\ndef ensure_encryption_key(env_file_path: Path = DEFAULT_ENV_FILE_PATH) -> str:\n with belief_scope(\"ensure_encryption_key\", f\"env_file_path={env_file_path}\"):\n existing_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if existing_key:\n Fernet(existing_key.encode())\n logger.reason(\"Using ENCRYPTION_KEY from process environment.\")\n return existing_key\n\n if env_file_path.exists():\n for raw_line in env_file_path.read_text(encoding=\"utf-8\").splitlines():\n if raw_line.startswith(\"ENCRYPTION_KEY=\"):\n persisted_key = raw_line.partition(\"=\")[2].strip()\n if persisted_key:\n Fernet(persisted_key.encode())\n os.environ[\"ENCRYPTION_KEY\"] = persisted_key\n logger.reason(f\"Loaded ENCRYPTION_KEY from {env_file_path}.\")\n return persisted_key\n\n generated_key = Fernet.generate_key().decode()\n with env_file_path.open(\"a\", encoding=\"utf-8\") as env_file:\n if env_file.tell() > 0:\n env_file.write(\"\\n\")\n env_file.write(f\"ENCRYPTION_KEY={generated_key}\\n\")\n\n os.environ[\"ENCRYPTION_KEY\"] = generated_key\n logger.reason(f\"Generated ENCRYPTION_KEY and persisted it to {env_file_path}.\")\n logger.reflect(\"Encryption key is available for runtime services.\")\n return generated_key\n\n\n# #endregion ensure_encryption_key\n\n# #endregion EncryptionKeyModule\n" }, { "contract_id": "ensure_encryption_key", "contract_type": "Function", "file_path": "backend/src/core/encryption_key.py", - "start_line": 25, - "end_line": 60, + "start_line": 23, + "end_line": 58, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -35396,15 +32585,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ensure_encryption_key:Function]\n# @PURPOSE: Ensure backend runtime has a persistent valid Fernet key.\n# @PRE: env_file_path points to a writable backend .env file or ENCRYPTION_KEY exists in process environment.\n# @POST: Returns a valid Fernet key and guarantees it is present in process environment.\n# @SIDE_EFFECT: May create or append backend/.env when key is missing.\ndef ensure_encryption_key(env_file_path: Path = DEFAULT_ENV_FILE_PATH) -> str:\n with belief_scope(\"ensure_encryption_key\", f\"env_file_path={env_file_path}\"):\n existing_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if existing_key:\n Fernet(existing_key.encode())\n logger.reason(\"Using ENCRYPTION_KEY from process environment.\")\n return existing_key\n\n if env_file_path.exists():\n for raw_line in env_file_path.read_text(encoding=\"utf-8\").splitlines():\n if raw_line.startswith(\"ENCRYPTION_KEY=\"):\n persisted_key = raw_line.partition(\"=\")[2].strip()\n if persisted_key:\n Fernet(persisted_key.encode())\n os.environ[\"ENCRYPTION_KEY\"] = persisted_key\n logger.reason(f\"Loaded ENCRYPTION_KEY from {env_file_path}.\")\n return persisted_key\n\n generated_key = Fernet.generate_key().decode()\n with env_file_path.open(\"a\", encoding=\"utf-8\") as env_file:\n if env_file.tell() > 0:\n env_file.write(\"\\n\")\n env_file.write(f\"ENCRYPTION_KEY={generated_key}\\n\")\n\n os.environ[\"ENCRYPTION_KEY\"] = generated_key\n logger.reason(f\"Generated ENCRYPTION_KEY and persisted it to {env_file_path}.\")\n logger.reflect(\"Encryption key is available for runtime services.\")\n return generated_key\n\n\n# [/DEF:ensure_encryption_key:Function]\n" + "anchor_syntax": "region", + "body": "# #region ensure_encryption_key [TYPE Function]\n# @BRIEF Ensure backend runtime has a persistent valid Fernet key.\n# @PRE: env_file_path points to a writable backend .env file or ENCRYPTION_KEY exists in process environment.\n# @POST: Returns a valid Fernet key and guarantees it is present in process environment.\n# @SIDE_EFFECT: May create or append backend/.env when key is missing.\ndef ensure_encryption_key(env_file_path: Path = DEFAULT_ENV_FILE_PATH) -> str:\n with belief_scope(\"ensure_encryption_key\", f\"env_file_path={env_file_path}\"):\n existing_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if existing_key:\n Fernet(existing_key.encode())\n logger.reason(\"Using ENCRYPTION_KEY from process environment.\")\n return existing_key\n\n if env_file_path.exists():\n for raw_line in env_file_path.read_text(encoding=\"utf-8\").splitlines():\n if raw_line.startswith(\"ENCRYPTION_KEY=\"):\n persisted_key = raw_line.partition(\"=\")[2].strip()\n if persisted_key:\n Fernet(persisted_key.encode())\n os.environ[\"ENCRYPTION_KEY\"] = persisted_key\n logger.reason(f\"Loaded ENCRYPTION_KEY from {env_file_path}.\")\n return persisted_key\n\n generated_key = Fernet.generate_key().decode()\n with env_file_path.open(\"a\", encoding=\"utf-8\") as env_file:\n if env_file.tell() > 0:\n env_file.write(\"\\n\")\n env_file.write(f\"ENCRYPTION_KEY={generated_key}\\n\")\n\n os.environ[\"ENCRYPTION_KEY\"] = generated_key\n logger.reason(f\"Generated ENCRYPTION_KEY and persisted it to {env_file_path}.\")\n logger.reflect(\"Encryption key is available for runtime services.\")\n return generated_key\n\n\n# #endregion ensure_encryption_key\n" }, { "contract_id": "LoggerModule", "contract_type": "Module", "file_path": "backend/src/core/logger.py", "start_line": 1, - "end_line": 343, + "end_line": 313, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -35460,14 +32649,14 @@ } ], "anchor_syntax": "region", - "body": "# #region LoggerModule [C:5] [TYPE Module] [SEMANTICS logging,websocket,streaming,handler,cot,json]\n# @BRIEF Application logging system with CotJsonFormatter producing molecular CoT JSON output.\n# @LAYER Core\n# @RELATION USED_BY -> [All application modules]\n# @RELATION DEPENDS_ON -> [CotLoggerModule]\n# @RELATION DEPENDS_ON -> [WebSocketLogHandler]\n# @PRE Python 3.7+ with cot_logger ContextVars available.\n# @POST All log output is single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @SIDE_EFFECT Configures global logger handlers and formatters; seeds global _enable_belief_state and _task_log_level; writes JSON to console/files.\n# @RATIONALE Replaced BeliefFormatter text-based [Entry]/[Exit]/[Action] logging with CotJsonFormatter JSON output. Old format was not machine-parseable and mixed presentation with intent. CotJsonFormatter produces structured JSON consistent with cot_logger.py MarkerLogger protocol.\n# @REJECTED Keeping both formatters side-by-side was rejected (two inconsistent output formats would confuse log consumers).\n# @DATA_CONTRACT All log records conform to molecular CoT JSON schema: {ts, level, trace_id, src, marker, intent, span_id?, payload?, error?}\n# @INVARIANT CotJsonFormatter.format() always returns valid single-line JSON string.\nimport json\nimport logging\nimport threading\nimport types\nfrom datetime import datetime, timezone\nfrom contextlib import contextmanager\nfrom logging.handlers import RotatingFileHandler\n\nfrom .cot_logger import _trace_id, _span_id\nfrom .ws_log_handler import WebSocketLogHandler, LogEntry # noqa: F401\n\n# Thread-local storage for belief state\n_belief_state = threading.local()\n\n# Global flag for belief state logging\n_enable_belief_state = True\n\n# Global task log level filter\n_task_log_level = \"INFO\"\n\n\n# #region CotJsonFormatter [C:3] [TYPE Class] [SEMANTICS logging,formatter,json,cot,protocol]\n# @BRIEF JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @RELATION IMPLEMENTS -> [CotJsonFormat]\nclass CotJsonFormatter(logging.Formatter):\n \"\"\"JSON formatter implementing the molecular CoT logging protocol.\n\n Reads structured data from the LogRecord's extra attributes (marker, intent, payload,\n error, src) set via the ``extra`` kwarg. Falls back to plain message wrapping when no\n structured extra is present.\n\n Output format (single-line JSON)::\n\n {\n \"ts\": \"2026-05-12T10:30:00.123\",\n \"level\": \"INFO\",\n \"trace_id\": \"uuid-or-no-trace\",\n \"src\": \"module.name\",\n \"marker\": \"REASON|REFLECT|EXPLORE\",\n \"intent\": \"human-readable intent\",\n \"span_id\": \"optional-span\",\n \"payload\": { ... }, # optional\n \"error\": \"...\" # optional\n }\n \"\"\"\n\n # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n def format(self, record):\n # Try to extract structured data from extra kwargs on the record\n marker = getattr(record, 'marker', None)\n intent = getattr(record, 'intent', None)\n payload = getattr(record, 'payload', None)\n error = getattr(record, 'error', None)\n src = getattr(record, 'src', None) or record.name\n\n # For records that already come as JSON strings (e.g. from cot_logger.log()),\n # detect and pass through the message directly as intent.\n if not marker:\n marker = \"REASON\" # default for plain messages\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(timezone.utc).isoformat(timespec=\"milliseconds\"),\n \"level\": record.levelname,\n \"trace_id\": _trace_id.get() or \"no-trace\",\n \"src\": src,\n \"marker\": marker,\n \"intent\": intent,\n }\n\n span_id = _span_id.get()\n if span_id:\n log_obj[\"span_id\"] = span_id\n if payload:\n log_obj[\"payload\"] = payload\n if error:\n log_obj[\"error\"] = error\n\n return json.dumps(log_obj, ensure_ascii=False, default=str)\n # #endregion CotJsonFormatter.format\n\n\n# #endregion CotJsonFormatter\n\n\n# #region belief_scope [C:4] [TYPE Function] [SEMANTICS logging,context,belief_state,cot,marker]\n# @BRIEF Context manager for structured Belief State logging with CoT markers (REASON on entry, REFLECT on success, EXPLORE on failure).\n# @RELATION CALLED_BY -> [All C3+ modules using belief_scope]\n# @RELATION BINDS_TO -> [CotJsonFormatter]\n# @PRE anchor_id must be provided.\n# @POST Thread-local belief state is updated. Entry logged as REASON marker, success coherence as REFLECT marker, failure as EXPLORE marker.\n# @SIDE_EFFECT Writes debug-level log records with structured extra data; mutates _belief_state.anchor_id.\n@contextmanager\ndef belief_scope(anchor_id: str, message: str = \"\"):\n \"\"\"Context manager for structured Belief State logging with CoT markers.\n\n Emits structured markers via the ``extra`` dict so that CotJsonFormatter\n produces proper JSON output.\n\n Usage::\n\n with belief_scope(\"MyFunction\", \"Processing request\"):\n logger.info(\"Doing work\")\n \"\"\"\n old_anchor = getattr(_belief_state, 'anchor_id', None)\n _belief_state.anchor_id = anchor_id\n\n # Log Entry (REASON marker) — only when belief state logging is enabled\n if _enable_belief_state:\n entry_msg = message or f\"Enter {anchor_id}\"\n logger.debug(entry_msg, extra={\n \"marker\": \"REASON\",\n \"intent\": entry_msg,\n \"src\": anchor_id,\n })\n\n try:\n yield\n # Coherence OK / scope exit — always logged for internal tracking\n logger.debug(\"Coherence OK\", extra={\n \"marker\": \"REFLECT\",\n \"intent\": \"Coherence OK\",\n \"src\": anchor_id,\n \"payload\": {\"contract_id\": anchor_id},\n })\n except Exception as e:\n # Coherence FAILED — always logged for error tracking\n logger.debug(f\"Coherence FAILED: {e}\", extra={\n \"marker\": \"EXPLORE\",\n \"intent\": \"Coherence FAILED\",\n \"error\": str(e),\n \"src\": anchor_id,\n \"payload\": {\"contract_id\": anchor_id},\n })\n raise\n finally:\n # Restore old anchor\n _belief_state.anchor_id = old_anchor\n\n# #endregion belief_scope\n\n\n# #region configure_logger [C:4] [TYPE Function] [SEMANTICS logging,configuration,initialization,cot]\n# @BRIEF Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.\n# @RELATION CALLS -> [CotJsonFormatter]\n# @RELATION CALLS -> [CotLoggerModule]\n# @PRE config is a valid LoggingConfig instance.\n# @POST Logger levels, handlers, formatters, belief state flag, and task log level are updated. The 'cot' logger has CotJsonFormatter with propagation disabled.\n# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories.\ndef configure_logger(config):\n global _enable_belief_state, _task_log_level\n _enable_belief_state = config.enable_belief_state\n _task_log_level = config.task_log_level.upper()\n\n # Set logger level\n level = getattr(logging, config.level.upper(), logging.INFO)\n logger.setLevel(level)\n\n # Remove existing file handlers\n handlers_to_remove = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n for h in handlers_to_remove:\n logger.removeHandler(h)\n h.close()\n\n # Add file handler if file_path is set\n if config.file_path:\n from pathlib import Path\n log_file = Path(config.file_path)\n log_file.parent.mkdir(parents=True, exist_ok=True)\n\n file_handler = RotatingFileHandler(\n config.file_path,\n maxBytes=config.max_bytes,\n backupCount=config.backup_count\n )\n file_handler.setFormatter(CotJsonFormatter())\n logger.addHandler(file_handler)\n\n # Update existing handlers' formatters to CotJsonFormatter\n for handler in logger.handlers:\n if not isinstance(handler, RotatingFileHandler):\n handler.setFormatter(CotJsonFormatter())\n\n # Also configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n # Remove all existing handlers from the cot logger\n for h in list(cot_logger.handlers):\n cot_logger.removeHandler(h)\n h.close()\n # Add a console handler with CotJsonFormatter (if file path set, add file handler too)\n cot_console = logging.StreamHandler()\n cot_console.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_console)\n if config.file_path:\n from pathlib import Path\n log_file = Path(config.file_path)\n log_file.parent.mkdir(parents=True, exist_ok=True)\n cot_file = RotatingFileHandler(\n config.file_path,\n maxBytes=config.max_bytes,\n backupCount=config.backup_count\n )\n cot_file.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_file)\n # Disable propagation so cot messages don't double-emit via root loggers\n cot_logger.propagate = False\n\n# #endregion configure_logger\n\n\n# #region get_task_log_level [C:1] [TYPE Function] [SEMANTICS logging,configuration,getter]\ndef get_task_log_level() -> str:\n \"\"\"Returns the current task log level filter.\"\"\"\n return _task_log_level\n\n# #endregion get_task_log_level\n\n\n# #region should_log_task_level [C:2] [TYPE Function] [SEMANTICS logging,filter,level]\n# @BRIEF Checks if a log level should be recorded based on the current task log level threshold.\ndef should_log_task_level(level: str) -> bool:\n \"\"\"Checks if a log level should be recorded based on task_log_level setting.\"\"\"\n level_order = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n current_level = _task_log_level.upper()\n check_level = level.upper()\n\n current_order = level_order.get(current_level, 1) # Default to INFO\n check_order = level_order.get(check_level, 1)\n\n return check_order >= current_order\n\n# #endregion should_log_task_level\n\n\n# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]\n# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.\nlogger = logging.getLogger(\"superset_tools_app\")\nlogger.setLevel(logging.INFO)\n\n# Create CotJsonFormatter\n_formatter = CotJsonFormatter()\n\n# Add console handler\nconsole_handler = logging.StreamHandler()\nconsole_handler.setFormatter(_formatter)\nlogger.addHandler(console_handler)\n\n# Add WebSocket log handler\nwebsocket_log_handler = WebSocketLogHandler()\nwebsocket_log_handler.setFormatter(_formatter)\nlogger.addHandler(websocket_log_handler)\n\n# Example usage:\n# logger.info(\"Application started\", extra={\"context_key\": \"context_value\"})\n# logger.error(\"An error occurred\", exc_info=True)\n\n\n# #region explore [C:2] [TYPE Function] [SEMANTICS log,explore,marker,warning]\n# @BRIEF Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.\ndef explore(self, msg, *args, **kwargs):\n \"\"\"Log an EXPLORE marker — searching, alternatives, violated assumptions.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'EXPLORE', 'intent': msg}\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n\n# #endregion explore\n\n\n# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,info]\n# @BRIEF Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter.\ndef reason(self, msg, *args, **kwargs):\n \"\"\"Log a REASON marker — strict deduction, core logic.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REASON', 'intent': msg}\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n\n# #endregion reason\n\n\n# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,debug]\n# @BRIEF Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reflect(self, msg, *args, **kwargs):\n \"\"\"Log a REFLECT marker — self-check, structural validation.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REFLECT', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n\n# #endregion reflect\n\n\nlogger.explore = types.MethodType(explore, logger)\nlogger.reason = types.MethodType(reason, logger)\nlogger.reflect = types.MethodType(reflect, logger)\n\n# #endregion Logger\n\n\n# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief_scope,wrapper]\n# @BRIEF Decorator that wraps a function in a belief_scope context manager.\ndef believed(anchor_id: str):\n # #region believed.decorator [C:1] [TYPE Function] [SEMANTICS decorator,inner]\n def decorator(func):\n # #region believed.decorator.wrapper [C:1] [TYPE Function] [SEMANTICS wrapper,belief_scope]\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n # #endregion believed.decorator.wrapper\n return wrapper\n # #endregion believed.decorator\n return decorator\n\n# #endregion believed\n\n\n# #endregion LoggerModule\n" + "body": "# #region LoggerModule [C:5] [TYPE Module] [SEMANTICS pydantic, logging, json, formatter, structured]\n# @BRIEF Application logging system with CotJsonFormatter producing molecular CoT JSON output.\n# @LAYER Core\n# @RELATION USED_BY -> [All application modules]\n# @RELATION DEPENDS_ON -> [CotLoggerModule]\n# @RELATION DEPENDS_ON -> [WebSocketLogHandler]\n# @PRE Python 3.7+ with cot_logger ContextVars available.\n# @POST All log output is single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @SIDE_EFFECT Configures global logger handlers and formatters; seeds global _enable_belief_state and _task_log_level; writes JSON to console/files.\n# @RATIONALE Replaced BeliefFormatter text-based [Entry]/[Exit]/[Action] logging with CotJsonFormatter JSON output. Old format was not machine-parseable and mixed presentation with intent. CotJsonFormatter produces structured JSON consistent with cot_logger.py MarkerLogger protocol.\n# @REJECTED Keeping both formatters side-by-side was rejected (two inconsistent output formats would confuse log consumers).\n# @DATA_CONTRACT All log records conform to molecular CoT JSON schema: {ts, level, trace_id, src, marker, intent, span_id?, payload?, error?}\n# @INVARIANT CotJsonFormatter.format() always returns valid single-line JSON string.\nimport json\nimport logging\nimport threading\nimport types\nfrom contextlib import contextmanager\nfrom datetime import UTC, datetime\nfrom logging.handlers import RotatingFileHandler\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\nfrom .cot_logger import _span_id, _trace_id\nfrom .cot_logger import log as cot_log\nfrom .ws_log_handler import WebSocketLogHandler\n\n# Thread-local storage for belief state\n_belief_state = threading.local()\n\n# Global flag for belief state logging\n_enable_belief_state = True\n\n# Global task log level filter\n_task_log_level = \"INFO\"\n\n\n# #region CotJsonFormatter [C:3] [TYPE Class] [SEMANTICS logging,formatter,json,cot,protocol]\n# @BRIEF JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @RELATION IMPLEMENTS -> [CotJsonFormat]\nclass CotJsonFormatter(logging.Formatter):\n \"\"\"JSON formatter implementing the molecular CoT logging protocol.\n\n Reads structured data from the LogRecord's extra attributes (marker, intent, payload,\n error, src) set via the ``extra`` kwarg. Falls back to plain message wrapping when no\n structured extra is present.\n\n Output format (single-line JSON)::\n\n {\n \"ts\": \"2026-05-12T10:30:00.123\",\n \"level\": \"INFO\",\n \"trace_id\": \"uuid-or-no-trace\",\n \"src\": \"module.name\",\n \"marker\": \"REASON|REFLECT|EXPLORE\",\n \"intent\": \"human-readable intent\",\n \"span_id\": \"optional-span\",\n \"payload\": { ... }, # optional\n \"error\": \"...\" # optional\n }\n \"\"\"\n\n # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n # @INVARIANT Output is always valid single-line JSON.\n def format(self, record):\n # Try to extract structured data from extra kwargs on the record\n marker = getattr(record, 'marker', None)\n intent = getattr(record, 'intent', None)\n payload = getattr(record, 'payload', None)\n error = getattr(record, 'error', None)\n src = getattr(record, 'src', None) or record.name\n\n # Default marker for plain messages\n if not marker:\n marker = \"REASON\"\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(UTC).isoformat(timespec=\"milliseconds\"),\n \"level\": record.levelname,\n \"trace_id\": _trace_id.get() or \"no-trace\",\n \"src\": src,\n \"marker\": marker,\n \"intent\": intent,\n }\n\n span_id = _span_id.get()\n if span_id:\n log_obj[\"span_id\"] = span_id\n if payload:\n log_obj[\"payload\"] = payload\n if error:\n log_obj[\"error\"] = error\n\n return json.dumps(log_obj, ensure_ascii=False, default=str)\n # #endregion CotJsonFormatter.format\n# #endregion CotJsonFormatter\n\n\n# #region BeliefFormatter [C:2] [TYPE Class] [SEMANTICS logging,formatter,legacy,text]\n# @BRIEF Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.\n# @RATIONALE Kept for backward compatibility; may be used by external consumers of this module.\nclass BeliefFormatter(logging.Formatter):\n \"\"\"Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.\"\"\"\n def format(self, record):\n anchor_id = getattr(_belief_state, 'anchor_id', None)\n if anchor_id:\n msg = str(record.msg)\n markers = (\"[EXPLORE]\", \"[REASON]\", \"[REFLECT]\")\n\n if msg.startswith(f\"[{anchor_id}]\"):\n pass\n elif any(msg.startswith(m) for m in markers):\n record.msg = f\"[{anchor_id}]{msg}\"\n else:\n record.msg = f\"[{anchor_id}][REASON] {msg}\"\n\n return super().format(record)\n# #endregion BeliefFormatter\n\n\n# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\n# @BRIEF A Pydantic model representing a single, structured log entry.\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str\n message: str\n context: dict[str, Any] | None = None\n# #endregion LogEntry\n\n\n# #region belief_scope [C:3] [TYPE Function] [SEMANTICS logging,context,belief_state,cot]\n# @BRIEF Context manager for Molecular CoT structured logging. Uses cot_logger.log() for JSON output.\n# @PRE anchor_id must be provided.\n# @POST Thread-local belief state is updated; REASON/REFLECT/EXPLORE markers emitted via structured JSON logger.\n@contextmanager\ndef belief_scope(anchor_id: str, message: str = \"\"):\n old_anchor = getattr(_belief_state, 'anchor_id', None)\n _belief_state.anchor_id = anchor_id\n\n try:\n # Log entry as REASON (gated by _enable_belief_state)\n if _enable_belief_state:\n intent_msg = message or f\"Entering {anchor_id}\"\n cot_log(anchor_id, \"REASON\", intent_msg, level=\"DEBUG\")\n yield\n # Log success as REFLECT (always logged)\n cot_log(anchor_id, \"REFLECT\", \"Coherence OK\", level=\"DEBUG\")\n except Exception as e:\n # Log exception as EXPLORE (always logged)\n cot_log(anchor_id, \"EXPLORE\", \"Assumption violated\", error=str(e), level=\"WARNING\")\n raise\n finally:\n _belief_state.anchor_id = old_anchor\n# #endregion belief_scope\n\n\n# #region configure_logger [C:4] [TYPE Function] [SEMANTICS logging,configuration,initialization,cot]\n# @BRIEF Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.\n# @RELATION CALLS -> [CotJsonFormatter]\n# @RELATION CALLS -> [CotLoggerModule]\n# @PRE config is a valid LoggingConfig instance.\n# @POST Logger levels, handlers, formatters, belief state flag, and task log level are updated.\n# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories.\ndef configure_logger(config):\n global _enable_belief_state, _task_log_level\n _enable_belief_state = config.enable_belief_state\n _task_log_level = config.task_log_level.upper()\n\n # Set logger level\n level = getattr(logging, config.level.upper(), logging.INFO)\n logger.setLevel(level)\n\n # Remove existing file handlers\n handlers_to_remove = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n for h in handlers_to_remove:\n logger.removeHandler(h)\n h.close()\n\n # Add file handler if file_path is set\n if config.file_path:\n from pathlib import Path\n log_file = Path(config.file_path)\n log_file.parent.mkdir(parents=True, exist_ok=True)\n\n file_handler = RotatingFileHandler(\n config.file_path,\n maxBytes=config.max_bytes,\n backupCount=config.backup_count\n )\n file_handler.setFormatter(CotJsonFormatter())\n logger.addHandler(file_handler)\n\n # Update existing handlers' formatters to CotJsonFormatter\n for handler in logger.handlers:\n if not isinstance(handler, RotatingFileHandler):\n handler.setFormatter(CotJsonFormatter())\n\n # Configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n if not cot_logger.handlers:\n cot_handler = logging.StreamHandler()\n cot_handler.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_handler)\n# #endregion configure_logger\n\n\n# #region get_task_log_level [C:1] [TYPE Function] [SEMANTICS logging,configuration,getter]\n# @BRIEF Returns the current task log level filter.\ndef get_task_log_level() -> str:\n return _task_log_level\n# #endregion get_task_log_level\n\n\n# #region should_log_task_level [C:1] [TYPE Function] [SEMANTICS logging,filter,level]\n# @BRIEF Checks if a log level should be recorded based on task_log_level setting.\ndef should_log_task_level(level: str) -> bool:\n level_order = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n current_level = _task_log_level.upper()\n check_level = level.upper()\n\n current_order = level_order.get(current_level, 1)\n check_order = level_order.get(check_level, 1)\n\n return check_order >= current_order\n# #endregion should_log_task_level\n\n\n# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]\n# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.\nlogger = logging.getLogger(\"superset_tools_app\")\n\n# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief,scope]\n# @BRIEF A decorator that wraps a function in a belief scope.\ndef believed(anchor_id: str):\n def decorator(func):\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n return wrapper\n return decorator\n# #endregion believed\nlogger.setLevel(logging.INFO)\n\n# Create CotJsonFormatter\n_formatter = CotJsonFormatter()\n\n# Add console handler\nconsole_handler = logging.StreamHandler()\nconsole_handler.setFormatter(_formatter)\nlogger.addHandler(console_handler)\n\n# Add WebSocket log handler\nwebsocket_log_handler = WebSocketLogHandler()\nwebsocket_log_handler.setFormatter(_formatter)\nlogger.addHandler(websocket_log_handler)\n\n# Example usage:\n# logger.info(\"Application started\", extra={\"context_key\": \"context_value\"})\n# logger.error(\"An error occurred\", exc_info=True)\n\n\n# #region explore [C:2] [TYPE Function] [SEMANTICS log,explore,marker,warning]\n# @BRIEF Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.\ndef explore(self, msg, *args, **kwargs):\n \"\"\"Log an EXPLORE marker — searching, alternatives, violated assumptions.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'EXPLORE', 'intent': msg}\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n# #endregion explore\n\n\n# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,debug]\n# @BRIEF Logs a REASON marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reason(self, msg, *args, **kwargs):\n \"\"\"Log a REASON marker — strict deduction, core logic.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REASON', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n\n\n# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,debug]\n# @BRIEF Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reflect(self, msg, *args, **kwargs):\n \"\"\"Log a REFLECT marker — self-check, structural validation.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REFLECT', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n\nlogger.explore = types.MethodType(explore, logger)\nlogger.reason = types.MethodType(reason, logger)\nlogger.reflect = types.MethodType(reflect, logger)\n\n# #endregion Logger\n# #endregion LoggerModule\n" }, { "contract_id": "CotJsonFormatter", "contract_type": "Class", "file_path": "backend/src/core/logger.py", - "start_line": 35, - "end_line": 98, + "start_line": 39, + "end_line": 100, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -35484,36 +32673,149 @@ ], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region CotJsonFormatter [C:3] [TYPE Class] [SEMANTICS logging,formatter,json,cot,protocol]\n# @BRIEF JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @RELATION IMPLEMENTS -> [CotJsonFormat]\nclass CotJsonFormatter(logging.Formatter):\n \"\"\"JSON formatter implementing the molecular CoT logging protocol.\n\n Reads structured data from the LogRecord's extra attributes (marker, intent, payload,\n error, src) set via the ``extra`` kwarg. Falls back to plain message wrapping when no\n structured extra is present.\n\n Output format (single-line JSON)::\n\n {\n \"ts\": \"2026-05-12T10:30:00.123\",\n \"level\": \"INFO\",\n \"trace_id\": \"uuid-or-no-trace\",\n \"src\": \"module.name\",\n \"marker\": \"REASON|REFLECT|EXPLORE\",\n \"intent\": \"human-readable intent\",\n \"span_id\": \"optional-span\",\n \"payload\": { ... }, # optional\n \"error\": \"...\" # optional\n }\n \"\"\"\n\n # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n def format(self, record):\n # Try to extract structured data from extra kwargs on the record\n marker = getattr(record, 'marker', None)\n intent = getattr(record, 'intent', None)\n payload = getattr(record, 'payload', None)\n error = getattr(record, 'error', None)\n src = getattr(record, 'src', None) or record.name\n\n # For records that already come as JSON strings (e.g. from cot_logger.log()),\n # detect and pass through the message directly as intent.\n if not marker:\n marker = \"REASON\" # default for plain messages\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(timezone.utc).isoformat(timespec=\"milliseconds\"),\n \"level\": record.levelname,\n \"trace_id\": _trace_id.get() or \"no-trace\",\n \"src\": src,\n \"marker\": marker,\n \"intent\": intent,\n }\n\n span_id = _span_id.get()\n if span_id:\n log_obj[\"span_id\"] = span_id\n if payload:\n log_obj[\"payload\"] = payload\n if error:\n log_obj[\"error\"] = error\n\n return json.dumps(log_obj, ensure_ascii=False, default=str)\n # #endregion CotJsonFormatter.format\n\n\n# #endregion CotJsonFormatter\n" + "body": "# #region CotJsonFormatter [C:3] [TYPE Class] [SEMANTICS logging,formatter,json,cot,protocol]\n# @BRIEF JSON formatter implementing the molecular CoT logging protocol. Outputs single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @RELATION IMPLEMENTS -> [CotJsonFormat]\nclass CotJsonFormatter(logging.Formatter):\n \"\"\"JSON formatter implementing the molecular CoT logging protocol.\n\n Reads structured data from the LogRecord's extra attributes (marker, intent, payload,\n error, src) set via the ``extra`` kwarg. Falls back to plain message wrapping when no\n structured extra is present.\n\n Output format (single-line JSON)::\n\n {\n \"ts\": \"2026-05-12T10:30:00.123\",\n \"level\": \"INFO\",\n \"trace_id\": \"uuid-or-no-trace\",\n \"src\": \"module.name\",\n \"marker\": \"REASON|REFLECT|EXPLORE\",\n \"intent\": \"human-readable intent\",\n \"span_id\": \"optional-span\",\n \"payload\": { ... }, # optional\n \"error\": \"...\" # optional\n }\n \"\"\"\n\n # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n # @INVARIANT Output is always valid single-line JSON.\n def format(self, record):\n # Try to extract structured data from extra kwargs on the record\n marker = getattr(record, 'marker', None)\n intent = getattr(record, 'intent', None)\n payload = getattr(record, 'payload', None)\n error = getattr(record, 'error', None)\n src = getattr(record, 'src', None) or record.name\n\n # Default marker for plain messages\n if not marker:\n marker = \"REASON\"\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(UTC).isoformat(timespec=\"milliseconds\"),\n \"level\": record.levelname,\n \"trace_id\": _trace_id.get() or \"no-trace\",\n \"src\": src,\n \"marker\": marker,\n \"intent\": intent,\n }\n\n span_id = _span_id.get()\n if span_id:\n log_obj[\"span_id\"] = span_id\n if payload:\n log_obj[\"payload\"] = payload\n if error:\n log_obj[\"error\"] = error\n\n return json.dumps(log_obj, ensure_ascii=False, default=str)\n # #endregion CotJsonFormatter.format\n# #endregion CotJsonFormatter\n" }, { "contract_id": "CotJsonFormatter.format", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 60, - "end_line": 95, + "start_line": 64, + "end_line": 99, "tier": "TIER_1", "complexity": 2, "metadata": { "COMPLEXITY": 2, + "INVARIANT": "Output is always valid single-line JSON.", "PURPOSE": "Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Function" + } + } + ], "anchor_syntax": "region", - "body": " # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n def format(self, record):\n # Try to extract structured data from extra kwargs on the record\n marker = getattr(record, 'marker', None)\n intent = getattr(record, 'intent', None)\n payload = getattr(record, 'payload', None)\n error = getattr(record, 'error', None)\n src = getattr(record, 'src', None) or record.name\n\n # For records that already come as JSON strings (e.g. from cot_logger.log()),\n # detect and pass through the message directly as intent.\n if not marker:\n marker = \"REASON\" # default for plain messages\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(timezone.utc).isoformat(timespec=\"milliseconds\"),\n \"level\": record.levelname,\n \"trace_id\": _trace_id.get() or \"no-trace\",\n \"src\": src,\n \"marker\": marker,\n \"intent\": intent,\n }\n\n span_id = _span_id.get()\n if span_id:\n log_obj[\"span_id\"] = span_id\n if payload:\n log_obj[\"payload\"] = payload\n if error:\n log_obj[\"error\"] = error\n\n return json.dumps(log_obj, ensure_ascii=False, default=str)\n # #endregion CotJsonFormatter.format\n" + "body": " # #region CotJsonFormatter.format [C:2] [TYPE Function] [SEMANTICS format,json,record]\n # @BRIEF Format a LogRecord into a single-line CoT JSON string with molecular CoT protocol fields.\n # @INVARIANT Output is always valid single-line JSON.\n def format(self, record):\n # Try to extract structured data from extra kwargs on the record\n marker = getattr(record, 'marker', None)\n intent = getattr(record, 'intent', None)\n payload = getattr(record, 'payload', None)\n error = getattr(record, 'error', None)\n src = getattr(record, 'src', None) or record.name\n\n # Default marker for plain messages\n if not marker:\n marker = \"REASON\"\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(UTC).isoformat(timespec=\"milliseconds\"),\n \"level\": record.levelname,\n \"trace_id\": _trace_id.get() or \"no-trace\",\n \"src\": src,\n \"marker\": marker,\n \"intent\": intent,\n }\n\n span_id = _span_id.get()\n if span_id:\n log_obj[\"span_id\"] = span_id\n if payload:\n log_obj[\"payload\"] = payload\n if error:\n log_obj[\"error\"] = error\n\n return json.dumps(log_obj, ensure_ascii=False, default=str)\n # #endregion CotJsonFormatter.format\n" + }, + { + "contract_id": "BeliefFormatter", + "contract_type": "Class", + "file_path": "backend/src/core/logger.py", + "start_line": 103, + "end_line": 122, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.", + "RATIONALE": "Kept for backward compatibility; may be used by external consumers of this module." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Class' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "body": "# #region BeliefFormatter [C:2] [TYPE Class] [SEMANTICS logging,formatter,legacy,text]\n# @BRIEF Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.\n# @RATIONALE Kept for backward compatibility; may be used by external consumers of this module.\nclass BeliefFormatter(logging.Formatter):\n \"\"\"Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.\"\"\"\n def format(self, record):\n anchor_id = getattr(_belief_state, 'anchor_id', None)\n if anchor_id:\n msg = str(record.msg)\n markers = (\"[EXPLORE]\", \"[REASON]\", \"[REFLECT]\")\n\n if msg.startswith(f\"[{anchor_id}]\"):\n pass\n elif any(msg.startswith(m) for m in markers):\n record.msg = f\"[{anchor_id}]{msg}\"\n else:\n record.msg = f\"[{anchor_id}][REASON] {msg}\"\n\n return super().format(record)\n# #endregion BeliefFormatter\n" + }, + { + "contract_id": "LogEntry", + "contract_type": "Class", + "file_path": "backend/src/core/logger.py", + "start_line": 125, + "end_line": 132, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "A Pydantic model representing a single, structured log entry." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "body": "# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\n# @BRIEF A Pydantic model representing a single, structured log entry.\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str\n message: str\n context: dict[str, Any] | None = None\n# #endregion LogEntry\n" + }, + { + "contract_id": "belief_scope", + "contract_type": "Function", + "file_path": "backend/src/core/logger.py", + "start_line": 135, + "end_line": 158, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Thread-local belief state is updated; REASON/REFLECT/EXPLORE markers emitted via structured JSON logger.", + "PRE": "anchor_id must be provided.", + "PURPOSE": "Context manager for Molecular CoT structured logging. Uses cot_logger.log() for JSON output." + }, + "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", + "body": "# #region belief_scope [C:3] [TYPE Function] [SEMANTICS logging,context,belief_state,cot]\n# @BRIEF Context manager for Molecular CoT structured logging. Uses cot_logger.log() for JSON output.\n# @PRE anchor_id must be provided.\n# @POST Thread-local belief state is updated; REASON/REFLECT/EXPLORE markers emitted via structured JSON logger.\n@contextmanager\ndef belief_scope(anchor_id: str, message: str = \"\"):\n old_anchor = getattr(_belief_state, 'anchor_id', None)\n _belief_state.anchor_id = anchor_id\n\n try:\n # Log entry as REASON (gated by _enable_belief_state)\n if _enable_belief_state:\n intent_msg = message or f\"Entering {anchor_id}\"\n cot_log(anchor_id, \"REASON\", intent_msg, level=\"DEBUG\")\n yield\n # Log success as REFLECT (always logged)\n cot_log(anchor_id, \"REFLECT\", \"Coherence OK\", level=\"DEBUG\")\n except Exception as e:\n # Log exception as EXPLORE (always logged)\n cot_log(anchor_id, \"EXPLORE\", \"Assumption violated\", error=str(e), level=\"WARNING\")\n raise\n finally:\n _belief_state.anchor_id = old_anchor\n# #endregion belief_scope\n" }, { "contract_id": "configure_logger", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 158, - "end_line": 224, + "start_line": 161, + "end_line": 209, "tier": "TIER_2", "complexity": 4, "metadata": { "COMPLEXITY": 4, - "POST": "Logger levels, handlers, formatters, belief state flag, and task log level are updated. The 'cot' logger has CotJsonFormatter with propagation disabled.", + "POST": "Logger levels, handlers, formatters, belief state flag, and task log level are updated.", "PRE": "config is a valid LoggingConfig instance.", "PURPOSE": "Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.", "SIDE_EFFECT": "Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories." @@ -35534,47 +32836,68 @@ ], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region configure_logger [C:4] [TYPE Function] [SEMANTICS logging,configuration,initialization,cot]\n# @BRIEF Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.\n# @RELATION CALLS -> [CotJsonFormatter]\n# @RELATION CALLS -> [CotLoggerModule]\n# @PRE config is a valid LoggingConfig instance.\n# @POST Logger levels, handlers, formatters, belief state flag, and task log level are updated. The 'cot' logger has CotJsonFormatter with propagation disabled.\n# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories.\ndef configure_logger(config):\n global _enable_belief_state, _task_log_level\n _enable_belief_state = config.enable_belief_state\n _task_log_level = config.task_log_level.upper()\n\n # Set logger level\n level = getattr(logging, config.level.upper(), logging.INFO)\n logger.setLevel(level)\n\n # Remove existing file handlers\n handlers_to_remove = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n for h in handlers_to_remove:\n logger.removeHandler(h)\n h.close()\n\n # Add file handler if file_path is set\n if config.file_path:\n from pathlib import Path\n log_file = Path(config.file_path)\n log_file.parent.mkdir(parents=True, exist_ok=True)\n\n file_handler = RotatingFileHandler(\n config.file_path,\n maxBytes=config.max_bytes,\n backupCount=config.backup_count\n )\n file_handler.setFormatter(CotJsonFormatter())\n logger.addHandler(file_handler)\n\n # Update existing handlers' formatters to CotJsonFormatter\n for handler in logger.handlers:\n if not isinstance(handler, RotatingFileHandler):\n handler.setFormatter(CotJsonFormatter())\n\n # Also configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n # Remove all existing handlers from the cot logger\n for h in list(cot_logger.handlers):\n cot_logger.removeHandler(h)\n h.close()\n # Add a console handler with CotJsonFormatter (if file path set, add file handler too)\n cot_console = logging.StreamHandler()\n cot_console.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_console)\n if config.file_path:\n from pathlib import Path\n log_file = Path(config.file_path)\n log_file.parent.mkdir(parents=True, exist_ok=True)\n cot_file = RotatingFileHandler(\n config.file_path,\n maxBytes=config.max_bytes,\n backupCount=config.backup_count\n )\n cot_file.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_file)\n # Disable propagation so cot messages don't double-emit via root loggers\n cot_logger.propagate = False\n\n# #endregion configure_logger\n" + "body": "# #region configure_logger [C:4] [TYPE Function] [SEMANTICS logging,configuration,initialization,cot]\n# @BRIEF Configures the main logger and the CoT logger with CotJsonFormatter, file handlers, and global flags.\n# @RELATION CALLS -> [CotJsonFormatter]\n# @RELATION CALLS -> [CotLoggerModule]\n# @PRE config is a valid LoggingConfig instance.\n# @POST Logger levels, handlers, formatters, belief state flag, and task log level are updated.\n# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories.\ndef configure_logger(config):\n global _enable_belief_state, _task_log_level\n _enable_belief_state = config.enable_belief_state\n _task_log_level = config.task_log_level.upper()\n\n # Set logger level\n level = getattr(logging, config.level.upper(), logging.INFO)\n logger.setLevel(level)\n\n # Remove existing file handlers\n handlers_to_remove = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n for h in handlers_to_remove:\n logger.removeHandler(h)\n h.close()\n\n # Add file handler if file_path is set\n if config.file_path:\n from pathlib import Path\n log_file = Path(config.file_path)\n log_file.parent.mkdir(parents=True, exist_ok=True)\n\n file_handler = RotatingFileHandler(\n config.file_path,\n maxBytes=config.max_bytes,\n backupCount=config.backup_count\n )\n file_handler.setFormatter(CotJsonFormatter())\n logger.addHandler(file_handler)\n\n # Update existing handlers' formatters to CotJsonFormatter\n for handler in logger.handlers:\n if not isinstance(handler, RotatingFileHandler):\n handler.setFormatter(CotJsonFormatter())\n\n # Configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n if not cot_logger.handlers:\n cot_handler = logging.StreamHandler()\n cot_handler.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_handler)\n# #endregion configure_logger\n" }, { "contract_id": "get_task_log_level", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 227, - "end_line": 232, + "start_line": 212, + "end_line": 216, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "COMPLEXITY": 1, + "PURPOSE": "Returns the current task log level filter." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "region", - "body": "# #region get_task_log_level [C:1] [TYPE Function] [SEMANTICS logging,configuration,getter]\ndef get_task_log_level() -> str:\n \"\"\"Returns the current task log level filter.\"\"\"\n return _task_log_level\n\n# #endregion get_task_log_level\n" + "body": "# #region get_task_log_level [C:1] [TYPE Function] [SEMANTICS logging,configuration,getter]\n# @BRIEF Returns the current task log level filter.\ndef get_task_log_level() -> str:\n return _task_log_level\n# #endregion get_task_log_level\n" }, { "contract_id": "should_log_task_level", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 235, - "end_line": 248, + "start_line": 219, + "end_line": 230, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Checks if a log level should be recorded based on the current task log level threshold." + "COMPLEXITY": 1, + "PURPOSE": "Checks if a log level should be recorded based on task_log_level setting." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "region", - "body": "# #region should_log_task_level [C:2] [TYPE Function] [SEMANTICS logging,filter,level]\n# @BRIEF Checks if a log level should be recorded based on the current task log level threshold.\ndef should_log_task_level(level: str) -> bool:\n \"\"\"Checks if a log level should be recorded based on task_log_level setting.\"\"\"\n level_order = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n current_level = _task_log_level.upper()\n check_level = level.upper()\n\n current_order = level_order.get(current_level, 1) # Default to INFO\n check_order = level_order.get(check_level, 1)\n\n return check_order >= current_order\n\n# #endregion should_log_task_level\n" + "body": "# #region should_log_task_level [C:1] [TYPE Function] [SEMANTICS logging,filter,level]\n# @BRIEF Checks if a log level should be recorded based on task_log_level setting.\ndef should_log_task_level(level: str) -> bool:\n level_order = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n current_level = _task_log_level.upper()\n check_level = level.upper()\n\n current_order = level_order.get(current_level, 1)\n check_order = level_order.get(check_level, 1)\n\n return check_order >= current_order\n# #endregion should_log_task_level\n" }, { "contract_id": "Logger", "contract_type": "Global", "file_path": "backend/src/core/logger.py", - "start_line": 251, - "end_line": 323, + "start_line": 233, + "end_line": 312, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -35638,14 +32961,31 @@ } ], "anchor_syntax": "region", - "body": "# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]\n# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.\nlogger = logging.getLogger(\"superset_tools_app\")\nlogger.setLevel(logging.INFO)\n\n# Create CotJsonFormatter\n_formatter = CotJsonFormatter()\n\n# Add console handler\nconsole_handler = logging.StreamHandler()\nconsole_handler.setFormatter(_formatter)\nlogger.addHandler(console_handler)\n\n# Add WebSocket log handler\nwebsocket_log_handler = WebSocketLogHandler()\nwebsocket_log_handler.setFormatter(_formatter)\nlogger.addHandler(websocket_log_handler)\n\n# Example usage:\n# logger.info(\"Application started\", extra={\"context_key\": \"context_value\"})\n# logger.error(\"An error occurred\", exc_info=True)\n\n\n# #region explore [C:2] [TYPE Function] [SEMANTICS log,explore,marker,warning]\n# @BRIEF Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.\ndef explore(self, msg, *args, **kwargs):\n \"\"\"Log an EXPLORE marker — searching, alternatives, violated assumptions.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'EXPLORE', 'intent': msg}\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n\n# #endregion explore\n\n\n# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,info]\n# @BRIEF Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter.\ndef reason(self, msg, *args, **kwargs):\n \"\"\"Log a REASON marker — strict deduction, core logic.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REASON', 'intent': msg}\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n\n# #endregion reason\n\n\n# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,debug]\n# @BRIEF Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reflect(self, msg, *args, **kwargs):\n \"\"\"Log a REFLECT marker — self-check, structural validation.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REFLECT', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n\n# #endregion reflect\n\n\nlogger.explore = types.MethodType(explore, logger)\nlogger.reason = types.MethodType(reason, logger)\nlogger.reflect = types.MethodType(reflect, logger)\n\n# #endregion Logger\n" + "body": "# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]\n# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.\nlogger = logging.getLogger(\"superset_tools_app\")\n\n# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief,scope]\n# @BRIEF A decorator that wraps a function in a belief scope.\ndef believed(anchor_id: str):\n def decorator(func):\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n return wrapper\n return decorator\n# #endregion believed\nlogger.setLevel(logging.INFO)\n\n# Create CotJsonFormatter\n_formatter = CotJsonFormatter()\n\n# Add console handler\nconsole_handler = logging.StreamHandler()\nconsole_handler.setFormatter(_formatter)\nlogger.addHandler(console_handler)\n\n# Add WebSocket log handler\nwebsocket_log_handler = WebSocketLogHandler()\nwebsocket_log_handler.setFormatter(_formatter)\nlogger.addHandler(websocket_log_handler)\n\n# Example usage:\n# logger.info(\"Application started\", extra={\"context_key\": \"context_value\"})\n# logger.error(\"An error occurred\", exc_info=True)\n\n\n# #region explore [C:2] [TYPE Function] [SEMANTICS log,explore,marker,warning]\n# @BRIEF Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.\ndef explore(self, msg, *args, **kwargs):\n \"\"\"Log an EXPLORE marker — searching, alternatives, violated assumptions.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'EXPLORE', 'intent': msg}\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n# #endregion explore\n\n\n# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,debug]\n# @BRIEF Logs a REASON marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reason(self, msg, *args, **kwargs):\n \"\"\"Log a REASON marker — strict deduction, core logic.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REASON', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n\n\n# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,debug]\n# @BRIEF Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reflect(self, msg, *args, **kwargs):\n \"\"\"Log a REFLECT marker — self-check, structural validation.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REFLECT', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n\nlogger.explore = types.MethodType(explore, logger)\nlogger.reason = types.MethodType(reason, logger)\nlogger.reflect = types.MethodType(reflect, logger)\n\n# #endregion Logger\n" + }, + { + "contract_id": "believed", + "contract_type": "Function", + "file_path": "backend/src/core/logger.py", + "start_line": 237, + "end_line": 246, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "A decorator that wraps a function in a belief scope." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief,scope]\n# @BRIEF A decorator that wraps a function in a belief scope.\ndef believed(anchor_id: str):\n def decorator(func):\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n return wrapper\n return decorator\n# #endregion believed\n" }, { "contract_id": "explore", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 274, - "end_line": 286, + "start_line": 267, + "end_line": 278, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -35655,31 +32995,31 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region explore [C:2] [TYPE Function] [SEMANTICS log,explore,marker,warning]\n# @BRIEF Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.\ndef explore(self, msg, *args, **kwargs):\n \"\"\"Log an EXPLORE marker — searching, alternatives, violated assumptions.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'EXPLORE', 'intent': msg}\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n\n# #endregion explore\n" + "body": "# #region explore [C:2] [TYPE Function] [SEMANTICS log,explore,marker,warning]\n# @BRIEF Logs an EXPLORE marker (WARNING level) with structured extra data for CotJsonFormatter.\ndef explore(self, msg, *args, **kwargs):\n \"\"\"Log an EXPLORE marker — searching, alternatives, violated assumptions.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'EXPLORE', 'intent': msg}\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n# #endregion explore\n" }, { "contract_id": "reason", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 289, - "end_line": 301, + "start_line": 281, + "end_line": 292, "tier": "TIER_1", "complexity": 2, "metadata": { "COMPLEXITY": 2, - "PURPOSE": "Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter." + "PURPOSE": "Logs a REASON marker (DEBUG level) with structured extra data for CotJsonFormatter." }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,info]\n# @BRIEF Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter.\ndef reason(self, msg, *args, **kwargs):\n \"\"\"Log a REASON marker — strict deduction, core logic.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REASON', 'intent': msg}\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n\n# #endregion reason\n" + "body": "# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,debug]\n# @BRIEF Logs a REASON marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reason(self, msg, *args, **kwargs):\n \"\"\"Log a REASON marker — strict deduction, core logic.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REASON', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n" }, { "contract_id": "reflect", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 304, - "end_line": 316, + "start_line": 295, + "end_line": 306, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -35689,94 +33029,20 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,debug]\n# @BRIEF Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reflect(self, msg, *args, **kwargs):\n \"\"\"Log a REFLECT marker — self-check, structural validation.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REFLECT', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n\n# #endregion reflect\n" - }, - { - "contract_id": "believed", - "contract_type": "Function", - "file_path": "backend/src/core/logger.py", - "start_line": 326, - "end_line": 340, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Decorator that wraps a function in a belief_scope context manager." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief_scope,wrapper]\n# @BRIEF Decorator that wraps a function in a belief_scope context manager.\ndef believed(anchor_id: str):\n # #region believed.decorator [C:1] [TYPE Function] [SEMANTICS decorator,inner]\n def decorator(func):\n # #region believed.decorator.wrapper [C:1] [TYPE Function] [SEMANTICS wrapper,belief_scope]\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n # #endregion believed.decorator.wrapper\n return wrapper\n # #endregion believed.decorator\n return decorator\n\n# #endregion believed\n" - }, - { - "contract_id": "believed.decorator", - "contract_type": "Function", - "file_path": "backend/src/core/logger.py", - "start_line": 329, - "end_line": 337, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region believed.decorator [C:1] [TYPE Function] [SEMANTICS decorator,inner]\n def decorator(func):\n # #region believed.decorator.wrapper [C:1] [TYPE Function] [SEMANTICS wrapper,belief_scope]\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n # #endregion believed.decorator.wrapper\n return wrapper\n # #endregion believed.decorator\n" - }, - { - "contract_id": "believed.decorator.wrapper", - "contract_type": "Function", - "file_path": "backend/src/core/logger.py", - "start_line": 331, - "end_line": 335, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region believed.decorator.wrapper [C:1] [TYPE Function] [SEMANTICS wrapper,belief_scope]\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n # #endregion believed.decorator.wrapper\n" - }, - { - "contract_id": "test_logger", - "contract_type": "Module", - "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 1, - "end_line": 400, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Infra", - "PURPOSE": "Unit tests for logger module — CoT JSON format markers." - }, - "relations": [ - { - "source_id": "test_logger", - "relation_type": "VERIFIES", - "target_id": "src.core.logger", - "target_ref": "src.core.logger" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_logger:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for logger module — CoT JSON format markers.\n# @LAYER: Infra\n# @RELATION: VERIFIES -> src.core.logger\n\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent.parent / \"src\"))\n\nimport pytest\nimport logging\nimport json\nfrom src.core.logger import (\n belief_scope,\n logger,\n configure_logger,\n get_task_log_level,\n should_log_task_level,\n CotJsonFormatter,\n)\nfrom src.core.config_models import LoggingConfig\n\n\n@pytest.fixture(autouse=True)\ndef reset_logger_state():\n \"\"\"Reset logger state before each test to avoid cross-test contamination.\"\"\"\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n # Also reset the logger level for caplog to work correctly\n logging.getLogger(\"superset_tools_app\").setLevel(logging.DEBUG)\n yield\n # Reset after test too\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n\n# [DEF:test_belief_scope_logs_reason_reflect_at_debug:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REASON (entry) and REFLECT (coherence) markers.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n\n # Check that the records contain the expected markers\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'TestFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'TestFunction'\n ]\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n\n assert len(reason_records) >= 1, \"REASON marker not found for TestFunction\"\n assert len(reflect_records) >= 1, \"REFLECT marker not found for TestFunction\"\n assert len(info_records) >= 1, \"INFO log 'Doing something important' not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# [/DEF:test_belief_scope_logs_reason_reflect_at_debug:Function]\n\n\n# [DEF:test_belief_scope_error_handling:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that belief_scope logs EXPLORE marker on exception.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain EXPLORE marker with error info.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE marker on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with pytest.raises(ValueError):\n with belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n\n # Check that an EXPLORE marker was emitted\n explore_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'EXPLORE'\n and getattr(r, 'src', None) == 'FailingFunction'\n ]\n\n assert len(explore_records) >= 1, \"EXPLORE marker not found for FailingFunction\"\n assert 'Something went wrong' in explore_records[0].getMessage() or \\\n 'Something went wrong' in str(getattr(explore_records[0], 'error', ''))\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# [/DEF:test_belief_scope_error_handling:Function]\n\n\n# [DEF:test_belief_scope_success_coherence:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that belief_scope logs REFLECT marker on success.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_coherence(caplog):\n \"\"\"Test that belief_scope logs REFLECT marker on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"SuccessFunction\"):\n pass\n\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT'\n and getattr(r, 'src', None) == 'SuccessFunction'\n ]\n\n assert len(reflect_records) >= 1, \"REFLECT marker not found for SuccessFunction\"\n assert 'Coherence OK' in reflect_records[0].getMessage() or \\\n getattr(reflect_records[0], 'intent', '') == 'Coherence OK'\n\n\n# [/DEF:test_belief_scope_success_coherence:Function]\n\n\n# [DEF:test_belief_scope_reason_not_visible_at_info:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\n# @PRE: belief_scope is available. caplog fixture is used.\n# @POST: REASON/REFLECT markers are not captured at INFO level.\ndef test_belief_scope_reason_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n\n # The REASON and REFLECT markers are debug-level, so they should NOT appear at INFO\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n\n assert len(reason_records) == 0, \"REASON marker should not be visible at INFO\"\n assert len(reflect_records) == 0, \"REFLECT marker should not be visible at INFO\"\n\n # But the INFO-level message should be visible\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n assert len(info_records) >= 1, \"INFO log 'Doing something important' should be visible\"\n# [/DEF:test_belief_scope_reason_not_visible_at_info:Function]\n\n\n# [DEF:test_task_log_level_default:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that default task log level is INFO.\n# @PRE: None.\n# @POST: Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO (after reset fixture).\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# [/DEF:test_task_log_level_default:Function]\n\n\n# [DEF:test_should_log_task_level:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that should_log_task_level correctly filters log levels.\n# @PRE: None.\n# @POST: Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# [/DEF:test_should_log_task_level:Function]\n\n\n# [DEF:test_configure_logger_task_log_level:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that configure_logger updates task_log_level.\n# @PRE: LoggingConfig is available.\n# @POST: task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n# [/DEF:test_configure_logger_task_log_level:Function]\n\n\n# [DEF:test_enable_belief_state_flag:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that enable_belief_state flag controls belief_scope entry logging.\n# @PRE: LoggingConfig is available. caplog fixture is used.\n# @POST: REASON entry marker is suppressed when disabled; REFLECT coherence still logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n # REASON entry marker should NOT be logged when disabled\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reason_records) == 0, \"REASON entry should not be logged when disabled\"\n\n # REFLECT coherence marker should still be logged (internal tracking)\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reflect_records) >= 1, \"REFLECT coherence should still be logged\"\n# [/DEF:test_enable_belief_state_flag:Function]\n\n\n# [DEF:test_belief_scope_missing_anchor:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test @PRE condition: anchor_id must be provided\ndef test_belief_scope_missing_anchor():\n \"\"\"Test that belief_scope enforces anchor_id to be provided.\"\"\"\n import pytest\n from src.core.logger import belief_scope\n with pytest.raises(TypeError):\n # Missing required positional argument 'anchor_id'\n with belief_scope():\n pass\n# [/DEF:test_belief_scope_missing_anchor:Function]\n\n# [DEF:test_configure_logger_post_conditions:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated.\ndef test_configure_logger_post_conditions(tmp_path):\n \"\"\"Test that configure_logger satisfies all @POST conditions.\"\"\"\n import logging\n from logging.handlers import RotatingFileHandler\n from src.core.config_models import LoggingConfig\n from src.core.logger import configure_logger, logger, CotJsonFormatter, get_task_log_level\n import src.core.logger as logger_module\n\n log_file = tmp_path / \"test.log\"\n config = LoggingConfig(\n level=\"WARNING\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False,\n file_path=str(log_file)\n )\n\n configure_logger(config)\n\n # 1. Logger level is updated\n assert logger.level == logging.WARNING\n\n # 2. Handlers are updated (file handler removed old ones, added new one)\n file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n assert len(file_handlers) == 1\n import pathlib\n assert pathlib.Path(file_handlers[0].baseFilename) == log_file.resolve()\n\n # 3. Formatter is set to CotJsonFormatter\n for handler in logger.handlers:\n assert isinstance(handler.formatter, CotJsonFormatter)\n\n # 4. Global states\n assert getattr(logger_module, '_enable_belief_state') is False\n assert get_task_log_level() == \"DEBUG\"\n# [/DEF:test_configure_logger_post_conditions:Function]\n\n# [DEF:test_cot_json_formatter_output:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that CotJsonFormatter produces valid JSON with expected fields.\ndef test_cot_json_formatter_output():\n \"\"\"Test that CotJsonFormatter produces valid JSON with expected fields.\"\"\"\n import json\n from datetime import datetime, timezone\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord with structured extra data\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Test action\",\n args=(),\n exc_info=None,\n )\n record.marker = \"REASON\"\n record.intent = \"Test action\"\n record.src = \"TestModule\"\n record.payload = {\"key\": \"value\"}\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\"\n assert parsed[\"intent\"] == \"Test action\"\n assert parsed[\"src\"] == \"TestModule\"\n assert parsed[\"payload\"] == {\"key\": \"value\"}\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_output:Function]\n\n# [DEF:test_cot_json_formatter_plain_message:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that CotJsonFormatter wraps plain messages (no extra) with default marker.\ndef test_cot_json_formatter_plain_message():\n \"\"\"Test that CotJsonFormatter wraps plain messages with default marker.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord WITHOUT extra data (plain message)\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Plain info message\",\n args=(),\n exc_info=None,\n )\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\" # default for plain messages\n assert parsed[\"intent\"] == \"Plain info message\"\n assert parsed[\"src\"] == \"test.module\"\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_plain_message:Function]\n\n# [/DEF:test_logger:Module]\n" + "body": "# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,debug]\n# @BRIEF Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.\ndef reflect(self, msg, *args, **kwargs):\n \"\"\"Log a REFLECT marker — self-check, structural validation.\n\n Passes structured ``extra`` data so CotJsonFormatter produces proper JSON.\n \"\"\"\n user_extra = kwargs.pop('extra', {})\n extra = {'marker': 'REFLECT', 'intent': msg}\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n" }, { "contract_id": "test_belief_scope_logs_reason_reflect_at_debug", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 48, - "end_line": 89, + "start_line": 37, + "end_line": 64, "tier": "TIER_1", "complexity": 2, "metadata": { - "POST": "Logs are verified to contain REASON (entry) and REFLECT (coherence) markers.", + "POST": "Logs are verified to contain REASON and REFLECT markers at DEBUG level.", "PRE": "belief_scope is available. caplog fixture is used. Logger configured to DEBUG.", - "PURPOSE": "Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level." + "PURPOSE": "Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level." }, "relations": [ { @@ -35815,21 +33081,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_belief_scope_logs_reason_reflect_at_debug:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REASON (entry) and REFLECT (coherence) markers.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n\n # Check that the records contain the expected markers\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'TestFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'TestFunction'\n ]\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n\n assert len(reason_records) >= 1, \"REASON marker not found for TestFunction\"\n assert len(reflect_records) >= 1, \"REFLECT marker not found for TestFunction\"\n assert len(info_records) >= 1, \"INFO log 'Doing something important' not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# [/DEF:test_belief_scope_logs_reason_reflect_at_debug:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_belief_scope_logs_reason_reflect_at_debug [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REASON and REFLECT markers at DEBUG level.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n # Check that the logs contain the expected patterns\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REASON]\" in msg and \"TestFunction\" in msg for msg in log_messages), \"REASON log not found\"\n assert any(\"Doing something important\" in msg for msg in log_messages), \"Inline info log not found\"\n assert any(\"[REFLECT]\" in msg and \"TestFunction\" in msg for msg in log_messages), \"REFLECT log not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_logs_reason_reflect_at_debug\n" }, { "contract_id": "test_belief_scope_error_handling", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 92, - "end_line": 127, + "start_line": 65, + "end_line": 92, "tier": "TIER_1", "complexity": 2, "metadata": { - "POST": "Logs are verified to contain EXPLORE marker with error info.", + "POST": "Logs are verified to contain EXPLORE marker.", "PRE": "belief_scope is available. caplog fixture is used. Logger configured to DEBUG.", - "PURPOSE": "Test that belief_scope logs EXPLORE marker on exception." + "PURPOSE": "Test that belief_scope logs EXPLORE on exception." }, "relations": [ { @@ -35868,25 +33134,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_belief_scope_error_handling:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that belief_scope logs EXPLORE marker on exception.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain EXPLORE marker with error info.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE marker on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with pytest.raises(ValueError):\n with belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n\n # Check that an EXPLORE marker was emitted\n explore_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'EXPLORE'\n and getattr(r, 'src', None) == 'FailingFunction'\n ]\n\n assert len(explore_records) >= 1, \"EXPLORE marker not found for FailingFunction\"\n assert 'Something went wrong' in explore_records[0].getMessage() or \\\n 'Something went wrong' in str(getattr(explore_records[0], 'error', ''))\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# [/DEF:test_belief_scope_error_handling:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_belief_scope_error_handling [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test that belief_scope logs EXPLORE on exception.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain EXPLORE marker.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with pytest.raises(ValueError), belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REASON]\" in msg and \"FailingFunction\" in msg for msg in log_messages), \"REASON log not found\"\n assert any(\"[EXPLORE]\" in msg and \"FailingFunction\" in msg and \"Something went wrong\" in msg\n for msg in log_messages), \"EXPLORE log not found\"\n # REFLECT should not be logged on failure\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_error_handling\n" }, { - "contract_id": "test_belief_scope_success_coherence", + "contract_id": "test_belief_scope_success_reflect", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 130, - "end_line": 161, + "start_line": 93, + "end_line": 114, "tier": "TIER_1", "complexity": 2, "metadata": { "POST": "Logs are verified to contain REFLECT marker.", "PRE": "belief_scope is available. caplog fixture is used. Logger configured to DEBUG.", - "PURPOSE": "Test that belief_scope logs REFLECT marker on success." + "PURPOSE": "Test that belief_scope logs REFLECT on success." }, "relations": [ { - "source_id": "test_belief_scope_success_coherence", + "source_id": "test_belief_scope_success_reflect", "relation_type": "BINDS_TO", "target_id": "test_logger", "target_ref": "test_logger" @@ -35921,25 +33187,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_belief_scope_success_coherence:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that belief_scope logs REFLECT marker on success.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_coherence(caplog):\n \"\"\"Test that belief_scope logs REFLECT marker on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"SuccessFunction\"):\n pass\n\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT'\n and getattr(r, 'src', None) == 'SuccessFunction'\n ]\n\n assert len(reflect_records) >= 1, \"REFLECT marker not found for SuccessFunction\"\n assert 'Coherence OK' in reflect_records[0].getMessage() or \\\n getattr(reflect_records[0], 'intent', '') == 'Coherence OK'\n\n\n# [/DEF:test_belief_scope_success_coherence:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_belief_scope_success_reflect [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test that belief_scope logs REFLECT on success.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_reflect(caplog):\n \"\"\"Test that belief_scope logs REFLECT on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"SuccessFunction\"):\n pass\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REFLECT]\" in msg and \"SuccessFunction\" in msg for msg in log_messages), \"REFLECT log not found\"\n\n# #endregion test_belief_scope_success_reflect\n" }, { - "contract_id": "test_belief_scope_reason_not_visible_at_info", + "contract_id": "test_belief_scope_not_visible_at_info", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 164, - "end_line": 195, + "start_line": 115, + "end_line": 132, "tier": "TIER_1", "complexity": 2, "metadata": { - "POST": "REASON/REFLECT markers are not captured at INFO level.", + "POST": "REASON is visible at INFO (uses info()); REFLECT is not (uses debug()).", "PRE": "belief_scope is available. caplog fixture is used.", - "PURPOSE": "Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level." + "PURPOSE": "Test that belief_scope REFLECT logs are NOT visible at INFO level." }, "relations": [ { - "source_id": "test_belief_scope_reason_not_visible_at_info", + "source_id": "test_belief_scope_not_visible_at_info", "relation_type": "BINDS_TO", "target_id": "test_logger", "target_ref": "test_logger" @@ -35974,15 +33240,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_belief_scope_reason_not_visible_at_info:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\n# @PRE: belief_scope is available. caplog fixture is used.\n# @POST: REASON/REFLECT markers are not captured at INFO level.\ndef test_belief_scope_reason_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n\n # The REASON and REFLECT markers are debug-level, so they should NOT appear at INFO\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n\n assert len(reason_records) == 0, \"REASON marker should not be visible at INFO\"\n assert len(reflect_records) == 0, \"REFLECT marker should not be visible at INFO\"\n\n # But the INFO-level message should be visible\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n assert len(info_records) >= 1, \"INFO log 'Doing something important' should be visible\"\n# [/DEF:test_belief_scope_reason_not_visible_at_info:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_belief_scope_not_visible_at_info [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test that belief_scope REFLECT logs are NOT visible at INFO level.\n# @PRE: belief_scope is available. caplog fixture is used.\n# @POST: REASON is visible at INFO (uses info()); REFLECT is not (uses debug()).\ndef test_belief_scope_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REFLECT logs are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n log_messages = [record.message for record in caplog.records]\n # Inline INFO log should be visible\n assert any(\"Doing something important\" in msg for msg in log_messages), \"INFO log not found\"\n # REASON uses info() → visible at INFO level\n assert any(\"[REASON]\" in msg and \"InfoLevelFunction\" in msg for msg in log_messages), \"REASON log should be visible at INFO\"\n # REFLECT uses debug() → NOT visible at INFO level\n assert not any(\"[REFLECT]\" in msg for msg in log_messages), \"REFLECT log should not be visible at INFO\"\n# #endregion test_belief_scope_not_visible_at_info\n" }, { "contract_id": "test_task_log_level_default", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 198, - "end_line": 207, + "start_line": 133, + "end_line": 142, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -36027,15 +33293,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_task_log_level_default:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that default task log level is INFO.\n# @PRE: None.\n# @POST: Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO (after reset fixture).\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# [/DEF:test_task_log_level_default:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_task_log_level_default [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test that default task log level is INFO.\n# @PRE: None.\n# @POST: Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO (after reset fixture).\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# #endregion test_task_log_level_default\n" }, { "contract_id": "test_should_log_task_level", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 210, - "end_line": 222, + "start_line": 143, + "end_line": 155, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -36080,15 +33346,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_should_log_task_level:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that should_log_task_level correctly filters log levels.\n# @PRE: None.\n# @POST: Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# [/DEF:test_should_log_task_level:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_should_log_task_level [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test that should_log_task_level correctly filters log levels.\n# @PRE: None.\n# @POST: Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# #endregion test_should_log_task_level\n" }, { "contract_id": "test_configure_logger_task_log_level", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 225, - "end_line": 241, + "start_line": 156, + "end_line": 172, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -36133,21 +33399,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_configure_logger_task_log_level:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that configure_logger updates task_log_level.\n# @PRE: LoggingConfig is available.\n# @POST: task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n# [/DEF:test_configure_logger_task_log_level:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_configure_logger_task_log_level [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test that configure_logger updates task_log_level.\n# @PRE: LoggingConfig is available.\n# @POST: task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n# #endregion test_configure_logger_task_log_level\n" }, { "contract_id": "test_enable_belief_state_flag", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 244, - "end_line": 277, + "start_line": 173, + "end_line": 199, "tier": "TIER_1", "complexity": 2, "metadata": { - "POST": "REASON entry marker is suppressed when disabled; REFLECT coherence still logged.", + "POST": "belief_scope explicit REASON logs are controlled by the flag; REFLECT always logged.", "PRE": "LoggingConfig is available. caplog fixture is used.", - "PURPOSE": "Test that enable_belief_state flag controls belief_scope entry logging." + "PURPOSE": "Test that enable_belief_state flag controls belief_scope logging." }, "relations": [ { @@ -36186,15 +33452,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_enable_belief_state_flag:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that enable_belief_state flag controls belief_scope entry logging.\n# @PRE: LoggingConfig is available. caplog fixture is used.\n# @POST: REASON entry marker is suppressed when disabled; REFLECT coherence still logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n # REASON entry marker should NOT be logged when disabled\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reason_records) == 0, \"REASON entry should not be logged when disabled\"\n\n # REFLECT coherence marker should still be logged (internal tracking)\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reflect_records) >= 1, \"REFLECT coherence should still be logged\"\n# [/DEF:test_enable_belief_state_flag:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_enable_belief_state_flag [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test that enable_belief_state flag controls belief_scope logging.\n# @PRE: LoggingConfig is available. caplog fixture is used.\n# @POST: belief_scope explicit REASON logs are controlled by the flag; REFLECT always logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n log_messages = [record.message for record in caplog.records]\n\n # \"Entering DisabledFunction\" (explicit logger.reason()) should NOT be logged when disabled\n assert not any(\"Entering\" in msg for msg in log_messages), \"REASON entry should not be logged when disabled\"\n # REFLECT should still be logged (internal tracking, not gated by _enable_belief_state)\n assert any(\"[REFLECT]\" in msg for msg in log_messages), \"REFLECT should still be logged\"\n# #endregion test_enable_belief_state_flag\n" }, { "contract_id": "test_belief_scope_missing_anchor", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 280, - "end_line": 291, + "start_line": 200, + "end_line": 212, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -36219,15 +33485,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_belief_scope_missing_anchor:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test @PRE condition: anchor_id must be provided\ndef test_belief_scope_missing_anchor():\n \"\"\"Test that belief_scope enforces anchor_id to be provided.\"\"\"\n import pytest\n from src.core.logger import belief_scope\n with pytest.raises(TypeError):\n # Missing required positional argument 'anchor_id'\n with belief_scope():\n pass\n# [/DEF:test_belief_scope_missing_anchor:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_belief_scope_missing_anchor [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test @PRE condition: anchor_id must be provided\ndef test_belief_scope_missing_anchor():\n \"\"\"Test that belief_scope enforces anchor_id to be provided.\"\"\"\n import pytest\n\n from src.core.logger import belief_scope\n with pytest.raises(TypeError):\n # Missing required positional argument 'anchor_id'\n with belief_scope():\n pass\n# #endregion test_belief_scope_missing_anchor\n" }, { "contract_id": "test_configure_logger_post_conditions", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 293, - "end_line": 330, + "start_line": 213, + "end_line": 250, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -36252,98 +33518,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_configure_logger_post_conditions:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated.\ndef test_configure_logger_post_conditions(tmp_path):\n \"\"\"Test that configure_logger satisfies all @POST conditions.\"\"\"\n import logging\n from logging.handlers import RotatingFileHandler\n from src.core.config_models import LoggingConfig\n from src.core.logger import configure_logger, logger, CotJsonFormatter, get_task_log_level\n import src.core.logger as logger_module\n\n log_file = tmp_path / \"test.log\"\n config = LoggingConfig(\n level=\"WARNING\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False,\n file_path=str(log_file)\n )\n\n configure_logger(config)\n\n # 1. Logger level is updated\n assert logger.level == logging.WARNING\n\n # 2. Handlers are updated (file handler removed old ones, added new one)\n file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n assert len(file_handlers) == 1\n import pathlib\n assert pathlib.Path(file_handlers[0].baseFilename) == log_file.resolve()\n\n # 3. Formatter is set to CotJsonFormatter\n for handler in logger.handlers:\n assert isinstance(handler.formatter, CotJsonFormatter)\n\n # 4. Global states\n assert getattr(logger_module, '_enable_belief_state') is False\n assert get_task_log_level() == \"DEBUG\"\n# [/DEF:test_configure_logger_post_conditions:Function]\n" - }, - { - "contract_id": "test_cot_json_formatter_output", - "contract_type": "Function", - "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 332, - "end_line": 367, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Test that CotJsonFormatter produces valid JSON with expected fields." - }, - "relations": [ - { - "source_id": "test_cot_json_formatter_output", - "relation_type": "BINDS_TO", - "target_id": "test_logger", - "target_ref": "test_logger" - } - ], - "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": "def", - "body": "# [DEF:test_cot_json_formatter_output:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that CotJsonFormatter produces valid JSON with expected fields.\ndef test_cot_json_formatter_output():\n \"\"\"Test that CotJsonFormatter produces valid JSON with expected fields.\"\"\"\n import json\n from datetime import datetime, timezone\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord with structured extra data\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Test action\",\n args=(),\n exc_info=None,\n )\n record.marker = \"REASON\"\n record.intent = \"Test action\"\n record.src = \"TestModule\"\n record.payload = {\"key\": \"value\"}\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\"\n assert parsed[\"intent\"] == \"Test action\"\n assert parsed[\"src\"] == \"TestModule\"\n assert parsed[\"payload\"] == {\"key\": \"value\"}\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_output:Function]\n" - }, - { - "contract_id": "test_cot_json_formatter_plain_message", - "contract_type": "Function", - "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 369, - "end_line": 398, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Test that CotJsonFormatter wraps plain messages (no extra) with default marker." - }, - "relations": [ - { - "source_id": "test_cot_json_formatter_plain_message", - "relation_type": "BINDS_TO", - "target_id": "test_logger", - "target_ref": "test_logger" - } - ], - "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": "def", - "body": "# [DEF:test_cot_json_formatter_plain_message:Function]\n# @RELATION: BINDS_TO -> test_logger\n# @PURPOSE: Test that CotJsonFormatter wraps plain messages (no extra) with default marker.\ndef test_cot_json_formatter_plain_message():\n \"\"\"Test that CotJsonFormatter wraps plain messages with default marker.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord WITHOUT extra data (plain message)\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Plain info message\",\n args=(),\n exc_info=None,\n )\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\" # default for plain messages\n assert parsed[\"intent\"] == \"Plain info message\"\n assert parsed[\"src\"] == \"test.module\"\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_plain_message:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_configure_logger_post_conditions [TYPE Function]\n# @RELATION BINDS_TO -> test_logger\n# @BRIEF Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated.\ndef test_configure_logger_post_conditions(tmp_path):\n \"\"\"Test that configure_logger satisfies all @POST conditions.\"\"\"\n import logging\n from logging.handlers import RotatingFileHandler\n\n import src.core.logger as logger_module\n from src.core.config_models import LoggingConfig\n from src.core.logger import BeliefFormatter, configure_logger, get_task_log_level, logger\n log_file = tmp_path / \"test.log\"\n config = LoggingConfig(\n level=\"WARNING\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False,\n file_path=str(log_file)\n )\n\n configure_logger(config)\n\n # 1. Logger level is updated\n assert logger.level == logging.WARNING\n\n # 2. Handlers are updated (file handler removed old ones, added new one)\n file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n assert len(file_handlers) == 1\n import pathlib\n assert pathlib.Path(file_handlers[0].baseFilename) == log_file.resolve()\n\n # 3. Formatter is set to BeliefFormatter\n for handler in logger.handlers:\n assert isinstance(handler.formatter, BeliefFormatter)\n\n # 4. Global states\n assert logger_module._enable_belief_state is False\n assert get_task_log_level() == \"DEBUG\"\n# #endregion test_configure_logger_post_conditions\n" }, { "contract_id": "IdMappingServiceModule", "contract_type": "Module", "file_path": "backend/src/core/mapping_service.py", "start_line": 1, - "end_line": 301, + "end_line": 278, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[environment_id, resource_type, uuid] -> Output[remote_integer_id|None]", "INVARIANT": "sync_environment must handle remote API failures gracefully.", "LAYER": "Core", "POST": "Mapping synchronization and lookup APIs are available for environment-scoped UUID-to-integer resolution.", "PRE": "Database session is valid and Superset client factory returns authenticated clients for requested environments.", "PURPOSE": "Service for tracking and synchronizing Superset Resource IDs (UUID <-> Integer ID)", - "SEMANTICS": [ - "mapping", - "ids", - "synchronization", - "environments", - "cross-filters" - ], "SIDE_EFFECT": "Reads/writes ResourceMapping rows, emits logs, and schedules periodic sync jobs.", "TEST_DATA": "mock_superset_resources -> {'chart': [{'id': 42, 'uuid': '1234', 'slice_name': 'test'}], 'dataset': [{'id': 99, 'uuid': '5678', 'table_name': 'data'}]}" }, @@ -36387,19 +33580,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:IdMappingServiceModule:Module]\n#\n# @COMPLEXITY: 5\n# @SEMANTICS: mapping, ids, synchronization, environments, cross-filters\n# @PURPOSE: Service for tracking and synchronizing Superset Resource IDs (UUID <-> Integer ID)\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> [MappingModels]\n# @RELATION: DEPENDS_ON -> [LoggerModule]\n# @PRE: Database session is valid and Superset client factory returns authenticated clients for requested environments.\n# @POST: Mapping synchronization and lookup APIs are available for environment-scoped UUID-to-integer resolution.\n# @SIDE_EFFECT: Reads/writes ResourceMapping rows, emits logs, and schedules periodic sync jobs.\n# @DATA_CONTRACT: Input[environment_id, resource_type, uuid] -> Output[remote_integer_id|None]\n# @TEST_DATA: mock_superset_resources -> {'chart': [{'id': 42, 'uuid': '1234', 'slice_name': 'test'}], 'dataset': [{'id': 99, 'uuid': '5678', 'table_name': 'data'}]}\n#\n# @INVARIANT: sync_environment must handle remote API failures gracefully.\n\n# [SECTION: IMPORTS]\nfrom typing import Dict, List, Optional\nfrom datetime import datetime, timezone\nfrom sqlalchemy.orm import Session\nfrom apscheduler.schedulers.background import BackgroundScheduler\nfrom apscheduler.triggers.cron import CronTrigger\nfrom src.models.mapping import ResourceMapping, ResourceType\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import logger, belief_scope\n\nlog = MarkerLogger(\"IdMapping\")\n# [/SECTION]\n\n\n# [DEF:IdMappingService:Class]\n# @COMPLEXITY: 5\n# @PURPOSE: Service handling the cataloging and retrieval of remote Superset Integer IDs.\n# @PRE: db_session is an active SQLAlchemy Session bound to mapping tables.\n# @POST: Service instance provides scheduler control and environment-scoped mapping synchronization APIs.\n# @RELATION: DEPENDS_ON -> [MappingModels]\n# @RELATION: DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: self.db remains the authoritative session for all mapping operations.\n# @SIDE_EFFECT: Instantiates an in-process scheduler and performs database writes during sync cycles.\n# @DATA_CONTRACT: Input[db_session] -> Output[IdMappingService]\n#\n# @TEST_CONTRACT: IdMappingServiceModel ->\n# {\n# required_fields: {db_session: Session},\n# invariants: [\n# \"sync_environment correctly creates or updates ResourceMapping records\",\n# \"get_remote_id returns an integer or None\",\n# \"get_remote_ids_batch returns a dictionary of valid UUIDs to integers\"\n# ]\n# }\n# @TEST_FIXTURE: valid_mapping_service -> {\"db_session\": \"MockSession()\"}\n# @TEST_EDGE: sync_api_failure -> handles exception gracefully\n# @TEST_EDGE: get_remote_id_not_found -> returns None\n# @TEST_EDGE: get_batch_empty_list -> returns empty dict\n# @TEST_INVARIANT: resilient_fetching -> verifies: [sync_api_failure]\nclass IdMappingService:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the mapping service.\n def __init__(self, db_session: Session):\n self.db = db_session\n self.scheduler = BackgroundScheduler()\n self._sync_job = None\n\n # [/DEF:__init__:Function]\n\n # [DEF:start_scheduler:Function]\n # @PURPOSE: Starts the background scheduler with a given cron string.\n # @PARAM: cron_string (str) - Cron expression for the sync interval.\n # @PARAM: environments (List[str]) - List of environment IDs to sync.\n # @PARAM: superset_client_factory - Function to get a client for an environment.\n def start_scheduler(\n self, cron_string: str, environments: List[str], superset_client_factory\n ):\n with belief_scope(\"IdMappingService.start_scheduler\"):\n if self._sync_job:\n self.scheduler.remove_job(self._sync_job.id)\n log.reflect(\"Removed existing sync job.\")\n\n def sync_all():\n for env_id in environments:\n client = superset_client_factory(env_id)\n if client:\n self.sync_environment(env_id, client)\n\n self._sync_job = self.scheduler.add_job(\n sync_all,\n CronTrigger.from_crontab(cron_string),\n id=\"id_mapping_sync_job\",\n replace_existing=True,\n )\n\n if not self.scheduler.running:\n self.scheduler.start()\n log.reason(f\"Started background scheduler with cron: {cron_string}\")\n else:\n log.reason(f\"Updated background scheduler with cron: {cron_string}\")\n\n # [/DEF:start_scheduler:Function]\n\n # [DEF:sync_environment:Function]\n # @PURPOSE: Fully synchronizes mapping for a specific environment.\n # @PARAM: environment_id (str) - Target environment ID.\n # @PARAM: superset_client - Instance capable of hitting the Superset API.\n # @PRE: environment_id exists in the database.\n # @POST: ResourceMapping records for the environment are created or updated.\n def sync_environment(\n self, environment_id: str, superset_client, incremental: bool = False\n ) -> None:\n \"\"\"\n Polls the Superset APIs for the target environment and updates the local mapping table.\n If incremental=True, only fetches items changed since the max last_synced_at date.\n \"\"\"\n with belief_scope(\"IdMappingService.sync_environment\"):\n log.reason(f\"Starting sync for environment {environment_id} (incremental={incremental})\")\n\n # Implementation Note: In a real scenario, superset_client needs to be an instance\n # capable of auth & iteration over /api/v1/chart/, /api/v1/dataset/, /api/v1/dashboard/\n # Here we structure the logic according to the spec.\n\n types_to_poll = [\n (ResourceType.CHART, \"chart\", \"slice_name\"),\n (ResourceType.DATASET, \"dataset\", \"table_name\"),\n (\n ResourceType.DASHBOARD,\n \"dashboard\",\n \"slug\",\n ), # Note: dashboard slug or dashboard_title\n ]\n\n total_synced = 0\n total_deleted = 0\n try:\n for res_enum, endpoint, name_field in types_to_poll:\n log.reason(f\"Polling {endpoint} endpoint\")\n\n # Simulated API Fetch (Would be: superset_client.get(f\"/api/v1/{endpoint}/\")... )\n # This relies on the superset API structure, e.g. { \"result\": [{\"id\": 1, \"uuid\": \"...\", name_field: \"...\"}] }\n # We assume superset_client provides a generic method to fetch all pages.\n\n try:\n since_dttm = None\n if incremental:\n from sqlalchemy.sql import func\n\n max_date = (\n self.db.query(func.max(ResourceMapping.last_synced_at))\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n .scalar()\n )\n\n if max_date:\n # We subtract a bit for safety overlap\n from datetime import timedelta\n\n since_dttm = max_date - timedelta(minutes=5)\n log.reason(\n f\"Incremental sync since {since_dttm}\"\n )\n\n resources = superset_client.get_all_resources(\n endpoint, since_dttm=since_dttm\n )\n\n # Track which UUIDs we see in this sync cycle\n synced_uuids = set()\n\n for res in resources:\n res_uuid = res.get(\"uuid\")\n raw_id = res.get(\"id\")\n res_name = res.get(name_field)\n\n if not res_uuid or raw_id is None:\n continue\n\n synced_uuids.add(res_uuid)\n res_id = str(raw_id) # Store as string\n\n # Upsert Logic\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n )\n .first()\n )\n\n if mapping:\n mapping.remote_integer_id = res_id\n mapping.resource_name = res_name\n mapping.last_synced_at = datetime.now(timezone.utc)\n else:\n new_mapping = ResourceMapping(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n remote_integer_id=res_id,\n resource_name=res_name,\n last_synced_at=datetime.now(timezone.utc),\n )\n self.db.add(new_mapping)\n\n total_synced += 1\n\n # Delete stale mappings: rows for this env+type whose UUID\n # was NOT returned by the API (resource was deleted remotely)\n # We only do this on full syncs, because incremental syncs don't return all UUIDs\n if not incremental:\n stale_query = self.db.query(ResourceMapping).filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n if synced_uuids:\n stale_query = stale_query.filter(\n ResourceMapping.uuid.notin_(synced_uuids)\n )\n deleted = stale_query.delete(synchronize_session=\"fetch\")\n if deleted:\n total_deleted += deleted\n log.reason(f\"Removed {deleted} stale {endpoint} mapping(s) for {environment_id}\")\n\n except Exception as loop_e:\n log.explore(f\"Error polling {endpoint}\", error=str(loop_e))\n # Continue to next resource type instead of blowing up the whole sync\n\n self.db.commit()\n log.reflect(f\"Successfully synced {total_synced} items and deleted {total_deleted} stale items.\")\n\n except Exception as e:\n self.db.rollback()\n log.explore(\"Critical sync failure\", error=str(e))\n raise\n\n # [/DEF:sync_environment:Function]\n\n # [DEF:get_remote_id:Function]\n # @PURPOSE: Retrieves the remote integer ID for a given universal UUID.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuid (str)\n # @RETURN: Optional[int]\n def get_remote_id(\n self, environment_id: str, resource_type: ResourceType, uuid: str\n ) -> Optional[int]:\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id, resource_type=resource_type, uuid=uuid\n )\n .first()\n )\n\n if mapping:\n try:\n return int(mapping.remote_integer_id)\n except ValueError:\n return None\n return None\n\n # [/DEF:get_remote_id:Function]\n\n # [DEF:get_remote_ids_batch:Function]\n # @PURPOSE: Retrieves remote integer IDs for a list of universal UUIDs efficiently.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuids (List[str])\n # @RETURN: Dict[str, int] - Mapping of UUID -> Integer ID\n def get_remote_ids_batch(\n self, environment_id: str, resource_type: ResourceType, uuids: List[str]\n ) -> Dict[str, int]:\n if not uuids:\n return {}\n\n mappings = (\n self.db.query(ResourceMapping)\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == resource_type,\n ResourceMapping.uuid.in_(uuids),\n )\n .all()\n )\n\n result = {}\n for m in mappings:\n try:\n result[m.uuid] = int(m.remote_integer_id)\n except ValueError:\n pass\n\n return result\n\n # [/DEF:get_remote_ids_batch:Function]\n\n\n# [/DEF:IdMappingService:Class]\n# [/DEF:IdMappingServiceModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region IdMappingServiceModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, mapping, sync, schedule, superset, resource]\n#\n# @BRIEF Service for tracking and synchronizing Superset Resource IDs (UUID <-> Integer ID)\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE: Database session is valid and Superset client factory returns authenticated clients for requested environments.\n# @POST: Mapping synchronization and lookup APIs are available for environment-scoped UUID-to-integer resolution.\n# @SIDE_EFFECT: Reads/writes ResourceMapping rows, emits logs, and schedules periodic sync jobs.\n# @DATA_CONTRACT: Input[environment_id, resource_type, uuid] -> Output[remote_integer_id|None]\n# @TEST_DATA: mock_superset_resources -> {'chart': [{'id': 42, 'uuid': '1234', 'slice_name': 'test'}], 'dataset': [{'id': 99, 'uuid': '5678', 'table_name': 'data'}]}\n#\n# @INVARIANT: sync_environment must handle remote API failures gracefully.\nfrom datetime import UTC, datetime\n\nfrom apscheduler.schedulers.background import BackgroundScheduler\nfrom apscheduler.triggers.cron import CronTrigger\nfrom sqlalchemy.orm import Session\n\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.logger import belief_scope, logger\nfrom src.models.mapping import ResourceMapping, ResourceType\n\n\n# #region IdMappingService [C:5] [TYPE Class]\n# @BRIEF Service handling the cataloging and retrieval of remote Superset Integer IDs.\n# @PRE: db_session is an active SQLAlchemy Session bound to mapping tables.\n# @POST: Service instance provides scheduler control and environment-scoped mapping synchronization APIs.\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: self.db remains the authoritative session for all mapping operations.\n# @SIDE_EFFECT: Instantiates an in-process scheduler and performs database writes during sync cycles.\n# @DATA_CONTRACT: Input[db_session] -> Output[IdMappingService]\n#\n# @TEST_CONTRACT: IdMappingServiceModel ->\n# {\n# required_fields: {db_session: Session},\n# invariants: [\n# \"sync_environment correctly creates or updates ResourceMapping records\",\n# \"get_remote_id returns an integer or None\",\n# \"get_remote_ids_batch returns a dictionary of valid UUIDs to integers\"\n# ]\n# }\n# @TEST_FIXTURE: valid_mapping_service -> {\"db_session\": \"MockSession()\"}\n# @TEST_EDGE: sync_api_failure -> handles exception gracefully\n# @TEST_EDGE: get_remote_id_not_found -> returns None\n# @TEST_EDGE: get_batch_empty_list -> returns empty dict\n# @TEST_INVARIANT: resilient_fetching -> verifies: [sync_api_failure]\nclass IdMappingService:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the mapping service.\n def __init__(self, db_session: Session):\n self.db = db_session\n self.scheduler = BackgroundScheduler()\n self._sync_job = None\n # #endregion __init__\n # #region start_scheduler [TYPE Function]\n # @PURPOSE: Starts the background scheduler with a given cron string.\n # @PARAM: cron_string (str) - Cron expression for the sync interval.\n # @PARAM: environments (List[str]) - List of environment IDs to sync.\n # @PARAM: superset_client_factory - Function to get a client for an environment.\n def start_scheduler(\n self, cron_string: str, environments: list[str], superset_client_factory\n ):\n with belief_scope(\"IdMappingService.start_scheduler\"):\n if self._sync_job:\n self.scheduler.remove_job(self._sync_job.id)\n logger.info(\n \"[IdMappingService.start_scheduler][Reflect] Removed existing sync job.\"\n )\n def sync_all():\n seed_trace_id()\n for env_id in environments:\n client = superset_client_factory(env_id)\n if client:\n self.sync_environment(env_id, client)\n self._sync_job = self.scheduler.add_job(\n sync_all,\n CronTrigger.from_crontab(cron_string),\n id=\"id_mapping_sync_job\",\n replace_existing=True,\n )\n if not self.scheduler.running:\n self.scheduler.start()\n logger.info(\n f\"[IdMappingService.start_scheduler][Coherence:OK] Started background scheduler with cron: {cron_string}\"\n )\n else:\n logger.info(\n f\"[IdMappingService.start_scheduler][Coherence:OK] Updated background scheduler with cron: {cron_string}\"\n )\n # #endregion start_scheduler\n # #region sync_environment [TYPE Function]\n # @PURPOSE: Fully synchronizes mapping for a specific environment.\n # @PARAM: environment_id (str) - Target environment ID.\n # @PARAM: superset_client - Instance capable of hitting the Superset API.\n # @PRE: environment_id exists in the database.\n # @POST: ResourceMapping records for the environment are created or updated.\n def sync_environment(\n self, environment_id: str, superset_client, incremental: bool = False\n ) -> None:\n \"\"\"\n Polls the Superset APIs for the target environment and updates the local mapping table.\n If incremental=True, only fetches items changed since the max last_synced_at date.\n \"\"\"\n seed_trace_id()\n with belief_scope(\"IdMappingService.sync_environment\"):\n logger.reason(\n f\"Starting sync for environment {environment_id}\",\n extra={\"src\": \"IdMappingService.sync_environment\", \"payload\": {\"environment_id\": environment_id, \"incremental\": incremental}},\n )\n # Implementation Note: In a real scenario, superset_client needs to be an instance\n # capable of auth & iteration over /api/v1/chart/, /api/v1/dataset/, /api/v1/dashboard/\n # Here we structure the logic according to the spec.\n types_to_poll = [\n (ResourceType.CHART, \"chart\", \"slice_name\"),\n (ResourceType.DATASET, \"dataset\", \"table_name\"),\n (\n ResourceType.DASHBOARD,\n \"dashboard\",\n \"slug\",\n ), # Note: dashboard slug or dashboard_title\n ]\n total_synced = 0\n total_deleted = 0\n try:\n for res_enum, endpoint, name_field in types_to_poll:\n logger.debug(\n f\"[IdMappingService.sync_environment][Explore] Polling {endpoint} endpoint\"\n )\n # Simulated API Fetch (Would be: superset_client.get(f\"/api/v1/{endpoint}/\")... )\n # This relies on the superset API structure, e.g. { \"result\": [{\"id\": 1, \"uuid\": \"...\", name_field: \"...\"}] }\n # We assume superset_client provides a generic method to fetch all pages.\n try:\n since_dttm = None\n if incremental:\n from sqlalchemy.sql import func\n max_date = (\n self.db.query(func.max(ResourceMapping.last_synced_at))\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n .scalar()\n )\n if max_date:\n # We subtract a bit for safety overlap\n from datetime import timedelta\n since_dttm = max_date - timedelta(minutes=5)\n logger.debug(\n f\"[IdMappingService.sync_environment] Incremental sync since {since_dttm}\"\n )\n resources = superset_client.get_all_resources(\n endpoint, since_dttm=since_dttm\n )\n # Track which UUIDs we see in this sync cycle\n synced_uuids = set()\n for res in resources:\n res_uuid = res.get(\"uuid\")\n raw_id = res.get(\"id\")\n res_name = res.get(name_field)\n if not res_uuid or raw_id is None:\n continue\n synced_uuids.add(res_uuid)\n res_id = str(raw_id) # Store as string\n # Upsert Logic\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n )\n .first()\n )\n if mapping:\n mapping.remote_integer_id = res_id\n mapping.resource_name = res_name\n mapping.last_synced_at = datetime.now(UTC)\n else:\n new_mapping = ResourceMapping(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n remote_integer_id=res_id,\n resource_name=res_name,\n last_synced_at=datetime.now(UTC),\n )\n self.db.add(new_mapping)\n total_synced += 1\n # Delete stale mappings: rows for this env+type whose UUID\n # was NOT returned by the API (resource was deleted remotely)\n # We only do this on full syncs, because incremental syncs don't return all UUIDs\n if not incremental:\n stale_query = self.db.query(ResourceMapping).filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n if synced_uuids:\n stale_query = stale_query.filter(\n ResourceMapping.uuid.notin_(synced_uuids)\n )\n deleted = stale_query.delete(synchronize_session=\"fetch\")\n if deleted:\n total_deleted += deleted\n logger.reason(\n f\"Removed {deleted} stale {endpoint} mapping(s) for {environment_id}\",\n extra={\"src\": \"IdMappingService.sync_environment\", \"payload\": {\"deleted\": deleted, \"endpoint\": endpoint, \"environment_id\": environment_id}},\n )\n except Exception as loop_e:\n logger.error(\n f\"[IdMappingService.sync_environment][Reason] Error polling {endpoint}: {loop_e}\"\n )\n # Continue to next resource type instead of blowing up the whole sync\n self.db.commit()\n logger.info(\n f\"[IdMappingService.sync_environment][Coherence:OK] Successfully synced {total_synced} items and deleted {total_deleted} stale items.\"\n )\n except Exception as e:\n self.db.rollback()\n logger.error(\n f\"[IdMappingService.sync_environment][Coherence:Failed] Critical sync failure: {e}\"\n )\n raise\n # #endregion sync_environment\n # #region get_remote_id [TYPE Function]\n # @PURPOSE: Retrieves the remote integer ID for a given universal UUID.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuid (str)\n # @RETURN: Optional[int]\n def get_remote_id(\n self, environment_id: str, resource_type: ResourceType, uuid: str\n ) -> int | None:\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id, resource_type=resource_type, uuid=uuid\n )\n .first()\n )\n if mapping:\n try:\n return int(mapping.remote_integer_id)\n except ValueError:\n return None\n return None\n # #endregion get_remote_id\n # #region get_remote_ids_batch [TYPE Function]\n # @PURPOSE: Retrieves remote integer IDs for a list of universal UUIDs efficiently.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuids (List[str])\n # @RETURN: Dict[str, int] - Mapping of UUID -> Integer ID\n def get_remote_ids_batch(\n self, environment_id: str, resource_type: ResourceType, uuids: list[str]\n ) -> dict[str, int]:\n if not uuids:\n return {}\n mappings = (\n self.db.query(ResourceMapping)\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == resource_type,\n ResourceMapping.uuid.in_(uuids),\n )\n .all()\n )\n result = {}\n for m in mappings:\n try:\n result[m.uuid] = int(m.remote_integer_id)\n except ValueError:\n pass\n return result\n # #endregion get_remote_ids_batch\n# #endregion IdMappingService\n# #endregion IdMappingServiceModule\n" }, { "contract_id": "IdMappingService", "contract_type": "Class", "file_path": "backend/src/core/mapping_service.py", - "start_line": 31, - "end_line": 300, + "start_line": 25, + "end_line": 277, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[db_session] -> Output[IdMappingService]", "INVARIANT": "self.db remains the authoritative session for all mapping operations.", "POST": "Service instance provides scheduler control and environment-scoped mapping synchronization APIs.", @@ -36463,15 +33656,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:IdMappingService:Class]\n# @COMPLEXITY: 5\n# @PURPOSE: Service handling the cataloging and retrieval of remote Superset Integer IDs.\n# @PRE: db_session is an active SQLAlchemy Session bound to mapping tables.\n# @POST: Service instance provides scheduler control and environment-scoped mapping synchronization APIs.\n# @RELATION: DEPENDS_ON -> [MappingModels]\n# @RELATION: DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: self.db remains the authoritative session for all mapping operations.\n# @SIDE_EFFECT: Instantiates an in-process scheduler and performs database writes during sync cycles.\n# @DATA_CONTRACT: Input[db_session] -> Output[IdMappingService]\n#\n# @TEST_CONTRACT: IdMappingServiceModel ->\n# {\n# required_fields: {db_session: Session},\n# invariants: [\n# \"sync_environment correctly creates or updates ResourceMapping records\",\n# \"get_remote_id returns an integer or None\",\n# \"get_remote_ids_batch returns a dictionary of valid UUIDs to integers\"\n# ]\n# }\n# @TEST_FIXTURE: valid_mapping_service -> {\"db_session\": \"MockSession()\"}\n# @TEST_EDGE: sync_api_failure -> handles exception gracefully\n# @TEST_EDGE: get_remote_id_not_found -> returns None\n# @TEST_EDGE: get_batch_empty_list -> returns empty dict\n# @TEST_INVARIANT: resilient_fetching -> verifies: [sync_api_failure]\nclass IdMappingService:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the mapping service.\n def __init__(self, db_session: Session):\n self.db = db_session\n self.scheduler = BackgroundScheduler()\n self._sync_job = None\n\n # [/DEF:__init__:Function]\n\n # [DEF:start_scheduler:Function]\n # @PURPOSE: Starts the background scheduler with a given cron string.\n # @PARAM: cron_string (str) - Cron expression for the sync interval.\n # @PARAM: environments (List[str]) - List of environment IDs to sync.\n # @PARAM: superset_client_factory - Function to get a client for an environment.\n def start_scheduler(\n self, cron_string: str, environments: List[str], superset_client_factory\n ):\n with belief_scope(\"IdMappingService.start_scheduler\"):\n if self._sync_job:\n self.scheduler.remove_job(self._sync_job.id)\n log.reflect(\"Removed existing sync job.\")\n\n def sync_all():\n for env_id in environments:\n client = superset_client_factory(env_id)\n if client:\n self.sync_environment(env_id, client)\n\n self._sync_job = self.scheduler.add_job(\n sync_all,\n CronTrigger.from_crontab(cron_string),\n id=\"id_mapping_sync_job\",\n replace_existing=True,\n )\n\n if not self.scheduler.running:\n self.scheduler.start()\n log.reason(f\"Started background scheduler with cron: {cron_string}\")\n else:\n log.reason(f\"Updated background scheduler with cron: {cron_string}\")\n\n # [/DEF:start_scheduler:Function]\n\n # [DEF:sync_environment:Function]\n # @PURPOSE: Fully synchronizes mapping for a specific environment.\n # @PARAM: environment_id (str) - Target environment ID.\n # @PARAM: superset_client - Instance capable of hitting the Superset API.\n # @PRE: environment_id exists in the database.\n # @POST: ResourceMapping records for the environment are created or updated.\n def sync_environment(\n self, environment_id: str, superset_client, incremental: bool = False\n ) -> None:\n \"\"\"\n Polls the Superset APIs for the target environment and updates the local mapping table.\n If incremental=True, only fetches items changed since the max last_synced_at date.\n \"\"\"\n with belief_scope(\"IdMappingService.sync_environment\"):\n log.reason(f\"Starting sync for environment {environment_id} (incremental={incremental})\")\n\n # Implementation Note: In a real scenario, superset_client needs to be an instance\n # capable of auth & iteration over /api/v1/chart/, /api/v1/dataset/, /api/v1/dashboard/\n # Here we structure the logic according to the spec.\n\n types_to_poll = [\n (ResourceType.CHART, \"chart\", \"slice_name\"),\n (ResourceType.DATASET, \"dataset\", \"table_name\"),\n (\n ResourceType.DASHBOARD,\n \"dashboard\",\n \"slug\",\n ), # Note: dashboard slug or dashboard_title\n ]\n\n total_synced = 0\n total_deleted = 0\n try:\n for res_enum, endpoint, name_field in types_to_poll:\n log.reason(f\"Polling {endpoint} endpoint\")\n\n # Simulated API Fetch (Would be: superset_client.get(f\"/api/v1/{endpoint}/\")... )\n # This relies on the superset API structure, e.g. { \"result\": [{\"id\": 1, \"uuid\": \"...\", name_field: \"...\"}] }\n # We assume superset_client provides a generic method to fetch all pages.\n\n try:\n since_dttm = None\n if incremental:\n from sqlalchemy.sql import func\n\n max_date = (\n self.db.query(func.max(ResourceMapping.last_synced_at))\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n .scalar()\n )\n\n if max_date:\n # We subtract a bit for safety overlap\n from datetime import timedelta\n\n since_dttm = max_date - timedelta(minutes=5)\n log.reason(\n f\"Incremental sync since {since_dttm}\"\n )\n\n resources = superset_client.get_all_resources(\n endpoint, since_dttm=since_dttm\n )\n\n # Track which UUIDs we see in this sync cycle\n synced_uuids = set()\n\n for res in resources:\n res_uuid = res.get(\"uuid\")\n raw_id = res.get(\"id\")\n res_name = res.get(name_field)\n\n if not res_uuid or raw_id is None:\n continue\n\n synced_uuids.add(res_uuid)\n res_id = str(raw_id) # Store as string\n\n # Upsert Logic\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n )\n .first()\n )\n\n if mapping:\n mapping.remote_integer_id = res_id\n mapping.resource_name = res_name\n mapping.last_synced_at = datetime.now(timezone.utc)\n else:\n new_mapping = ResourceMapping(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n remote_integer_id=res_id,\n resource_name=res_name,\n last_synced_at=datetime.now(timezone.utc),\n )\n self.db.add(new_mapping)\n\n total_synced += 1\n\n # Delete stale mappings: rows for this env+type whose UUID\n # was NOT returned by the API (resource was deleted remotely)\n # We only do this on full syncs, because incremental syncs don't return all UUIDs\n if not incremental:\n stale_query = self.db.query(ResourceMapping).filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n if synced_uuids:\n stale_query = stale_query.filter(\n ResourceMapping.uuid.notin_(synced_uuids)\n )\n deleted = stale_query.delete(synchronize_session=\"fetch\")\n if deleted:\n total_deleted += deleted\n log.reason(f\"Removed {deleted} stale {endpoint} mapping(s) for {environment_id}\")\n\n except Exception as loop_e:\n log.explore(f\"Error polling {endpoint}\", error=str(loop_e))\n # Continue to next resource type instead of blowing up the whole sync\n\n self.db.commit()\n log.reflect(f\"Successfully synced {total_synced} items and deleted {total_deleted} stale items.\")\n\n except Exception as e:\n self.db.rollback()\n log.explore(\"Critical sync failure\", error=str(e))\n raise\n\n # [/DEF:sync_environment:Function]\n\n # [DEF:get_remote_id:Function]\n # @PURPOSE: Retrieves the remote integer ID for a given universal UUID.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuid (str)\n # @RETURN: Optional[int]\n def get_remote_id(\n self, environment_id: str, resource_type: ResourceType, uuid: str\n ) -> Optional[int]:\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id, resource_type=resource_type, uuid=uuid\n )\n .first()\n )\n\n if mapping:\n try:\n return int(mapping.remote_integer_id)\n except ValueError:\n return None\n return None\n\n # [/DEF:get_remote_id:Function]\n\n # [DEF:get_remote_ids_batch:Function]\n # @PURPOSE: Retrieves remote integer IDs for a list of universal UUIDs efficiently.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuids (List[str])\n # @RETURN: Dict[str, int] - Mapping of UUID -> Integer ID\n def get_remote_ids_batch(\n self, environment_id: str, resource_type: ResourceType, uuids: List[str]\n ) -> Dict[str, int]:\n if not uuids:\n return {}\n\n mappings = (\n self.db.query(ResourceMapping)\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == resource_type,\n ResourceMapping.uuid.in_(uuids),\n )\n .all()\n )\n\n result = {}\n for m in mappings:\n try:\n result[m.uuid] = int(m.remote_integer_id)\n except ValueError:\n pass\n\n return result\n\n # [/DEF:get_remote_ids_batch:Function]\n\n\n# [/DEF:IdMappingService:Class]\n" + "anchor_syntax": "region", + "body": "# #region IdMappingService [C:5] [TYPE Class]\n# @BRIEF Service handling the cataloging and retrieval of remote Superset Integer IDs.\n# @PRE: db_session is an active SQLAlchemy Session bound to mapping tables.\n# @POST: Service instance provides scheduler control and environment-scoped mapping synchronization APIs.\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: self.db remains the authoritative session for all mapping operations.\n# @SIDE_EFFECT: Instantiates an in-process scheduler and performs database writes during sync cycles.\n# @DATA_CONTRACT: Input[db_session] -> Output[IdMappingService]\n#\n# @TEST_CONTRACT: IdMappingServiceModel ->\n# {\n# required_fields: {db_session: Session},\n# invariants: [\n# \"sync_environment correctly creates or updates ResourceMapping records\",\n# \"get_remote_id returns an integer or None\",\n# \"get_remote_ids_batch returns a dictionary of valid UUIDs to integers\"\n# ]\n# }\n# @TEST_FIXTURE: valid_mapping_service -> {\"db_session\": \"MockSession()\"}\n# @TEST_EDGE: sync_api_failure -> handles exception gracefully\n# @TEST_EDGE: get_remote_id_not_found -> returns None\n# @TEST_EDGE: get_batch_empty_list -> returns empty dict\n# @TEST_INVARIANT: resilient_fetching -> verifies: [sync_api_failure]\nclass IdMappingService:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the mapping service.\n def __init__(self, db_session: Session):\n self.db = db_session\n self.scheduler = BackgroundScheduler()\n self._sync_job = None\n # #endregion __init__\n # #region start_scheduler [TYPE Function]\n # @PURPOSE: Starts the background scheduler with a given cron string.\n # @PARAM: cron_string (str) - Cron expression for the sync interval.\n # @PARAM: environments (List[str]) - List of environment IDs to sync.\n # @PARAM: superset_client_factory - Function to get a client for an environment.\n def start_scheduler(\n self, cron_string: str, environments: list[str], superset_client_factory\n ):\n with belief_scope(\"IdMappingService.start_scheduler\"):\n if self._sync_job:\n self.scheduler.remove_job(self._sync_job.id)\n logger.info(\n \"[IdMappingService.start_scheduler][Reflect] Removed existing sync job.\"\n )\n def sync_all():\n seed_trace_id()\n for env_id in environments:\n client = superset_client_factory(env_id)\n if client:\n self.sync_environment(env_id, client)\n self._sync_job = self.scheduler.add_job(\n sync_all,\n CronTrigger.from_crontab(cron_string),\n id=\"id_mapping_sync_job\",\n replace_existing=True,\n )\n if not self.scheduler.running:\n self.scheduler.start()\n logger.info(\n f\"[IdMappingService.start_scheduler][Coherence:OK] Started background scheduler with cron: {cron_string}\"\n )\n else:\n logger.info(\n f\"[IdMappingService.start_scheduler][Coherence:OK] Updated background scheduler with cron: {cron_string}\"\n )\n # #endregion start_scheduler\n # #region sync_environment [TYPE Function]\n # @PURPOSE: Fully synchronizes mapping for a specific environment.\n # @PARAM: environment_id (str) - Target environment ID.\n # @PARAM: superset_client - Instance capable of hitting the Superset API.\n # @PRE: environment_id exists in the database.\n # @POST: ResourceMapping records for the environment are created or updated.\n def sync_environment(\n self, environment_id: str, superset_client, incremental: bool = False\n ) -> None:\n \"\"\"\n Polls the Superset APIs for the target environment and updates the local mapping table.\n If incremental=True, only fetches items changed since the max last_synced_at date.\n \"\"\"\n seed_trace_id()\n with belief_scope(\"IdMappingService.sync_environment\"):\n logger.reason(\n f\"Starting sync for environment {environment_id}\",\n extra={\"src\": \"IdMappingService.sync_environment\", \"payload\": {\"environment_id\": environment_id, \"incremental\": incremental}},\n )\n # Implementation Note: In a real scenario, superset_client needs to be an instance\n # capable of auth & iteration over /api/v1/chart/, /api/v1/dataset/, /api/v1/dashboard/\n # Here we structure the logic according to the spec.\n types_to_poll = [\n (ResourceType.CHART, \"chart\", \"slice_name\"),\n (ResourceType.DATASET, \"dataset\", \"table_name\"),\n (\n ResourceType.DASHBOARD,\n \"dashboard\",\n \"slug\",\n ), # Note: dashboard slug or dashboard_title\n ]\n total_synced = 0\n total_deleted = 0\n try:\n for res_enum, endpoint, name_field in types_to_poll:\n logger.debug(\n f\"[IdMappingService.sync_environment][Explore] Polling {endpoint} endpoint\"\n )\n # Simulated API Fetch (Would be: superset_client.get(f\"/api/v1/{endpoint}/\")... )\n # This relies on the superset API structure, e.g. { \"result\": [{\"id\": 1, \"uuid\": \"...\", name_field: \"...\"}] }\n # We assume superset_client provides a generic method to fetch all pages.\n try:\n since_dttm = None\n if incremental:\n from sqlalchemy.sql import func\n max_date = (\n self.db.query(func.max(ResourceMapping.last_synced_at))\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n .scalar()\n )\n if max_date:\n # We subtract a bit for safety overlap\n from datetime import timedelta\n since_dttm = max_date - timedelta(minutes=5)\n logger.debug(\n f\"[IdMappingService.sync_environment] Incremental sync since {since_dttm}\"\n )\n resources = superset_client.get_all_resources(\n endpoint, since_dttm=since_dttm\n )\n # Track which UUIDs we see in this sync cycle\n synced_uuids = set()\n for res in resources:\n res_uuid = res.get(\"uuid\")\n raw_id = res.get(\"id\")\n res_name = res.get(name_field)\n if not res_uuid or raw_id is None:\n continue\n synced_uuids.add(res_uuid)\n res_id = str(raw_id) # Store as string\n # Upsert Logic\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n )\n .first()\n )\n if mapping:\n mapping.remote_integer_id = res_id\n mapping.resource_name = res_name\n mapping.last_synced_at = datetime.now(UTC)\n else:\n new_mapping = ResourceMapping(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n remote_integer_id=res_id,\n resource_name=res_name,\n last_synced_at=datetime.now(UTC),\n )\n self.db.add(new_mapping)\n total_synced += 1\n # Delete stale mappings: rows for this env+type whose UUID\n # was NOT returned by the API (resource was deleted remotely)\n # We only do this on full syncs, because incremental syncs don't return all UUIDs\n if not incremental:\n stale_query = self.db.query(ResourceMapping).filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n if synced_uuids:\n stale_query = stale_query.filter(\n ResourceMapping.uuid.notin_(synced_uuids)\n )\n deleted = stale_query.delete(synchronize_session=\"fetch\")\n if deleted:\n total_deleted += deleted\n logger.reason(\n f\"Removed {deleted} stale {endpoint} mapping(s) for {environment_id}\",\n extra={\"src\": \"IdMappingService.sync_environment\", \"payload\": {\"deleted\": deleted, \"endpoint\": endpoint, \"environment_id\": environment_id}},\n )\n except Exception as loop_e:\n logger.error(\n f\"[IdMappingService.sync_environment][Reason] Error polling {endpoint}: {loop_e}\"\n )\n # Continue to next resource type instead of blowing up the whole sync\n self.db.commit()\n logger.info(\n f\"[IdMappingService.sync_environment][Coherence:OK] Successfully synced {total_synced} items and deleted {total_deleted} stale items.\"\n )\n except Exception as e:\n self.db.rollback()\n logger.error(\n f\"[IdMappingService.sync_environment][Coherence:Failed] Critical sync failure: {e}\"\n )\n raise\n # #endregion sync_environment\n # #region get_remote_id [TYPE Function]\n # @PURPOSE: Retrieves the remote integer ID for a given universal UUID.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuid (str)\n # @RETURN: Optional[int]\n def get_remote_id(\n self, environment_id: str, resource_type: ResourceType, uuid: str\n ) -> int | None:\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id, resource_type=resource_type, uuid=uuid\n )\n .first()\n )\n if mapping:\n try:\n return int(mapping.remote_integer_id)\n except ValueError:\n return None\n return None\n # #endregion get_remote_id\n # #region get_remote_ids_batch [TYPE Function]\n # @PURPOSE: Retrieves remote integer IDs for a list of universal UUIDs efficiently.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuids (List[str])\n # @RETURN: Dict[str, int] - Mapping of UUID -> Integer ID\n def get_remote_ids_batch(\n self, environment_id: str, resource_type: ResourceType, uuids: list[str]\n ) -> dict[str, int]:\n if not uuids:\n return {}\n mappings = (\n self.db.query(ResourceMapping)\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == resource_type,\n ResourceMapping.uuid.in_(uuids),\n )\n .all()\n )\n result = {}\n for m in mappings:\n try:\n result[m.uuid] = int(m.remote_integer_id)\n except ValueError:\n pass\n return result\n # #endregion get_remote_ids_batch\n# #endregion IdMappingService\n" }, { "contract_id": "start_scheduler", "contract_type": "Function", "file_path": "backend/src/core/mapping_service.py", - "start_line": 66, - "end_line": 98, + "start_line": 57, + "end_line": 92, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36496,15 +33689,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:start_scheduler:Function]\n # @PURPOSE: Starts the background scheduler with a given cron string.\n # @PARAM: cron_string (str) - Cron expression for the sync interval.\n # @PARAM: environments (List[str]) - List of environment IDs to sync.\n # @PARAM: superset_client_factory - Function to get a client for an environment.\n def start_scheduler(\n self, cron_string: str, environments: List[str], superset_client_factory\n ):\n with belief_scope(\"IdMappingService.start_scheduler\"):\n if self._sync_job:\n self.scheduler.remove_job(self._sync_job.id)\n log.reflect(\"Removed existing sync job.\")\n\n def sync_all():\n for env_id in environments:\n client = superset_client_factory(env_id)\n if client:\n self.sync_environment(env_id, client)\n\n self._sync_job = self.scheduler.add_job(\n sync_all,\n CronTrigger.from_crontab(cron_string),\n id=\"id_mapping_sync_job\",\n replace_existing=True,\n )\n\n if not self.scheduler.running:\n self.scheduler.start()\n log.reason(f\"Started background scheduler with cron: {cron_string}\")\n else:\n log.reason(f\"Updated background scheduler with cron: {cron_string}\")\n\n # [/DEF:start_scheduler:Function]\n" + "anchor_syntax": "region", + "body": " # #region start_scheduler [TYPE Function]\n # @PURPOSE: Starts the background scheduler with a given cron string.\n # @PARAM: cron_string (str) - Cron expression for the sync interval.\n # @PARAM: environments (List[str]) - List of environment IDs to sync.\n # @PARAM: superset_client_factory - Function to get a client for an environment.\n def start_scheduler(\n self, cron_string: str, environments: list[str], superset_client_factory\n ):\n with belief_scope(\"IdMappingService.start_scheduler\"):\n if self._sync_job:\n self.scheduler.remove_job(self._sync_job.id)\n logger.info(\n \"[IdMappingService.start_scheduler][Reflect] Removed existing sync job.\"\n )\n def sync_all():\n seed_trace_id()\n for env_id in environments:\n client = superset_client_factory(env_id)\n if client:\n self.sync_environment(env_id, client)\n self._sync_job = self.scheduler.add_job(\n sync_all,\n CronTrigger.from_crontab(cron_string),\n id=\"id_mapping_sync_job\",\n replace_existing=True,\n )\n if not self.scheduler.running:\n self.scheduler.start()\n logger.info(\n f\"[IdMappingService.start_scheduler][Coherence:OK] Started background scheduler with cron: {cron_string}\"\n )\n else:\n logger.info(\n f\"[IdMappingService.start_scheduler][Coherence:OK] Updated background scheduler with cron: {cron_string}\"\n )\n # #endregion start_scheduler\n" }, { "contract_id": "sync_environment", "contract_type": "Function", "file_path": "backend/src/core/mapping_service.py", - "start_line": 100, - "end_line": 238, + "start_line": 93, + "end_line": 225, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36549,15 +33742,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:sync_environment:Function]\n # @PURPOSE: Fully synchronizes mapping for a specific environment.\n # @PARAM: environment_id (str) - Target environment ID.\n # @PARAM: superset_client - Instance capable of hitting the Superset API.\n # @PRE: environment_id exists in the database.\n # @POST: ResourceMapping records for the environment are created or updated.\n def sync_environment(\n self, environment_id: str, superset_client, incremental: bool = False\n ) -> None:\n \"\"\"\n Polls the Superset APIs for the target environment and updates the local mapping table.\n If incremental=True, only fetches items changed since the max last_synced_at date.\n \"\"\"\n with belief_scope(\"IdMappingService.sync_environment\"):\n log.reason(f\"Starting sync for environment {environment_id} (incremental={incremental})\")\n\n # Implementation Note: In a real scenario, superset_client needs to be an instance\n # capable of auth & iteration over /api/v1/chart/, /api/v1/dataset/, /api/v1/dashboard/\n # Here we structure the logic according to the spec.\n\n types_to_poll = [\n (ResourceType.CHART, \"chart\", \"slice_name\"),\n (ResourceType.DATASET, \"dataset\", \"table_name\"),\n (\n ResourceType.DASHBOARD,\n \"dashboard\",\n \"slug\",\n ), # Note: dashboard slug or dashboard_title\n ]\n\n total_synced = 0\n total_deleted = 0\n try:\n for res_enum, endpoint, name_field in types_to_poll:\n log.reason(f\"Polling {endpoint} endpoint\")\n\n # Simulated API Fetch (Would be: superset_client.get(f\"/api/v1/{endpoint}/\")... )\n # This relies on the superset API structure, e.g. { \"result\": [{\"id\": 1, \"uuid\": \"...\", name_field: \"...\"}] }\n # We assume superset_client provides a generic method to fetch all pages.\n\n try:\n since_dttm = None\n if incremental:\n from sqlalchemy.sql import func\n\n max_date = (\n self.db.query(func.max(ResourceMapping.last_synced_at))\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n .scalar()\n )\n\n if max_date:\n # We subtract a bit for safety overlap\n from datetime import timedelta\n\n since_dttm = max_date - timedelta(minutes=5)\n log.reason(\n f\"Incremental sync since {since_dttm}\"\n )\n\n resources = superset_client.get_all_resources(\n endpoint, since_dttm=since_dttm\n )\n\n # Track which UUIDs we see in this sync cycle\n synced_uuids = set()\n\n for res in resources:\n res_uuid = res.get(\"uuid\")\n raw_id = res.get(\"id\")\n res_name = res.get(name_field)\n\n if not res_uuid or raw_id is None:\n continue\n\n synced_uuids.add(res_uuid)\n res_id = str(raw_id) # Store as string\n\n # Upsert Logic\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n )\n .first()\n )\n\n if mapping:\n mapping.remote_integer_id = res_id\n mapping.resource_name = res_name\n mapping.last_synced_at = datetime.now(timezone.utc)\n else:\n new_mapping = ResourceMapping(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n remote_integer_id=res_id,\n resource_name=res_name,\n last_synced_at=datetime.now(timezone.utc),\n )\n self.db.add(new_mapping)\n\n total_synced += 1\n\n # Delete stale mappings: rows for this env+type whose UUID\n # was NOT returned by the API (resource was deleted remotely)\n # We only do this on full syncs, because incremental syncs don't return all UUIDs\n if not incremental:\n stale_query = self.db.query(ResourceMapping).filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n if synced_uuids:\n stale_query = stale_query.filter(\n ResourceMapping.uuid.notin_(synced_uuids)\n )\n deleted = stale_query.delete(synchronize_session=\"fetch\")\n if deleted:\n total_deleted += deleted\n log.reason(f\"Removed {deleted} stale {endpoint} mapping(s) for {environment_id}\")\n\n except Exception as loop_e:\n log.explore(f\"Error polling {endpoint}\", error=str(loop_e))\n # Continue to next resource type instead of blowing up the whole sync\n\n self.db.commit()\n log.reflect(f\"Successfully synced {total_synced} items and deleted {total_deleted} stale items.\")\n\n except Exception as e:\n self.db.rollback()\n log.explore(\"Critical sync failure\", error=str(e))\n raise\n\n # [/DEF:sync_environment:Function]\n" + "anchor_syntax": "region", + "body": " # #region sync_environment [TYPE Function]\n # @PURPOSE: Fully synchronizes mapping for a specific environment.\n # @PARAM: environment_id (str) - Target environment ID.\n # @PARAM: superset_client - Instance capable of hitting the Superset API.\n # @PRE: environment_id exists in the database.\n # @POST: ResourceMapping records for the environment are created or updated.\n def sync_environment(\n self, environment_id: str, superset_client, incremental: bool = False\n ) -> None:\n \"\"\"\n Polls the Superset APIs for the target environment and updates the local mapping table.\n If incremental=True, only fetches items changed since the max last_synced_at date.\n \"\"\"\n seed_trace_id()\n with belief_scope(\"IdMappingService.sync_environment\"):\n logger.reason(\n f\"Starting sync for environment {environment_id}\",\n extra={\"src\": \"IdMappingService.sync_environment\", \"payload\": {\"environment_id\": environment_id, \"incremental\": incremental}},\n )\n # Implementation Note: In a real scenario, superset_client needs to be an instance\n # capable of auth & iteration over /api/v1/chart/, /api/v1/dataset/, /api/v1/dashboard/\n # Here we structure the logic according to the spec.\n types_to_poll = [\n (ResourceType.CHART, \"chart\", \"slice_name\"),\n (ResourceType.DATASET, \"dataset\", \"table_name\"),\n (\n ResourceType.DASHBOARD,\n \"dashboard\",\n \"slug\",\n ), # Note: dashboard slug or dashboard_title\n ]\n total_synced = 0\n total_deleted = 0\n try:\n for res_enum, endpoint, name_field in types_to_poll:\n logger.debug(\n f\"[IdMappingService.sync_environment][Explore] Polling {endpoint} endpoint\"\n )\n # Simulated API Fetch (Would be: superset_client.get(f\"/api/v1/{endpoint}/\")... )\n # This relies on the superset API structure, e.g. { \"result\": [{\"id\": 1, \"uuid\": \"...\", name_field: \"...\"}] }\n # We assume superset_client provides a generic method to fetch all pages.\n try:\n since_dttm = None\n if incremental:\n from sqlalchemy.sql import func\n max_date = (\n self.db.query(func.max(ResourceMapping.last_synced_at))\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n .scalar()\n )\n if max_date:\n # We subtract a bit for safety overlap\n from datetime import timedelta\n since_dttm = max_date - timedelta(minutes=5)\n logger.debug(\n f\"[IdMappingService.sync_environment] Incremental sync since {since_dttm}\"\n )\n resources = superset_client.get_all_resources(\n endpoint, since_dttm=since_dttm\n )\n # Track which UUIDs we see in this sync cycle\n synced_uuids = set()\n for res in resources:\n res_uuid = res.get(\"uuid\")\n raw_id = res.get(\"id\")\n res_name = res.get(name_field)\n if not res_uuid or raw_id is None:\n continue\n synced_uuids.add(res_uuid)\n res_id = str(raw_id) # Store as string\n # Upsert Logic\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n )\n .first()\n )\n if mapping:\n mapping.remote_integer_id = res_id\n mapping.resource_name = res_name\n mapping.last_synced_at = datetime.now(UTC)\n else:\n new_mapping = ResourceMapping(\n environment_id=environment_id,\n resource_type=res_enum,\n uuid=res_uuid,\n remote_integer_id=res_id,\n resource_name=res_name,\n last_synced_at=datetime.now(UTC),\n )\n self.db.add(new_mapping)\n total_synced += 1\n # Delete stale mappings: rows for this env+type whose UUID\n # was NOT returned by the API (resource was deleted remotely)\n # We only do this on full syncs, because incremental syncs don't return all UUIDs\n if not incremental:\n stale_query = self.db.query(ResourceMapping).filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == res_enum,\n )\n if synced_uuids:\n stale_query = stale_query.filter(\n ResourceMapping.uuid.notin_(synced_uuids)\n )\n deleted = stale_query.delete(synchronize_session=\"fetch\")\n if deleted:\n total_deleted += deleted\n logger.reason(\n f\"Removed {deleted} stale {endpoint} mapping(s) for {environment_id}\",\n extra={\"src\": \"IdMappingService.sync_environment\", \"payload\": {\"deleted\": deleted, \"endpoint\": endpoint, \"environment_id\": environment_id}},\n )\n except Exception as loop_e:\n logger.error(\n f\"[IdMappingService.sync_environment][Reason] Error polling {endpoint}: {loop_e}\"\n )\n # Continue to next resource type instead of blowing up the whole sync\n self.db.commit()\n logger.info(\n f\"[IdMappingService.sync_environment][Coherence:OK] Successfully synced {total_synced} items and deleted {total_deleted} stale items.\"\n )\n except Exception as e:\n self.db.rollback()\n logger.error(\n f\"[IdMappingService.sync_environment][Coherence:Failed] Critical sync failure: {e}\"\n )\n raise\n # #endregion sync_environment\n" }, { "contract_id": "get_remote_id", "contract_type": "Function", "file_path": "backend/src/core/mapping_service.py", - "start_line": 240, - "end_line": 264, + "start_line": 226, + "end_line": 248, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36589,15 +33782,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:get_remote_id:Function]\n # @PURPOSE: Retrieves the remote integer ID for a given universal UUID.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuid (str)\n # @RETURN: Optional[int]\n def get_remote_id(\n self, environment_id: str, resource_type: ResourceType, uuid: str\n ) -> Optional[int]:\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id, resource_type=resource_type, uuid=uuid\n )\n .first()\n )\n\n if mapping:\n try:\n return int(mapping.remote_integer_id)\n except ValueError:\n return None\n return None\n\n # [/DEF:get_remote_id:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_remote_id [TYPE Function]\n # @PURPOSE: Retrieves the remote integer ID for a given universal UUID.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuid (str)\n # @RETURN: Optional[int]\n def get_remote_id(\n self, environment_id: str, resource_type: ResourceType, uuid: str\n ) -> int | None:\n mapping = (\n self.db.query(ResourceMapping)\n .filter_by(\n environment_id=environment_id, resource_type=resource_type, uuid=uuid\n )\n .first()\n )\n if mapping:\n try:\n return int(mapping.remote_integer_id)\n except ValueError:\n return None\n return None\n # #endregion get_remote_id\n" }, { "contract_id": "get_remote_ids_batch", "contract_type": "Function", "file_path": "backend/src/core/mapping_service.py", - "start_line": 266, - "end_line": 297, + "start_line": 249, + "end_line": 276, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36629,26 +33822,20 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:get_remote_ids_batch:Function]\n # @PURPOSE: Retrieves remote integer IDs for a list of universal UUIDs efficiently.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuids (List[str])\n # @RETURN: Dict[str, int] - Mapping of UUID -> Integer ID\n def get_remote_ids_batch(\n self, environment_id: str, resource_type: ResourceType, uuids: List[str]\n ) -> Dict[str, int]:\n if not uuids:\n return {}\n\n mappings = (\n self.db.query(ResourceMapping)\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == resource_type,\n ResourceMapping.uuid.in_(uuids),\n )\n .all()\n )\n\n result = {}\n for m in mappings:\n try:\n result[m.uuid] = int(m.remote_integer_id)\n except ValueError:\n pass\n\n return result\n\n # [/DEF:get_remote_ids_batch:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_remote_ids_batch [TYPE Function]\n # @PURPOSE: Retrieves remote integer IDs for a list of universal UUIDs efficiently.\n # @PARAM: environment_id (str)\n # @PARAM: resource_type (ResourceType)\n # @PARAM: uuids (List[str])\n # @RETURN: Dict[str, int] - Mapping of UUID -> Integer ID\n def get_remote_ids_batch(\n self, environment_id: str, resource_type: ResourceType, uuids: list[str]\n ) -> dict[str, int]:\n if not uuids:\n return {}\n mappings = (\n self.db.query(ResourceMapping)\n .filter(\n ResourceMapping.environment_id == environment_id,\n ResourceMapping.resource_type == resource_type,\n ResourceMapping.uuid.in_(uuids),\n )\n .all()\n )\n result = {}\n for m in mappings:\n try:\n result[m.uuid] = int(m.remote_integer_id)\n except ValueError:\n pass\n return result\n # #endregion get_remote_ids_batch\n" }, { "contract_id": "middleware_package", "contract_type": "Package", "file_path": "backend/src/core/middleware/__init__.py", "start_line": 1, - "end_line": 5, + "end_line": 3, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "FastAPI/Starlette middleware package for request-level context and tracing.", - "SEMANTICS": [ - "middleware", - "package", - "starlette", - "fastapi" - ] + "COMPLEXITY": 1, + "PURPOSE": "FastAPI/Starlette middleware package for request-level context and tracing." }, "relations": [], "schema_warnings": [ @@ -36704,52 +33891,22 @@ "actual_complexity": 1, "contract_type": "Package" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Package'", - "detail": { - "actual_type": "Package", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Package' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Package" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:middleware_package:Package]\n# @COMPLEXITY: 1\n# @SEMANTICS: middleware, package, starlette, fastapi\n# @PURPOSE: FastAPI/Starlette middleware package for request-level context and tracing.\n# [/DEF:middleware_package:Package]\n" + "anchor_syntax": "region", + "body": "# #region middleware_package [C:1] [TYPE Package] [SEMANTICS fastapi, package, tracing]\n# @BRIEF FastAPI/Starlette middleware package for request-level context and tracing.\n# #endregion middleware_package\n" }, { "contract_id": "TraceContextMiddlewareModule", "contract_type": "Module", "file_path": "backend/src/core/middleware/trace.py", "start_line": 1, - "end_line": 77, + "end_line": 72, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "FastAPI/Starlette middleware that seeds a trace_id for every incoming HTTP request.", - "SEMANTICS": [ - "middleware", - "trace", - "context", - "starlette", - "fastapi" - ] + "COMPLEXITY": 3, + "PURPOSE": "FastAPI/Starlette middleware that seeds a trace_id for every incoming HTTP request." }, "relations": [], "schema_warnings": [ @@ -36763,62 +33920,32 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TraceContextMiddlewareModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: middleware, trace, context, starlette, fastapi\n# @PURPOSE: FastAPI/Starlette middleware that seeds a trace_id for every incoming HTTP request.\n# Optionally extracts X-Trace-ID header for cross-service trace propagation.\n# @LAYER: Core\n# @RELATION: [DEPENDS_ON] -> [CotLoggerModule]\n# @RELATION: [CALLED_BY] -> [AppModule]\n# @PRE: FastAPI app instance with Starlette middleware support.\n# @POST: Every request gets a trace_id via seed_trace_id(). Existing X-Trace-ID header is\n# preserved and used as the trace_id when present.\n# @SIDE_EFFECT: Sets ContextVar _trace_id for the duration of the request.\n\nfrom starlette.middleware.base import BaseHTTPMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.types import ASGIApp\n\nfrom ..cot_logger import seed_trace_id, set_trace_id\n\n# [DEF:TraceContextMiddleware:Class]\n# @COMPLEXITY: 2\n# @SEMANTICS: middleware, trace, context, dispatch\n# @BRIEF: Starlette BaseHTTPMiddleware that seeds a trace_id per request.\nclass TraceContextMiddleware(BaseHTTPMiddleware):\n \"\"\"FastAPI/Starlette middleware that seeds a trace_id for every request.\n\n If the incoming request carries an ``X-Trace-ID`` header, that value is\n used as the trace_id (enabling cross-service trace chaining). Otherwise a\n new UUID4 is generated.\n\n Usage::\n\n from backend.src.core.middleware.trace import TraceContextMiddleware\n app.add_middleware(TraceContextMiddleware)\n \"\"\"\n\n # [DEF:TraceContextMiddleware.__init__:Function]\n # @BRIEF: Standard BaseHTTPMiddleware initialiser.\n def __init__(self, app: ASGIApp) -> None:\n super().__init__(app)\n\n # [/DEF:TraceContextMiddleware.__init__:Function]\n\n # [DEF:TraceContextMiddleware.dispatch:Function]\n # @COMPLEXITY: 2\n # @SEMANTICS: dispatch, trace, seed, header, call_next\n # @BRIEF: Dispatch handler that seeds trace_id before passing to the next middleware.\n async def dispatch(\n self, request: Request, call_next\n ) -> Response:\n \"\"\"Intercept every request, seed the trace_id, and forward.\n\n If the client sent an ``X-Trace-ID`` header, use it; otherwise\n ``seed_trace_id()`` generates a new UUID4.\n\n Args:\n request: The incoming Starlette Request.\n call_next: The next middleware or route handler.\n\n Returns:\n Response from the downstream handler.\n \"\"\"\n incoming_trace_id = request.headers.get(\"X-Trace-ID\")\n if incoming_trace_id:\n set_trace_id(incoming_trace_id)\n else:\n seed_trace_id()\n\n response = await call_next(request)\n return response\n\n # [/DEF:TraceContextMiddleware.dispatch:Function]\n\n\n# [/DEF:TraceContextMiddleware:Class]\n# [/DEF:TraceContextMiddlewareModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region TraceContextMiddlewareModule [C:3] [TYPE Module] [SEMANTICS fastapi, middleware, trace, context, request]\n# @BRIEF FastAPI/Starlette middleware that seeds a trace_id for every incoming HTTP request.\n# Optionally extracts X-Trace-ID header for cross-service trace propagation.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [CotLoggerModule]\n# @RELATION CALLED_BY -> [AppModule]\n# @PRE: FastAPI app instance with Starlette middleware support.\n# @POST: Every request gets a trace_id via seed_trace_id(). Existing X-Trace-ID header is\n# preserved and used as the trace_id when present.\n# @SIDE_EFFECT: Sets ContextVar _trace_id for the duration of the request.\n\nfrom starlette.middleware.base import BaseHTTPMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.types import ASGIApp\n\nfrom ..cot_logger import seed_trace_id, set_trace_id\n\n\n# #region TraceContextMiddleware [C:2] [TYPE Class] [SEMANTICS middleware, trace, context, dispatch]\n# @BRIEF Starlette BaseHTTPMiddleware that seeds a trace_id per request.\nclass TraceContextMiddleware(BaseHTTPMiddleware):\n \"\"\"FastAPI/Starlette middleware that seeds a trace_id for every request.\n\n If the incoming request carries an ``X-Trace-ID`` header, that value is\n used as the trace_id (enabling cross-service trace chaining). Otherwise a\n new UUID4 is generated.\n\n Usage::\n\n from backend.src.core.middleware.trace import TraceContextMiddleware\n app.add_middleware(TraceContextMiddleware)\n \"\"\"\n\n # #region TraceContextMiddleware.__init__ [TYPE Function]\n # @BRIEF Standard BaseHTTPMiddleware initialiser.\n def __init__(self, app: ASGIApp) -> None:\n super().__init__(app)\n\n # #endregion TraceContextMiddleware.__init__\n\n # #region TraceContextMiddleware.dispatch [C:2] [TYPE Function] [SEMANTICS dispatch, trace, seed, header, call_next]\n # @BRIEF Dispatch handler that seeds trace_id before passing to the next middleware.\n async def dispatch(\n self, request: Request, call_next\n ) -> Response:\n \"\"\"Intercept every request, seed the trace_id, and forward.\n\n If the client sent an ``X-Trace-ID`` header, use it; otherwise\n ``seed_trace_id()`` generates a new UUID4.\n\n Args:\n request: The incoming Starlette Request.\n call_next: The next middleware or route handler.\n\n Returns:\n Response from the downstream handler.\n \"\"\"\n incoming_trace_id = request.headers.get(\"X-Trace-ID\")\n if incoming_trace_id:\n set_trace_id(incoming_trace_id)\n else:\n seed_trace_id()\n\n response = await call_next(request)\n return response\n\n # #endregion TraceContextMiddleware.dispatch\n\n\n# #endregion TraceContextMiddleware\n# #endregion TraceContextMiddlewareModule\n" }, { "contract_id": "TraceContextMiddleware", "contract_type": "Class", "file_path": "backend/src/core/middleware/trace.py", - "start_line": 21, - "end_line": 76, + "start_line": 20, + "end_line": 71, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Starlette BaseHTTPMiddleware that seeds a trace_id per request.", - "SEMANTICS": [ - "middleware", - "trace", - "context", - "dispatch" - ] + "COMPLEXITY": 2, + "PURPOSE": "Starlette BaseHTTPMiddleware that seeds a trace_id per request." }, "relations": [], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TraceContextMiddleware:Class]\n# @COMPLEXITY: 2\n# @SEMANTICS: middleware, trace, context, dispatch\n# @BRIEF: Starlette BaseHTTPMiddleware that seeds a trace_id per request.\nclass TraceContextMiddleware(BaseHTTPMiddleware):\n \"\"\"FastAPI/Starlette middleware that seeds a trace_id for every request.\n\n If the incoming request carries an ``X-Trace-ID`` header, that value is\n used as the trace_id (enabling cross-service trace chaining). Otherwise a\n new UUID4 is generated.\n\n Usage::\n\n from backend.src.core.middleware.trace import TraceContextMiddleware\n app.add_middleware(TraceContextMiddleware)\n \"\"\"\n\n # [DEF:TraceContextMiddleware.__init__:Function]\n # @BRIEF: Standard BaseHTTPMiddleware initialiser.\n def __init__(self, app: ASGIApp) -> None:\n super().__init__(app)\n\n # [/DEF:TraceContextMiddleware.__init__:Function]\n\n # [DEF:TraceContextMiddleware.dispatch:Function]\n # @COMPLEXITY: 2\n # @SEMANTICS: dispatch, trace, seed, header, call_next\n # @BRIEF: Dispatch handler that seeds trace_id before passing to the next middleware.\n async def dispatch(\n self, request: Request, call_next\n ) -> Response:\n \"\"\"Intercept every request, seed the trace_id, and forward.\n\n If the client sent an ``X-Trace-ID`` header, use it; otherwise\n ``seed_trace_id()`` generates a new UUID4.\n\n Args:\n request: The incoming Starlette Request.\n call_next: The next middleware or route handler.\n\n Returns:\n Response from the downstream handler.\n \"\"\"\n incoming_trace_id = request.headers.get(\"X-Trace-ID\")\n if incoming_trace_id:\n set_trace_id(incoming_trace_id)\n else:\n seed_trace_id()\n\n response = await call_next(request)\n return response\n\n # [/DEF:TraceContextMiddleware.dispatch:Function]\n\n\n# [/DEF:TraceContextMiddleware:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region TraceContextMiddleware [C:2] [TYPE Class] [SEMANTICS middleware, trace, context, dispatch]\n# @BRIEF Starlette BaseHTTPMiddleware that seeds a trace_id per request.\nclass TraceContextMiddleware(BaseHTTPMiddleware):\n \"\"\"FastAPI/Starlette middleware that seeds a trace_id for every request.\n\n If the incoming request carries an ``X-Trace-ID`` header, that value is\n used as the trace_id (enabling cross-service trace chaining). Otherwise a\n new UUID4 is generated.\n\n Usage::\n\n from backend.src.core.middleware.trace import TraceContextMiddleware\n app.add_middleware(TraceContextMiddleware)\n \"\"\"\n\n # #region TraceContextMiddleware.__init__ [TYPE Function]\n # @BRIEF Standard BaseHTTPMiddleware initialiser.\n def __init__(self, app: ASGIApp) -> None:\n super().__init__(app)\n\n # #endregion TraceContextMiddleware.__init__\n\n # #region TraceContextMiddleware.dispatch [C:2] [TYPE Function] [SEMANTICS dispatch, trace, seed, header, call_next]\n # @BRIEF Dispatch handler that seeds trace_id before passing to the next middleware.\n async def dispatch(\n self, request: Request, call_next\n ) -> Response:\n \"\"\"Intercept every request, seed the trace_id, and forward.\n\n If the client sent an ``X-Trace-ID`` header, use it; otherwise\n ``seed_trace_id()`` generates a new UUID4.\n\n Args:\n request: The incoming Starlette Request.\n call_next: The next middleware or route handler.\n\n Returns:\n Response from the downstream handler.\n \"\"\"\n incoming_trace_id = request.headers.get(\"X-Trace-ID\")\n if incoming_trace_id:\n set_trace_id(incoming_trace_id)\n else:\n seed_trace_id()\n\n response = await call_next(request)\n return response\n\n # #endregion TraceContextMiddleware.dispatch\n\n\n# #endregion TraceContextMiddleware\n" }, { "contract_id": "TraceContextMiddleware.__init__", "contract_type": "Function", "file_path": "backend/src/core/middleware/trace.py", - "start_line": 38, - "end_line": 43, + "start_line": 35, + "end_line": 40, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36836,56 +33963,25 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:TraceContextMiddleware.__init__:Function]\n # @BRIEF: Standard BaseHTTPMiddleware initialiser.\n def __init__(self, app: ASGIApp) -> None:\n super().__init__(app)\n\n # [/DEF:TraceContextMiddleware.__init__:Function]\n" + "anchor_syntax": "region", + "body": " # #region TraceContextMiddleware.__init__ [TYPE Function]\n # @BRIEF Standard BaseHTTPMiddleware initialiser.\n def __init__(self, app: ASGIApp) -> None:\n super().__init__(app)\n\n # #endregion TraceContextMiddleware.__init__\n" }, { "contract_id": "TraceContextMiddleware.dispatch", "contract_type": "Function", "file_path": "backend/src/core/middleware/trace.py", - "start_line": 45, - "end_line": 73, + "start_line": 42, + "end_line": 68, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Dispatch handler that seeds trace_id before passing to the next middleware.", - "SEMANTICS": [ - "dispatch", - "trace", - "seed", - "header", - "call_next" - ] + "COMPLEXITY": 2, + "PURPOSE": "Dispatch handler that seeds trace_id before passing to the next middleware." }, "relations": [], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:TraceContextMiddleware.dispatch:Function]\n # @COMPLEXITY: 2\n # @SEMANTICS: dispatch, trace, seed, header, call_next\n # @BRIEF: Dispatch handler that seeds trace_id before passing to the next middleware.\n async def dispatch(\n self, request: Request, call_next\n ) -> Response:\n \"\"\"Intercept every request, seed the trace_id, and forward.\n\n If the client sent an ``X-Trace-ID`` header, use it; otherwise\n ``seed_trace_id()`` generates a new UUID4.\n\n Args:\n request: The incoming Starlette Request.\n call_next: The next middleware or route handler.\n\n Returns:\n Response from the downstream handler.\n \"\"\"\n incoming_trace_id = request.headers.get(\"X-Trace-ID\")\n if incoming_trace_id:\n set_trace_id(incoming_trace_id)\n else:\n seed_trace_id()\n\n response = await call_next(request)\n return response\n\n # [/DEF:TraceContextMiddleware.dispatch:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": " # #region TraceContextMiddleware.dispatch [C:2] [TYPE Function] [SEMANTICS dispatch, trace, seed, header, call_next]\n # @BRIEF Dispatch handler that seeds trace_id before passing to the next middleware.\n async def dispatch(\n self, request: Request, call_next\n ) -> Response:\n \"\"\"Intercept every request, seed the trace_id, and forward.\n\n If the client sent an ``X-Trace-ID`` header, use it; otherwise\n ``seed_trace_id()`` generates a new UUID4.\n\n Args:\n request: The incoming Starlette Request.\n call_next: The next middleware or route handler.\n\n Returns:\n Response from the downstream handler.\n \"\"\"\n incoming_trace_id = request.headers.get(\"X-Trace-ID\")\n if incoming_trace_id:\n set_trace_id(incoming_trace_id)\n else:\n seed_trace_id()\n\n response = await call_next(request)\n return response\n\n # #endregion TraceContextMiddleware.dispatch\n" }, { "contract_id": "MigrationPackage", @@ -36898,29 +33994,22 @@ "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:MigrationPackage:Module]\n\nfrom .dry_run_orchestrator import MigrationDryRunService\nfrom .archive_parser import MigrationArchiveParser\n\n__all__ = [\"MigrationDryRunService\", \"MigrationArchiveParser\"]\n\n# [/DEF:MigrationPackage:Module]\n" + "anchor_syntax": "region", + "body": "# #region MigrationPackage [TYPE Module] [SEMANTICS migration, package, init]\n\nfrom .archive_parser import MigrationArchiveParser\nfrom .dry_run_orchestrator import MigrationDryRunService\n\n__all__ = [\"MigrationArchiveParser\", \"MigrationDryRunService\"]\n\n# #endregion MigrationPackage\n" }, { "contract_id": "MigrationArchiveParserModule", "contract_type": "Module", "file_path": "backend/src/core/migration/archive_parser.py", "start_line": 1, - "end_line": 158, + "end_line": 141, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Parsing is read-only and never mutates archive files.", "LAYER": "Core", - "PURPOSE": "Parse Superset export ZIP archives into normalized object catalogs for diffing.", - "SEMANTICS": [ - "migration", - "zip", - "parser", - "yaml", - "metadata" - ] + "PURPOSE": "Parse Superset export ZIP archives into normalized object catalogs for diffing." }, "relations": [ { @@ -36941,15 +34030,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationArchiveParserModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: migration, zip, parser, yaml, metadata\n# @PURPOSE: 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.\n\nimport json\nimport tempfile\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional\n\nimport yaml\n\nfrom ..logger import logger, belief_scope\n\n\n# [DEF:MigrationArchiveParser:Class]\n# @PURPOSE: 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 # [DEF:extract_objects_from_zip: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\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\n return result\n\n # [/DEF:extract_objects_from_zip:Function]\n\n # [DEF:_collect_yaml_objects: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, \"r\") 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 \"[MigrationArchiveParser._collect_yaml_objects][REFLECT] skip_invalid_yaml path=%s error=%s\",\n file_path,\n exc,\n )\n return objects\n\n # [/DEF:_collect_yaml_objects:Function]\n\n # [DEF:_normalize_object_payload: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 ) -> Optional[Dict[str, Any]]:\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\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\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\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\n return None\n\n # [/DEF:_normalize_object_payload:Function]\n\n\n# [/DEF:MigrationArchiveParser:Class]\n# [/DEF:MigrationArchiveParserModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "MigrationArchiveParser", "contract_type": "Class", "file_path": "backend/src/core/migration/archive_parser.py", - "start_line": 20, - "end_line": 157, + "start_line": 17, + "end_line": 140, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37049,15 +34138,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationArchiveParser:Class]\n# @PURPOSE: 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 # [DEF:extract_objects_from_zip: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\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\n return result\n\n # [/DEF:extract_objects_from_zip:Function]\n\n # [DEF:_collect_yaml_objects: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, \"r\") 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 \"[MigrationArchiveParser._collect_yaml_objects][REFLECT] skip_invalid_yaml path=%s error=%s\",\n file_path,\n exc,\n )\n return objects\n\n # [/DEF:_collect_yaml_objects:Function]\n\n # [DEF:_normalize_object_payload: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 ) -> Optional[Dict[str, Any]]:\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\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\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\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\n return None\n\n # [/DEF:_normalize_object_payload:Function]\n\n\n# [/DEF:MigrationArchiveParser:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "extract_objects_from_zip", "contract_type": "Function", "file_path": "backend/src/core/migration/archive_parser.py", - "start_line": 26, - "end_line": 54, + "start_line": 23, + "end_line": 48, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37118,15 +34207,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:extract_objects_from_zip: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\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\n return result\n\n # [/DEF:extract_objects_from_zip:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_collect_yaml_objects", "contract_type": "Function", "file_path": "backend/src/core/migration/archive_parser.py", - "start_line": 56, - "end_line": 84, + "start_line": 49, + "end_line": 75, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37180,15 +34269,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_collect_yaml_objects: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, \"r\") 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 \"[MigrationArchiveParser._collect_yaml_objects][REFLECT] skip_invalid_yaml path=%s error=%s\",\n file_path,\n exc,\n )\n return objects\n\n # [/DEF:_collect_yaml_objects:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_normalize_object_payload", "contract_type": "Function", "file_path": "backend/src/core/migration/archive_parser.py", - "start_line": 86, - "end_line": 154, + "start_line": 76, + "end_line": 139, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37226,29 +34315,22 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_object_payload: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 ) -> Optional[Dict[str, Any]]:\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\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\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\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\n return None\n\n # [/DEF:_normalize_object_payload:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "MigrationDryRunOrchestratorModule", "contract_type": "Module", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", "start_line": 1, - "end_line": 363, + "end_line": 339, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Dry run is informative only and must not mutate target environment.", "LAYER": "Core", - "PURPOSE": "Compute pre-flight migration diff and risk scoring without apply.", - "SEMANTICS": [ - "migration", - "dry_run", - "diff", - "risk", - "superset" - ] + "PURPOSE": "Compute pre-flight migration diff and risk scoring without apply." }, "relations": [ { @@ -37287,15 +34369,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationDryRunOrchestratorModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: migration, dry_run, diff, risk, superset\n# @PURPOSE: 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.\n\nfrom datetime import datetime, timezone\nimport json\nfrom typing import Any, Dict, List\n\nfrom sqlalchemy.orm import Session\n\nfrom ...models.dashboard import DashboardSelection\nfrom ...models.mapping import DatabaseMapping\nfrom ..logger import logger, belief_scope\nfrom .archive_parser import MigrationArchiveParser\nfrom .risk_assessor import build_risks, score_risks\nfrom ..migration_engine import MigrationEngine\nfrom ..superset_client import SupersetClient\nfrom ..utils.fileio import create_temp_file\n\n\n# [DEF:MigrationDryRunService:Class]\n# @PURPOSE: 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 # [DEF:__init__: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\n # [/DEF:__init__:Function]\n\n # [DEF:run: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.explore(\n \"[MigrationDryRunService.run][EXPLORE] starting dry-run pipeline\"\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\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\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:\n with 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\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\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\n logger.reason(\n \"[MigrationDryRunService.run][REASON] dry-run payload assembled\"\n )\n return {\n \"generated_at\": datetime.now(timezone.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\n # [/DEF:run:Function]\n\n # [DEF:_load_db_mapping:Function]\n # @PURPOSE: Resolve UUID mapping for optional DB config replacement.\n def _load_db_mapping(\n self, db: Session, selection: DashboardSelection\n ) -> Dict[str, str]:\n rows = (\n db.query(DatabaseMapping)\n .filter(\n DatabaseMapping.source_env_id == selection.source_env_id,\n DatabaseMapping.target_env_id == selection.target_env_id,\n )\n .all()\n )\n return {row.source_db_uuid: row.target_db_uuid for row in rows}\n\n # [/DEF:_load_db_mapping:Function]\n\n # [DEF:_accumulate_objects:Function]\n # @PURPOSE: Merge extracted resources by UUID to avoid duplicates.\n def _accumulate_objects(\n self,\n target: Dict[str, Dict[str, Dict[str, Any]]],\n source: Dict[str, List[Dict[str, Any]]],\n ) -> None:\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in source.get(object_type, []):\n uuid = item.get(\"uuid\")\n if uuid:\n target[object_type][str(uuid)] = item\n\n # [/DEF:_accumulate_objects:Function]\n\n # [DEF:_index_by_uuid:Function]\n # @PURPOSE: Build UUID-index map for normalized resources.\n def _index_by_uuid(\n self, objects: List[Dict[str, Any]]\n ) -> Dict[str, Dict[str, Any]]:\n indexed: Dict[str, Dict[str, Any]] = {}\n for obj in objects:\n uuid = obj.get(\"uuid\")\n if uuid:\n indexed[str(uuid)] = obj\n return indexed\n\n # [/DEF:_index_by_uuid:Function]\n\n # [DEF:_build_object_diff: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\n # [/DEF:_build_object_diff:Function]\n\n # [DEF:_build_target_signatures:Function]\n # @PURPOSE: Pull target metadata and normalize it into comparable signatures.\n def _build_target_signatures(\n self, client: SupersetClient\n ) -> Dict[str, List[Dict[str, Any]]]:\n _, dashboards = client.get_dashboards(\n query={\n \"columns\": [\n \"uuid\",\n \"dashboard_title\",\n \"slug\",\n \"position_json\",\n \"json_metadata\",\n \"description\",\n \"owners\",\n ],\n }\n )\n _, datasets = client.get_datasets(\n query={\n \"columns\": [\n \"uuid\",\n \"table_name\",\n \"schema\",\n \"database_uuid\",\n \"sql\",\n \"columns\",\n \"metrics\",\n ],\n }\n )\n _, charts = client.get_charts(\n query={\n \"columns\": [\n \"uuid\",\n \"slice_name\",\n \"viz_type\",\n \"params\",\n \"query_context\",\n \"datasource_uuid\",\n \"dataset_uuid\",\n ],\n }\n )\n return {\n \"dashboards\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"dashboard_title\"),\n \"owners\": item.get(\"owners\") or [],\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"dashboard_title\"),\n \"slug\": item.get(\"slug\"),\n \"position_json\": item.get(\"position_json\"),\n \"json_metadata\": item.get(\"json_metadata\"),\n \"description\": item.get(\"description\"),\n \"owners\": item.get(\"owners\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in dashboards\n if item.get(\"uuid\")\n ],\n \"datasets\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"table_name\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"table_name\"),\n \"schema\": item.get(\"schema\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"sql\": item.get(\"sql\"),\n \"columns\": item.get(\"columns\"),\n \"metrics\": item.get(\"metrics\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in datasets\n if item.get(\"uuid\")\n ],\n \"charts\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"dataset_uuid\": item.get(\"datasource_uuid\")\n or item.get(\"dataset_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"viz_type\": item.get(\"viz_type\"),\n \"params\": item.get(\"params\"),\n \"query_context\": item.get(\"query_context\"),\n \"datasource_uuid\": item.get(\"datasource_uuid\"),\n \"dataset_uuid\": item.get(\"dataset_uuid\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in charts\n if item.get(\"uuid\")\n ],\n }\n\n # [/DEF:_build_target_signatures:Function]\n\n # [DEF:_build_risks:Function]\n # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch.\n def _build_risks(\n self,\n source_objects: Dict[str, List[Dict[str, Any]]],\n target_objects: Dict[str, List[Dict[str, Any]]],\n diff: Dict[str, Dict[str, List[Dict[str, Any]]]],\n target_client: SupersetClient,\n ) -> List[Dict[str, Any]]:\n return build_risks(source_objects, target_objects, diff, target_client)\n\n # [/DEF:_build_risks:Function]\n\n\n# [/DEF:MigrationDryRunService:Class]\n# [/DEF:MigrationDryRunOrchestratorModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "MigrationDryRunService", "contract_type": "Class", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 28, - "end_line": 362, + "start_line": 25, + "end_line": 338, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37515,15 +34597,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationDryRunService:Class]\n# @PURPOSE: 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 # [DEF:__init__: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\n # [/DEF:__init__:Function]\n\n # [DEF:run: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.explore(\n \"[MigrationDryRunService.run][EXPLORE] starting dry-run pipeline\"\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\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\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:\n with 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\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\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\n logger.reason(\n \"[MigrationDryRunService.run][REASON] dry-run payload assembled\"\n )\n return {\n \"generated_at\": datetime.now(timezone.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\n # [/DEF:run:Function]\n\n # [DEF:_load_db_mapping:Function]\n # @PURPOSE: Resolve UUID mapping for optional DB config replacement.\n def _load_db_mapping(\n self, db: Session, selection: DashboardSelection\n ) -> Dict[str, str]:\n rows = (\n db.query(DatabaseMapping)\n .filter(\n DatabaseMapping.source_env_id == selection.source_env_id,\n DatabaseMapping.target_env_id == selection.target_env_id,\n )\n .all()\n )\n return {row.source_db_uuid: row.target_db_uuid for row in rows}\n\n # [/DEF:_load_db_mapping:Function]\n\n # [DEF:_accumulate_objects:Function]\n # @PURPOSE: Merge extracted resources by UUID to avoid duplicates.\n def _accumulate_objects(\n self,\n target: Dict[str, Dict[str, Dict[str, Any]]],\n source: Dict[str, List[Dict[str, Any]]],\n ) -> None:\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in source.get(object_type, []):\n uuid = item.get(\"uuid\")\n if uuid:\n target[object_type][str(uuid)] = item\n\n # [/DEF:_accumulate_objects:Function]\n\n # [DEF:_index_by_uuid:Function]\n # @PURPOSE: Build UUID-index map for normalized resources.\n def _index_by_uuid(\n self, objects: List[Dict[str, Any]]\n ) -> Dict[str, Dict[str, Any]]:\n indexed: Dict[str, Dict[str, Any]] = {}\n for obj in objects:\n uuid = obj.get(\"uuid\")\n if uuid:\n indexed[str(uuid)] = obj\n return indexed\n\n # [/DEF:_index_by_uuid:Function]\n\n # [DEF:_build_object_diff: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\n # [/DEF:_build_object_diff:Function]\n\n # [DEF:_build_target_signatures:Function]\n # @PURPOSE: Pull target metadata and normalize it into comparable signatures.\n def _build_target_signatures(\n self, client: SupersetClient\n ) -> Dict[str, List[Dict[str, Any]]]:\n _, dashboards = client.get_dashboards(\n query={\n \"columns\": [\n \"uuid\",\n \"dashboard_title\",\n \"slug\",\n \"position_json\",\n \"json_metadata\",\n \"description\",\n \"owners\",\n ],\n }\n )\n _, datasets = client.get_datasets(\n query={\n \"columns\": [\n \"uuid\",\n \"table_name\",\n \"schema\",\n \"database_uuid\",\n \"sql\",\n \"columns\",\n \"metrics\",\n ],\n }\n )\n _, charts = client.get_charts(\n query={\n \"columns\": [\n \"uuid\",\n \"slice_name\",\n \"viz_type\",\n \"params\",\n \"query_context\",\n \"datasource_uuid\",\n \"dataset_uuid\",\n ],\n }\n )\n return {\n \"dashboards\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"dashboard_title\"),\n \"owners\": item.get(\"owners\") or [],\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"dashboard_title\"),\n \"slug\": item.get(\"slug\"),\n \"position_json\": item.get(\"position_json\"),\n \"json_metadata\": item.get(\"json_metadata\"),\n \"description\": item.get(\"description\"),\n \"owners\": item.get(\"owners\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in dashboards\n if item.get(\"uuid\")\n ],\n \"datasets\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"table_name\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"table_name\"),\n \"schema\": item.get(\"schema\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"sql\": item.get(\"sql\"),\n \"columns\": item.get(\"columns\"),\n \"metrics\": item.get(\"metrics\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in datasets\n if item.get(\"uuid\")\n ],\n \"charts\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"dataset_uuid\": item.get(\"datasource_uuid\")\n or item.get(\"dataset_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"viz_type\": item.get(\"viz_type\"),\n \"params\": item.get(\"params\"),\n \"query_context\": item.get(\"query_context\"),\n \"datasource_uuid\": item.get(\"datasource_uuid\"),\n \"dataset_uuid\": item.get(\"dataset_uuid\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in charts\n if item.get(\"uuid\")\n ],\n }\n\n # [/DEF:_build_target_signatures:Function]\n\n # [DEF:_build_risks:Function]\n # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch.\n def _build_risks(\n self,\n source_objects: Dict[str, List[Dict[str, Any]]],\n target_objects: Dict[str, List[Dict[str, Any]]],\n diff: Dict[str, Dict[str, List[Dict[str, Any]]]],\n target_client: SupersetClient,\n ) -> List[Dict[str, Any]]:\n return build_risks(source_objects, target_objects, diff, target_client)\n\n # [/DEF:_build_risks:Function]\n\n\n# [/DEF:MigrationDryRunService:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_load_db_mapping", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 161, - "end_line": 176, + "start_line": 150, + "end_line": 164, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37541,15 +34623,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_load_db_mapping:Function]\n # @PURPOSE: Resolve UUID mapping for optional DB config replacement.\n def _load_db_mapping(\n self, db: Session, selection: DashboardSelection\n ) -> Dict[str, str]:\n rows = (\n db.query(DatabaseMapping)\n .filter(\n DatabaseMapping.source_env_id == selection.source_env_id,\n DatabaseMapping.target_env_id == selection.target_env_id,\n )\n .all()\n )\n return {row.source_db_uuid: row.target_db_uuid for row in rows}\n\n # [/DEF:_load_db_mapping:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_accumulate_objects", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 178, - "end_line": 191, + "start_line": 165, + "end_line": 177, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37567,15 +34649,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_accumulate_objects:Function]\n # @PURPOSE: Merge extracted resources by UUID to avoid duplicates.\n def _accumulate_objects(\n self,\n target: Dict[str, Dict[str, Dict[str, Any]]],\n source: Dict[str, List[Dict[str, Any]]],\n ) -> None:\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in source.get(object_type, []):\n uuid = item.get(\"uuid\")\n if uuid:\n target[object_type][str(uuid)] = item\n\n # [/DEF:_accumulate_objects:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_index_by_uuid", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 193, - "end_line": 205, + "start_line": 178, + "end_line": 189, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37593,15 +34675,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_index_by_uuid:Function]\n # @PURPOSE: Build UUID-index map for normalized resources.\n def _index_by_uuid(\n self, objects: List[Dict[str, Any]]\n ) -> Dict[str, Dict[str, Any]]:\n indexed: Dict[str, Dict[str, Any]] = {}\n for obj in objects:\n uuid = obj.get(\"uuid\")\n if uuid:\n indexed[str(uuid)] = obj\n return indexed\n\n # [/DEF:_index_by_uuid:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_build_object_diff", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 207, - "end_line": 233, + "start_line": 190, + "end_line": 215, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37635,15 +34717,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_build_object_diff: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\n # [/DEF:_build_object_diff:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_build_target_signatures", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 235, - "end_line": 346, + "start_line": 216, + "end_line": 326, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37661,15 +34743,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_build_target_signatures:Function]\n # @PURPOSE: Pull target metadata and normalize it into comparable signatures.\n def _build_target_signatures(\n self, client: SupersetClient\n ) -> Dict[str, List[Dict[str, Any]]]:\n _, dashboards = client.get_dashboards(\n query={\n \"columns\": [\n \"uuid\",\n \"dashboard_title\",\n \"slug\",\n \"position_json\",\n \"json_metadata\",\n \"description\",\n \"owners\",\n ],\n }\n )\n _, datasets = client.get_datasets(\n query={\n \"columns\": [\n \"uuid\",\n \"table_name\",\n \"schema\",\n \"database_uuid\",\n \"sql\",\n \"columns\",\n \"metrics\",\n ],\n }\n )\n _, charts = client.get_charts(\n query={\n \"columns\": [\n \"uuid\",\n \"slice_name\",\n \"viz_type\",\n \"params\",\n \"query_context\",\n \"datasource_uuid\",\n \"dataset_uuid\",\n ],\n }\n )\n return {\n \"dashboards\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"dashboard_title\"),\n \"owners\": item.get(\"owners\") or [],\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"dashboard_title\"),\n \"slug\": item.get(\"slug\"),\n \"position_json\": item.get(\"position_json\"),\n \"json_metadata\": item.get(\"json_metadata\"),\n \"description\": item.get(\"description\"),\n \"owners\": item.get(\"owners\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in dashboards\n if item.get(\"uuid\")\n ],\n \"datasets\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"table_name\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"table_name\"),\n \"schema\": item.get(\"schema\"),\n \"database_uuid\": item.get(\"database_uuid\"),\n \"sql\": item.get(\"sql\"),\n \"columns\": item.get(\"columns\"),\n \"metrics\": item.get(\"metrics\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in datasets\n if item.get(\"uuid\")\n ],\n \"charts\": [\n {\n \"uuid\": str(item.get(\"uuid\")),\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"dataset_uuid\": item.get(\"datasource_uuid\")\n or item.get(\"dataset_uuid\"),\n \"signature\": json.dumps(\n {\n \"title\": item.get(\"slice_name\") or item.get(\"name\"),\n \"viz_type\": item.get(\"viz_type\"),\n \"params\": item.get(\"params\"),\n \"query_context\": item.get(\"query_context\"),\n \"datasource_uuid\": item.get(\"datasource_uuid\"),\n \"dataset_uuid\": item.get(\"dataset_uuid\"),\n },\n sort_keys=True,\n default=str,\n ),\n }\n for item in charts\n if item.get(\"uuid\")\n ],\n }\n\n # [/DEF:_build_target_signatures:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_build_risks", "contract_type": "Function", "file_path": "backend/src/core/migration/dry_run_orchestrator.py", - "start_line": 348, - "end_line": 359, + "start_line": 327, + "end_line": 337, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37687,32 +34769,25 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_build_risks:Function]\n # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch.\n def _build_risks(\n self,\n source_objects: Dict[str, List[Dict[str, Any]]],\n target_objects: Dict[str, List[Dict[str, Any]]],\n diff: Dict[str, Dict[str, List[Dict[str, Any]]]],\n target_client: SupersetClient,\n ) -> List[Dict[str, Any]]:\n return build_risks(source_objects, target_objects, diff, target_client)\n\n # [/DEF:_build_risks:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "RiskAssessorModule", "contract_type": "Module", "file_path": "backend/src/core/migration/risk_assessor.py", "start_line": 1, - "end_line": 205, + "end_line": 200, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Module[build_risks, score_risks]", "INVARIANT": "Risk scoring must remain bounded to [0,100] and preserve severity-to-weight mapping.", "LAYER": "Domain", "POST": "Risk scoring output preserves item list and provides bounded score with derived level.", "PRE": "Risk assessor functions receive normalized migration object collections from dry-run orchestration.", "PURPOSE": "Compute deterministic migration risk items and aggregate score for dry-run reporting.", - "SEMANTICS": [ - "migration", - "dry_run", - "risk", - "scoring", - "preflight" - ], "SIDE_EFFECT": "Emits diagnostic logs and performs read-only metadata requests via Superset client.", "TEST_CONTRACT": "[source_objects,target_objects,diff,target_client] -> [List[RiskItem]]", "TEST_EDGE": "[external_fail] -> [target_client get_databases exception propagates to caller]", @@ -37990,15 +35065,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RiskAssessorModule:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: migration, dry_run, risk, scoring, preflight\n# @PURPOSE: Compute deterministic migration risk items and aggregate score for dry-run reporting.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [SupersetClient]\n# @RELATION: CONTAINS -> [index_by_uuid]\n# @RELATION: CONTAINS -> [extract_owner_identifiers]\n# @RELATION: CONTAINS -> [build_risks]\n# @RELATION: CONTAINS -> [score_risks]\n# @INVARIANT: Risk scoring must remain bounded to [0,100] and preserve severity-to-weight mapping.\n# @PRE: Risk assessor functions receive normalized migration object collections from dry-run orchestration.\n# @POST: Risk scoring output preserves item list and provides bounded score with derived level.\n# @SIDE_EFFECT: Emits diagnostic logs and performs read-only metadata requests via Superset client.\n# @DATA_CONTRACT: Module[build_risks, score_risks]\n# @TEST_CONTRACT: [source_objects,target_objects,diff,target_client] -> [List[RiskItem]]\n# @TEST_SCENARIO: [overwrite_update_objects] -> [medium overwrite_existing risk is emitted for each update diff item]\n# @TEST_SCENARIO: [missing_datasource_dataset] -> [high missing_datasource risk is emitted]\n# @TEST_SCENARIO: [owner_mismatch_dashboard] -> [low owner_mismatch risk is emitted]\n# @TEST_EDGE: [missing_field] -> [object without uuid is ignored by indexer]\n# @TEST_EDGE: [invalid_type] -> [non-list owners input normalizes to empty identifiers]\n# @TEST_EDGE: [external_fail] -> [target_client get_databases exception propagates to caller]\n# @TEST_INVARIANT: [score_upper_bound_100] -> VERIFIED_BY: [severity_weight_aggregation]\n# @UX_STATE: [Idle] -> [N/A backend domain module]\n# @UX_FEEDBACK: [N/A] -> [No direct UI side effects in this module]\n# @UX_RECOVERY: [N/A] -> [Caller-level retry/recovery]\n# @UX_REACTIVITY: [N/A] -> [Backend synchronous function contracts]\n\nfrom typing import Any, Dict, List\n\nfrom ..logger import belief_scope\nfrom ..cot_logger import MarkerLogger\nfrom ..superset_client import SupersetClient\n\nlog = MarkerLogger(\"RiskAssessor\")\n\n\n# [DEF:index_by_uuid:Function]\n# @PURPOSE: Build UUID-index from normalized objects.\n# @PRE: Input list items are dict-like payloads potentially containing \"uuid\".\n# @POST: Returns mapping keyed by string uuid; only truthy uuid values are included.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Dict[str, Any]]\ndef index_by_uuid(objects: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:\n with belief_scope(\"risk_assessor.index_by_uuid\"):\n log.reason(\"Building UUID index\", payload={\"objects_count\": len(objects)})\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 log.reflect(\"UUID index built\", payload={\"indexed_count\": len(indexed)})\n return indexed\n\n\n# [/DEF:index_by_uuid:Function]\n\n\n# [DEF:extract_owner_identifiers:Function]\n# @PURPOSE: Normalize owner payloads for stable comparison.\n# @PRE: Owners may be list payload, scalar values, or None.\n# @POST: Returns sorted unique owner identifiers as strings.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: Any -> List[str]\ndef extract_owner_identifiers(owners: Any) -> List[str]:\n with belief_scope(\"risk_assessor.extract_owner_identifiers\"):\n log.reason(\"Normalizing owner identifiers\")\n if not isinstance(owners, list):\n log.reflect(\"Owners payload is not list; returning empty identifiers\")\n return []\n ids: List[str] = []\n for owner in owners:\n if isinstance(owner, dict):\n if owner.get(\"username\"):\n ids.append(str(owner[\"username\"]))\n elif owner.get(\"id\") is not None:\n ids.append(str(owner[\"id\"]))\n elif owner is not None:\n ids.append(str(owner))\n normalized_ids = sorted(set(ids))\n log.reflect(\n \"Owner identifiers normalized\", payload={\"owner_count\": len(normalized_ids)}\n )\n return normalized_ids\n\n\n# [/DEF:extract_owner_identifiers:Function]\n\n\n# [DEF:build_risks:Function]\n# @PURPOSE: Build risk list from computed diffs and target catalog state.\n# @RELATION: DEPENDS_ON -> [index_by_uuid]\n# @RELATION: DEPENDS_ON -> [extract_owner_identifiers]\n# @PRE: source_objects/target_objects/diff contain dashboards/charts/datasets keys with expected list structures.\n# @PRE: target_client is authenticated/usable for database list retrieval.\n# @POST: Returns list of deterministic risk items derived from overwrite, missing datasource, reference, and owner mismatch checks.\n# @SIDE_EFFECT: Calls target Superset API for databases metadata and emits logs.\n# @DATA_CONTRACT: (\n# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],\n# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],\n# @DATA_CONTRACT: Dict[str, Dict[str, List[Dict[str, Any]]]],\n# @DATA_CONTRACT: SupersetClient\n# @DATA_CONTRACT: ) -> List[Dict[str, Any]]\ndef build_risks(\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 with belief_scope(\"risk_assessor.build_risks\"):\n log.reason(\"Building migration risks from diff payload\")\n risks: List[Dict[str, Any]] = []\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in diff[object_type][\"update\"]:\n risks.append(\n {\n \"code\": \"overwrite_existing\",\n \"severity\": \"medium\",\n \"object_type\": object_type[:-1],\n \"object_uuid\": item[\"uuid\"],\n \"message\": f\"Object will be updated in target: {item.get('title') or item['uuid']}\",\n }\n )\n\n target_dataset_uuids = set(index_by_uuid(target_objects[\"datasets\"]).keys())\n _, target_databases = target_client.get_databases(query={\"columns\": [\"uuid\"]})\n target_database_uuids = {\n str(item.get(\"uuid\")) for item in target_databases if item.get(\"uuid\")\n }\n\n for dataset in source_objects[\"datasets\"]:\n db_uuid = dataset.get(\"database_uuid\")\n if db_uuid and str(db_uuid) not in target_database_uuids:\n risks.append(\n {\n \"code\": \"missing_datasource\",\n \"severity\": \"high\",\n \"object_type\": \"dataset\",\n \"object_uuid\": dataset.get(\"uuid\"),\n \"message\": f\"Target datasource is missing for dataset {dataset.get('title') or dataset.get('uuid')}\",\n }\n )\n\n for chart in source_objects[\"charts\"]:\n ds_uuid = chart.get(\"dataset_uuid\")\n if ds_uuid and str(ds_uuid) not in target_dataset_uuids:\n risks.append(\n {\n \"code\": \"breaking_reference\",\n \"severity\": \"high\",\n \"object_type\": \"chart\",\n \"object_uuid\": chart.get(\"uuid\"),\n \"message\": f\"Chart references dataset not found on target: {ds_uuid}\",\n }\n )\n\n source_dash = index_by_uuid(source_objects[\"dashboards\"])\n target_dash = index_by_uuid(target_objects[\"dashboards\"])\n for item in diff[\"dashboards\"][\"update\"]:\n source_obj = source_dash.get(item[\"uuid\"])\n target_obj = target_dash.get(item[\"uuid\"])\n if not source_obj or not target_obj:\n continue\n source_owners = extract_owner_identifiers(source_obj.get(\"owners\"))\n target_owners = extract_owner_identifiers(target_obj.get(\"owners\"))\n if source_owners and target_owners and source_owners != target_owners:\n risks.append(\n {\n \"code\": \"owner_mismatch\",\n \"severity\": \"low\",\n \"object_type\": \"dashboard\",\n \"object_uuid\": item[\"uuid\"],\n \"message\": f\"Owner mismatch for dashboard {item.get('title') or item['uuid']}\",\n }\n )\n log.reflect(\"Risk list assembled\", payload={\"risk_count\": len(risks)})\n return risks\n\n\n# [/DEF:build_risks:Function]\n\n\n# [DEF:score_risks:Function]\n# @PURPOSE: Aggregate risk list into score and level.\n# @PRE: risk_items contains optional severity fields expected in {high,medium,low} or defaults to low weight.\n# @POST: Returns dict with score in [0,100], derived level, and original items.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Any]\ndef score_risks(risk_items: List[Dict[str, Any]]) -> Dict[str, Any]:\n with belief_scope(\"risk_assessor.score_risks\"):\n log.reason(\"Scoring risk items\", payload={\"risk_items_count\": len(risk_items)})\n weights = {\"high\": 25, \"medium\": 10, \"low\": 5}\n score = min(\n 100, sum(weights.get(item.get(\"severity\", \"low\"), 5) for item in risk_items)\n )\n level = \"low\" if score < 25 else \"medium\" if score < 60 else \"high\"\n result = {\"score\": score, \"level\": level, \"items\": risk_items}\n log.reflect(\"Risk score computed\", payload={\"score\": score, \"level\": level})\n return result\n\n\n# [/DEF:score_risks:Function]\n\n\n# [/DEF:RiskAssessorModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region RiskAssessorModule [C:5] [TYPE Module] [SEMANTICS tenacity, migration, dry-run]\n# @BRIEF Compute deterministic migration risk items and aggregate score for dry-run reporting.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION CONTAINS -> [index_by_uuid]\n# @RELATION CONTAINS -> [extract_owner_identifiers]\n# @RELATION CONTAINS -> [build_risks]\n# @RELATION CONTAINS -> [score_risks]\n# @INVARIANT: Risk scoring must remain bounded to [0,100] and preserve severity-to-weight mapping.\n# @PRE: Risk assessor functions receive normalized migration object collections from dry-run orchestration.\n# @POST: Risk scoring output preserves item list and provides bounded score with derived level.\n# @SIDE_EFFECT: Emits diagnostic logs and performs read-only metadata requests via Superset client.\n# @DATA_CONTRACT: Module[build_risks, score_risks]\n# @TEST_CONTRACT: [source_objects,target_objects,diff,target_client] -> [List[RiskItem]]\n# @TEST_SCENARIO: [overwrite_update_objects] -> [medium overwrite_existing risk is emitted for each update diff item]\n# @TEST_SCENARIO: [missing_datasource_dataset] -> [high missing_datasource risk is emitted]\n# @TEST_SCENARIO: [owner_mismatch_dashboard] -> [low owner_mismatch risk is emitted]\n# @TEST_EDGE: [missing_field] -> [object without uuid is ignored by indexer]\n# @TEST_EDGE: [invalid_type] -> [non-list owners input normalizes to empty identifiers]\n# @TEST_EDGE: [external_fail] -> [target_client get_databases exception propagates to caller]\n# @TEST_INVARIANT: [score_upper_bound_100] -> VERIFIED_BY: [severity_weight_aggregation]\n# @UX_STATE: [Idle] -> [N/A backend domain module]\n# @UX_FEEDBACK: [N/A] -> [No direct UI side effects in this module]\n# @UX_RECOVERY: [N/A] -> [Caller-level retry/recovery]\n# @UX_REACTIVITY: [N/A] -> [Backend synchronous function contracts]\n\nfrom typing import Any\n\nfrom ..logger import belief_scope, logger\nfrom ..superset_client import SupersetClient\n\n\n# #region index_by_uuid [TYPE Function]\n# @BRIEF Build UUID-index from normalized objects.\n# @PRE: Input list items are dict-like payloads potentially containing \"uuid\".\n# @POST: Returns mapping keyed by string uuid; only truthy uuid values are included.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Dict[str, Any]]\ndef index_by_uuid(objects: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:\n with belief_scope(\"risk_assessor.index_by_uuid\"):\n logger.reason(\"Building UUID index\", extra={\"objects_count\": len(objects)})\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 logger.reflect(\"UUID index built\", extra={\"indexed_count\": len(indexed)})\n return indexed\n\n\n# #endregion index_by_uuid\n\n\n# #region extract_owner_identifiers [TYPE Function]\n# @BRIEF Normalize owner payloads for stable comparison.\n# @PRE: Owners may be list payload, scalar values, or None.\n# @POST: Returns sorted unique owner identifiers as strings.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: Any -> List[str]\ndef extract_owner_identifiers(owners: Any) -> list[str]:\n with belief_scope(\"risk_assessor.extract_owner_identifiers\"):\n logger.reason(\"Normalizing owner identifiers\")\n if not isinstance(owners, list):\n logger.reflect(\"Owners payload is not list; returning empty identifiers\")\n return []\n ids: list[str] = []\n for owner in owners:\n if isinstance(owner, dict):\n if owner.get(\"username\"):\n ids.append(str(owner[\"username\"]))\n elif owner.get(\"id\") is not None:\n ids.append(str(owner[\"id\"]))\n elif owner is not None:\n ids.append(str(owner))\n normalized_ids = sorted(set(ids))\n logger.reflect(\n \"Owner identifiers normalized\", extra={\"owner_count\": len(normalized_ids)}\n )\n return normalized_ids\n\n\n# #endregion extract_owner_identifiers\n\n\n# #region build_risks [TYPE Function]\n# @BRIEF Build risk list from computed diffs and target catalog state.\n# @RELATION DEPENDS_ON -> [index_by_uuid]\n# @RELATION DEPENDS_ON -> [extract_owner_identifiers]\n# @PRE: source_objects/target_objects/diff contain dashboards/charts/datasets keys with expected list structures.\n# @PRE: target_client is authenticated/usable for database list retrieval.\n# @POST: Returns list of deterministic risk items derived from overwrite, missing datasource, reference, and owner mismatch checks.\n# @SIDE_EFFECT: Calls target Superset API for databases metadata and emits logs.\n# @DATA_CONTRACT: (\n# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],\n# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],\n# @DATA_CONTRACT: Dict[str, Dict[str, List[Dict[str, Any]]]],\n# @DATA_CONTRACT: SupersetClient\n# @DATA_CONTRACT: ) -> List[Dict[str, Any]]\ndef build_risks(\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 with belief_scope(\"risk_assessor.build_risks\"):\n logger.reason(\"Building migration risks from diff payload\")\n risks: list[dict[str, Any]] = []\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in diff[object_type][\"update\"]:\n risks.append(\n {\n \"code\": \"overwrite_existing\",\n \"severity\": \"medium\",\n \"object_type\": object_type[:-1],\n \"object_uuid\": item[\"uuid\"],\n \"message\": f\"Object will be updated in target: {item.get('title') or item['uuid']}\",\n }\n )\n\n target_dataset_uuids = set(index_by_uuid(target_objects[\"datasets\"]).keys())\n _, target_databases = target_client.get_databases(query={\"columns\": [\"uuid\"]})\n target_database_uuids = {\n str(item.get(\"uuid\")) for item in target_databases if item.get(\"uuid\")\n }\n\n for dataset in source_objects[\"datasets\"]:\n db_uuid = dataset.get(\"database_uuid\")\n if db_uuid and str(db_uuid) not in target_database_uuids:\n risks.append(\n {\n \"code\": \"missing_datasource\",\n \"severity\": \"high\",\n \"object_type\": \"dataset\",\n \"object_uuid\": dataset.get(\"uuid\"),\n \"message\": f\"Target datasource is missing for dataset {dataset.get('title') or dataset.get('uuid')}\",\n }\n )\n\n for chart in source_objects[\"charts\"]:\n ds_uuid = chart.get(\"dataset_uuid\")\n if ds_uuid and str(ds_uuid) not in target_dataset_uuids:\n risks.append(\n {\n \"code\": \"breaking_reference\",\n \"severity\": \"high\",\n \"object_type\": \"chart\",\n \"object_uuid\": chart.get(\"uuid\"),\n \"message\": f\"Chart references dataset not found on target: {ds_uuid}\",\n }\n )\n\n source_dash = index_by_uuid(source_objects[\"dashboards\"])\n target_dash = index_by_uuid(target_objects[\"dashboards\"])\n for item in diff[\"dashboards\"][\"update\"]:\n source_obj = source_dash.get(item[\"uuid\"])\n target_obj = target_dash.get(item[\"uuid\"])\n if not source_obj or not target_obj:\n continue\n source_owners = extract_owner_identifiers(source_obj.get(\"owners\"))\n target_owners = extract_owner_identifiers(target_obj.get(\"owners\"))\n if source_owners and target_owners and source_owners != target_owners:\n risks.append(\n {\n \"code\": \"owner_mismatch\",\n \"severity\": \"low\",\n \"object_type\": \"dashboard\",\n \"object_uuid\": item[\"uuid\"],\n \"message\": f\"Owner mismatch for dashboard {item.get('title') or item['uuid']}\",\n }\n )\n logger.reflect(\"Risk list assembled\", extra={\"risk_count\": len(risks)})\n return risks\n\n\n# #endregion build_risks\n\n\n# #region score_risks [TYPE Function]\n# @BRIEF Aggregate risk list into score and level.\n# @PRE: risk_items contains optional severity fields expected in {high,medium,low} or defaults to low weight.\n# @POST: Returns dict with score in [0,100], derived level, and original items.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Any]\ndef score_risks(risk_items: list[dict[str, Any]]) -> dict[str, Any]:\n with belief_scope(\"risk_assessor.score_risks\"):\n logger.reason(\"Scoring risk items\", extra={\"risk_items_count\": len(risk_items)})\n weights = {\"high\": 25, \"medium\": 10, \"low\": 5}\n score = min(\n 100, sum(weights.get(item.get(\"severity\", \"low\"), 5) for item in risk_items)\n )\n level = \"low\" if score < 25 else \"medium\" if score < 60 else \"high\"\n result = {\"score\": score, \"level\": level, \"items\": risk_items}\n logger.reflect(\"Risk score computed\", extra={\"score\": score, \"level\": level})\n return result\n\n\n# #endregion score_risks\n\n\n# #endregion RiskAssessorModule\n" }, { "contract_id": "index_by_uuid", "contract_type": "Function", "file_path": "backend/src/core/migration/risk_assessor.py", - "start_line": 38, - "end_line": 56, + "start_line": 33, + "end_line": 51, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38056,15 +35131,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:index_by_uuid:Function]\n# @PURPOSE: Build UUID-index from normalized objects.\n# @PRE: Input list items are dict-like payloads potentially containing \"uuid\".\n# @POST: Returns mapping keyed by string uuid; only truthy uuid values are included.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Dict[str, Any]]\ndef index_by_uuid(objects: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:\n with belief_scope(\"risk_assessor.index_by_uuid\"):\n log.reason(\"Building UUID index\", payload={\"objects_count\": len(objects)})\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 log.reflect(\"UUID index built\", payload={\"indexed_count\": len(indexed)})\n return indexed\n\n\n# [/DEF:index_by_uuid:Function]\n" + "anchor_syntax": "region", + "body": "# #region index_by_uuid [TYPE Function]\n# @BRIEF Build UUID-index from normalized objects.\n# @PRE: Input list items are dict-like payloads potentially containing \"uuid\".\n# @POST: Returns mapping keyed by string uuid; only truthy uuid values are included.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Dict[str, Any]]\ndef index_by_uuid(objects: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:\n with belief_scope(\"risk_assessor.index_by_uuid\"):\n logger.reason(\"Building UUID index\", extra={\"objects_count\": len(objects)})\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 logger.reflect(\"UUID index built\", extra={\"indexed_count\": len(indexed)})\n return indexed\n\n\n# #endregion index_by_uuid\n" }, { "contract_id": "extract_owner_identifiers", "contract_type": "Function", "file_path": "backend/src/core/migration/risk_assessor.py", - "start_line": 59, - "end_line": 87, + "start_line": 54, + "end_line": 82, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38122,15 +35197,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:extract_owner_identifiers:Function]\n# @PURPOSE: Normalize owner payloads for stable comparison.\n# @PRE: Owners may be list payload, scalar values, or None.\n# @POST: Returns sorted unique owner identifiers as strings.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: Any -> List[str]\ndef extract_owner_identifiers(owners: Any) -> List[str]:\n with belief_scope(\"risk_assessor.extract_owner_identifiers\"):\n log.reason(\"Normalizing owner identifiers\")\n if not isinstance(owners, list):\n log.reflect(\"Owners payload is not list; returning empty identifiers\")\n return []\n ids: List[str] = []\n for owner in owners:\n if isinstance(owner, dict):\n if owner.get(\"username\"):\n ids.append(str(owner[\"username\"]))\n elif owner.get(\"id\") is not None:\n ids.append(str(owner[\"id\"]))\n elif owner is not None:\n ids.append(str(owner))\n normalized_ids = sorted(set(ids))\n log.reflect(\n \"Owner identifiers normalized\", payload={\"owner_count\": len(normalized_ids)}\n )\n return normalized_ids\n\n\n# [/DEF:extract_owner_identifiers:Function]\n" + "anchor_syntax": "region", + "body": "# #region extract_owner_identifiers [TYPE Function]\n# @BRIEF Normalize owner payloads for stable comparison.\n# @PRE: Owners may be list payload, scalar values, or None.\n# @POST: Returns sorted unique owner identifiers as strings.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: Any -> List[str]\ndef extract_owner_identifiers(owners: Any) -> list[str]:\n with belief_scope(\"risk_assessor.extract_owner_identifiers\"):\n logger.reason(\"Normalizing owner identifiers\")\n if not isinstance(owners, list):\n logger.reflect(\"Owners payload is not list; returning empty identifiers\")\n return []\n ids: list[str] = []\n for owner in owners:\n if isinstance(owner, dict):\n if owner.get(\"username\"):\n ids.append(str(owner[\"username\"]))\n elif owner.get(\"id\") is not None:\n ids.append(str(owner[\"id\"]))\n elif owner is not None:\n ids.append(str(owner))\n normalized_ids = sorted(set(ids))\n logger.reflect(\n \"Owner identifiers normalized\", extra={\"owner_count\": len(normalized_ids)}\n )\n return normalized_ids\n\n\n# #endregion extract_owner_identifiers\n" }, { "contract_id": "build_risks", "contract_type": "Function", "file_path": "backend/src/core/migration/risk_assessor.py", - "start_line": 90, - "end_line": 180, + "start_line": 85, + "end_line": 175, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38210,15 +35285,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_risks:Function]\n# @PURPOSE: Build risk list from computed diffs and target catalog state.\n# @RELATION: DEPENDS_ON -> [index_by_uuid]\n# @RELATION: DEPENDS_ON -> [extract_owner_identifiers]\n# @PRE: source_objects/target_objects/diff contain dashboards/charts/datasets keys with expected list structures.\n# @PRE: target_client is authenticated/usable for database list retrieval.\n# @POST: Returns list of deterministic risk items derived from overwrite, missing datasource, reference, and owner mismatch checks.\n# @SIDE_EFFECT: Calls target Superset API for databases metadata and emits logs.\n# @DATA_CONTRACT: (\n# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],\n# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],\n# @DATA_CONTRACT: Dict[str, Dict[str, List[Dict[str, Any]]]],\n# @DATA_CONTRACT: SupersetClient\n# @DATA_CONTRACT: ) -> List[Dict[str, Any]]\ndef build_risks(\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 with belief_scope(\"risk_assessor.build_risks\"):\n log.reason(\"Building migration risks from diff payload\")\n risks: List[Dict[str, Any]] = []\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in diff[object_type][\"update\"]:\n risks.append(\n {\n \"code\": \"overwrite_existing\",\n \"severity\": \"medium\",\n \"object_type\": object_type[:-1],\n \"object_uuid\": item[\"uuid\"],\n \"message\": f\"Object will be updated in target: {item.get('title') or item['uuid']}\",\n }\n )\n\n target_dataset_uuids = set(index_by_uuid(target_objects[\"datasets\"]).keys())\n _, target_databases = target_client.get_databases(query={\"columns\": [\"uuid\"]})\n target_database_uuids = {\n str(item.get(\"uuid\")) for item in target_databases if item.get(\"uuid\")\n }\n\n for dataset in source_objects[\"datasets\"]:\n db_uuid = dataset.get(\"database_uuid\")\n if db_uuid and str(db_uuid) not in target_database_uuids:\n risks.append(\n {\n \"code\": \"missing_datasource\",\n \"severity\": \"high\",\n \"object_type\": \"dataset\",\n \"object_uuid\": dataset.get(\"uuid\"),\n \"message\": f\"Target datasource is missing for dataset {dataset.get('title') or dataset.get('uuid')}\",\n }\n )\n\n for chart in source_objects[\"charts\"]:\n ds_uuid = chart.get(\"dataset_uuid\")\n if ds_uuid and str(ds_uuid) not in target_dataset_uuids:\n risks.append(\n {\n \"code\": \"breaking_reference\",\n \"severity\": \"high\",\n \"object_type\": \"chart\",\n \"object_uuid\": chart.get(\"uuid\"),\n \"message\": f\"Chart references dataset not found on target: {ds_uuid}\",\n }\n )\n\n source_dash = index_by_uuid(source_objects[\"dashboards\"])\n target_dash = index_by_uuid(target_objects[\"dashboards\"])\n for item in diff[\"dashboards\"][\"update\"]:\n source_obj = source_dash.get(item[\"uuid\"])\n target_obj = target_dash.get(item[\"uuid\"])\n if not source_obj or not target_obj:\n continue\n source_owners = extract_owner_identifiers(source_obj.get(\"owners\"))\n target_owners = extract_owner_identifiers(target_obj.get(\"owners\"))\n if source_owners and target_owners and source_owners != target_owners:\n risks.append(\n {\n \"code\": \"owner_mismatch\",\n \"severity\": \"low\",\n \"object_type\": \"dashboard\",\n \"object_uuid\": item[\"uuid\"],\n \"message\": f\"Owner mismatch for dashboard {item.get('title') or item['uuid']}\",\n }\n )\n log.reflect(\"Risk list assembled\", payload={\"risk_count\": len(risks)})\n return risks\n\n\n# [/DEF:build_risks:Function]\n" + "anchor_syntax": "region", + "body": "# #region build_risks [TYPE Function]\n# @BRIEF Build risk list from computed diffs and target catalog state.\n# @RELATION DEPENDS_ON -> [index_by_uuid]\n# @RELATION DEPENDS_ON -> [extract_owner_identifiers]\n# @PRE: source_objects/target_objects/diff contain dashboards/charts/datasets keys with expected list structures.\n# @PRE: target_client is authenticated/usable for database list retrieval.\n# @POST: Returns list of deterministic risk items derived from overwrite, missing datasource, reference, and owner mismatch checks.\n# @SIDE_EFFECT: Calls target Superset API for databases metadata and emits logs.\n# @DATA_CONTRACT: (\n# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],\n# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],\n# @DATA_CONTRACT: Dict[str, Dict[str, List[Dict[str, Any]]]],\n# @DATA_CONTRACT: SupersetClient\n# @DATA_CONTRACT: ) -> List[Dict[str, Any]]\ndef build_risks(\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 with belief_scope(\"risk_assessor.build_risks\"):\n logger.reason(\"Building migration risks from diff payload\")\n risks: list[dict[str, Any]] = []\n for object_type in (\"dashboards\", \"charts\", \"datasets\"):\n for item in diff[object_type][\"update\"]:\n risks.append(\n {\n \"code\": \"overwrite_existing\",\n \"severity\": \"medium\",\n \"object_type\": object_type[:-1],\n \"object_uuid\": item[\"uuid\"],\n \"message\": f\"Object will be updated in target: {item.get('title') or item['uuid']}\",\n }\n )\n\n target_dataset_uuids = set(index_by_uuid(target_objects[\"datasets\"]).keys())\n _, target_databases = target_client.get_databases(query={\"columns\": [\"uuid\"]})\n target_database_uuids = {\n str(item.get(\"uuid\")) for item in target_databases if item.get(\"uuid\")\n }\n\n for dataset in source_objects[\"datasets\"]:\n db_uuid = dataset.get(\"database_uuid\")\n if db_uuid and str(db_uuid) not in target_database_uuids:\n risks.append(\n {\n \"code\": \"missing_datasource\",\n \"severity\": \"high\",\n \"object_type\": \"dataset\",\n \"object_uuid\": dataset.get(\"uuid\"),\n \"message\": f\"Target datasource is missing for dataset {dataset.get('title') or dataset.get('uuid')}\",\n }\n )\n\n for chart in source_objects[\"charts\"]:\n ds_uuid = chart.get(\"dataset_uuid\")\n if ds_uuid and str(ds_uuid) not in target_dataset_uuids:\n risks.append(\n {\n \"code\": \"breaking_reference\",\n \"severity\": \"high\",\n \"object_type\": \"chart\",\n \"object_uuid\": chart.get(\"uuid\"),\n \"message\": f\"Chart references dataset not found on target: {ds_uuid}\",\n }\n )\n\n source_dash = index_by_uuid(source_objects[\"dashboards\"])\n target_dash = index_by_uuid(target_objects[\"dashboards\"])\n for item in diff[\"dashboards\"][\"update\"]:\n source_obj = source_dash.get(item[\"uuid\"])\n target_obj = target_dash.get(item[\"uuid\"])\n if not source_obj or not target_obj:\n continue\n source_owners = extract_owner_identifiers(source_obj.get(\"owners\"))\n target_owners = extract_owner_identifiers(target_obj.get(\"owners\"))\n if source_owners and target_owners and source_owners != target_owners:\n risks.append(\n {\n \"code\": \"owner_mismatch\",\n \"severity\": \"low\",\n \"object_type\": \"dashboard\",\n \"object_uuid\": item[\"uuid\"],\n \"message\": f\"Owner mismatch for dashboard {item.get('title') or item['uuid']}\",\n }\n )\n logger.reflect(\"Risk list assembled\", extra={\"risk_count\": len(risks)})\n return risks\n\n\n# #endregion build_risks\n" }, { "contract_id": "score_risks", "contract_type": "Function", "file_path": "backend/src/core/migration/risk_assessor.py", - "start_line": 183, - "end_line": 202, + "start_line": 178, + "end_line": 197, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38276,58 +35351,49 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:score_risks:Function]\n# @PURPOSE: Aggregate risk list into score and level.\n# @PRE: risk_items contains optional severity fields expected in {high,medium,low} or defaults to low weight.\n# @POST: Returns dict with score in [0,100], derived level, and original items.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Any]\ndef score_risks(risk_items: List[Dict[str, Any]]) -> Dict[str, Any]:\n with belief_scope(\"risk_assessor.score_risks\"):\n log.reason(\"Scoring risk items\", payload={\"risk_items_count\": len(risk_items)})\n weights = {\"high\": 25, \"medium\": 10, \"low\": 5}\n score = min(\n 100, sum(weights.get(item.get(\"severity\", \"low\"), 5) for item in risk_items)\n )\n level = \"low\" if score < 25 else \"medium\" if score < 60 else \"high\"\n result = {\"score\": score, \"level\": level, \"items\": risk_items}\n log.reflect(\"Risk score computed\", payload={\"score\": score, \"level\": level})\n return result\n\n\n# [/DEF:score_risks:Function]\n" + "anchor_syntax": "region", + "body": "# #region score_risks [TYPE Function]\n# @BRIEF Aggregate risk list into score and level.\n# @PRE: risk_items contains optional severity fields expected in {high,medium,low} or defaults to low weight.\n# @POST: Returns dict with score in [0,100], derived level, and original items.\n# @SIDE_EFFECT: Emits reasoning/reflective logs only.\n# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Any]\ndef score_risks(risk_items: list[dict[str, Any]]) -> dict[str, Any]:\n with belief_scope(\"risk_assessor.score_risks\"):\n logger.reason(\"Scoring risk items\", extra={\"risk_items_count\": len(risk_items)})\n weights = {\"high\": 25, \"medium\": 10, \"low\": 5}\n score = min(\n 100, sum(weights.get(item.get(\"severity\", \"low\"), 5) for item in risk_items)\n )\n level = \"low\" if score < 25 else \"medium\" if score < 60 else \"high\"\n result = {\"score\": score, \"level\": level, \"items\": risk_items}\n logger.reflect(\"Risk score computed\", extra={\"score\": score, \"level\": level})\n return result\n\n\n# #endregion score_risks\n" }, { "contract_id": "MigrationEngineModule", "contract_type": "Module", "file_path": "backend/src/core/migration_engine.py", "start_line": 1, - "end_line": 304, + "end_line": 262, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[zip_path, output_path, db_mapping, target_env_id?, fix_cross_filters?] -> Output[Transformed Superset archive]", "INVARIANT": "ZIP structure and non-targeted metadata must remain valid after transformation.", "LAYER": "Domain", "POST": "Migration engine contracts preserve ZIP integrity while exposing transformation entrypoints for import pipelines.", "PRE": "Input archives are readable Superset exports and optional mapping collaborators expose remote id lookup APIs.", "PURPOSE": "Transforms Superset export ZIP archives while preserving archive integrity and patching mapped identifiers.", - "SEMANTICS": [ - "migration", - "engine", - "zip", - "yaml", - "transformation", - "cross-filter", - "id-mapping" - ], "SIDE_EFFECT": "Reads and writes temporary archive contents during transformation workflows and emits structured belief-state logs." }, "relations": [ { "source_id": "MigrationEngineModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LoggerModule", "target_ref": "[LoggerModule]" }, { "source_id": "MigrationEngineModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "IdMappingService", "target_ref": "[IdMappingService]" }, { "source_id": "MigrationEngineModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ResourceType", "target_ref": "[ResourceType]" }, { "source_id": "MigrationEngineModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "yaml", "target_ref": "[yaml]" } @@ -38350,89 +35416,17 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationEngineModule:Module]\n#\n# @COMPLEXITY: 5\n# @SEMANTICS: migration, engine, zip, yaml, transformation, cross-filter, id-mapping\n# @PURPOSE: Transforms Superset export ZIP archives while preserving archive integrity and patching mapped identifiers.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[LoggerModule]\n# @RELATION: [DEPENDS_ON] ->[IdMappingService]\n# @RELATION: [DEPENDS_ON] ->[ResourceType]\n# @RELATION: [DEPENDS_ON] ->[yaml]\n# @PRE: Input archives are readable Superset exports and optional mapping collaborators expose remote id lookup APIs.\n# @POST: Migration engine contracts preserve ZIP integrity while exposing transformation entrypoints for import pipelines.\n# @SIDE_EFFECT: Reads and writes temporary archive contents during transformation workflows and emits structured belief-state logs.\n# @DATA_CONTRACT: Input[zip_path, output_path, db_mapping, target_env_id?, fix_cross_filters?] -> Output[Transformed Superset archive]\n# @INVARIANT: ZIP structure and non-targeted metadata must remain valid after transformation.\n\n# [SECTION: IMPORTS]\nimport zipfile\nimport yaml\nimport os\nimport json\nimport re\nimport tempfile\nfrom pathlib import Path\nfrom typing import Dict, Optional, List\nfrom .logger import logger, belief_scope\nfrom src.core.mapping_service import IdMappingService\nfrom src.models.mapping import ResourceType\n# [/SECTION]\n\n\n# [DEF:MigrationEngine:Class]\n# @PURPOSE: Engine for transforming Superset export ZIPs.\n# @RELATION: CONTAINS -> [__init__, transform_zip, _transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\nclass MigrationEngine:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations.\n # @PRE: mapping_service is None or implements batch remote ID lookup for ResourceType.CHART.\n # @POST: self.mapping_service is assigned and available for optional cross-filter patching flows.\n # @SIDE_EFFECT: Mutates in-memory engine state by storing dependency reference.\n # @DATA_CONTRACT: Input[Optional[IdMappingService]] -> Output[MigrationEngine]\n # @PARAM: mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.\n def __init__(self, mapping_service: Optional[IdMappingService] = None):\n with belief_scope(\"MigrationEngine.__init__\"):\n logger.reason(\"Initializing MigrationEngine\")\n self.mapping_service = mapping_service\n logger.reflect(\"MigrationEngine initialized\")\n\n # [/DEF:__init__:Function]\n\n # [DEF:transform_zip:Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION: DEPENDS_ON -> [_transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\n # @PARAM: zip_path (str) - Path to the source ZIP file.\n # @PARAM: output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM: db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM: strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM: target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM: fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE: zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST: Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT: Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT: Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN: bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: Dict[str, str],\n strip_databases: bool = True,\n target_env_id: Optional[str] = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n\n # [/DEF:transform_zip:Function]\n\n # [DEF:_transform_yaml:Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM: file_path (Path) - Path to the YAML file.\n # @PARAM: db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE: file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST: database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT: Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT: Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: Dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n\n with open(file_path, \"r\") as f:\n data = yaml.safe_load(f)\n\n if not data:\n return\n\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n\n # [/DEF:_transform_yaml:Function]\n\n # [DEF:_extract_chart_uuids_from_archive:Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE: temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST: Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT: Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT: Input[Path] -> Output[Dict[int,str]]\n # @PARAM: temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN: Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> Dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf, \"r\") as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n pass\n return mapping\n\n # [/DEF:_extract_chart_uuids_from_archive:Function]\n\n # [DEF:_patch_dashboard_metadata:Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE: file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST: json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT: Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT: Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM: file_path (Path)\n # @PARAM: target_env_id (str)\n # @PARAM: source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: Dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n\n with open(file_path, \"r\") as f:\n data = yaml.safe_load(f)\n\n if not data or \"json_metadata\" not in data:\n return\n\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n\n # [/DEF:_patch_dashboard_metadata:Function]\n\n# [/DEF:MigrationEngine:Class]\n\n# [/DEF:MigrationEngineModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region MigrationEngineModule [C:5] [TYPE Module] [SEMANTICS migration, superset, archive, migration-engine]\n#\n# @BRIEF Transforms Superset export ZIP archives while preserving archive integrity and patching mapped identifiers.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION DEPENDS_ON -> [ResourceType]\n# @RELATION DEPENDS_ON -> [yaml]\n# @PRE: Input archives are readable Superset exports and optional mapping collaborators expose remote id lookup APIs.\n# @POST: Migration engine contracts preserve ZIP integrity while exposing transformation entrypoints for import pipelines.\n# @SIDE_EFFECT: Reads and writes temporary archive contents during transformation workflows and emits structured belief-state logs.\n# @DATA_CONTRACT: Input[zip_path, output_path, db_mapping, target_env_id?, fix_cross_filters?] -> Output[Transformed Superset archive]\n# @INVARIANT: ZIP structure and non-targeted metadata must remain valid after transformation.\nimport json\nimport os\nimport re\nimport tempfile\nimport zipfile\nfrom pathlib import Path\n\nimport yaml\n\nfrom src.core.mapping_service import IdMappingService\nfrom src.models.mapping import ResourceType\n\nfrom .logger import belief_scope, logger\n\n\n# #region MigrationEngine [TYPE Class]\n# @BRIEF Engine for transforming Superset export ZIPs.\n# @RELATION CONTAINS -> [__init__, transform_zip, _transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\nclass MigrationEngine:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations.\n # @PRE: mapping_service is None or implements batch remote ID lookup for ResourceType.CHART.\n # @POST: self.mapping_service is assigned and available for optional cross-filter patching flows.\n # @SIDE_EFFECT: Mutates in-memory engine state by storing dependency reference.\n # @DATA_CONTRACT: Input[Optional[IdMappingService]] -> Output[MigrationEngine]\n # @PARAM: mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.\n def __init__(self, mapping_service: IdMappingService | None = None):\n with belief_scope(\"MigrationEngine.__init__\"):\n logger.reason(\"Initializing MigrationEngine\")\n self.mapping_service = mapping_service\n logger.reflect(\"MigrationEngine initialized\")\n # #endregion __init__\n # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION: DEPENDS_ON -> [_transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\n # @PARAM: zip_path (str) - Path to the source ZIP file.\n # @PARAM: output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM: db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM: strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM: target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM: fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE: zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST: Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT: Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT: Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN: bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n # #region _transform_yaml [TYPE Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM: file_path (Path) - Path to the YAML file.\n # @PARAM: db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE: file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST: database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT: Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT: Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data:\n return\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n # #endregion _transform_yaml\n # #region _extract_chart_uuids_from_archive [TYPE Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE: temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST: Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT: Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT: Input[Path] -> Output[Dict[int,str]]\n # @PARAM: temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN: Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf) as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n pass\n return mapping\n # #endregion _extract_chart_uuids_from_archive\n # #region _patch_dashboard_metadata [TYPE Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE: file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST: json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT: Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT: Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM: file_path (Path)\n # @PARAM: target_env_id (str)\n # @PARAM: source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data or \"json_metadata\" not in data:\n return\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n # #endregion _patch_dashboard_metadata\n# #endregion MigrationEngine\n# #endregion MigrationEngineModule\n" }, { "contract_id": "MigrationEngine", "contract_type": "Class", "file_path": "backend/src/core/migration_engine.py", - "start_line": 32, - "end_line": 302, + "start_line": 29, + "end_line": 261, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38484,15 +35478,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationEngine:Class]\n# @PURPOSE: Engine for transforming Superset export ZIPs.\n# @RELATION: CONTAINS -> [__init__, transform_zip, _transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\nclass MigrationEngine:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations.\n # @PRE: mapping_service is None or implements batch remote ID lookup for ResourceType.CHART.\n # @POST: self.mapping_service is assigned and available for optional cross-filter patching flows.\n # @SIDE_EFFECT: Mutates in-memory engine state by storing dependency reference.\n # @DATA_CONTRACT: Input[Optional[IdMappingService]] -> Output[MigrationEngine]\n # @PARAM: mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.\n def __init__(self, mapping_service: Optional[IdMappingService] = None):\n with belief_scope(\"MigrationEngine.__init__\"):\n logger.reason(\"Initializing MigrationEngine\")\n self.mapping_service = mapping_service\n logger.reflect(\"MigrationEngine initialized\")\n\n # [/DEF:__init__:Function]\n\n # [DEF:transform_zip:Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION: DEPENDS_ON -> [_transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\n # @PARAM: zip_path (str) - Path to the source ZIP file.\n # @PARAM: output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM: db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM: strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM: target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM: fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE: zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST: Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT: Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT: Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN: bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: Dict[str, str],\n strip_databases: bool = True,\n target_env_id: Optional[str] = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n\n # [/DEF:transform_zip:Function]\n\n # [DEF:_transform_yaml:Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM: file_path (Path) - Path to the YAML file.\n # @PARAM: db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE: file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST: database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT: Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT: Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: Dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n\n with open(file_path, \"r\") as f:\n data = yaml.safe_load(f)\n\n if not data:\n return\n\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n\n # [/DEF:_transform_yaml:Function]\n\n # [DEF:_extract_chart_uuids_from_archive:Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE: temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST: Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT: Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT: Input[Path] -> Output[Dict[int,str]]\n # @PARAM: temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN: Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> Dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf, \"r\") as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n pass\n return mapping\n\n # [/DEF:_extract_chart_uuids_from_archive:Function]\n\n # [DEF:_patch_dashboard_metadata:Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE: file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST: json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT: Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT: Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM: file_path (Path)\n # @PARAM: target_env_id (str)\n # @PARAM: source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: Dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n\n with open(file_path, \"r\") as f:\n data = yaml.safe_load(f)\n\n if not data or \"json_metadata\" not in data:\n return\n\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n\n # [/DEF:_patch_dashboard_metadata:Function]\n\n# [/DEF:MigrationEngine:Class]\n" + "anchor_syntax": "region", + "body": "# #region MigrationEngine [TYPE Class]\n# @BRIEF Engine for transforming Superset export ZIPs.\n# @RELATION CONTAINS -> [__init__, transform_zip, _transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\nclass MigrationEngine:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes migration orchestration dependencies for ZIP/YAML metadata transformations.\n # @PRE: mapping_service is None or implements batch remote ID lookup for ResourceType.CHART.\n # @POST: self.mapping_service is assigned and available for optional cross-filter patching flows.\n # @SIDE_EFFECT: Mutates in-memory engine state by storing dependency reference.\n # @DATA_CONTRACT: Input[Optional[IdMappingService]] -> Output[MigrationEngine]\n # @PARAM: mapping_service (Optional[IdMappingService]) - Used for resolving target environment integer IDs.\n def __init__(self, mapping_service: IdMappingService | None = None):\n with belief_scope(\"MigrationEngine.__init__\"):\n logger.reason(\"Initializing MigrationEngine\")\n self.mapping_service = mapping_service\n logger.reflect(\"MigrationEngine initialized\")\n # #endregion __init__\n # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION: DEPENDS_ON -> [_transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\n # @PARAM: zip_path (str) - Path to the source ZIP file.\n # @PARAM: output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM: db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM: strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM: target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM: fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE: zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST: Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT: Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT: Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN: bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n # #region _transform_yaml [TYPE Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM: file_path (Path) - Path to the YAML file.\n # @PARAM: db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE: file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST: database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT: Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT: Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data:\n return\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n # #endregion _transform_yaml\n # #region _extract_chart_uuids_from_archive [TYPE Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE: temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST: Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT: Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT: Input[Path] -> Output[Dict[int,str]]\n # @PARAM: temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN: Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf) as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n pass\n return mapping\n # #endregion _extract_chart_uuids_from_archive\n # #region _patch_dashboard_metadata [TYPE Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE: file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST: json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT: Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT: Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM: file_path (Path)\n # @PARAM: target_env_id (str)\n # @PARAM: source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data or \"json_metadata\" not in data:\n return\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n # #endregion _patch_dashboard_metadata\n# #endregion MigrationEngine\n" }, { "contract_id": "transform_zip", "contract_type": "Function", "file_path": "backend/src/core/migration_engine.py", - "start_line": 51, - "end_line": 149, + "start_line": 46, + "end_line": 131, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38580,15 +35574,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:transform_zip:Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION: DEPENDS_ON -> [_transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\n # @PARAM: zip_path (str) - Path to the source ZIP file.\n # @PARAM: output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM: db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM: strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM: target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM: fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE: zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST: Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT: Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT: Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN: bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: Dict[str, str],\n strip_databases: bool = True,\n target_env_id: Optional[str] = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n\n # [/DEF:transform_zip:Function]\n" + "anchor_syntax": "region", + "body": " # #region transform_zip [TYPE Function]\n # @PURPOSE: Extracts ZIP, replaces database UUIDs in YAMLs, patches cross-filters, and re-packages.\n # @RELATION: DEPENDS_ON -> [_transform_yaml, _extract_chart_uuids_from_archive, _patch_dashboard_metadata]\n # @PARAM: zip_path (str) - Path to the source ZIP file.\n # @PARAM: output_path (str) - Path where the transformed ZIP will be saved.\n # @PARAM: db_mapping (Dict[str, str]) - Mapping of source UUID to target UUID.\n # @PARAM: strip_databases (bool) - Whether to remove the databases directory from the archive.\n # @PARAM: target_env_id (Optional[str]) - Used if fix_cross_filters is True to know which environment map to use.\n # @PARAM: fix_cross_filters (bool) - Whether to patch dashboard json_metadata.\n # @PRE: zip_path points to a readable ZIP; output_path parent is writable; db_mapping keys/values are UUID strings.\n # @POST: Returns True only when extraction, transformation, and packaging complete without exception.\n # @SIDE_EFFECT: Reads/writes filesystem archives, creates temporary directory, emits structured logs.\n # @DATA_CONTRACT: Input[(str zip_path, str output_path, Dict[str,str] db_mapping, bool strip_databases, Optional[str] target_env_id, bool fix_cross_filters)] -> Output[bool]\n # @RETURN: bool - True if successful.\n def transform_zip(\n self,\n zip_path: str,\n output_path: str,\n db_mapping: dict[str, str],\n strip_databases: bool = True,\n target_env_id: str | None = None,\n fix_cross_filters: bool = False,\n ) -> bool:\n \"\"\"\n Transform a Superset export ZIP by replacing database UUIDs and optionally fixing cross-filters.\n \"\"\"\n with belief_scope(\"MigrationEngine.transform_zip\"):\n logger.reason(f\"Starting ZIP transformation: {zip_path} -> {output_path}\")\n with tempfile.TemporaryDirectory() as temp_dir_str:\n temp_dir = Path(temp_dir_str)\n try:\n # 1. Extract\n logger.reason(f\"Extracting source archive to {temp_dir}\")\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n zf.extractall(temp_dir)\n # 2. Transform YAMLs (Databases)\n dataset_files = list(temp_dir.glob(\"**/datasets/**/*.yaml\")) + list(\n temp_dir.glob(\"**/datasets/*.yaml\")\n )\n dataset_files = list(set(dataset_files))\n logger.reason(\n f\"Transforming {len(dataset_files)} dataset YAML files\"\n )\n for ds_file in dataset_files:\n self._transform_yaml(ds_file, db_mapping)\n # 2.5 Patch Cross-Filters (Dashboards)\n if fix_cross_filters:\n if self.mapping_service and target_env_id:\n dash_files = list(\n temp_dir.glob(\"**/dashboards/**/*.yaml\")\n ) + list(temp_dir.glob(\"**/dashboards/*.yaml\"))\n dash_files = list(set(dash_files))\n logger.reason(\n f\"Patching cross-filters for {len(dash_files)} dashboards\"\n )\n # Gather all source UUID-to-ID mappings from the archive first\n source_id_to_uuid_map = (\n self._extract_chart_uuids_from_archive(temp_dir)\n )\n for dash_file in dash_files:\n self._patch_dashboard_metadata(\n dash_file, target_env_id, source_id_to_uuid_map\n )\n else:\n logger.explore(\n \"Cross-filter patching requested but mapping service or target_env_id is missing\"\n )\n # 3. Re-package\n logger.reason(\n f\"Re-packaging transformed archive (strip_databases={strip_databases})\"\n )\n with zipfile.ZipFile(output_path, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(temp_dir):\n rel_root = Path(root).relative_to(temp_dir)\n if strip_databases and \"databases\" in rel_root.parts:\n continue\n for file in files:\n file_path = Path(root) / file\n arcname = file_path.relative_to(temp_dir)\n zf.write(file_path, arcname)\n logger.reflect(\"ZIP transformation completed successfully\")\n return True\n except Exception as e:\n logger.explore(f\"Error transforming ZIP: {e}\")\n return False\n # #endregion transform_zip\n" }, { "contract_id": "_transform_yaml", "contract_type": "Function", "file_path": "backend/src/core/migration_engine.py", - "start_line": 151, - "end_line": 179, + "start_line": 132, + "end_line": 156, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38653,15 +35647,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_transform_yaml:Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM: file_path (Path) - Path to the YAML file.\n # @PARAM: db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE: file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST: database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT: Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT: Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: Dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n\n with open(file_path, \"r\") as f:\n data = yaml.safe_load(f)\n\n if not data:\n return\n\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n\n # [/DEF:_transform_yaml:Function]\n" + "anchor_syntax": "region", + "body": " # #region _transform_yaml [TYPE Function]\n # @PURPOSE: Replaces database_uuid in a single YAML file.\n # @PARAM: file_path (Path) - Path to the YAML file.\n # @PARAM: db_mapping (Dict[str, str]) - UUID mapping dictionary.\n # @PRE: file_path exists, is readable YAML, and db_mapping contains source->target UUID pairs.\n # @POST: database_uuid is replaced in-place only when source UUID is present in db_mapping.\n # @SIDE_EFFECT: Reads and conditionally rewrites YAML file on disk.\n # @DATA_CONTRACT: Input[(Path file_path, Dict[str,str] db_mapping)] -> Output[None]\n def _transform_yaml(self, file_path: Path, db_mapping: dict[str, str]):\n with belief_scope(\"MigrationEngine._transform_yaml\"):\n if not file_path.exists():\n logger.explore(f\"YAML file not found: {file_path}\")\n raise FileNotFoundError(str(file_path))\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data:\n return\n source_uuid = data.get(\"database_uuid\")\n if source_uuid in db_mapping:\n logger.reason(f\"Replacing database UUID in {file_path.name}\")\n data[\"database_uuid\"] = db_mapping[source_uuid]\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(f\"Database UUID patched in {file_path.name}\")\n # #endregion _transform_yaml\n" }, { "contract_id": "_extract_chart_uuids_from_archive", "contract_type": "Function", "file_path": "backend/src/core/migration_engine.py", - "start_line": 181, - "end_line": 209, + "start_line": 157, + "end_line": 184, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38733,15 +35727,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_extract_chart_uuids_from_archive:Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE: temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST: Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT: Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT: Input[Path] -> Output[Dict[int,str]]\n # @PARAM: temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN: Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> Dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf, \"r\") as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n pass\n return mapping\n\n # [/DEF:_extract_chart_uuids_from_archive:Function]\n" + "anchor_syntax": "region", + "body": " # #region _extract_chart_uuids_from_archive [TYPE Function]\n # @PURPOSE: Scans extracted chart YAML files and builds a source chart ID to UUID lookup map.\n # @PRE: temp_dir exists and points to extracted archive root with optional chart YAML resources.\n # @POST: Returns a best-effort Dict[int, str] containing only parseable chart id/uuid pairs.\n # @SIDE_EFFECT: Reads chart YAML files from filesystem; suppresses per-file parsing failures.\n # @DATA_CONTRACT: Input[Path] -> Output[Dict[int,str]]\n # @PARAM: temp_dir (Path) - Root dir of unpacked archive.\n # @RETURN: Dict[int, str] - Mapping of source Integer ID to UUID.\n def _extract_chart_uuids_from_archive(self, temp_dir: Path) -> dict[int, str]:\n with belief_scope(\"MigrationEngine._extract_chart_uuids_from_archive\"):\n # Implementation Note: This is a placeholder for the logic that extracts\n # actual Source IDs. In a real scenario, this involves parsing chart YAMLs\n # or manifesting the export metadata structure where source IDs are stored.\n # For simplicity in US1 MVP, we assume it's read from chart files if present.\n mapping = {}\n chart_files = list(temp_dir.glob(\"**/charts/**/*.yaml\")) + list(\n temp_dir.glob(\"**/charts/*.yaml\")\n )\n for cf in set(chart_files):\n try:\n with open(cf) as f:\n cdata = yaml.safe_load(f)\n if cdata and \"id\" in cdata and \"uuid\" in cdata:\n mapping[cdata[\"id\"]] = cdata[\"uuid\"]\n except Exception:\n pass\n return mapping\n # #endregion _extract_chart_uuids_from_archive\n" }, { "contract_id": "_patch_dashboard_metadata", "contract_type": "Function", "file_path": "backend/src/core/migration_engine.py", - "start_line": 211, - "end_line": 300, + "start_line": 185, + "end_line": 260, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38806,27 +35800,21 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_patch_dashboard_metadata:Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE: file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST: json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT: Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT: Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM: file_path (Path)\n # @PARAM: target_env_id (str)\n # @PARAM: source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: Dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n\n with open(file_path, \"r\") as f:\n data = yaml.safe_load(f)\n\n if not data or \"json_metadata\" not in data:\n return\n\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n\n # [/DEF:_patch_dashboard_metadata:Function]\n" + "anchor_syntax": "region", + "body": " # #region _patch_dashboard_metadata [TYPE Function]\n # @PURPOSE: Rewrites dashboard json_metadata chart/dataset integer identifiers using target environment mappings.\n # @PRE: file_path points to dashboard YAML with json_metadata; target_env_id is non-empty; source_map contains source id->uuid.\n # @POST: json_metadata is re-serialized with mapped integer IDs when remote mappings are available; otherwise file remains unchanged.\n # @SIDE_EFFECT: Reads/writes YAML file, performs mapping lookup via mapping_service, emits logs for recoverable/terminal failures.\n # @DATA_CONTRACT: Input[(Path file_path, str target_env_id, Dict[int,str] source_map)] -> Output[None]\n # @PARAM: file_path (Path)\n # @PARAM: target_env_id (str)\n # @PARAM: source_map (Dict[int, str])\n def _patch_dashboard_metadata(\n self, file_path: Path, target_env_id: str, source_map: dict[int, str]\n ):\n with belief_scope(\"MigrationEngine._patch_dashboard_metadata\"):\n try:\n if not file_path.exists():\n return\n with open(file_path) as f:\n data = yaml.safe_load(f)\n if not data or \"json_metadata\" not in data:\n return\n metadata_str = data[\"json_metadata\"]\n if not metadata_str:\n return\n # Fetch target UUIDs for everything we know:\n uuids_needed = list(source_map.values())\n logger.reason(\n f\"Resolving {len(uuids_needed)} remote IDs for dashboard metadata patching\"\n )\n target_ids = self.mapping_service.get_remote_ids_batch(\n target_env_id, ResourceType.CHART, uuids_needed\n )\n if not target_ids:\n logger.reflect(\n \"No remote target IDs found in mapping database for this dashboard.\"\n )\n return\n # Map Source Int -> Target Int\n source_to_target = {}\n missing_targets = []\n for s_id, s_uuid in source_map.items():\n if s_uuid in target_ids:\n source_to_target[s_id] = target_ids[s_uuid]\n else:\n missing_targets.append(s_id)\n if missing_targets:\n logger.explore(\n f\"Missing target IDs for source IDs: {missing_targets}. Cross-filters might break.\"\n )\n if not source_to_target:\n logger.reflect(\"No source IDs matched remotely. Skipping patch.\")\n return\n logger.reason(\n f\"Patching {len(source_to_target)} ID references in json_metadata\"\n )\n new_metadata_str = metadata_str\n for s_id, t_id in source_to_target.items():\n new_metadata_str = re.sub(\n r'(\"datasetId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n new_metadata_str = re.sub(\n r'(\"chartId\"\\s*:\\s*)' + str(s_id) + r\"(\\b)\",\n r\"\\g<1>\" + str(t_id) + r\"\\g<2>\",\n new_metadata_str,\n )\n # Re-parse to validate valid JSON\n data[\"json_metadata\"] = json.dumps(json.loads(new_metadata_str))\n with open(file_path, \"w\") as f:\n yaml.dump(data, f)\n logger.reflect(\n f\"Dashboard metadata patched and saved: {file_path.name}\"\n )\n except Exception as e:\n logger.explore(f\"Metadata patch failed for {file_path.name}: {e}\")\n # #endregion _patch_dashboard_metadata\n" }, { "contract_id": "PluginBase", "contract_type": "Class", "file_path": "backend/src/core/plugin_base.py", - "start_line": 7, - "end_line": 128, + "start_line": 9, + "end_line": 121, "tier": "TIER_1", "complexity": 1, "metadata": { "INVARIANT": "All plugins MUST inherit from this class.", "LAYER": "Core", - "PURPOSE": "Defines the abstract base class that all plugins must implement to be recognized by the system. It enforces a common structure for plugin metadata and execution.", - "SEMANTICS": [ - "plugin", - "interface", - "base", - "abstract" - ] + "PURPOSE": "Defines the abstract base class that all plugins must implement to be recognized by the system. It enforces a common structure for plugin metadata and execution." }, "relations": [ { @@ -38877,29 +35865,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -38910,8 +35875,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PluginBase:Class]\n# @SEMANTICS: plugin, interface, base, abstract\n# @PURPOSE: Defines the abstract base class that all plugins must implement to be recognized by the system. It enforces a common structure for plugin metadata and execution.\n# @LAYER: Core\n# @RELATION: Used by PluginLoader to identify valid plugins.\n# @INVARIANT: All plugins MUST inherit from this class.\nclass PluginBase(ABC):\n \"\"\"\n Base class for all plugins.\n Plugins must inherit from this class and implement the abstract methods.\n \"\"\"\n\n @property\n @abstractmethod\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - Plugin ID.\n def id(self) -> str:\n \"\"\"A unique identifier for the plugin.\"\"\"\n with belief_scope(\"id\"):\n pass\n # [/DEF:id:Function]\n\n @property\n @abstractmethod\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n \"\"\"A human-readable name for the plugin.\"\"\"\n with belief_scope(\"name\"):\n pass\n # [/DEF:name:Function]\n\n @property\n @abstractmethod\n # [DEF:description:Function]\n # @PURPOSE: Returns a brief description of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n \"\"\"A brief description of what the plugin does.\"\"\"\n with belief_scope(\"description\"):\n pass\n # [/DEF:description:Function]\n\n @property\n @abstractmethod\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - Plugin version.\n def version(self) -> str:\n \"\"\"The version of the plugin.\"\"\"\n with belief_scope(\"version\"):\n pass\n # [/DEF:version:Function]\n\n @property\n # [DEF:required_permission:Function]\n # @PURPOSE: Returns the required permission string to execute this plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string permission.\n # @RETURN: str - Required permission (e.g., \"plugin:backup:execute\").\n def required_permission(self) -> str:\n \"\"\"The permission string required to execute this plugin.\"\"\"\n with belief_scope(\"required_permission\"):\n return f\"plugin:{self.id}:execute\"\n # [/DEF:required_permission:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the plugin's UI, if applicable.\n # @PRE: Plugin instance exists.\n # @POST: Returns string route or None.\n # @RETURN: Optional[str] - Frontend route.\n def ui_route(self) -> Optional[str]:\n \"\"\"\n The frontend route for the plugin's UI.\n Returns None if the plugin does not have a dedicated UI page.\n \"\"\"\n with belief_scope(\"ui_route\"):\n return None\n # [/DEF:ui_route:Function]\n\n @abstractmethod\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the plugin's input parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dict schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n \"\"\"\n Returns the JSON schema for the plugin's input parameters.\n This schema will be used to generate the frontend form.\n \"\"\"\n with belief_scope(\"get_schema\"):\n pass\n # [/DEF:get_schema:Function]\n\n @abstractmethod\n # [DEF:execute:Function]\n # @PURPOSE: Executes the plugin's core logic.\n # @PARAM: params (Dict[str, Any]) - Validated input parameters.\n # @PRE: params must be a dictionary.\n # @POST: Plugin execution is completed.\n async def execute(self, params: Dict[str, Any]):\n with belief_scope(\"execute\"):\n pass\n \"\"\"\n Executes the plugin's logic.\n The `params` argument will be validated against the schema returned by `get_schema()`.\n \"\"\"\n pass\n # [/DEF:execute:Function]\n# [/DEF:PluginBase:Class]\n" + "anchor_syntax": "region", + "body": "# #region PluginBase [TYPE Class] [SEMANTICS plugin, interface, base, abstract]\n# @BRIEF Defines the abstract base class that all plugins must implement to be recognized by the system. It enforces a common structure for plugin metadata and execution.\n# @LAYER: Core\n# @RELATION Used by PluginLoader to identify valid plugins.\n# @INVARIANT: All plugins MUST inherit from this class.\nclass PluginBase(ABC):\n \"\"\"\n Base class for all plugins.\n Plugins must inherit from this class and implement the abstract methods.\n \"\"\"\n @property\n @abstractmethod\n # #region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - Plugin ID.\n def id(self) -> str:\n \"\"\"A unique identifier for the plugin.\"\"\"\n with belief_scope(\"id\"):\n pass\n # #endregion id\n @property\n @abstractmethod\n # #region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n \"\"\"A human-readable name for the plugin.\"\"\"\n with belief_scope(\"name\"):\n pass\n # #endregion name\n @property\n @abstractmethod\n # #region description [TYPE Function]\n # @PURPOSE: Returns a brief description of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n \"\"\"A brief description of what the plugin does.\"\"\"\n with belief_scope(\"description\"):\n pass\n # #endregion description\n @property\n @abstractmethod\n # #region version [TYPE Function]\n # @PURPOSE: Returns the version of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - Plugin version.\n def version(self) -> str:\n \"\"\"The version of the plugin.\"\"\"\n with belief_scope(\"version\"):\n pass\n # #endregion version\n @property\n # #region required_permission [TYPE Function]\n # @PURPOSE: Returns the required permission string to execute this plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string permission.\n # @RETURN: str - Required permission (e.g., \"plugin:backup:execute\").\n def required_permission(self) -> str:\n \"\"\"The permission string required to execute this plugin.\"\"\"\n with belief_scope(\"required_permission\"):\n return f\"plugin:{self.id}:execute\"\n # #endregion required_permission\n @property\n # #region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the plugin's UI, if applicable.\n # @PRE: Plugin instance exists.\n # @POST: Returns string route or None.\n # @RETURN: Optional[str] - Frontend route.\n def ui_route(self) -> str | None:\n \"\"\"\n The frontend route for the plugin's UI.\n Returns None if the plugin does not have a dedicated UI page.\n \"\"\"\n with belief_scope(\"ui_route\"):\n return None\n # #endregion ui_route\n @abstractmethod\n # #region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for the plugin's input parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dict schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n \"\"\"\n Returns the JSON schema for the plugin's input parameters.\n This schema will be used to generate the frontend form.\n \"\"\"\n with belief_scope(\"get_schema\"):\n pass\n # #endregion get_schema\n @abstractmethod\n # #region execute [TYPE Function]\n # @PURPOSE: Executes the plugin's core logic.\n # @PARAM: params (Dict[str, Any]) - Validated input parameters.\n # @PRE: params must be a dictionary.\n # @POST: Plugin execution is completed.\n async def execute(self, params: dict[str, Any]):\n with belief_scope(\"execute\"):\n pass\n \"\"\"\n Executes the plugin's logic.\n The `params` argument will be validated against the schema returned by `get_schema()`.\n \"\"\"\n pass\n # #endregion execute\n# #endregion PluginBase\n" }, { "contract_id": "id", @@ -38963,15 +35928,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - Plugin ID.\n def id(self) -> str:\n \"\"\"A unique identifier for the plugin.\"\"\"\n with belief_scope(\"id\"):\n pass\n # [/DEF:id:Function]\n" + "anchor_syntax": "region", + "body": " # #region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - Plugin ID.\n def id(self) -> str:\n \"\"\"A unique identifier for the plugin.\"\"\"\n with belief_scope(\"id\"):\n pass\n # #endregion id\n" }, { "contract_id": "name", "contract_type": "Function", "file_path": "backend/src/core/plugin_base.py", - "start_line": 34, - "end_line": 43, + "start_line": 33, + "end_line": 42, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39016,15 +35981,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n \"\"\"A human-readable name for the plugin.\"\"\"\n with belief_scope(\"name\"):\n pass\n # [/DEF:name:Function]\n" + "anchor_syntax": "region", + "body": " # #region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n \"\"\"A human-readable name for the plugin.\"\"\"\n with belief_scope(\"name\"):\n pass\n # #endregion name\n" }, { "contract_id": "description", "contract_type": "Function", "file_path": "backend/src/core/plugin_base.py", - "start_line": 47, - "end_line": 56, + "start_line": 45, + "end_line": 54, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39069,15 +36034,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:description:Function]\n # @PURPOSE: Returns a brief description of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n \"\"\"A brief description of what the plugin does.\"\"\"\n with belief_scope(\"description\"):\n pass\n # [/DEF:description:Function]\n" + "anchor_syntax": "region", + "body": " # #region description [TYPE Function]\n # @PURPOSE: Returns a brief description of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n \"\"\"A brief description of what the plugin does.\"\"\"\n with belief_scope(\"description\"):\n pass\n # #endregion description\n" }, { "contract_id": "version", "contract_type": "Function", "file_path": "backend/src/core/plugin_base.py", - "start_line": 60, - "end_line": 69, + "start_line": 57, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39122,15 +36087,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:version:Function]\n # @PURPOSE: Returns the version of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - Plugin version.\n def version(self) -> str:\n \"\"\"The version of the plugin.\"\"\"\n with belief_scope(\"version\"):\n pass\n # [/DEF:version:Function]\n" + "anchor_syntax": "region", + "body": " # #region version [TYPE Function]\n # @PURPOSE: Returns the version of the plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - Plugin version.\n def version(self) -> str:\n \"\"\"The version of the plugin.\"\"\"\n with belief_scope(\"version\"):\n pass\n # #endregion version\n" }, { "contract_id": "required_permission", "contract_type": "Function", "file_path": "backend/src/core/plugin_base.py", - "start_line": 72, - "end_line": 81, + "start_line": 68, + "end_line": 77, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39175,15 +36140,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:required_permission:Function]\n # @PURPOSE: Returns the required permission string to execute this plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string permission.\n # @RETURN: str - Required permission (e.g., \"plugin:backup:execute\").\n def required_permission(self) -> str:\n \"\"\"The permission string required to execute this plugin.\"\"\"\n with belief_scope(\"required_permission\"):\n return f\"plugin:{self.id}:execute\"\n # [/DEF:required_permission:Function]\n" + "anchor_syntax": "region", + "body": " # #region required_permission [TYPE Function]\n # @PURPOSE: Returns the required permission string to execute this plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string permission.\n # @RETURN: str - Required permission (e.g., \"plugin:backup:execute\").\n def required_permission(self) -> str:\n \"\"\"The permission string required to execute this plugin.\"\"\"\n with belief_scope(\"required_permission\"):\n return f\"plugin:{self.id}:execute\"\n # #endregion required_permission\n" }, { "contract_id": "ui_route", "contract_type": "Function", "file_path": "backend/src/core/plugin_base.py", - "start_line": 84, - "end_line": 96, + "start_line": 79, + "end_line": 91, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39228,15 +36193,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the plugin's UI, if applicable.\n # @PRE: Plugin instance exists.\n # @POST: Returns string route or None.\n # @RETURN: Optional[str] - Frontend route.\n def ui_route(self) -> Optional[str]:\n \"\"\"\n The frontend route for the plugin's UI.\n Returns None if the plugin does not have a dedicated UI page.\n \"\"\"\n with belief_scope(\"ui_route\"):\n return None\n # [/DEF:ui_route:Function]\n" + "anchor_syntax": "region", + "body": " # #region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the plugin's UI, if applicable.\n # @PRE: Plugin instance exists.\n # @POST: Returns string route or None.\n # @RETURN: Optional[str] - Frontend route.\n def ui_route(self) -> str | None:\n \"\"\"\n The frontend route for the plugin's UI.\n Returns None if the plugin does not have a dedicated UI page.\n \"\"\"\n with belief_scope(\"ui_route\"):\n return None\n # #endregion ui_route\n" }, { "contract_id": "get_schema", "contract_type": "Function", "file_path": "backend/src/core/plugin_base.py", - "start_line": 99, - "end_line": 111, + "start_line": 93, + "end_line": 105, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39281,15 +36246,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the plugin's input parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dict schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n \"\"\"\n Returns the JSON schema for the plugin's input parameters.\n This schema will be used to generate the frontend form.\n \"\"\"\n with belief_scope(\"get_schema\"):\n pass\n # [/DEF:get_schema:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for the plugin's input parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dict schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n \"\"\"\n Returns the JSON schema for the plugin's input parameters.\n This schema will be used to generate the frontend form.\n \"\"\"\n with belief_scope(\"get_schema\"):\n pass\n # #endregion get_schema\n" }, { "contract_id": "execute", "contract_type": "Function", "file_path": "backend/src/core/plugin_base.py", - "start_line": 114, - "end_line": 127, + "start_line": 107, + "end_line": 120, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39334,26 +36299,20 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:execute:Function]\n # @PURPOSE: Executes the plugin's core logic.\n # @PARAM: params (Dict[str, Any]) - Validated input parameters.\n # @PRE: params must be a dictionary.\n # @POST: Plugin execution is completed.\n async def execute(self, params: Dict[str, Any]):\n with belief_scope(\"execute\"):\n pass\n \"\"\"\n Executes the plugin's logic.\n The `params` argument will be validated against the schema returned by `get_schema()`.\n \"\"\"\n pass\n # [/DEF:execute:Function]\n" + "anchor_syntax": "region", + "body": " # #region execute [TYPE Function]\n # @PURPOSE: Executes the plugin's core logic.\n # @PARAM: params (Dict[str, Any]) - Validated input parameters.\n # @PRE: params must be a dictionary.\n # @POST: Plugin execution is completed.\n async def execute(self, params: dict[str, Any]):\n with belief_scope(\"execute\"):\n pass\n \"\"\"\n Executes the plugin's logic.\n The `params` argument will be validated against the schema returned by `get_schema()`.\n \"\"\"\n pass\n # #endregion execute\n" }, { "contract_id": "PluginConfig", "contract_type": "Class", "file_path": "backend/src/core/plugin_base.py", - "start_line": 130, - "end_line": 143, + "start_line": 122, + "end_line": 134, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "Core", - "PURPOSE": "A Pydantic model used to represent the validated configuration and metadata of a loaded plugin. This object is what gets exposed to the API layer.", - "SEMANTICS": [ - "plugin", - "config", - "schema", - "pydantic" - ] + "PURPOSE": "A Pydantic model used to represent the validated configuration and metadata of a loaded plugin. This object is what gets exposed to the API layer." }, "relations": [ { @@ -39395,29 +36354,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -39428,27 +36364,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PluginConfig:Class]\n# @SEMANTICS: plugin, config, schema, pydantic\n# @PURPOSE: A Pydantic model used to represent the validated configuration and metadata of a loaded plugin. This object is what gets exposed to the API layer.\n# @LAYER: Core\n# @RELATION: Instantiated by PluginLoader after validating a PluginBase instance.\nclass PluginConfig(BaseModel):\n \"\"\"Pydantic model for plugin configuration.\"\"\"\n id: str = Field(..., description=\"Unique identifier for the plugin\")\n name: str = Field(..., description=\"Human-readable name for the plugin\")\n description: str = Field(..., description=\"Brief description of what the plugin does\")\n version: str = Field(..., description=\"Version of the plugin\")\n ui_route: Optional[str] = Field(None, description=\"Frontend route for the plugin UI\")\n input_schema: Dict[str, Any] = Field(..., description=\"JSON schema for input parameters\", alias=\"schema\")\n# [/DEF:PluginConfig:Class]\n" + "anchor_syntax": "region", + "body": "# #region PluginConfig [TYPE Class] [SEMANTICS plugin, config, schema, pydantic]\n# @BRIEF A Pydantic model used to represent the validated configuration and metadata of a loaded plugin. This object is what gets exposed to the API layer.\n# @LAYER: Core\n# @RELATION Instantiated by PluginLoader after validating a PluginBase instance.\nclass PluginConfig(BaseModel):\n \"\"\"Pydantic model for plugin configuration.\"\"\"\n id: str = Field(..., description=\"Unique identifier for the plugin\")\n name: str = Field(..., description=\"Human-readable name for the plugin\")\n description: str = Field(..., description=\"Brief description of what the plugin does\")\n version: str = Field(..., description=\"Version of the plugin\")\n ui_route: str | None = Field(None, description=\"Frontend route for the plugin UI\")\n input_schema: dict[str, Any] = Field(..., description=\"JSON schema for input parameters\", alias=\"schema\")\n# #endregion PluginConfig\n" }, { "contract_id": "PluginLoader", "contract_type": "Class", "file_path": "backend/src/core/plugin_loader.py", - "start_line": 8, - "end_line": 192, + "start_line": 9, + "end_line": 176, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Core", - "PURPOSE": "Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface.", - "SEMANTICS": [ - "plugin", - "loader", - "dynamic", - "import" - ] + "PURPOSE": "Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface." }, "relations": [ { @@ -39480,40 +36410,17 @@ "actual_complexity": 3, "contract_type": "Class" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:PluginLoader:Class]\n# @COMPLEXITY: 3\n# @SEMANTICS: plugin, loader, dynamic, import\n# @PURPOSE: Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface.\n# @LAYER: Core\n# @RELATION: Depends on PluginBase. It is used by the main application to discover and manage available plugins.\nclass PluginLoader:\n \"\"\"\n Scans a directory for Python modules, loads them, and identifies classes\n that inherit from PluginBase.\n \"\"\"\n\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the PluginLoader with a directory to scan.\n # @PRE: plugin_dir is a valid directory path.\n # @POST: Plugins are loaded and registered.\n # @PARAM: plugin_dir (str) - The directory containing plugin modules.\n def __init__(self, plugin_dir: str):\n with belief_scope(\"__init__\"):\n self.plugin_dir = plugin_dir\n self._plugins: Dict[str, PluginBase] = {}\n self._plugin_configs: Dict[str, PluginConfig] = {}\n self._load_plugins()\n # [/DEF:__init__:Function]\n\n # [DEF:_load_plugins:Function]\n # @PURPOSE: Scans the plugin directory and loads all valid plugins.\n # @PRE: plugin_dir exists or can be created.\n # @POST: _load_module is called for each .py file.\n def _load_plugins(self):\n with belief_scope(\"_load_plugins\"):\n \"\"\"\n Scans the plugin directory, imports modules, and registers valid plugins.\n \"\"\"\n if not os.path.exists(self.plugin_dir):\n os.makedirs(self.plugin_dir)\n\n # Add the plugin directory's parent to sys.path to enable relative imports within plugins\n # This assumes plugin_dir is something like 'backend/src/plugins'\n # and we want 'backend/src' to be on the path for 'from ..core...' imports\n plugin_parent_dir = os.path.abspath(os.path.join(self.plugin_dir, os.pardir))\n if plugin_parent_dir not in sys.path:\n sys.path.insert(0, plugin_parent_dir)\n\n for filename in os.listdir(self.plugin_dir):\n file_path = os.path.join(self.plugin_dir, filename)\n \n # Handle directory-based plugins (packages)\n if os.path.isdir(file_path):\n init_file = os.path.join(file_path, \"__init__.py\")\n if os.path.exists(init_file):\n self._load_module(filename, init_file)\n continue\n\n # Handle single-file plugins\n if filename.endswith(\".py\") and filename != \"__init__.py\":\n module_name = filename[:-3]\n self._load_module(module_name, file_path)\n # [/DEF:_load_plugins:Function]\n\n # [DEF:_load_module:Function]\n # @PURPOSE: Loads a single Python module and discovers PluginBase implementations.\n # @PRE: module_name and file_path are valid.\n # @POST: Plugin classes are instantiated and registered.\n # @PARAM: module_name (str) - The name of the module.\n # @PARAM: file_path (str) - The path to the module file.\n def _load_module(self, module_name: str, file_path: str):\n with belief_scope(\"_load_module\"):\n \"\"\"\n Loads a single Python module and extracts PluginBase subclasses.\n \"\"\"\n # All runtime code is imported through the canonical `src` package root.\n package_name = f\"src.plugins.{module_name}\"\n \n # print(f\"DEBUG: Loading plugin {module_name} as {package_name}\")\n spec = importlib.util.spec_from_file_location(package_name, file_path)\n if spec is None or spec.loader is None:\n print(f\"Could not load module spec for {package_name}\") # Replace with proper logging\n return\n\n module = importlib.util.module_from_spec(spec)\n try:\n spec.loader.exec_module(module)\n except Exception as e:\n print(f\"Error loading plugin module {module_name}: {e}\") # Replace with proper logging\n return\n\n for attribute_name in dir(module):\n attribute = getattr(module, attribute_name)\n if (\n isinstance(attribute, type)\n and issubclass(attribute, PluginBase)\n and attribute is not PluginBase\n ):\n try:\n plugin_instance = attribute()\n self._register_plugin(plugin_instance)\n except Exception as e:\n print(f\"Error instantiating plugin {attribute_name} in {module_name}: {e}\") # Replace with proper logging\n # [/DEF:_load_module:Function]\n\n # [DEF:_register_plugin:Function]\n # @PURPOSE: Registers a PluginBase instance and its configuration.\n # @PRE: plugin_instance is a valid implementation of PluginBase.\n # @POST: Plugin is added to _plugins and _plugin_configs.\n # @PARAM: plugin_instance (PluginBase) - The plugin instance to register.\n def _register_plugin(self, plugin_instance: PluginBase):\n with belief_scope(\"_register_plugin\"):\n \"\"\"\n Registers a valid plugin instance.\n \"\"\"\n plugin_id = plugin_instance.id\n if plugin_id in self._plugins:\n print(f\"Warning: Duplicate plugin ID '{plugin_id}' found. Skipping.\") # Replace with proper logging\n return\n\n try:\n schema = plugin_instance.get_schema()\n # Basic validation to ensure it's a dictionary\n if not isinstance(schema, dict):\n raise TypeError(\"get_schema() must return a dictionary.\")\n \n plugin_config = PluginConfig(\n id=plugin_instance.id,\n name=plugin_instance.name,\n description=plugin_instance.description,\n version=plugin_instance.version,\n ui_route=plugin_instance.ui_route,\n schema=schema,\n )\n # The following line is commented out because it requires a schema to be passed to validate against.\n # The schema provided by the plugin is the one being validated, not the data.\n # validate(instance={}, schema=schema)\n self._plugins[plugin_id] = plugin_instance\n self._plugin_configs[plugin_id] = plugin_config\n from ..core.logger import logger\n logger.info(f\"Plugin '{plugin_instance.name}' (ID: {plugin_id}) loaded successfully.\")\n except Exception as e:\n from ..core.logger import logger\n logger.error(f\"Error validating plugin '{plugin_instance.name}' (ID: {plugin_id}): {e}\")\n # [/DEF:_register_plugin:Function]\n\n\n # [DEF:get_plugin:Function]\n # @PURPOSE: Retrieves a loaded plugin instance by its ID.\n # @PRE: plugin_id is a string.\n # @POST: Returns plugin instance or None.\n # @PARAM: plugin_id (str) - The unique identifier of the plugin.\n # @RETURN: Optional[PluginBase] - The plugin instance if found, otherwise None.\n def get_plugin(self, plugin_id: str) -> Optional[PluginBase]:\n with belief_scope(\"get_plugin\"):\n \"\"\"\n Returns a loaded plugin instance by its ID.\n \"\"\"\n return self._plugins.get(plugin_id)\n # [/DEF:get_plugin:Function]\n\n # [DEF:get_all_plugin_configs:Function]\n # @PURPOSE: Returns a list of all registered plugin configurations.\n # @PRE: None.\n # @POST: Returns list of all PluginConfig objects.\n # @RETURN: List[PluginConfig] - A list of plugin configurations.\n def get_all_plugin_configs(self) -> List[PluginConfig]:\n with belief_scope(\"get_all_plugin_configs\"):\n \"\"\"\n Returns a list of all loaded plugin configurations.\n \"\"\"\n return list(self._plugin_configs.values())\n # [/DEF:get_all_plugin_configs:Function]\n\n # [DEF:has_plugin:Function]\n # @PURPOSE: Checks if a plugin with the given ID is registered.\n # @PRE: plugin_id is a string.\n # @POST: Returns True if plugin exists.\n # @PARAM: plugin_id (str) - The unique identifier of the plugin.\n # @RETURN: bool - True if the plugin is registered, False otherwise.\n def has_plugin(self, plugin_id: str) -> bool:\n with belief_scope(\"has_plugin\"):\n \"\"\"\n Checks if a plugin with the given ID is loaded.\n \"\"\"\n return plugin_id in self._plugins\n # [/DEF:has_plugin:Function]\n\n# [/DEF:PluginLoader:Class]\n" + "anchor_syntax": "region", + "body": "# #region PluginLoader [C:3] [TYPE Class] [SEMANTICS plugin, loader, dynamic, import]\n# @BRIEF Scans a specified directory for Python modules, dynamically loads them, and registers any classes that are valid implementations of the PluginBase interface.\n# @LAYER: Core\n# @RELATION Depends on PluginBase. It is used by the main application to discover and manage available plugins.\nclass PluginLoader:\n \"\"\"\n Scans a directory for Python modules, loads them, and identifies classes\n that inherit from PluginBase.\n \"\"\"\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the PluginLoader with a directory to scan.\n # @PRE: plugin_dir is a valid directory path.\n # @POST: Plugins are loaded and registered.\n # @PARAM: plugin_dir (str) - The directory containing plugin modules.\n def __init__(self, plugin_dir: str):\n with belief_scope(\"__init__\"):\n self.plugin_dir = plugin_dir\n self._plugins: dict[str, PluginBase] = {}\n self._plugin_configs: dict[str, PluginConfig] = {}\n self._load_plugins()\n # #endregion __init__\n # #region _load_plugins [TYPE Function]\n # @PURPOSE: Scans the plugin directory and loads all valid plugins.\n # @PRE: plugin_dir exists or can be created.\n # @POST: _load_module is called for each .py file.\n def _load_plugins(self):\n with belief_scope(\"_load_plugins\"):\n \"\"\"\n Scans the plugin directory, imports modules, and registers valid plugins.\n \"\"\"\n if not os.path.exists(self.plugin_dir):\n os.makedirs(self.plugin_dir)\n # Add the plugin directory's parent to sys.path to enable relative imports within plugins\n # This assumes plugin_dir is something like 'backend/src/plugins'\n # and we want 'backend/src' to be on the path for 'from ..core...' imports\n plugin_parent_dir = os.path.abspath(os.path.join(self.plugin_dir, os.pardir))\n if plugin_parent_dir not in sys.path:\n sys.path.insert(0, plugin_parent_dir)\n for filename in os.listdir(self.plugin_dir):\n file_path = os.path.join(self.plugin_dir, filename)\n\n # Handle directory-based plugins (packages)\n if os.path.isdir(file_path):\n init_file = os.path.join(file_path, \"__init__.py\")\n if os.path.exists(init_file):\n self._load_module(filename, init_file)\n continue\n # Handle single-file plugins\n if filename.endswith(\".py\") and filename != \"__init__.py\":\n module_name = filename[:-3]\n self._load_module(module_name, file_path)\n # #endregion _load_plugins\n # #region _load_module [TYPE Function]\n # @PURPOSE: Loads a single Python module and discovers PluginBase implementations.\n # @PRE: module_name and file_path are valid.\n # @POST: Plugin classes are instantiated and registered.\n # @PARAM: module_name (str) - The name of the module.\n # @PARAM: file_path (str) - The path to the module file.\n def _load_module(self, module_name: str, file_path: str):\n with belief_scope(\"_load_module\"):\n \"\"\"\n Loads a single Python module and extracts PluginBase subclasses.\n \"\"\"\n # All runtime code is imported through the canonical `src` package root.\n package_name = f\"src.plugins.{module_name}\"\n\n # print(f\"DEBUG: Loading plugin {module_name} as {package_name}\")\n spec = importlib.util.spec_from_file_location(package_name, file_path)\n if spec is None or spec.loader is None:\n print(f\"Could not load module spec for {package_name}\") # Replace with proper logging\n return\n module = importlib.util.module_from_spec(spec)\n try:\n spec.loader.exec_module(module)\n except Exception as e:\n print(f\"Error loading plugin module {module_name}: {e}\") # Replace with proper logging\n return\n for attribute_name in dir(module):\n attribute = getattr(module, attribute_name)\n if (\n isinstance(attribute, type)\n and issubclass(attribute, PluginBase)\n and attribute is not PluginBase\n ):\n try:\n plugin_instance = attribute()\n self._register_plugin(plugin_instance)\n except Exception as e:\n print(f\"Error instantiating plugin {attribute_name} in {module_name}: {e}\") # Replace with proper logging\n # #endregion _load_module\n # #region _register_plugin [TYPE Function]\n # @PURPOSE: Registers a PluginBase instance and its configuration.\n # @PRE: plugin_instance is a valid implementation of PluginBase.\n # @POST: Plugin is added to _plugins and _plugin_configs.\n # @PARAM: plugin_instance (PluginBase) - The plugin instance to register.\n def _register_plugin(self, plugin_instance: PluginBase):\n with belief_scope(\"_register_plugin\"):\n \"\"\"\n Registers a valid plugin instance.\n \"\"\"\n plugin_id = plugin_instance.id\n if plugin_id in self._plugins:\n print(f\"Warning: Duplicate plugin ID '{plugin_id}' found. Skipping.\") # Replace with proper logging\n return\n try:\n schema = plugin_instance.get_schema()\n # Basic validation to ensure it's a dictionary\n if not isinstance(schema, dict):\n raise TypeError(\"get_schema() must return a dictionary.\")\n\n plugin_config = PluginConfig(\n id=plugin_instance.id,\n name=plugin_instance.name,\n description=plugin_instance.description,\n version=plugin_instance.version,\n ui_route=plugin_instance.ui_route,\n schema=schema,\n )\n # The following line is commented out because it requires a schema to be passed to validate against.\n # The schema provided by the plugin is the one being validated, not the data.\n # validate(instance={}, schema=schema)\n self._plugins[plugin_id] = plugin_instance\n self._plugin_configs[plugin_id] = plugin_config\n from ..core.logger import logger\n logger.info(f\"Plugin '{plugin_instance.name}' (ID: {plugin_id}) loaded successfully.\")\n except Exception as e:\n from ..core.logger import logger\n logger.error(f\"Error validating plugin '{plugin_instance.name}' (ID: {plugin_id}): {e}\")\n # #endregion _register_plugin\n # #region get_plugin [TYPE Function]\n # @PURPOSE: Retrieves a loaded plugin instance by its ID.\n # @PRE: plugin_id is a string.\n # @POST: Returns plugin instance or None.\n # @PARAM: plugin_id (str) - The unique identifier of the plugin.\n # @RETURN: Optional[PluginBase] - The plugin instance if found, otherwise None.\n def get_plugin(self, plugin_id: str) -> PluginBase | None:\n with belief_scope(\"get_plugin\"):\n \"\"\"\n Returns a loaded plugin instance by its ID.\n \"\"\"\n return self._plugins.get(plugin_id)\n # #endregion get_plugin\n # #region get_all_plugin_configs [TYPE Function]\n # @PURPOSE: Returns a list of all registered plugin configurations.\n # @PRE: None.\n # @POST: Returns list of all PluginConfig objects.\n # @RETURN: List[PluginConfig] - A list of plugin configurations.\n def get_all_plugin_configs(self) -> list[PluginConfig]:\n with belief_scope(\"get_all_plugin_configs\"):\n \"\"\"\n Returns a list of all loaded plugin configurations.\n \"\"\"\n return list(self._plugin_configs.values())\n # #endregion get_all_plugin_configs\n # #region has_plugin [TYPE Function]\n # @PURPOSE: Checks if a plugin with the given ID is registered.\n # @PRE: plugin_id is a string.\n # @POST: Returns True if plugin exists.\n # @PARAM: plugin_id (str) - The unique identifier of the plugin.\n # @RETURN: bool - True if the plugin is registered, False otherwise.\n def has_plugin(self, plugin_id: str) -> bool:\n with belief_scope(\"has_plugin\"):\n \"\"\"\n Checks if a plugin with the given ID is loaded.\n \"\"\"\n return plugin_id in self._plugins\n # #endregion has_plugin\n# #endregion PluginLoader\n" }, { "contract_id": "_load_plugins", "contract_type": "Function", "file_path": "backend/src/core/plugin_loader.py", - "start_line": 33, - "end_line": 66, + "start_line": 30, + "end_line": 60, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39551,15 +36458,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_load_plugins:Function]\n # @PURPOSE: Scans the plugin directory and loads all valid plugins.\n # @PRE: plugin_dir exists or can be created.\n # @POST: _load_module is called for each .py file.\n def _load_plugins(self):\n with belief_scope(\"_load_plugins\"):\n \"\"\"\n Scans the plugin directory, imports modules, and registers valid plugins.\n \"\"\"\n if not os.path.exists(self.plugin_dir):\n os.makedirs(self.plugin_dir)\n\n # Add the plugin directory's parent to sys.path to enable relative imports within plugins\n # This assumes plugin_dir is something like 'backend/src/plugins'\n # and we want 'backend/src' to be on the path for 'from ..core...' imports\n plugin_parent_dir = os.path.abspath(os.path.join(self.plugin_dir, os.pardir))\n if plugin_parent_dir not in sys.path:\n sys.path.insert(0, plugin_parent_dir)\n\n for filename in os.listdir(self.plugin_dir):\n file_path = os.path.join(self.plugin_dir, filename)\n \n # Handle directory-based plugins (packages)\n if os.path.isdir(file_path):\n init_file = os.path.join(file_path, \"__init__.py\")\n if os.path.exists(init_file):\n self._load_module(filename, init_file)\n continue\n\n # Handle single-file plugins\n if filename.endswith(\".py\") and filename != \"__init__.py\":\n module_name = filename[:-3]\n self._load_module(module_name, file_path)\n # [/DEF:_load_plugins:Function]\n" + "anchor_syntax": "region", + "body": " # #region _load_plugins [TYPE Function]\n # @PURPOSE: Scans the plugin directory and loads all valid plugins.\n # @PRE: plugin_dir exists or can be created.\n # @POST: _load_module is called for each .py file.\n def _load_plugins(self):\n with belief_scope(\"_load_plugins\"):\n \"\"\"\n Scans the plugin directory, imports modules, and registers valid plugins.\n \"\"\"\n if not os.path.exists(self.plugin_dir):\n os.makedirs(self.plugin_dir)\n # Add the plugin directory's parent to sys.path to enable relative imports within plugins\n # This assumes plugin_dir is something like 'backend/src/plugins'\n # and we want 'backend/src' to be on the path for 'from ..core...' imports\n plugin_parent_dir = os.path.abspath(os.path.join(self.plugin_dir, os.pardir))\n if plugin_parent_dir not in sys.path:\n sys.path.insert(0, plugin_parent_dir)\n for filename in os.listdir(self.plugin_dir):\n file_path = os.path.join(self.plugin_dir, filename)\n\n # Handle directory-based plugins (packages)\n if os.path.isdir(file_path):\n init_file = os.path.join(file_path, \"__init__.py\")\n if os.path.exists(init_file):\n self._load_module(filename, init_file)\n continue\n # Handle single-file plugins\n if filename.endswith(\".py\") and filename != \"__init__.py\":\n module_name = filename[:-3]\n self._load_module(module_name, file_path)\n # #endregion _load_plugins\n" }, { "contract_id": "_load_module", "contract_type": "Function", "file_path": "backend/src/core/plugin_loader.py", - "start_line": 68, - "end_line": 107, + "start_line": 61, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39604,15 +36511,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_load_module:Function]\n # @PURPOSE: Loads a single Python module and discovers PluginBase implementations.\n # @PRE: module_name and file_path are valid.\n # @POST: Plugin classes are instantiated and registered.\n # @PARAM: module_name (str) - The name of the module.\n # @PARAM: file_path (str) - The path to the module file.\n def _load_module(self, module_name: str, file_path: str):\n with belief_scope(\"_load_module\"):\n \"\"\"\n Loads a single Python module and extracts PluginBase subclasses.\n \"\"\"\n # All runtime code is imported through the canonical `src` package root.\n package_name = f\"src.plugins.{module_name}\"\n \n # print(f\"DEBUG: Loading plugin {module_name} as {package_name}\")\n spec = importlib.util.spec_from_file_location(package_name, file_path)\n if spec is None or spec.loader is None:\n print(f\"Could not load module spec for {package_name}\") # Replace with proper logging\n return\n\n module = importlib.util.module_from_spec(spec)\n try:\n spec.loader.exec_module(module)\n except Exception as e:\n print(f\"Error loading plugin module {module_name}: {e}\") # Replace with proper logging\n return\n\n for attribute_name in dir(module):\n attribute = getattr(module, attribute_name)\n if (\n isinstance(attribute, type)\n and issubclass(attribute, PluginBase)\n and attribute is not PluginBase\n ):\n try:\n plugin_instance = attribute()\n self._register_plugin(plugin_instance)\n except Exception as e:\n print(f\"Error instantiating plugin {attribute_name} in {module_name}: {e}\") # Replace with proper logging\n # [/DEF:_load_module:Function]\n" + "anchor_syntax": "region", + "body": " # #region _load_module [TYPE Function]\n # @PURPOSE: Loads a single Python module and discovers PluginBase implementations.\n # @PRE: module_name and file_path are valid.\n # @POST: Plugin classes are instantiated and registered.\n # @PARAM: module_name (str) - The name of the module.\n # @PARAM: file_path (str) - The path to the module file.\n def _load_module(self, module_name: str, file_path: str):\n with belief_scope(\"_load_module\"):\n \"\"\"\n Loads a single Python module and extracts PluginBase subclasses.\n \"\"\"\n # All runtime code is imported through the canonical `src` package root.\n package_name = f\"src.plugins.{module_name}\"\n\n # print(f\"DEBUG: Loading plugin {module_name} as {package_name}\")\n spec = importlib.util.spec_from_file_location(package_name, file_path)\n if spec is None or spec.loader is None:\n print(f\"Could not load module spec for {package_name}\") # Replace with proper logging\n return\n module = importlib.util.module_from_spec(spec)\n try:\n spec.loader.exec_module(module)\n except Exception as e:\n print(f\"Error loading plugin module {module_name}: {e}\") # Replace with proper logging\n return\n for attribute_name in dir(module):\n attribute = getattr(module, attribute_name)\n if (\n isinstance(attribute, type)\n and issubclass(attribute, PluginBase)\n and attribute is not PluginBase\n ):\n try:\n plugin_instance = attribute()\n self._register_plugin(plugin_instance)\n except Exception as e:\n print(f\"Error instantiating plugin {attribute_name} in {module_name}: {e}\") # Replace with proper logging\n # #endregion _load_module\n" }, { "contract_id": "_register_plugin", "contract_type": "Function", "file_path": "backend/src/core/plugin_loader.py", - "start_line": 109, - "end_line": 148, + "start_line": 99, + "end_line": 137, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39657,15 +36564,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_register_plugin:Function]\n # @PURPOSE: Registers a PluginBase instance and its configuration.\n # @PRE: plugin_instance is a valid implementation of PluginBase.\n # @POST: Plugin is added to _plugins and _plugin_configs.\n # @PARAM: plugin_instance (PluginBase) - The plugin instance to register.\n def _register_plugin(self, plugin_instance: PluginBase):\n with belief_scope(\"_register_plugin\"):\n \"\"\"\n Registers a valid plugin instance.\n \"\"\"\n plugin_id = plugin_instance.id\n if plugin_id in self._plugins:\n print(f\"Warning: Duplicate plugin ID '{plugin_id}' found. Skipping.\") # Replace with proper logging\n return\n\n try:\n schema = plugin_instance.get_schema()\n # Basic validation to ensure it's a dictionary\n if not isinstance(schema, dict):\n raise TypeError(\"get_schema() must return a dictionary.\")\n \n plugin_config = PluginConfig(\n id=plugin_instance.id,\n name=plugin_instance.name,\n description=plugin_instance.description,\n version=plugin_instance.version,\n ui_route=plugin_instance.ui_route,\n schema=schema,\n )\n # The following line is commented out because it requires a schema to be passed to validate against.\n # The schema provided by the plugin is the one being validated, not the data.\n # validate(instance={}, schema=schema)\n self._plugins[plugin_id] = plugin_instance\n self._plugin_configs[plugin_id] = plugin_config\n from ..core.logger import logger\n logger.info(f\"Plugin '{plugin_instance.name}' (ID: {plugin_id}) loaded successfully.\")\n except Exception as e:\n from ..core.logger import logger\n logger.error(f\"Error validating plugin '{plugin_instance.name}' (ID: {plugin_id}): {e}\")\n # [/DEF:_register_plugin:Function]\n" + "anchor_syntax": "region", + "body": " # #region _register_plugin [TYPE Function]\n # @PURPOSE: Registers a PluginBase instance and its configuration.\n # @PRE: plugin_instance is a valid implementation of PluginBase.\n # @POST: Plugin is added to _plugins and _plugin_configs.\n # @PARAM: plugin_instance (PluginBase) - The plugin instance to register.\n def _register_plugin(self, plugin_instance: PluginBase):\n with belief_scope(\"_register_plugin\"):\n \"\"\"\n Registers a valid plugin instance.\n \"\"\"\n plugin_id = plugin_instance.id\n if plugin_id in self._plugins:\n print(f\"Warning: Duplicate plugin ID '{plugin_id}' found. Skipping.\") # Replace with proper logging\n return\n try:\n schema = plugin_instance.get_schema()\n # Basic validation to ensure it's a dictionary\n if not isinstance(schema, dict):\n raise TypeError(\"get_schema() must return a dictionary.\")\n\n plugin_config = PluginConfig(\n id=plugin_instance.id,\n name=plugin_instance.name,\n description=plugin_instance.description,\n version=plugin_instance.version,\n ui_route=plugin_instance.ui_route,\n schema=schema,\n )\n # The following line is commented out because it requires a schema to be passed to validate against.\n # The schema provided by the plugin is the one being validated, not the data.\n # validate(instance={}, schema=schema)\n self._plugins[plugin_id] = plugin_instance\n self._plugin_configs[plugin_id] = plugin_config\n from ..core.logger import logger\n logger.info(f\"Plugin '{plugin_instance.name}' (ID: {plugin_id}) loaded successfully.\")\n except Exception as e:\n from ..core.logger import logger\n logger.error(f\"Error validating plugin '{plugin_instance.name}' (ID: {plugin_id}): {e}\")\n # #endregion _register_plugin\n" }, { "contract_id": "get_plugin", "contract_type": "Function", "file_path": "backend/src/core/plugin_loader.py", - "start_line": 151, - "end_line": 163, + "start_line": 138, + "end_line": 150, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39717,15 +36624,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:get_plugin:Function]\n # @PURPOSE: Retrieves a loaded plugin instance by its ID.\n # @PRE: plugin_id is a string.\n # @POST: Returns plugin instance or None.\n # @PARAM: plugin_id (str) - The unique identifier of the plugin.\n # @RETURN: Optional[PluginBase] - The plugin instance if found, otherwise None.\n def get_plugin(self, plugin_id: str) -> Optional[PluginBase]:\n with belief_scope(\"get_plugin\"):\n \"\"\"\n Returns a loaded plugin instance by its ID.\n \"\"\"\n return self._plugins.get(plugin_id)\n # [/DEF:get_plugin:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_plugin [TYPE Function]\n # @PURPOSE: Retrieves a loaded plugin instance by its ID.\n # @PRE: plugin_id is a string.\n # @POST: Returns plugin instance or None.\n # @PARAM: plugin_id (str) - The unique identifier of the plugin.\n # @RETURN: Optional[PluginBase] - The plugin instance if found, otherwise None.\n def get_plugin(self, plugin_id: str) -> PluginBase | None:\n with belief_scope(\"get_plugin\"):\n \"\"\"\n Returns a loaded plugin instance by its ID.\n \"\"\"\n return self._plugins.get(plugin_id)\n # #endregion get_plugin\n" }, { "contract_id": "get_all_plugin_configs", "contract_type": "Function", "file_path": "backend/src/core/plugin_loader.py", - "start_line": 165, - "end_line": 176, + "start_line": 151, + "end_line": 162, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39770,15 +36677,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:get_all_plugin_configs:Function]\n # @PURPOSE: Returns a list of all registered plugin configurations.\n # @PRE: None.\n # @POST: Returns list of all PluginConfig objects.\n # @RETURN: List[PluginConfig] - A list of plugin configurations.\n def get_all_plugin_configs(self) -> List[PluginConfig]:\n with belief_scope(\"get_all_plugin_configs\"):\n \"\"\"\n Returns a list of all loaded plugin configurations.\n \"\"\"\n return list(self._plugin_configs.values())\n # [/DEF:get_all_plugin_configs:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_all_plugin_configs [TYPE Function]\n # @PURPOSE: Returns a list of all registered plugin configurations.\n # @PRE: None.\n # @POST: Returns list of all PluginConfig objects.\n # @RETURN: List[PluginConfig] - A list of plugin configurations.\n def get_all_plugin_configs(self) -> list[PluginConfig]:\n with belief_scope(\"get_all_plugin_configs\"):\n \"\"\"\n Returns a list of all loaded plugin configurations.\n \"\"\"\n return list(self._plugin_configs.values())\n # #endregion get_all_plugin_configs\n" }, { "contract_id": "has_plugin", "contract_type": "Function", "file_path": "backend/src/core/plugin_loader.py", - "start_line": 178, - "end_line": 190, + "start_line": 163, + "end_line": 175, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39830,27 +36737,21 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:has_plugin:Function]\n # @PURPOSE: Checks if a plugin with the given ID is registered.\n # @PRE: plugin_id is a string.\n # @POST: Returns True if plugin exists.\n # @PARAM: plugin_id (str) - The unique identifier of the plugin.\n # @RETURN: bool - True if the plugin is registered, False otherwise.\n def has_plugin(self, plugin_id: str) -> bool:\n with belief_scope(\"has_plugin\"):\n \"\"\"\n Checks if a plugin with the given ID is loaded.\n \"\"\"\n return plugin_id in self._plugins\n # [/DEF:has_plugin:Function]\n" + "anchor_syntax": "region", + "body": " # #region has_plugin [TYPE Function]\n # @PURPOSE: Checks if a plugin with the given ID is registered.\n # @PRE: plugin_id is a string.\n # @POST: Returns True if plugin exists.\n # @PARAM: plugin_id (str) - The unique identifier of the plugin.\n # @RETURN: bool - True if the plugin is registered, False otherwise.\n def has_plugin(self, plugin_id: str) -> bool:\n with belief_scope(\"has_plugin\"):\n \"\"\"\n Checks if a plugin with the given ID is loaded.\n \"\"\"\n return plugin_id in self._plugins\n # #endregion has_plugin\n" }, { "contract_id": "SchedulerModule", "contract_type": "Module", "file_path": "backend/src/core/scheduler.py", "start_line": 1, - "end_line": 215, + "end_line": 253, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Core", - "PURPOSE": "Manages scheduled tasks using APScheduler.", - "SEMANTICS": [ - "scheduler", - "apscheduler", - "cron", - "backup" - ] + "PURPOSE": "Manages scheduled tasks using APScheduler." }, "relations": [ { @@ -39861,25 +36762,20 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SchedulerModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: scheduler, apscheduler, cron, backup\n# @PURPOSE: Manages scheduled tasks using APScheduler.\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> TaskManager\n\n# [SECTION: IMPORTS]\nfrom apscheduler.schedulers.background import BackgroundScheduler\nfrom apscheduler.triggers.cron import CronTrigger\nfrom .logger import logger, belief_scope\nfrom .config_manager import ConfigManager\nfrom .cot_logger import MarkerLogger, get_trace_id, set_trace_id\n\nlog = MarkerLogger(\"SchedulerService\")\nlog_tsc = MarkerLogger(\"ThrottledSchedulerConfigurator\")\nimport asyncio\nfrom datetime import datetime, time, timedelta, date\n# [/SECTION]\n\n\n# [DEF:SchedulerService:Class]\n# @COMPLEXITY: 3\n# @SEMANTICS: scheduler, service, apscheduler\n# @PURPOSE: Provides a service to manage scheduled backup tasks.\n# @RELATION: DEPENDS_ON -> [ThrottledSchedulerConfigurator]\nclass SchedulerService:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the scheduler service with task and config managers.\n # @PRE: task_manager and config_manager must be provided.\n # @POST: Scheduler instance is created but not started.\n def __init__(self, task_manager, config_manager: ConfigManager):\n with belief_scope(\"SchedulerService.__init__\"):\n self.task_manager = task_manager\n self.config_manager = config_manager\n self.scheduler = BackgroundScheduler()\n self.loop = asyncio.get_event_loop()\n\n # [/DEF:__init__:Function]\n\n # [DEF:start:Function]\n # @PURPOSE: Starts the background scheduler and loads initial schedules.\n # @PRE: Scheduler should be initialized.\n # @POST: Scheduler is running and schedules are loaded.\n def start(self):\n with belief_scope(\"SchedulerService.start\"):\n if not self.scheduler.running:\n self.scheduler.start()\n log.reflect(\"Scheduler started\")\n self.load_schedules()\n\n # [/DEF:start:Function]\n\n # [DEF:stop:Function]\n # @PURPOSE: Stops the background scheduler.\n # @PRE: Scheduler should be running.\n # @POST: Scheduler is shut down.\n def stop(self):\n with belief_scope(\"SchedulerService.stop\"):\n if self.scheduler.running:\n self.scheduler.shutdown()\n log.reflect(\"Scheduler stopped\")\n\n # [/DEF:stop:Function]\n\n # [DEF:load_schedules:Function]\n # @PURPOSE: Loads backup schedules from configuration and registers them.\n # @PRE: config_manager must have valid configuration.\n # @POST: All enabled backup jobs are added to the scheduler.\n def load_schedules(self):\n with belief_scope(\"SchedulerService.load_schedules\"):\n # Clear existing jobs\n self.scheduler.remove_all_jobs()\n\n config = self.config_manager.get_config()\n for env in config.environments:\n if env.backup_schedule and env.backup_schedule.enabled:\n self.add_backup_job(env.id, env.backup_schedule.cron_expression)\n\n # [/DEF:load_schedules:Function]\n\n # [DEF:add_backup_job:Function]\n # @PURPOSE: Adds a scheduled backup job for an environment.\n # @PRE: env_id and cron_expression must be valid strings.\n # @POST: A new job is added to the scheduler or replaced if it already exists.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PARAM: cron_expression (str) - The cron expression for the schedule.\n def add_backup_job(self, env_id: str, cron_expression: str):\n with belief_scope(\n \"SchedulerService.add_backup_job\",\n f\"env_id={env_id}, cron={cron_expression}\",\n ):\n job_id = f\"backup_{env_id}\"\n try:\n self.scheduler.add_job(\n self._trigger_backup,\n CronTrigger.from_crontab(cron_expression),\n id=job_id,\n args=[env_id],\n replace_existing=True,\n )\n log.reflect(\n \"Scheduled backup job added\",\n payload={\"env_id\": env_id, \"cron\": cron_expression},\n )\n except Exception as e:\n log.explore(\"Failed to add backup job\", error=str(e), payload={\"env_id\": env_id, \"cron\": cron_expression})\n logger.error(f\"Failed to add backup job for environment {env_id}: {e}\")\n\n # [/DEF:add_backup_job:Function]\n\n # [DEF:_trigger_backup:Function]\n # @PURPOSE: Triggered by the scheduler to start a backup task.\n # @PRE: env_id must be a valid environment ID.\n # @POST: A new backup task is created in the task manager if not already running.\n # @PARAM: env_id (str) - The ID of the environment.\n def _trigger_backup(self, env_id: str):\n with belief_scope(\"SchedulerService._trigger_backup\", f\"env_id={env_id}\"):\n log.reason(\"Triggering scheduled backup\", payload={\"env_id\": env_id})\n\n # Check if a backup is already running for this environment\n active_tasks = self.task_manager.get_tasks(limit=100)\n for task in active_tasks:\n if (\n task.plugin_id == \"superset-backup\"\n and task.status in [\"PENDING\", \"RUNNING\"]\n and task.params.get(\"environment_id\") == env_id\n ):\n log.explore(\"Backup already running, skipping scheduled run\", error=\"Duplicate backup prevented\", payload={\"env_id\": env_id})\n return\n\n # Run the backup task\n # We need to run this in the event loop since create_task is async\n trace_id = get_trace_id()\n\n async def _backup_task():\n if trace_id:\n set_trace_id(trace_id)\n await self.task_manager.create_task(\n \"superset-backup\", {\"environment_id\": env_id}\n )\n\n asyncio.run_coroutine_threadsafe(_backup_task(), self.loop)\n\n # [/DEF:_trigger_backup:Function]\n\n\n# [/DEF:SchedulerService:Class]\n\n\n# [DEF:ThrottledSchedulerConfigurator:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: scheduler, throttling, distribution\n# @PURPOSE: Distributes validation tasks evenly within an execution window.\n# @PRE: Validation policies provide a finite dashboard list and a valid execution window.\n# @POST: Produces deterministic per-dashboard run timestamps within the configured window.\n# @RELATION: DEPENDS_ON -> SchedulerModule\n# @INVARIANT: Returned schedule size always matches number of dashboard IDs.\n# @SIDE_EFFECT: Emits warning logs for degenerate or near-zero scheduling windows.\n# @DATA_CONTRACT: Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]]\nclass ThrottledSchedulerConfigurator:\n # [DEF:calculate_schedule:Function]\n # @PURPOSE: Calculates execution times for N tasks within a window.\n # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date).\n # @POST: Returns List[datetime] of scheduled times.\n # @INVARIANT: Tasks are distributed with near-even spacing.\n @staticmethod\n def calculate_schedule(\n window_start: time, window_end: time, dashboard_ids: list, current_date: date\n ) -> list:\n with belief_scope(\"ThrottledSchedulerConfigurator.calculate_schedule\"):\n n = len(dashboard_ids)\n if n == 0:\n return []\n\n start_dt = datetime.combine(current_date, window_start)\n end_dt = datetime.combine(current_date, window_end)\n\n # Handle window crossing midnight\n if end_dt < start_dt:\n end_dt += timedelta(days=1)\n\n total_seconds = (end_dt - start_dt).total_seconds()\n\n # Minimum interval of 1 second to avoid division by zero or negative\n if total_seconds <= 0:\n log_tsc.explore(\"Window size is zero or negative, falling back to start time\", error=f\"total_seconds={total_seconds}\", payload={\"n\": n})\n return [start_dt] * n\n\n # If window is too small for even distribution (e.g. 10 tasks in 5 seconds),\n # we still distribute them but they might be very close.\n # The requirement says \"near-even spacing\".\n\n if n == 1:\n return [start_dt]\n\n interval = total_seconds / (n - 1) if n > 1 else 0\n\n # If interval is too small (e.g. < 1s), we might want a fallback,\n # but the spec says \"handle too-small windows with explicit fallback/warning\".\n if interval < 1:\n log_tsc.explore(\"Window too small for task distribution\", error=f\"interval={interval:.2f}s\", payload={\"n\": n})\n\n scheduled_times = []\n for i in range(n):\n scheduled_times.append(start_dt + timedelta(seconds=i * interval))\n\n return scheduled_times\n\n # [/DEF:calculate_schedule:Function]\n\n\n# [/DEF:ThrottledSchedulerConfigurator:Class]\n\n# [/DEF:SchedulerModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region SchedulerModule [C:3] [TYPE Module] [SEMANTICS scheduler, schedule, scheduler-service]\n# @BRIEF Manages scheduled tasks using APScheduler.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> TaskManager\nimport asyncio\nfrom datetime import date, datetime, time, timedelta\n\nfrom apscheduler.schedulers.background import BackgroundScheduler\nfrom apscheduler.triggers.cron import CronTrigger\n\nfrom .config_manager import ConfigManager\nfrom .cot_logger import seed_trace_id\nfrom .database import SessionLocal\nfrom .logger import belief_scope, logger\n\n\n# #region SchedulerService [C:3] [TYPE Class] [SEMANTICS scheduler, service, apscheduler]\n# @BRIEF Provides a service to manage scheduled backup tasks.\n# @RELATION DEPENDS_ON -> [ThrottledSchedulerConfigurator]\nclass SchedulerService:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the scheduler service with task and config managers.\n # @PRE: task_manager and config_manager must be provided.\n # @POST: Scheduler instance is created but not started.\n def __init__(self, task_manager, config_manager: ConfigManager):\n with belief_scope(\"SchedulerService.__init__\"):\n self.task_manager = task_manager\n self.config_manager = config_manager\n self.scheduler = BackgroundScheduler()\n self.loop = asyncio.get_event_loop()\n # #endregion __init__\n # #region start [TYPE Function]\n # @PURPOSE: Starts the background scheduler and loads initial schedules.\n # @PRE: Scheduler should be initialized.\n # @POST: Scheduler is running and schedules are loaded.\n def start(self):\n with belief_scope(\"SchedulerService.start\"):\n if not self.scheduler.running:\n self.scheduler.start()\n logger.info(\"Scheduler started.\")\n self.load_schedules()\n # #endregion start\n # #region stop [TYPE Function]\n # @PURPOSE: Stops the background scheduler.\n # @PRE: Scheduler should be running.\n # @POST: Scheduler is shut down.\n def stop(self):\n with belief_scope(\"SchedulerService.stop\"):\n if self.scheduler.running:\n self.scheduler.shutdown()\n logger.info(\"Scheduler stopped.\")\n # #endregion stop\n # #region load_schedules [C:4] [TYPE Function] [SEMANTICS scheduler,backup,translation,apscheduler]\n # @BRIEF Load backup and active translation schedules from config and DB, re-registering all jobs.\n # @PRE config_manager must have valid configuration; database is accessible.\n # @POST All enabled backup jobs and active translation schedules are re-registered in APScheduler.\n # @SIDE_EFFECT Removes all existing APScheduler jobs; queries translation_schedules table; registers APScheduler jobs for translation.\n def load_schedules(self):\n with belief_scope(\"SchedulerService.load_schedules\"):\n # Clear existing jobs\n self.scheduler.remove_all_jobs()\n config = self.config_manager.get_config()\n for env in config.environments:\n if env.backup_schedule and env.backup_schedule.enabled:\n self.add_backup_job(env.id, env.backup_schedule.cron_expression)\n # Re-register active translation schedules from the database\n try:\n from ..models.translate import TranslationSchedule\n db = SessionLocal()\n try:\n active_schedules = (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active == True)\n .all()\n )\n for sched in active_schedules:\n self.add_translation_job(\n schedule_id=sched.id,\n job_id=sched.job_id,\n cron_expression=sched.cron_expression,\n timezone=sched.timezone,\n execution_mode=getattr(sched, 'execution_mode', 'full'),\n )\n if active_schedules:\n logger.reason(f\"Loaded {len(active_schedules)} active translation schedule(s)\")\n finally:\n db.close()\n except Exception as e:\n logger.explore(\"Failed to load translation schedules on startup\",\n extra={\"error\": str(e)})\n # #endregion load_schedules\n # #region add_backup_job [TYPE Function]\n # @PURPOSE: Adds a scheduled backup job for an environment.\n # @PRE: env_id and cron_expression must be valid strings.\n # @POST: A new job is added to the scheduler or replaced if it already exists.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PARAM: cron_expression (str) - The cron expression for the schedule.\n def add_backup_job(self, env_id: str, cron_expression: str):\n with belief_scope(\n \"SchedulerService.add_backup_job\",\n f\"env_id={env_id}, cron={cron_expression}\",\n ):\n job_id = f\"backup_{env_id}\"\n try:\n self.scheduler.add_job(\n self._trigger_backup,\n CronTrigger.from_crontab(cron_expression),\n id=job_id,\n args=[env_id],\n replace_existing=True,\n )\n logger.info(\n f\"Scheduled backup job added for environment {env_id}: {cron_expression}\"\n )\n except Exception as e:\n logger.error(f\"Failed to add backup job for environment {env_id}: {e}\")\n # #endregion add_backup_job\n # #region add_translation_job [C:4] [TYPE Function] [SEMANTICS scheduler,translation,cron,apscheduler]\n # @BRIEF Register a translation schedule with APScheduler.\n # @PRE schedule_id, job_id, and cron_expression are valid strings.\n # @POST A new APScheduler job is registered or replaced if it already exists.\n # @SIDE_EFFECT Mutates APScheduler state; calls execute_scheduled_translation on trigger.\n # @RELATION DEPENDS_ON -> [execute_scheduled_translation]\n # @RELATION DEPENDS_ON -> [SessionLocal]\n def add_translation_job(self, schedule_id: str, job_id: str, cron_expression: str, timezone: str = \"UTC\", execution_mode: str = \"full\"):\n with belief_scope(\n \"SchedulerService.add_translation_job\",\n f\"schedule_id={schedule_id}, job_id={job_id}, cron={cron_expression}\",\n ):\n from zoneinfo import ZoneInfo\n\n from ..plugins.translate.scheduler import execute_scheduled_translation\n\n job_id_aps = f\"translate_{schedule_id}\"\n try:\n tz = ZoneInfo(timezone)\n self.scheduler.add_job(\n execute_scheduled_translation,\n CronTrigger.from_crontab(cron_expression, timezone=tz),\n id=job_id_aps,\n args=[schedule_id, job_id, SessionLocal, self.config_manager, execution_mode],\n replace_existing=True,\n )\n logger.reason(\n f\"Translation schedule registered: {job_id_aps} ({cron_expression})\"\n )\n except Exception as e:\n logger.explore(\n f\"Failed to register translation schedule: {job_id_aps}\",\n extra={\"error\": str(e)},\n )\n # #endregion add_translation_job\n # #region remove_translation_job [C:4] [TYPE Function] [SEMANTICS scheduler,translation,remove,apscheduler]\n # @BRIEF Remove a translation schedule from APScheduler.\n # @PRE schedule_id is a valid string.\n # @POST The APScheduler job is removed if it exists; silently ignored otherwise.\n # @SIDE_EFFECT Mutates APScheduler state.\n def remove_translation_job(self, schedule_id: str):\n with belief_scope(\n \"SchedulerService.remove_translation_job\",\n f\"schedule_id={schedule_id}\",\n ):\n job_id_aps = f\"translate_{schedule_id}\"\n try:\n self.scheduler.remove_job(job_id_aps)\n logger.info(f\"Translation schedule removed: {job_id_aps}\")\n except Exception:\n logger.reason(f\"Translation schedule not found (already removed): {job_id_aps}\")\n # #endregion remove_translation_job\n # #region _trigger_backup [TYPE Function]\n # @PURPOSE: Triggered by the scheduler to start a backup task.\n # @PRE: env_id must be a valid environment ID.\n # @POST: A new backup task is created in the task manager if not already running.\n # @PARAM: env_id (str) - The ID of the environment.\n def _trigger_backup(self, env_id: str):\n seed_trace_id()\n with belief_scope(\"SchedulerService._trigger_backup\", f\"env_id={env_id}\"):\n logger.info(f\"Triggering scheduled backup for environment {env_id}\")\n # Check if a backup is already running for this environment\n active_tasks = self.task_manager.get_tasks(limit=100)\n for task in active_tasks:\n if (\n task.plugin_id == \"superset-backup\"\n and task.status in [\"PENDING\", \"RUNNING\"]\n and task.params.get(\"environment_id\") == env_id\n ):\n logger.warning(\n f\"Backup already running for environment {env_id}. Skipping scheduled run.\"\n )\n return\n # Run the backup task\n # We need to run this in the event loop since create_task is async\n asyncio.run_coroutine_threadsafe(\n self.task_manager.create_task(\n \"superset-backup\", {\"environment_id\": env_id}\n ),\n self.loop,\n )\n # #endregion _trigger_backup\n# #endregion SchedulerService\n# #region ThrottledSchedulerConfigurator [C:5] [TYPE Class] [SEMANTICS scheduler, throttling, distribution]\n# @BRIEF Distributes validation tasks evenly within an execution window.\n# @PRE: Validation policies provide a finite dashboard list and a valid execution window.\n# @POST: Produces deterministic per-dashboard run timestamps within the configured window.\n# @RELATION DEPENDS_ON -> SchedulerModule\n# @INVARIANT: Returned schedule size always matches number of dashboard IDs.\n# @SIDE_EFFECT: Emits warning logs for degenerate or near-zero scheduling windows.\n# @DATA_CONTRACT: Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]]\nclass ThrottledSchedulerConfigurator:\n # #region calculate_schedule [TYPE Function]\n # @PURPOSE: Calculates execution times for N tasks within a window.\n # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date).\n # @POST: Returns List[datetime] of scheduled times.\n # @INVARIANT: Tasks are distributed with near-even spacing.\n @staticmethod\n def calculate_schedule(\n window_start: time, window_end: time, dashboard_ids: list, current_date: date\n ) -> list:\n with belief_scope(\"ThrottledSchedulerConfigurator.calculate_schedule\"):\n n = len(dashboard_ids)\n if n == 0:\n return []\n start_dt = datetime.combine(current_date, window_start)\n end_dt = datetime.combine(current_date, window_end)\n # Handle window crossing midnight\n if end_dt < start_dt:\n end_dt += timedelta(days=1)\n total_seconds = (end_dt - start_dt).total_seconds()\n # Minimum interval of 1 second to avoid division by zero or negative\n if total_seconds <= 0:\n logger.warning(\n f\"[calculate_schedule] Window size is zero or negative. Falling back to start time for all {n} tasks.\"\n )\n return [start_dt] * n\n # If window is too small for even distribution (e.g. 10 tasks in 5 seconds),\n # we still distribute them but they might be very close.\n # The requirement says \"near-even spacing\".\n if n == 1:\n return [start_dt]\n interval = total_seconds / (n - 1) if n > 1 else 0\n # If interval is too small (e.g. < 1s), we might want a fallback,\n # but the spec says \"handle too-small windows with explicit fallback/warning\".\n if interval < 1:\n logger.warning(\n f\"[calculate_schedule] Window too small for {n} tasks (interval {interval:.2f}s). Tasks will be highly concentrated.\"\n )\n scheduled_times = []\n for i in range(n):\n scheduled_times.append(start_dt + timedelta(seconds=i * interval))\n return scheduled_times\n # #endregion calculate_schedule\n# #endregion ThrottledSchedulerConfigurator\n# #endregion SchedulerModule\n" }, { "contract_id": "SchedulerService", "contract_type": "Class", "file_path": "backend/src/core/scheduler.py", - "start_line": 22, - "end_line": 148, + "start_line": 17, + "end_line": 200, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Provides a service to manage scheduled backup tasks.", - "SEMANTICS": [ - "scheduler", - "service", - "apscheduler" - ] + "COMPLEXITY": 3, + "PURPOSE": "Provides a service to manage scheduled backup tasks." }, "relations": [ { @@ -39889,40 +36785,16 @@ "target_ref": "[ThrottledSchedulerConfigurator]" } ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:SchedulerService:Class]\n# @COMPLEXITY: 3\n# @SEMANTICS: scheduler, service, apscheduler\n# @PURPOSE: Provides a service to manage scheduled backup tasks.\n# @RELATION: DEPENDS_ON -> [ThrottledSchedulerConfigurator]\nclass SchedulerService:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the scheduler service with task and config managers.\n # @PRE: task_manager and config_manager must be provided.\n # @POST: Scheduler instance is created but not started.\n def __init__(self, task_manager, config_manager: ConfigManager):\n with belief_scope(\"SchedulerService.__init__\"):\n self.task_manager = task_manager\n self.config_manager = config_manager\n self.scheduler = BackgroundScheduler()\n self.loop = asyncio.get_event_loop()\n\n # [/DEF:__init__:Function]\n\n # [DEF:start:Function]\n # @PURPOSE: Starts the background scheduler and loads initial schedules.\n # @PRE: Scheduler should be initialized.\n # @POST: Scheduler is running and schedules are loaded.\n def start(self):\n with belief_scope(\"SchedulerService.start\"):\n if not self.scheduler.running:\n self.scheduler.start()\n log.reflect(\"Scheduler started\")\n self.load_schedules()\n\n # [/DEF:start:Function]\n\n # [DEF:stop:Function]\n # @PURPOSE: Stops the background scheduler.\n # @PRE: Scheduler should be running.\n # @POST: Scheduler is shut down.\n def stop(self):\n with belief_scope(\"SchedulerService.stop\"):\n if self.scheduler.running:\n self.scheduler.shutdown()\n log.reflect(\"Scheduler stopped\")\n\n # [/DEF:stop:Function]\n\n # [DEF:load_schedules:Function]\n # @PURPOSE: Loads backup schedules from configuration and registers them.\n # @PRE: config_manager must have valid configuration.\n # @POST: All enabled backup jobs are added to the scheduler.\n def load_schedules(self):\n with belief_scope(\"SchedulerService.load_schedules\"):\n # Clear existing jobs\n self.scheduler.remove_all_jobs()\n\n config = self.config_manager.get_config()\n for env in config.environments:\n if env.backup_schedule and env.backup_schedule.enabled:\n self.add_backup_job(env.id, env.backup_schedule.cron_expression)\n\n # [/DEF:load_schedules:Function]\n\n # [DEF:add_backup_job:Function]\n # @PURPOSE: Adds a scheduled backup job for an environment.\n # @PRE: env_id and cron_expression must be valid strings.\n # @POST: A new job is added to the scheduler or replaced if it already exists.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PARAM: cron_expression (str) - The cron expression for the schedule.\n def add_backup_job(self, env_id: str, cron_expression: str):\n with belief_scope(\n \"SchedulerService.add_backup_job\",\n f\"env_id={env_id}, cron={cron_expression}\",\n ):\n job_id = f\"backup_{env_id}\"\n try:\n self.scheduler.add_job(\n self._trigger_backup,\n CronTrigger.from_crontab(cron_expression),\n id=job_id,\n args=[env_id],\n replace_existing=True,\n )\n log.reflect(\n \"Scheduled backup job added\",\n payload={\"env_id\": env_id, \"cron\": cron_expression},\n )\n except Exception as e:\n log.explore(\"Failed to add backup job\", error=str(e), payload={\"env_id\": env_id, \"cron\": cron_expression})\n logger.error(f\"Failed to add backup job for environment {env_id}: {e}\")\n\n # [/DEF:add_backup_job:Function]\n\n # [DEF:_trigger_backup:Function]\n # @PURPOSE: Triggered by the scheduler to start a backup task.\n # @PRE: env_id must be a valid environment ID.\n # @POST: A new backup task is created in the task manager if not already running.\n # @PARAM: env_id (str) - The ID of the environment.\n def _trigger_backup(self, env_id: str):\n with belief_scope(\"SchedulerService._trigger_backup\", f\"env_id={env_id}\"):\n log.reason(\"Triggering scheduled backup\", payload={\"env_id\": env_id})\n\n # Check if a backup is already running for this environment\n active_tasks = self.task_manager.get_tasks(limit=100)\n for task in active_tasks:\n if (\n task.plugin_id == \"superset-backup\"\n and task.status in [\"PENDING\", \"RUNNING\"]\n and task.params.get(\"environment_id\") == env_id\n ):\n log.explore(\"Backup already running, skipping scheduled run\", error=\"Duplicate backup prevented\", payload={\"env_id\": env_id})\n return\n\n # Run the backup task\n # We need to run this in the event loop since create_task is async\n trace_id = get_trace_id()\n\n async def _backup_task():\n if trace_id:\n set_trace_id(trace_id)\n await self.task_manager.create_task(\n \"superset-backup\", {\"environment_id\": env_id}\n )\n\n asyncio.run_coroutine_threadsafe(_backup_task(), self.loop)\n\n # [/DEF:_trigger_backup:Function]\n\n\n# [/DEF:SchedulerService:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region SchedulerService [C:3] [TYPE Class] [SEMANTICS scheduler, service, apscheduler]\n# @BRIEF Provides a service to manage scheduled backup tasks.\n# @RELATION DEPENDS_ON -> [ThrottledSchedulerConfigurator]\nclass SchedulerService:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the scheduler service with task and config managers.\n # @PRE: task_manager and config_manager must be provided.\n # @POST: Scheduler instance is created but not started.\n def __init__(self, task_manager, config_manager: ConfigManager):\n with belief_scope(\"SchedulerService.__init__\"):\n self.task_manager = task_manager\n self.config_manager = config_manager\n self.scheduler = BackgroundScheduler()\n self.loop = asyncio.get_event_loop()\n # #endregion __init__\n # #region start [TYPE Function]\n # @PURPOSE: Starts the background scheduler and loads initial schedules.\n # @PRE: Scheduler should be initialized.\n # @POST: Scheduler is running and schedules are loaded.\n def start(self):\n with belief_scope(\"SchedulerService.start\"):\n if not self.scheduler.running:\n self.scheduler.start()\n logger.info(\"Scheduler started.\")\n self.load_schedules()\n # #endregion start\n # #region stop [TYPE Function]\n # @PURPOSE: Stops the background scheduler.\n # @PRE: Scheduler should be running.\n # @POST: Scheduler is shut down.\n def stop(self):\n with belief_scope(\"SchedulerService.stop\"):\n if self.scheduler.running:\n self.scheduler.shutdown()\n logger.info(\"Scheduler stopped.\")\n # #endregion stop\n # #region load_schedules [C:4] [TYPE Function] [SEMANTICS scheduler,backup,translation,apscheduler]\n # @BRIEF Load backup and active translation schedules from config and DB, re-registering all jobs.\n # @PRE config_manager must have valid configuration; database is accessible.\n # @POST All enabled backup jobs and active translation schedules are re-registered in APScheduler.\n # @SIDE_EFFECT Removes all existing APScheduler jobs; queries translation_schedules table; registers APScheduler jobs for translation.\n def load_schedules(self):\n with belief_scope(\"SchedulerService.load_schedules\"):\n # Clear existing jobs\n self.scheduler.remove_all_jobs()\n config = self.config_manager.get_config()\n for env in config.environments:\n if env.backup_schedule and env.backup_schedule.enabled:\n self.add_backup_job(env.id, env.backup_schedule.cron_expression)\n # Re-register active translation schedules from the database\n try:\n from ..models.translate import TranslationSchedule\n db = SessionLocal()\n try:\n active_schedules = (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active == True)\n .all()\n )\n for sched in active_schedules:\n self.add_translation_job(\n schedule_id=sched.id,\n job_id=sched.job_id,\n cron_expression=sched.cron_expression,\n timezone=sched.timezone,\n execution_mode=getattr(sched, 'execution_mode', 'full'),\n )\n if active_schedules:\n logger.reason(f\"Loaded {len(active_schedules)} active translation schedule(s)\")\n finally:\n db.close()\n except Exception as e:\n logger.explore(\"Failed to load translation schedules on startup\",\n extra={\"error\": str(e)})\n # #endregion load_schedules\n # #region add_backup_job [TYPE Function]\n # @PURPOSE: Adds a scheduled backup job for an environment.\n # @PRE: env_id and cron_expression must be valid strings.\n # @POST: A new job is added to the scheduler or replaced if it already exists.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PARAM: cron_expression (str) - The cron expression for the schedule.\n def add_backup_job(self, env_id: str, cron_expression: str):\n with belief_scope(\n \"SchedulerService.add_backup_job\",\n f\"env_id={env_id}, cron={cron_expression}\",\n ):\n job_id = f\"backup_{env_id}\"\n try:\n self.scheduler.add_job(\n self._trigger_backup,\n CronTrigger.from_crontab(cron_expression),\n id=job_id,\n args=[env_id],\n replace_existing=True,\n )\n logger.info(\n f\"Scheduled backup job added for environment {env_id}: {cron_expression}\"\n )\n except Exception as e:\n logger.error(f\"Failed to add backup job for environment {env_id}: {e}\")\n # #endregion add_backup_job\n # #region add_translation_job [C:4] [TYPE Function] [SEMANTICS scheduler,translation,cron,apscheduler]\n # @BRIEF Register a translation schedule with APScheduler.\n # @PRE schedule_id, job_id, and cron_expression are valid strings.\n # @POST A new APScheduler job is registered or replaced if it already exists.\n # @SIDE_EFFECT Mutates APScheduler state; calls execute_scheduled_translation on trigger.\n # @RELATION DEPENDS_ON -> [execute_scheduled_translation]\n # @RELATION DEPENDS_ON -> [SessionLocal]\n def add_translation_job(self, schedule_id: str, job_id: str, cron_expression: str, timezone: str = \"UTC\", execution_mode: str = \"full\"):\n with belief_scope(\n \"SchedulerService.add_translation_job\",\n f\"schedule_id={schedule_id}, job_id={job_id}, cron={cron_expression}\",\n ):\n from zoneinfo import ZoneInfo\n\n from ..plugins.translate.scheduler import execute_scheduled_translation\n\n job_id_aps = f\"translate_{schedule_id}\"\n try:\n tz = ZoneInfo(timezone)\n self.scheduler.add_job(\n execute_scheduled_translation,\n CronTrigger.from_crontab(cron_expression, timezone=tz),\n id=job_id_aps,\n args=[schedule_id, job_id, SessionLocal, self.config_manager, execution_mode],\n replace_existing=True,\n )\n logger.reason(\n f\"Translation schedule registered: {job_id_aps} ({cron_expression})\"\n )\n except Exception as e:\n logger.explore(\n f\"Failed to register translation schedule: {job_id_aps}\",\n extra={\"error\": str(e)},\n )\n # #endregion add_translation_job\n # #region remove_translation_job [C:4] [TYPE Function] [SEMANTICS scheduler,translation,remove,apscheduler]\n # @BRIEF Remove a translation schedule from APScheduler.\n # @PRE schedule_id is a valid string.\n # @POST The APScheduler job is removed if it exists; silently ignored otherwise.\n # @SIDE_EFFECT Mutates APScheduler state.\n def remove_translation_job(self, schedule_id: str):\n with belief_scope(\n \"SchedulerService.remove_translation_job\",\n f\"schedule_id={schedule_id}\",\n ):\n job_id_aps = f\"translate_{schedule_id}\"\n try:\n self.scheduler.remove_job(job_id_aps)\n logger.info(f\"Translation schedule removed: {job_id_aps}\")\n except Exception:\n logger.reason(f\"Translation schedule not found (already removed): {job_id_aps}\")\n # #endregion remove_translation_job\n # #region _trigger_backup [TYPE Function]\n # @PURPOSE: Triggered by the scheduler to start a backup task.\n # @PRE: env_id must be a valid environment ID.\n # @POST: A new backup task is created in the task manager if not already running.\n # @PARAM: env_id (str) - The ID of the environment.\n def _trigger_backup(self, env_id: str):\n seed_trace_id()\n with belief_scope(\"SchedulerService._trigger_backup\", f\"env_id={env_id}\"):\n logger.info(f\"Triggering scheduled backup for environment {env_id}\")\n # Check if a backup is already running for this environment\n active_tasks = self.task_manager.get_tasks(limit=100)\n for task in active_tasks:\n if (\n task.plugin_id == \"superset-backup\"\n and task.status in [\"PENDING\", \"RUNNING\"]\n and task.params.get(\"environment_id\") == env_id\n ):\n logger.warning(\n f\"Backup already running for environment {env_id}. Skipping scheduled run.\"\n )\n return\n # Run the backup task\n # We need to run this in the event loop since create_task is async\n asyncio.run_coroutine_threadsafe(\n self.task_manager.create_task(\n \"superset-backup\", {\"environment_id\": env_id}\n ),\n self.loop,\n )\n # #endregion _trigger_backup\n# #endregion SchedulerService\n" }, { "contract_id": "start", "contract_type": "Function", "file_path": "backend/src/core/scheduler.py", - "start_line": 41, - "end_line": 52, + "start_line": 32, + "end_line": 42, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39960,15 +36832,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:start:Function]\n # @PURPOSE: Starts the background scheduler and loads initial schedules.\n # @PRE: Scheduler should be initialized.\n # @POST: Scheduler is running and schedules are loaded.\n def start(self):\n with belief_scope(\"SchedulerService.start\"):\n if not self.scheduler.running:\n self.scheduler.start()\n log.reflect(\"Scheduler started\")\n self.load_schedules()\n\n # [/DEF:start:Function]\n" + "anchor_syntax": "region", + "body": " # #region start [TYPE Function]\n # @PURPOSE: Starts the background scheduler and loads initial schedules.\n # @PRE: Scheduler should be initialized.\n # @POST: Scheduler is running and schedules are loaded.\n def start(self):\n with belief_scope(\"SchedulerService.start\"):\n if not self.scheduler.running:\n self.scheduler.start()\n logger.info(\"Scheduler started.\")\n self.load_schedules()\n # #endregion start\n" }, { "contract_id": "stop", "contract_type": "Function", "file_path": "backend/src/core/scheduler.py", - "start_line": 54, - "end_line": 64, + "start_line": 43, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40006,61 +36878,45 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:stop:Function]\n # @PURPOSE: Stops the background scheduler.\n # @PRE: Scheduler should be running.\n # @POST: Scheduler is shut down.\n def stop(self):\n with belief_scope(\"SchedulerService.stop\"):\n if self.scheduler.running:\n self.scheduler.shutdown()\n log.reflect(\"Scheduler stopped\")\n\n # [/DEF:stop:Function]\n" + "anchor_syntax": "region", + "body": " # #region stop [TYPE Function]\n # @PURPOSE: Stops the background scheduler.\n # @PRE: Scheduler should be running.\n # @POST: Scheduler is shut down.\n def stop(self):\n with belief_scope(\"SchedulerService.stop\"):\n if self.scheduler.running:\n self.scheduler.shutdown()\n logger.info(\"Scheduler stopped.\")\n # #endregion stop\n" }, { "contract_id": "load_schedules", "contract_type": "Function", "file_path": "backend/src/core/scheduler.py", - "start_line": 66, - "end_line": 80, - "tier": "TIER_1", - "complexity": 1, + "start_line": 53, + "end_line": 91, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "POST": "All enabled backup jobs are added to the scheduler.", - "PRE": "config_manager must have valid configuration.", - "PURPOSE": "Loads backup schedules from configuration and registers them." + "COMPLEXITY": 4, + "POST": "All enabled backup jobs and active translation schedules are re-registered in APScheduler.", + "PRE": "config_manager must have valid configuration; database is accessible.", + "PURPOSE": "Load backup and active translation schedules from config and DB, re-registering all jobs.", + "SIDE_EFFECT": "Removes all existing APScheduler jobs; queries translation_schedules table; registers APScheduler jobs for translation." }, "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, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 4, "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": " # [DEF:load_schedules:Function]\n # @PURPOSE: Loads backup schedules from configuration and registers them.\n # @PRE: config_manager must have valid configuration.\n # @POST: All enabled backup jobs are added to the scheduler.\n def load_schedules(self):\n with belief_scope(\"SchedulerService.load_schedules\"):\n # Clear existing jobs\n self.scheduler.remove_all_jobs()\n\n config = self.config_manager.get_config()\n for env in config.environments:\n if env.backup_schedule and env.backup_schedule.enabled:\n self.add_backup_job(env.id, env.backup_schedule.cron_expression)\n\n # [/DEF:load_schedules:Function]\n" + "anchor_syntax": "region", + "body": " # #region load_schedules [C:4] [TYPE Function] [SEMANTICS scheduler,backup,translation,apscheduler]\n # @BRIEF Load backup and active translation schedules from config and DB, re-registering all jobs.\n # @PRE config_manager must have valid configuration; database is accessible.\n # @POST All enabled backup jobs and active translation schedules are re-registered in APScheduler.\n # @SIDE_EFFECT Removes all existing APScheduler jobs; queries translation_schedules table; registers APScheduler jobs for translation.\n def load_schedules(self):\n with belief_scope(\"SchedulerService.load_schedules\"):\n # Clear existing jobs\n self.scheduler.remove_all_jobs()\n config = self.config_manager.get_config()\n for env in config.environments:\n if env.backup_schedule and env.backup_schedule.enabled:\n self.add_backup_job(env.id, env.backup_schedule.cron_expression)\n # Re-register active translation schedules from the database\n try:\n from ..models.translate import TranslationSchedule\n db = SessionLocal()\n try:\n active_schedules = (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active == True)\n .all()\n )\n for sched in active_schedules:\n self.add_translation_job(\n schedule_id=sched.id,\n job_id=sched.job_id,\n cron_expression=sched.cron_expression,\n timezone=sched.timezone,\n execution_mode=getattr(sched, 'execution_mode', 'full'),\n )\n if active_schedules:\n logger.reason(f\"Loaded {len(active_schedules)} active translation schedule(s)\")\n finally:\n db.close()\n except Exception as e:\n logger.explore(\"Failed to load translation schedules on startup\",\n extra={\"error\": str(e)})\n # #endregion load_schedules\n" }, { "contract_id": "add_backup_job", "contract_type": "Function", "file_path": "backend/src/core/scheduler.py", - "start_line": 82, - "end_line": 110, + "start_line": 92, + "end_line": 117, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40105,15 +36961,78 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:add_backup_job:Function]\n # @PURPOSE: Adds a scheduled backup job for an environment.\n # @PRE: env_id and cron_expression must be valid strings.\n # @POST: A new job is added to the scheduler or replaced if it already exists.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PARAM: cron_expression (str) - The cron expression for the schedule.\n def add_backup_job(self, env_id: str, cron_expression: str):\n with belief_scope(\n \"SchedulerService.add_backup_job\",\n f\"env_id={env_id}, cron={cron_expression}\",\n ):\n job_id = f\"backup_{env_id}\"\n try:\n self.scheduler.add_job(\n self._trigger_backup,\n CronTrigger.from_crontab(cron_expression),\n id=job_id,\n args=[env_id],\n replace_existing=True,\n )\n log.reflect(\n \"Scheduled backup job added\",\n payload={\"env_id\": env_id, \"cron\": cron_expression},\n )\n except Exception as e:\n log.explore(\"Failed to add backup job\", error=str(e), payload={\"env_id\": env_id, \"cron\": cron_expression})\n logger.error(f\"Failed to add backup job for environment {env_id}: {e}\")\n\n # [/DEF:add_backup_job:Function]\n" + "anchor_syntax": "region", + "body": " # #region add_backup_job [TYPE Function]\n # @PURPOSE: Adds a scheduled backup job for an environment.\n # @PRE: env_id and cron_expression must be valid strings.\n # @POST: A new job is added to the scheduler or replaced if it already exists.\n # @PARAM: env_id (str) - The ID of the environment.\n # @PARAM: cron_expression (str) - The cron expression for the schedule.\n def add_backup_job(self, env_id: str, cron_expression: str):\n with belief_scope(\n \"SchedulerService.add_backup_job\",\n f\"env_id={env_id}, cron={cron_expression}\",\n ):\n job_id = f\"backup_{env_id}\"\n try:\n self.scheduler.add_job(\n self._trigger_backup,\n CronTrigger.from_crontab(cron_expression),\n id=job_id,\n args=[env_id],\n replace_existing=True,\n )\n logger.info(\n f\"Scheduled backup job added for environment {env_id}: {cron_expression}\"\n )\n except Exception as e:\n logger.error(f\"Failed to add backup job for environment {env_id}: {e}\")\n # #endregion add_backup_job\n" + }, + { + "contract_id": "add_translation_job", + "contract_type": "Function", + "file_path": "backend/src/core/scheduler.py", + "start_line": 118, + "end_line": 152, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "A new APScheduler job is registered or replaced if it already exists.", + "PRE": "schedule_id, job_id, and cron_expression are valid strings.", + "PURPOSE": "Register a translation schedule with APScheduler.", + "SIDE_EFFECT": "Mutates APScheduler state; calls execute_scheduled_translation on trigger." + }, + "relations": [ + { + "source_id": "add_translation_job", + "relation_type": "DEPENDS_ON", + "target_id": "execute_scheduled_translation", + "target_ref": "[execute_scheduled_translation]" + }, + { + "source_id": "add_translation_job", + "relation_type": "DEPENDS_ON", + "target_id": "SessionLocal", + "target_ref": "[SessionLocal]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": " # #region add_translation_job [C:4] [TYPE Function] [SEMANTICS scheduler,translation,cron,apscheduler]\n # @BRIEF Register a translation schedule with APScheduler.\n # @PRE schedule_id, job_id, and cron_expression are valid strings.\n # @POST A new APScheduler job is registered or replaced if it already exists.\n # @SIDE_EFFECT Mutates APScheduler state; calls execute_scheduled_translation on trigger.\n # @RELATION DEPENDS_ON -> [execute_scheduled_translation]\n # @RELATION DEPENDS_ON -> [SessionLocal]\n def add_translation_job(self, schedule_id: str, job_id: str, cron_expression: str, timezone: str = \"UTC\", execution_mode: str = \"full\"):\n with belief_scope(\n \"SchedulerService.add_translation_job\",\n f\"schedule_id={schedule_id}, job_id={job_id}, cron={cron_expression}\",\n ):\n from zoneinfo import ZoneInfo\n\n from ..plugins.translate.scheduler import execute_scheduled_translation\n\n job_id_aps = f\"translate_{schedule_id}\"\n try:\n tz = ZoneInfo(timezone)\n self.scheduler.add_job(\n execute_scheduled_translation,\n CronTrigger.from_crontab(cron_expression, timezone=tz),\n id=job_id_aps,\n args=[schedule_id, job_id, SessionLocal, self.config_manager, execution_mode],\n replace_existing=True,\n )\n logger.reason(\n f\"Translation schedule registered: {job_id_aps} ({cron_expression})\"\n )\n except Exception as e:\n logger.explore(\n f\"Failed to register translation schedule: {job_id_aps}\",\n extra={\"error\": str(e)},\n )\n # #endregion add_translation_job\n" + }, + { + "contract_id": "remove_translation_job", + "contract_type": "Function", + "file_path": "backend/src/core/scheduler.py", + "start_line": 153, + "end_line": 169, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "The APScheduler job is removed if it exists; silently ignored otherwise.", + "PRE": "schedule_id is a valid string.", + "PURPOSE": "Remove a translation schedule from APScheduler.", + "SIDE_EFFECT": "Mutates APScheduler state." + }, + "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", + "body": " # #region remove_translation_job [C:4] [TYPE Function] [SEMANTICS scheduler,translation,remove,apscheduler]\n # @BRIEF Remove a translation schedule from APScheduler.\n # @PRE schedule_id is a valid string.\n # @POST The APScheduler job is removed if it exists; silently ignored otherwise.\n # @SIDE_EFFECT Mutates APScheduler state.\n def remove_translation_job(self, schedule_id: str):\n with belief_scope(\n \"SchedulerService.remove_translation_job\",\n f\"schedule_id={schedule_id}\",\n ):\n job_id_aps = f\"translate_{schedule_id}\"\n try:\n self.scheduler.remove_job(job_id_aps)\n logger.info(f\"Translation schedule removed: {job_id_aps}\")\n except Exception:\n logger.reason(f\"Translation schedule not found (already removed): {job_id_aps}\")\n # #endregion remove_translation_job\n" }, { "contract_id": "_trigger_backup", "contract_type": "Function", "file_path": "backend/src/core/scheduler.py", - "start_line": 112, - "end_line": 145, + "start_line": 170, + "end_line": 199, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40158,29 +37077,24 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_trigger_backup:Function]\n # @PURPOSE: Triggered by the scheduler to start a backup task.\n # @PRE: env_id must be a valid environment ID.\n # @POST: A new backup task is created in the task manager if not already running.\n # @PARAM: env_id (str) - The ID of the environment.\n def _trigger_backup(self, env_id: str):\n with belief_scope(\"SchedulerService._trigger_backup\", f\"env_id={env_id}\"):\n log.reason(\"Triggering scheduled backup\", payload={\"env_id\": env_id})\n\n # Check if a backup is already running for this environment\n active_tasks = self.task_manager.get_tasks(limit=100)\n for task in active_tasks:\n if (\n task.plugin_id == \"superset-backup\"\n and task.status in [\"PENDING\", \"RUNNING\"]\n and task.params.get(\"environment_id\") == env_id\n ):\n log.explore(\"Backup already running, skipping scheduled run\", error=\"Duplicate backup prevented\", payload={\"env_id\": env_id})\n return\n\n # Run the backup task\n # We need to run this in the event loop since create_task is async\n trace_id = get_trace_id()\n\n async def _backup_task():\n if trace_id:\n set_trace_id(trace_id)\n await self.task_manager.create_task(\n \"superset-backup\", {\"environment_id\": env_id}\n )\n\n asyncio.run_coroutine_threadsafe(_backup_task(), self.loop)\n\n # [/DEF:_trigger_backup:Function]\n" + "anchor_syntax": "region", + "body": " # #region _trigger_backup [TYPE Function]\n # @PURPOSE: Triggered by the scheduler to start a backup task.\n # @PRE: env_id must be a valid environment ID.\n # @POST: A new backup task is created in the task manager if not already running.\n # @PARAM: env_id (str) - The ID of the environment.\n def _trigger_backup(self, env_id: str):\n seed_trace_id()\n with belief_scope(\"SchedulerService._trigger_backup\", f\"env_id={env_id}\"):\n logger.info(f\"Triggering scheduled backup for environment {env_id}\")\n # Check if a backup is already running for this environment\n active_tasks = self.task_manager.get_tasks(limit=100)\n for task in active_tasks:\n if (\n task.plugin_id == \"superset-backup\"\n and task.status in [\"PENDING\", \"RUNNING\"]\n and task.params.get(\"environment_id\") == env_id\n ):\n logger.warning(\n f\"Backup already running for environment {env_id}. Skipping scheduled run.\"\n )\n return\n # Run the backup task\n # We need to run this in the event loop since create_task is async\n asyncio.run_coroutine_threadsafe(\n self.task_manager.create_task(\n \"superset-backup\", {\"environment_id\": env_id}\n ),\n self.loop,\n )\n # #endregion _trigger_backup\n" }, { "contract_id": "ThrottledSchedulerConfigurator", "contract_type": "Class", "file_path": "backend/src/core/scheduler.py", - "start_line": 151, - "end_line": 213, + "start_line": 201, + "end_line": 252, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]]", "INVARIANT": "Returned schedule size always matches number of dashboard IDs.", "POST": "Produces deterministic per-dashboard run timestamps within the configured window.", "PRE": "Validation policies provide a finite dashboard list and a valid execution window.", "PURPOSE": "Distributes validation tasks evenly within an execution window.", - "SEMANTICS": [ - "scheduler", - "throttling", - "distribution" - ], "SIDE_EFFECT": "Emits warning logs for degenerate or near-zero scheduling windows." }, "relations": [ @@ -40192,29 +37106,6 @@ } ], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, { "code": "missing_required_tag", "tag": "RATIONALE", @@ -40234,15 +37125,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ThrottledSchedulerConfigurator:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: scheduler, throttling, distribution\n# @PURPOSE: Distributes validation tasks evenly within an execution window.\n# @PRE: Validation policies provide a finite dashboard list and a valid execution window.\n# @POST: Produces deterministic per-dashboard run timestamps within the configured window.\n# @RELATION: DEPENDS_ON -> SchedulerModule\n# @INVARIANT: Returned schedule size always matches number of dashboard IDs.\n# @SIDE_EFFECT: Emits warning logs for degenerate or near-zero scheduling windows.\n# @DATA_CONTRACT: Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]]\nclass ThrottledSchedulerConfigurator:\n # [DEF:calculate_schedule:Function]\n # @PURPOSE: Calculates execution times for N tasks within a window.\n # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date).\n # @POST: Returns List[datetime] of scheduled times.\n # @INVARIANT: Tasks are distributed with near-even spacing.\n @staticmethod\n def calculate_schedule(\n window_start: time, window_end: time, dashboard_ids: list, current_date: date\n ) -> list:\n with belief_scope(\"ThrottledSchedulerConfigurator.calculate_schedule\"):\n n = len(dashboard_ids)\n if n == 0:\n return []\n\n start_dt = datetime.combine(current_date, window_start)\n end_dt = datetime.combine(current_date, window_end)\n\n # Handle window crossing midnight\n if end_dt < start_dt:\n end_dt += timedelta(days=1)\n\n total_seconds = (end_dt - start_dt).total_seconds()\n\n # Minimum interval of 1 second to avoid division by zero or negative\n if total_seconds <= 0:\n log_tsc.explore(\"Window size is zero or negative, falling back to start time\", error=f\"total_seconds={total_seconds}\", payload={\"n\": n})\n return [start_dt] * n\n\n # If window is too small for even distribution (e.g. 10 tasks in 5 seconds),\n # we still distribute them but they might be very close.\n # The requirement says \"near-even spacing\".\n\n if n == 1:\n return [start_dt]\n\n interval = total_seconds / (n - 1) if n > 1 else 0\n\n # If interval is too small (e.g. < 1s), we might want a fallback,\n # but the spec says \"handle too-small windows with explicit fallback/warning\".\n if interval < 1:\n log_tsc.explore(\"Window too small for task distribution\", error=f\"interval={interval:.2f}s\", payload={\"n\": n})\n\n scheduled_times = []\n for i in range(n):\n scheduled_times.append(start_dt + timedelta(seconds=i * interval))\n\n return scheduled_times\n\n # [/DEF:calculate_schedule:Function]\n\n\n# [/DEF:ThrottledSchedulerConfigurator:Class]\n" + "anchor_syntax": "region", + "body": "# #region ThrottledSchedulerConfigurator [C:5] [TYPE Class] [SEMANTICS scheduler, throttling, distribution]\n# @BRIEF Distributes validation tasks evenly within an execution window.\n# @PRE: Validation policies provide a finite dashboard list and a valid execution window.\n# @POST: Produces deterministic per-dashboard run timestamps within the configured window.\n# @RELATION DEPENDS_ON -> SchedulerModule\n# @INVARIANT: Returned schedule size always matches number of dashboard IDs.\n# @SIDE_EFFECT: Emits warning logs for degenerate or near-zero scheduling windows.\n# @DATA_CONTRACT: Input[window_start, window_end, dashboard_ids, current_date] -> Output[List[datetime]]\nclass ThrottledSchedulerConfigurator:\n # #region calculate_schedule [TYPE Function]\n # @PURPOSE: Calculates execution times for N tasks within a window.\n # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date).\n # @POST: Returns List[datetime] of scheduled times.\n # @INVARIANT: Tasks are distributed with near-even spacing.\n @staticmethod\n def calculate_schedule(\n window_start: time, window_end: time, dashboard_ids: list, current_date: date\n ) -> list:\n with belief_scope(\"ThrottledSchedulerConfigurator.calculate_schedule\"):\n n = len(dashboard_ids)\n if n == 0:\n return []\n start_dt = datetime.combine(current_date, window_start)\n end_dt = datetime.combine(current_date, window_end)\n # Handle window crossing midnight\n if end_dt < start_dt:\n end_dt += timedelta(days=1)\n total_seconds = (end_dt - start_dt).total_seconds()\n # Minimum interval of 1 second to avoid division by zero or negative\n if total_seconds <= 0:\n logger.warning(\n f\"[calculate_schedule] Window size is zero or negative. Falling back to start time for all {n} tasks.\"\n )\n return [start_dt] * n\n # If window is too small for even distribution (e.g. 10 tasks in 5 seconds),\n # we still distribute them but they might be very close.\n # The requirement says \"near-even spacing\".\n if n == 1:\n return [start_dt]\n interval = total_seconds / (n - 1) if n > 1 else 0\n # If interval is too small (e.g. < 1s), we might want a fallback,\n # but the spec says \"handle too-small windows with explicit fallback/warning\".\n if interval < 1:\n logger.warning(\n f\"[calculate_schedule] Window too small for {n} tasks (interval {interval:.2f}s). Tasks will be highly concentrated.\"\n )\n scheduled_times = []\n for i in range(n):\n scheduled_times.append(start_dt + timedelta(seconds=i * interval))\n return scheduled_times\n # #endregion calculate_schedule\n# #endregion ThrottledSchedulerConfigurator\n" }, { "contract_id": "calculate_schedule", "contract_type": "Function", "file_path": "backend/src/core/scheduler.py", - "start_line": 162, - "end_line": 210, + "start_line": 210, + "end_line": 251, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40290,35 +37181,24 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:calculate_schedule:Function]\n # @PURPOSE: Calculates execution times for N tasks within a window.\n # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date).\n # @POST: Returns List[datetime] of scheduled times.\n # @INVARIANT: Tasks are distributed with near-even spacing.\n @staticmethod\n def calculate_schedule(\n window_start: time, window_end: time, dashboard_ids: list, current_date: date\n ) -> list:\n with belief_scope(\"ThrottledSchedulerConfigurator.calculate_schedule\"):\n n = len(dashboard_ids)\n if n == 0:\n return []\n\n start_dt = datetime.combine(current_date, window_start)\n end_dt = datetime.combine(current_date, window_end)\n\n # Handle window crossing midnight\n if end_dt < start_dt:\n end_dt += timedelta(days=1)\n\n total_seconds = (end_dt - start_dt).total_seconds()\n\n # Minimum interval of 1 second to avoid division by zero or negative\n if total_seconds <= 0:\n log_tsc.explore(\"Window size is zero or negative, falling back to start time\", error=f\"total_seconds={total_seconds}\", payload={\"n\": n})\n return [start_dt] * n\n\n # If window is too small for even distribution (e.g. 10 tasks in 5 seconds),\n # we still distribute them but they might be very close.\n # The requirement says \"near-even spacing\".\n\n if n == 1:\n return [start_dt]\n\n interval = total_seconds / (n - 1) if n > 1 else 0\n\n # If interval is too small (e.g. < 1s), we might want a fallback,\n # but the spec says \"handle too-small windows with explicit fallback/warning\".\n if interval < 1:\n log_tsc.explore(\"Window too small for task distribution\", error=f\"interval={interval:.2f}s\", payload={\"n\": n})\n\n scheduled_times = []\n for i in range(n):\n scheduled_times.append(start_dt + timedelta(seconds=i * interval))\n\n return scheduled_times\n\n # [/DEF:calculate_schedule:Function]\n" + "anchor_syntax": "region", + "body": " # #region calculate_schedule [TYPE Function]\n # @PURPOSE: Calculates execution times for N tasks within a window.\n # @PRE: window_start, window_end (time), dashboard_ids (List), current_date (date).\n # @POST: Returns List[datetime] of scheduled times.\n # @INVARIANT: Tasks are distributed with near-even spacing.\n @staticmethod\n def calculate_schedule(\n window_start: time, window_end: time, dashboard_ids: list, current_date: date\n ) -> list:\n with belief_scope(\"ThrottledSchedulerConfigurator.calculate_schedule\"):\n n = len(dashboard_ids)\n if n == 0:\n return []\n start_dt = datetime.combine(current_date, window_start)\n end_dt = datetime.combine(current_date, window_end)\n # Handle window crossing midnight\n if end_dt < start_dt:\n end_dt += timedelta(days=1)\n total_seconds = (end_dt - start_dt).total_seconds()\n # Minimum interval of 1 second to avoid division by zero or negative\n if total_seconds <= 0:\n logger.warning(\n f\"[calculate_schedule] Window size is zero or negative. Falling back to start time for all {n} tasks.\"\n )\n return [start_dt] * n\n # If window is too small for even distribution (e.g. 10 tasks in 5 seconds),\n # we still distribute them but they might be very close.\n # The requirement says \"near-even spacing\".\n if n == 1:\n return [start_dt]\n interval = total_seconds / (n - 1) if n > 1 else 0\n # If interval is too small (e.g. < 1s), we might want a fallback,\n # but the spec says \"handle too-small windows with explicit fallback/warning\".\n if interval < 1:\n logger.warning(\n f\"[calculate_schedule] Window too small for {n} tasks (interval {interval:.2f}s). Tasks will be highly concentrated.\"\n )\n scheduled_times = []\n for i in range(n):\n scheduled_times.append(start_dt + timedelta(seconds=i * interval))\n return scheduled_times\n # #endregion calculate_schedule\n" }, { "contract_id": "SupersetClientModule", "contract_type": "Module", "file_path": "backend/src/core/superset_client/__init__.py", "start_line": 1, - "end_line": 71, + "end_line": 68, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "All network operations must use the internal APIClient instance.", "LAYER": "Infra", "PUBLIC_API": "SupersetClient", "PURPOSE": "Предоставляет высокоуровневый клиент для взаимодействия с Superset REST API, инкапсулируя логику запросов, обработку ошибок и пагинацию.", - "RATIONALE": "Decomposed from monolithic superset_client.py (2145 lines) into", - "SEMANTICS": [ - "superset", - "api", - "client", - "rest", - "http", - "dashboard", - "dataset", - "import", - "export" - ] + "RATIONALE": "Decomposed from monolithic superset_client.py (2145 lines) into" }, "relations": [ { @@ -40378,19 +37258,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetClientModule:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, api, client, rest, http, dashboard, dataset, import, export\n# @PURPOSE: Предоставляет высокоуровневый клиент для взаимодействия с 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 ._user_projection import SupersetUserProjectionMixin\nfrom ._dashboards_list import SupersetDashboardsListMixin\nfrom ._dashboards_filters import SupersetDashboardsFiltersMixin\nfrom ._dashboards_crud import SupersetDashboardsCrudMixin\nfrom ._charts import SupersetChartsMixin\nfrom ._datasets import SupersetDatasetsMixin\nfrom ._datasets_preview import SupersetDatasetsPreviewMixin\nfrom ._datasets_preview_filters import SupersetDatasetsPreviewFiltersMixin\nfrom ._databases import SupersetDatabasesMixin\n\n\n# [DEF:SupersetClient:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Класс-обёртка над 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# [/DEF:SupersetClient:Class]\n\n# [/DEF:SupersetClientModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "SupersetClient", "contract_type": "Class", "file_path": "backend/src/core/superset_client/__init__.py", - "start_line": 33, - "end_line": 69, + "start_line": 31, + "end_line": 66, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Класс-обёртка над Superset REST API, предоставляющий методы для работы с дашбордами и датасетами." }, "relations": [ @@ -40474,31 +37354,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetClient:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Класс-обёртка над 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# [/DEF:SupersetClient:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SupersetClientBase", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_base.py", "start_line": 1, - "end_line": 300, + "end_line": 264, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Base class for SupersetClient providing initialization, authentication, pagination, and import/export helpers.", - "SEMANTICS": [ - "superset", - "api", - "client", - "base", - "pagination", - "auth", - "import", - "export" - ] + "PURPOSE": "Base class for SupersetClient providing initialization, authentication, pagination, and import/export helpers." }, "relations": [ { @@ -40527,413 +37397,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetClientBase:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, api, client, base, pagination, auth, import, export\n# @PURPOSE: Base class for SupersetClient providing initialization, authentication, pagination, and import/export helpers.\n# @RELATION: DEPENDS_ON -> [ConfigModels]\n# @RELATION: DEPENDS_ON -> [APIClient]\n# @RELATION: DEPENDS_ON -> [SupersetAPIError]\n# @RELATION: DEPENDS_ON -> [get_filename_from_headers]\n\nimport json\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional, Tuple, Union, cast\nfrom requests import Response\n\nfrom ..cot_logger import MarkerLogger\nfrom ..logger import belief_scope\nfrom ..utils.network import APIClient, SupersetAPIError\nfrom ..utils.fileio import get_filename_from_headers\nfrom ..config_models import Environment\n\nlog = MarkerLogger(\"SupersetClientBase\")\n\n\n# [DEF:SupersetClientBase:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Base class providing Superset client initialization, auth, pagination, and import/export plumbing.\n# @RELATION: DEPENDS_ON -> [ConfigModels]\n# @RELATION: DEPENDS_ON -> [APIClient]\n# @RELATION: DEPENDS_ON -> [SupersetAPIError]\nclass SupersetClientBase:\n # [DEF:SupersetClientInit:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент.\n # @RELATION: DEPENDS_ON -> [Environment]\n # @RELATION: DEPENDS_ON -> [APIClient]\n def __init__(self, env: Environment):\n with belief_scope(\"SupersetClientInit\"):\n log.reason(\n \"Initializing Superset client for environment\",\n payload={\"environment\": getattr(env, \"id\", None), \"env_name\": env.name},\n )\n self.env = env\n # Construct auth payload expected by Superset API\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = APIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport: bool = False\n log.reflect(\n \"Superset client initialized\",\n payload={\"environment\": getattr(self.env, \"id\", None)},\n )\n\n # [/DEF:SupersetClientInit:Function]\n\n # [DEF:SupersetClientAuthenticate:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Authenticates the client using the configured credentials.\n # @RELATION: CALLS -> [APIClient]\n def authenticate(self) -> Dict[str, str]:\n with belief_scope(\"SupersetClientAuthenticate\"):\n log.reason(\n \"Authenticating Superset client\",\n payload={\"environment\": getattr(self.env, \"id\", None)},\n )\n tokens = self.network.authenticate()\n log.reflect(\n \"Superset client authentication completed\",\n payload={\n \"environment\": getattr(self.env, \"id\", None),\n \"token_keys\": sorted(tokens.keys()),\n },\n )\n return tokens\n # [/DEF:SupersetClientAuthenticate:Function]\n\n @property\n # [DEF:SupersetClientHeaders:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом.\n def headers(self) -> dict:\n with belief_scope(\"headers\"):\n return self.network.headers\n\n # [/DEF:SupersetClientHeaders:Function]\n\n # --- Pagination helpers ---\n\n # [DEF:SupersetClientValidateQueryParams:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Ensures query parameters have default page and page_size.\n def _validate_query_params(self, query: Optional[Dict]) -> Dict:\n with belief_scope(\"_validate_query_params\"):\n # Superset list endpoints commonly cap page_size at 100.\n # Using 100 avoids partial fetches when larger values are silently truncated.\n base_query = {\"page\": 0, \"page_size\": 100}\n return {**base_query, **(query or {})}\n\n # [/DEF:SupersetClientValidateQueryParams:Function]\n\n # [DEF:SupersetClientFetchTotalObjectCount:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Fetches the total number of items for a given endpoint.\n # @RELATION: CALLS -> [APIClient]\n def _fetch_total_object_count(self, endpoint: str) -> int:\n with belief_scope(\"_fetch_total_object_count\"):\n return self.network.fetch_paginated_count(\n endpoint=endpoint,\n query_params={\"page\": 0, \"page_size\": 1},\n count_field=\"count\",\n )\n\n # [/DEF:SupersetClientFetchTotalObjectCount:Function]\n\n # [DEF:SupersetClientFetchAllPages:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Iterates through all pages to collect all data items.\n # @RELATION: CALLS -> [APIClient]\n def _fetch_all_pages(self, endpoint: str, pagination_options: Dict) -> List[Dict]:\n with belief_scope(\"_fetch_all_pages\"):\n return self.network.fetch_paginated_data(\n endpoint=endpoint, pagination_options=pagination_options\n )\n\n # [/DEF:SupersetClientFetchAllPages:Function]\n\n # --- Import/Export helpers ---\n\n # [DEF:SupersetClientDoImport:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Performs the actual multipart upload for import.\n # @RELATION: CALLS -> [APIClient]\n def _do_import(self, file_name: Union[str, Path]) -> Dict:\n with belief_scope(\"_do_import\"):\n log.reason(\"Uploading file for import\", payload={\"file_name\": str(file_name)})\n file_path = Path(file_name)\n if not file_path.exists():\n log.explore(\"Import file does not exist\", error=\"FileNotFound\", payload={\"file_name\": str(file_name)})\n raise FileNotFoundError(f\"File does not exist: {file_name}\")\n\n return self.network.upload_file(\n endpoint=\"/dashboard/import/\",\n file_info={\n \"file_obj\": file_path,\n \"file_name\": file_path.name,\n \"form_field\": \"formData\",\n },\n extra_data={\"overwrite\": \"true\"},\n timeout=self.env.timeout * 2,\n )\n\n # [/DEF:SupersetClientDoImport:Function]\n\n # [DEF:SupersetClientValidateExportResponse:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Validates that the export response is a non-empty ZIP archive.\n def _validate_export_response(self, response: Response, dashboard_id: int) -> None:\n with belief_scope(\"_validate_export_response\"):\n content_type = response.headers.get(\"Content-Type\", \"\")\n if \"application/zip\" not in content_type:\n raise SupersetAPIError(\n f\"Получен не ZIP-архив (Content-Type: {content_type})\"\n )\n if not response.content:\n raise SupersetAPIError(\"Получены пустые данные при экспорте\")\n\n # [/DEF:SupersetClientValidateExportResponse:Function]\n\n # [DEF:SupersetClientResolveExportFilename:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Determines the filename for an exported dashboard.\n def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str:\n with belief_scope(\"_resolve_export_filename\"):\n filename = get_filename_from_headers(dict(response.headers))\n if not filename:\n from datetime import datetime\n\n timestamp = datetime.now().strftime(\"%Y%m%dT%H%M%S\")\n filename = f\"dashboard_export_{dashboard_id}_{timestamp}.zip\"\n log.reflect(\n \"Export filename not found in headers, using generated name\",\n payload={\"dashboard_id\": dashboard_id, \"generated_filename\": filename},\n )\n return filename\n\n # [/DEF:SupersetClientResolveExportFilename:Function]\n\n # [DEF:SupersetClientValidateImportFile:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Validates that the file to be imported is a valid ZIP with metadata.yaml.\n def _validate_import_file(self, zip_path: Union[str, Path]) -> None:\n with belief_scope(\"_validate_import_file\"):\n path = Path(zip_path)\n if not path.exists():\n raise FileNotFoundError(f\"Файл {zip_path} не существует\")\n if not zipfile.is_zipfile(path):\n raise SupersetAPIError(f\"Файл {zip_path} не является ZIP-архивом\")\n with zipfile.ZipFile(path, \"r\") as zf:\n if not any(n.endswith(\"metadata.yaml\") for n in zf.namelist()):\n raise SupersetAPIError(\n f\"Архив {zip_path} не содержит 'metadata.yaml'\"\n )\n\n # [/DEF:SupersetClientValidateImportFile:Function]\n\n # [DEF:SupersetClientResolveTargetIdForDelete:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Resolves a dashboard ID from either an ID or a slug.\n # @RELATION: CALLS -> [SupersetClientGetDashboards]\n def _resolve_target_id_for_delete(\n self, dash_id: Optional[int], dash_slug: Optional[str]\n ) -> Optional[int]:\n with belief_scope(\"_resolve_target_id_for_delete\"):\n if dash_id is not None:\n return dash_id\n if dash_slug is not None:\n log.reason(\"Resolving dashboard ID by slug\", payload={\"slug\": dash_slug})\n try:\n _, candidates = self.get_dashboards(\n query={\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dash_slug}]\n }\n )\n if candidates:\n target_id = candidates[0][\"id\"]\n log.reflect(\"Resolved slug to dashboard ID\", payload={\"slug\": dash_slug, \"target_id\": target_id})\n return target_id\n except Exception as e:\n log.explore(\"Could not resolve slug to dashboard ID\", error=str(e), payload={\"slug\": dash_slug})\n return None\n\n # [/DEF:SupersetClientResolveTargetIdForDelete:Function]\n\n # [DEF:SupersetClientGetAllResources:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches all resources of a given type with id, uuid, and name columns.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_all_resources(\n self, resource_type: str, since_dttm: Optional[\"datetime\"] = None\n ) -> List[Dict]:\n with belief_scope(\n \"SupersetClient.get_all_resources\",\n f\"type={resource_type}, since={since_dttm}\",\n ):\n column_map = {\n \"chart\": {\n \"endpoint\": \"/chart/\",\n \"columns\": [\"id\", \"uuid\", \"slice_name\"],\n },\n \"dataset\": {\n \"endpoint\": \"/dataset/\",\n \"columns\": [\"id\", \"uuid\", \"table_name\"],\n },\n \"dashboard\": {\n \"endpoint\": \"/dashboard/\",\n \"columns\": [\"id\", \"uuid\", \"slug\", \"dashboard_title\"],\n },\n }\n config = column_map.get(resource_type)\n if not config:\n log.explore(\"Unknown resource type requested\", error=f\"Invalid resource type: {resource_type}\", payload={\"resource_type\": resource_type})\n return []\n\n query = {\"columns\": config[\"columns\"]}\n\n if since_dttm:\n import math\n\n # Use int milliseconds to be safe\n timestamp_ms = math.floor(since_dttm.timestamp() * 1000)\n\n query[\"filters\"] = [\n {\"col\": \"changed_on_dttm\", \"opr\": \"gt\", \"value\": timestamp_ms}\n ]\n\n validated = self._validate_query_params(query)\n data = self._fetch_all_pages(\n endpoint=config[\"endpoint\"],\n pagination_options={\"base_query\": validated, \"results_field\": \"result\"},\n )\n log.reflect(\n \"Resources fetched\",\n payload={\"resource_type\": resource_type, \"count\": len(data)},\n )\n return data\n\n # [/DEF:SupersetClientGetAllResources:Function]\n\n\n# [/DEF:SupersetClientBase:Class]\n# [/DEF:SupersetClientBase:Module]\n" - }, - { - "contract_id": "SupersetClientInit", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 33, - "end_line": 63, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент." - }, - "relations": [ - { - "source_id": "SupersetClientInit", - "relation_type": "DEPENDS_ON", - "target_id": "Environment", - "target_ref": "[Environment]" - }, - { - "source_id": "SupersetClientInit", - "relation_type": "DEPENDS_ON", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientInit:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент.\n # @RELATION: DEPENDS_ON -> [Environment]\n # @RELATION: DEPENDS_ON -> [APIClient]\n def __init__(self, env: Environment):\n with belief_scope(\"SupersetClientInit\"):\n log.reason(\n \"Initializing Superset client for environment\",\n payload={\"environment\": getattr(env, \"id\", None), \"env_name\": env.name},\n )\n self.env = env\n # Construct auth payload expected by Superset API\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = APIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport: bool = False\n log.reflect(\n \"Superset client initialized\",\n payload={\"environment\": getattr(self.env, \"id\", None)},\n )\n\n # [/DEF:SupersetClientInit:Function]\n" - }, - { - "contract_id": "SupersetClientAuthenticate", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 65, - "end_line": 84, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Authenticates the client using the configured credentials." - }, - "relations": [ - { - "source_id": "SupersetClientAuthenticate", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientAuthenticate:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Authenticates the client using the configured credentials.\n # @RELATION: CALLS -> [APIClient]\n def authenticate(self) -> Dict[str, str]:\n with belief_scope(\"SupersetClientAuthenticate\"):\n log.reason(\n \"Authenticating Superset client\",\n payload={\"environment\": getattr(self.env, \"id\", None)},\n )\n tokens = self.network.authenticate()\n log.reflect(\n \"Superset client authentication completed\",\n payload={\n \"environment\": getattr(self.env, \"id\", None),\n \"token_keys\": sorted(tokens.keys()),\n },\n )\n return tokens\n # [/DEF:SupersetClientAuthenticate:Function]\n" - }, - { - "contract_id": "SupersetClientHeaders", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 87, - "end_line": 94, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Возвращает базовые HTTP-заголовки, используемые сетевым клиентом." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientHeaders:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом.\n def headers(self) -> dict:\n with belief_scope(\"headers\"):\n return self.network.headers\n\n # [/DEF:SupersetClientHeaders:Function]\n" - }, - { - "contract_id": "SupersetClientValidateQueryParams", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 98, - "end_line": 108, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Ensures query parameters have default page and page_size." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientValidateQueryParams:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Ensures query parameters have default page and page_size.\n def _validate_query_params(self, query: Optional[Dict]) -> Dict:\n with belief_scope(\"_validate_query_params\"):\n # Superset list endpoints commonly cap page_size at 100.\n # Using 100 avoids partial fetches when larger values are silently truncated.\n base_query = {\"page\": 0, \"page_size\": 100}\n return {**base_query, **(query or {})}\n\n # [/DEF:SupersetClientValidateQueryParams:Function]\n" - }, - { - "contract_id": "SupersetClientFetchTotalObjectCount", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 110, - "end_line": 122, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Fetches the total number of items for a given endpoint." - }, - "relations": [ - { - "source_id": "SupersetClientFetchTotalObjectCount", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientFetchTotalObjectCount:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Fetches the total number of items for a given endpoint.\n # @RELATION: CALLS -> [APIClient]\n def _fetch_total_object_count(self, endpoint: str) -> int:\n with belief_scope(\"_fetch_total_object_count\"):\n return self.network.fetch_paginated_count(\n endpoint=endpoint,\n query_params={\"page\": 0, \"page_size\": 1},\n count_field=\"count\",\n )\n\n # [/DEF:SupersetClientFetchTotalObjectCount:Function]\n" - }, - { - "contract_id": "SupersetClientFetchAllPages", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 124, - "end_line": 134, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Iterates through all pages to collect all data items." - }, - "relations": [ - { - "source_id": "SupersetClientFetchAllPages", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientFetchAllPages:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Iterates through all pages to collect all data items.\n # @RELATION: CALLS -> [APIClient]\n def _fetch_all_pages(self, endpoint: str, pagination_options: Dict) -> List[Dict]:\n with belief_scope(\"_fetch_all_pages\"):\n return self.network.fetch_paginated_data(\n endpoint=endpoint, pagination_options=pagination_options\n )\n\n # [/DEF:SupersetClientFetchAllPages:Function]\n" - }, - { - "contract_id": "SupersetClientDoImport", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 138, - "end_line": 161, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Performs the actual multipart upload for import." - }, - "relations": [ - { - "source_id": "SupersetClientDoImport", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientDoImport:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Performs the actual multipart upload for import.\n # @RELATION: CALLS -> [APIClient]\n def _do_import(self, file_name: Union[str, Path]) -> Dict:\n with belief_scope(\"_do_import\"):\n log.reason(\"Uploading file for import\", payload={\"file_name\": str(file_name)})\n file_path = Path(file_name)\n if not file_path.exists():\n log.explore(\"Import file does not exist\", error=\"FileNotFound\", payload={\"file_name\": str(file_name)})\n raise FileNotFoundError(f\"File does not exist: {file_name}\")\n\n return self.network.upload_file(\n endpoint=\"/dashboard/import/\",\n file_info={\n \"file_obj\": file_path,\n \"file_name\": file_path.name,\n \"form_field\": \"formData\",\n },\n extra_data={\"overwrite\": \"true\"},\n timeout=self.env.timeout * 2,\n )\n\n # [/DEF:SupersetClientDoImport:Function]\n" - }, - { - "contract_id": "SupersetClientValidateExportResponse", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 163, - "end_line": 176, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Validates that the export response is a non-empty ZIP archive." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientValidateExportResponse:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Validates that the export response is a non-empty ZIP archive.\n def _validate_export_response(self, response: Response, dashboard_id: int) -> None:\n with belief_scope(\"_validate_export_response\"):\n content_type = response.headers.get(\"Content-Type\", \"\")\n if \"application/zip\" not in content_type:\n raise SupersetAPIError(\n f\"Получен не ZIP-архив (Content-Type: {content_type})\"\n )\n if not response.content:\n raise SupersetAPIError(\"Получены пустые данные при экспорте\")\n\n # [/DEF:SupersetClientValidateExportResponse:Function]\n" - }, - { - "contract_id": "SupersetClientResolveExportFilename", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 178, - "end_line": 195, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Determines the filename for an exported dashboard." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientResolveExportFilename:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Determines the filename for an exported dashboard.\n def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str:\n with belief_scope(\"_resolve_export_filename\"):\n filename = get_filename_from_headers(dict(response.headers))\n if not filename:\n from datetime import datetime\n\n timestamp = datetime.now().strftime(\"%Y%m%dT%H%M%S\")\n filename = f\"dashboard_export_{dashboard_id}_{timestamp}.zip\"\n log.reflect(\n \"Export filename not found in headers, using generated name\",\n payload={\"dashboard_id\": dashboard_id, \"generated_filename\": filename},\n )\n return filename\n\n # [/DEF:SupersetClientResolveExportFilename:Function]\n" - }, - { - "contract_id": "SupersetClientValidateImportFile", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 197, - "end_line": 213, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Validates that the file to be imported is a valid ZIP with metadata.yaml." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientValidateImportFile:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Validates that the file to be imported is a valid ZIP with metadata.yaml.\n def _validate_import_file(self, zip_path: Union[str, Path]) -> None:\n with belief_scope(\"_validate_import_file\"):\n path = Path(zip_path)\n if not path.exists():\n raise FileNotFoundError(f\"Файл {zip_path} не существует\")\n if not zipfile.is_zipfile(path):\n raise SupersetAPIError(f\"Файл {zip_path} не является ZIP-архивом\")\n with zipfile.ZipFile(path, \"r\") as zf:\n if not any(n.endswith(\"metadata.yaml\") for n in zf.namelist()):\n raise SupersetAPIError(\n f\"Архив {zip_path} не содержит 'metadata.yaml'\"\n )\n\n # [/DEF:SupersetClientValidateImportFile:Function]\n" - }, - { - "contract_id": "SupersetClientResolveTargetIdForDelete", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 215, - "end_line": 241, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Resolves a dashboard ID from either an ID or a slug." - }, - "relations": [ - { - "source_id": "SupersetClientResolveTargetIdForDelete", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDashboards", - "target_ref": "[SupersetClientGetDashboards]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientResolveTargetIdForDelete:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Resolves a dashboard ID from either an ID or a slug.\n # @RELATION: CALLS -> [SupersetClientGetDashboards]\n def _resolve_target_id_for_delete(\n self, dash_id: Optional[int], dash_slug: Optional[str]\n ) -> Optional[int]:\n with belief_scope(\"_resolve_target_id_for_delete\"):\n if dash_id is not None:\n return dash_id\n if dash_slug is not None:\n log.reason(\"Resolving dashboard ID by slug\", payload={\"slug\": dash_slug})\n try:\n _, candidates = self.get_dashboards(\n query={\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dash_slug}]\n }\n )\n if candidates:\n target_id = candidates[0][\"id\"]\n log.reflect(\"Resolved slug to dashboard ID\", payload={\"slug\": dash_slug, \"target_id\": target_id})\n return target_id\n except Exception as e:\n log.explore(\"Could not resolve slug to dashboard ID\", error=str(e), payload={\"slug\": dash_slug})\n return None\n\n # [/DEF:SupersetClientResolveTargetIdForDelete:Function]\n" - }, - { - "contract_id": "SupersetClientGetAllResources", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_base.py", - "start_line": 243, - "end_line": 296, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches all resources of a given type with id, uuid, and name columns." - }, - "relations": [ - { - "source_id": "SupersetClientGetAllResources", - "relation_type": "CALLS", - "target_id": "SupersetClientFetchAllPages", - "target_ref": "[SupersetClientFetchAllPages]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetAllResources:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches all resources of a given type with id, uuid, and name columns.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_all_resources(\n self, resource_type: str, since_dttm: Optional[\"datetime\"] = None\n ) -> List[Dict]:\n with belief_scope(\n \"SupersetClient.get_all_resources\",\n f\"type={resource_type}, since={since_dttm}\",\n ):\n column_map = {\n \"chart\": {\n \"endpoint\": \"/chart/\",\n \"columns\": [\"id\", \"uuid\", \"slice_name\"],\n },\n \"dataset\": {\n \"endpoint\": \"/dataset/\",\n \"columns\": [\"id\", \"uuid\", \"table_name\"],\n },\n \"dashboard\": {\n \"endpoint\": \"/dashboard/\",\n \"columns\": [\"id\", \"uuid\", \"slug\", \"dashboard_title\"],\n },\n }\n config = column_map.get(resource_type)\n if not config:\n log.explore(\"Unknown resource type requested\", error=f\"Invalid resource type: {resource_type}\", payload={\"resource_type\": resource_type})\n return []\n\n query = {\"columns\": config[\"columns\"]}\n\n if since_dttm:\n import math\n\n # Use int milliseconds to be safe\n timestamp_ms = math.floor(since_dttm.timestamp() * 1000)\n\n query[\"filters\"] = [\n {\"col\": \"changed_on_dttm\", \"opr\": \"gt\", \"value\": timestamp_ms}\n ]\n\n validated = self._validate_query_params(query)\n data = self._fetch_all_pages(\n endpoint=config[\"endpoint\"],\n pagination_options={\"base_query\": validated, \"results_field\": \"result\"},\n )\n log.reflect(\n \"Resources fetched\",\n payload={\"resource_type\": resource_type, \"count\": len(data)},\n )\n return data\n\n # [/DEF:SupersetClientGetAllResources:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetClientBase [C:3] [TYPE Module] [SEMANTICS superset, client, base, auth, pagination]\n# @LAYER: Infra\n# @BRIEF Base class for SupersetClient providing initialization, authentication, pagination, and import/export helpers.\n# @RELATION DEPENDS_ON -> [ConfigModels]\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetAPIError]\n# @RELATION DEPENDS_ON -> [get_filename_from_headers]\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any, Optional, cast\n\nfrom requests import Response\n\nfrom ..config_models import Environment\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\nfrom ..utils.fileio import get_filename_from_headers\nfrom ..utils.network import APIClient, SupersetAPIError\n\napp_logger = cast(Any, app_logger)\n# #region SupersetClientBase [C:3] [TYPE Class]\n# @BRIEF Base class providing Superset client initialization, auth, pagination, and import/export plumbing.\n# @RELATION DEPENDS_ON -> [ConfigModels]\n# @RELATION DEPENDS_ON -> [APIClient]\n# @RELATION DEPENDS_ON -> [SupersetAPIError]\nclass SupersetClientBase:\n # #region SupersetClientInit [TYPE Function] [C:3]\n # @PURPOSE: Инициализирует клиент, проверяет конфигурацию и создает сетевой клиент.\n # @RELATION: DEPENDS_ON -> [Environment]\n # @RELATION: DEPENDS_ON -> [APIClient]\n def __init__(self, env: Environment):\n with belief_scope(\"SupersetClientInit\"):\n app_logger.reason(\n \"Initializing Superset client for environment\",\n extra={\"environment\": getattr(env, \"id\", None), \"env_name\": env.name},\n )\n self.env = env\n # Construct auth payload expected by Superset API\n auth_payload = {\n \"username\": env.username,\n \"password\": env.password,\n \"provider\": \"db\",\n \"refresh\": \"true\",\n }\n self.network = APIClient(\n config={\"base_url\": env.url, \"auth\": auth_payload},\n verify_ssl=env.verify_ssl,\n timeout=env.timeout,\n )\n self.delete_before_reimport: bool = False\n app_logger.reflect(\n \"Superset client initialized\",\n extra={\"environment\": getattr(self.env, \"id\", None)},\n )\n # #endregion SupersetClientInit\n # #region SupersetClientAuthenticate [TYPE Function] [C:3]\n # @PURPOSE: Authenticates the client using the configured credentials.\n # @RELATION: CALLS -> [APIClient]\n def authenticate(self) -> dict[str, str]:\n with belief_scope(\"SupersetClientAuthenticate\"):\n app_logger.reason(\n \"Authenticating Superset client\",\n extra={\"environment\": getattr(self.env, \"id\", None)},\n )\n tokens = self.network.authenticate()\n app_logger.reflect(\n \"Superset client authentication completed\",\n extra={\n \"environment\": getattr(self.env, \"id\", None),\n \"token_keys\": sorted(tokens.keys()),\n },\n )\n return tokens\n # #endregion SupersetClientAuthenticate\n @property\n # #region SupersetClientHeaders [TYPE Function] [C:1]\n # @PURPOSE: Возвращает базовые HTTP-заголовки, используемые сетевым клиентом.\n def headers(self) -> dict:\n with belief_scope(\"headers\"):\n return self.network.headers\n # #endregion SupersetClientHeaders\n # --- Pagination helpers ---\n # #region SupersetClientValidateQueryParams [TYPE Function] [C:1]\n # @PURPOSE: Ensures query parameters have default page and page_size.\n def _validate_query_params(self, query: dict | None) -> dict:\n with belief_scope(\"_validate_query_params\"):\n # Superset list endpoints commonly cap page_size at 100.\n # Using 100 avoids partial fetches when larger values are silently truncated.\n base_query = {\"page\": 0, \"page_size\": 100}\n return {**base_query, **(query or {})}\n # #endregion SupersetClientValidateQueryParams\n # #region SupersetClientFetchTotalObjectCount [TYPE Function] [C:1]\n # @PURPOSE: Fetches the total number of items for a given endpoint.\n # @RELATION: CALLS -> [APIClient]\n def _fetch_total_object_count(self, endpoint: str) -> int:\n with belief_scope(\"_fetch_total_object_count\"):\n return self.network.fetch_paginated_count(\n endpoint=endpoint,\n query_params={\"page\": 0, \"page_size\": 1},\n count_field=\"count\",\n )\n # #endregion SupersetClientFetchTotalObjectCount\n # #region SupersetClientFetchAllPages [TYPE Function] [C:1]\n # @PURPOSE: Iterates through all pages to collect all data items.\n # @RELATION: CALLS -> [APIClient]\n def _fetch_all_pages(self, endpoint: str, pagination_options: dict) -> list[dict]:\n with belief_scope(\"_fetch_all_pages\"):\n return self.network.fetch_paginated_data(\n endpoint=endpoint, pagination_options=pagination_options\n )\n # #endregion SupersetClientFetchAllPages\n # --- Import/Export helpers ---\n # #region SupersetClientDoImport [TYPE Function] [C:1]\n # @PURPOSE: Performs the actual multipart upload for import.\n # @RELATION: CALLS -> [APIClient]\n def _do_import(self, file_name: str | Path) -> dict:\n with belief_scope(\"_do_import\"):\n app_logger.debug(f\"[_do_import][State] Uploading file: {file_name}\")\n file_path = Path(file_name)\n if not file_path.exists():\n app_logger.error(\n f\"[_do_import][Failure] File does not exist: {file_name}\"\n )\n raise FileNotFoundError(f\"File does not exist: {file_name}\")\n return self.network.upload_file(\n endpoint=\"/dashboard/import/\",\n file_info={\n \"file_obj\": file_path,\n \"file_name\": file_path.name,\n \"form_field\": \"formData\",\n },\n extra_data={\"overwrite\": \"true\"},\n timeout=self.env.timeout * 2,\n )\n # #endregion SupersetClientDoImport\n # #region SupersetClientValidateExportResponse [TYPE Function] [C:1]\n # @PURPOSE: Validates that the export response is a non-empty ZIP archive.\n def _validate_export_response(self, response: Response, dashboard_id: int) -> None:\n with belief_scope(\"_validate_export_response\"):\n content_type = response.headers.get(\"Content-Type\", \"\")\n if \"application/zip\" not in content_type:\n raise SupersetAPIError(\n f\"Получен не ZIP-архив (Content-Type: {content_type})\"\n )\n if not response.content:\n raise SupersetAPIError(\"Получены пустые данные при экспорте\")\n # #endregion SupersetClientValidateExportResponse\n # #region SupersetClientResolveExportFilename [TYPE Function] [C:1]\n # @PURPOSE: Determines the filename for an exported dashboard.\n def _resolve_export_filename(self, response: Response, dashboard_id: int) -> str:\n with belief_scope(\"_resolve_export_filename\"):\n filename = get_filename_from_headers(dict(response.headers))\n if not filename:\n from datetime import datetime\n timestamp = datetime.now().strftime(\"%Y%m%dT%H%M%S\")\n filename = f\"dashboard_export_{dashboard_id}_{timestamp}.zip\"\n app_logger.warning(\n \"[_resolve_export_filename][Warning] Generated filename: %s\",\n filename,\n )\n return filename\n # #endregion SupersetClientResolveExportFilename\n # #region SupersetClientValidateImportFile [TYPE Function] [C:1]\n # @PURPOSE: Validates that the file to be imported is a valid ZIP with metadata.yaml.\n def _validate_import_file(self, zip_path: str | Path) -> None:\n with belief_scope(\"_validate_import_file\"):\n path = Path(zip_path)\n if not path.exists():\n raise FileNotFoundError(f\"Файл {zip_path} не существует\")\n if not zipfile.is_zipfile(path):\n raise SupersetAPIError(f\"Файл {zip_path} не является ZIP-архивом\")\n with zipfile.ZipFile(path, \"r\") as zf:\n if not any(n.endswith(\"metadata.yaml\") for n in zf.namelist()):\n raise SupersetAPIError(\n f\"Архив {zip_path} не содержит 'metadata.yaml'\"\n )\n # #endregion SupersetClientValidateImportFile\n # #region SupersetClientResolveTargetIdForDelete [TYPE Function] [C:1]\n # @PURPOSE: Resolves a dashboard ID from either an ID or a slug.\n # @RELATION: CALLS -> [SupersetClientGetDashboards]\n def _resolve_target_id_for_delete(\n self, dash_id: int | None, dash_slug: str | None\n ) -> int | None:\n with belief_scope(\"_resolve_target_id_for_delete\"):\n if dash_id is not None:\n return dash_id\n if dash_slug is not None:\n app_logger.debug(\n \"[_resolve_target_id_for_delete][State] Resolving ID by slug '%s'.\",\n dash_slug,\n )\n try:\n _, candidates = self.get_dashboards(\n query={\n \"filters\": [{\"col\": \"slug\", \"op\": \"eq\", \"value\": dash_slug}]\n }\n )\n if candidates:\n target_id = candidates[0][\"id\"]\n app_logger.debug(\n \"[_resolve_target_id_for_delete][Success] Resolved slug to ID %s.\",\n target_id,\n )\n return target_id\n except Exception as e:\n app_logger.warning(\n \"[_resolve_target_id_for_delete][Warning] Could not resolve slug '%s' to ID: %s\",\n dash_slug,\n e,\n )\n return None\n # #endregion SupersetClientResolveTargetIdForDelete\n # #region SupersetClientGetAllResources [TYPE Function] [C:3]\n # @PURPOSE: Fetches all resources of a given type with id, uuid, and name columns.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_all_resources(\n self, resource_type: str, since_dttm: Optional[\"datetime\"] = None\n ) -> list[dict]:\n with belief_scope(\n \"SupersetClient.get_all_resources\",\n f\"type={resource_type}, since={since_dttm}\",\n ):\n column_map = {\n \"chart\": {\n \"endpoint\": \"/chart/\",\n \"columns\": [\"id\", \"uuid\", \"slice_name\"],\n },\n \"dataset\": {\n \"endpoint\": \"/dataset/\",\n \"columns\": [\"id\", \"uuid\", \"table_name\"],\n },\n \"dashboard\": {\n \"endpoint\": \"/dashboard/\",\n \"columns\": [\"id\", \"uuid\", \"slug\", \"dashboard_title\"],\n },\n }\n config = column_map.get(resource_type)\n if not config:\n app_logger.warning(\n \"[get_all_resources][Warning] Unknown resource type: %s\",\n resource_type,\n )\n return []\n query = {\"columns\": config[\"columns\"]}\n if since_dttm:\n import math\n # Use int milliseconds to be safe\n timestamp_ms = math.floor(since_dttm.timestamp() * 1000)\n query[\"filters\"] = [\n {\"col\": \"changed_on_dttm\", \"opr\": \"gt\", \"value\": timestamp_ms}\n ]\n validated = self._validate_query_params(query)\n data = self._fetch_all_pages(\n endpoint=config[\"endpoint\"],\n pagination_options={\"base_query\": validated, \"results_field\": \"result\"},\n )\n app_logger.reflect(\n f\"Fetched {len(data)} {resource_type} resources.\",\n extra={\"src\": \"get_all_resources\"},\n )\n return data\n # #endregion SupersetClientGetAllResources\n# #endregion SupersetClientBase\n# #endregion SupersetClientBase\n" }, { "contract_id": "SupersetChartsMixin", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_charts.py", "start_line": 1, - "end_line": 90, + "end_line": 71, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Chart domain mixin for SupersetClient — list, get, extract IDs from layout.", - "SEMANTICS": [ - "superset", - "charts", - "list", - "get", - "layout" - ] + "PURPOSE": "Chart domain mixin for SupersetClient — list, get, extract IDs from layout." }, "relations": [ { @@ -40944,105 +37422,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetChartsMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, charts, list, get, layout\n# @PURPOSE: Chart domain mixin for SupersetClient — list, get, extract IDs from layout.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n\nimport json\nimport re\nfrom typing import Any, Dict, List, Optional, Tuple, cast\n\nfrom ..logger import logger as app_logger, belief_scope\n\napp_logger = cast(Any, app_logger)\n\n\n# [DEF:SupersetChartsMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing all chart-related Superset API operations.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\nclass SupersetChartsMixin:\n # [DEF:SupersetClientGetChart:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches a single chart by ID.\n # @RELATION: CALLS -> [APIClient]\n def get_chart(self, chart_id: int) -> Dict:\n with belief_scope(\"SupersetClient.get_chart\", f\"id={chart_id}\"):\n response = self.network.request(method=\"GET\", endpoint=f\"/chart/{chart_id}\")\n return cast(Dict, response)\n\n # [/DEF:SupersetClientGetChart:Function]\n\n # [DEF:SupersetClientGetCharts:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches all charts with pagination support.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_charts(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_charts\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\"id\", \"uuid\", \"slice_name\", \"viz_type\"]\n\n paginated_data = self._fetch_all_pages(\n endpoint=\"/chart/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n return len(paginated_data), paginated_data\n\n # [/DEF:SupersetClientGetCharts:Function]\n\n # [DEF:SupersetClientExtractChartIdsFromLayout:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Traverses dashboard layout metadata and extracts chart IDs from common keys.\n def _extract_chart_ids_from_layout(\n self, payload: Any\n ) -> set:\n with belief_scope(\"_extract_chart_ids_from_layout\"):\n found = set()\n\n def walk(node):\n if isinstance(node, dict):\n for key, value in node.items():\n if key in (\"chartId\", \"chart_id\", \"slice_id\", \"sliceId\"):\n try:\n found.add(int(value))\n except (TypeError, ValueError):\n pass\n if key == \"id\" and isinstance(value, str):\n match = re.match(r\"^CHART-(\\d+)$\", value)\n if match:\n try:\n found.add(int(match.group(1)))\n except ValueError:\n pass\n walk(value)\n elif isinstance(node, list):\n for item in node:\n walk(item)\n\n walk(payload)\n return found\n\n # [/DEF:SupersetClientExtractChartIdsFromLayout:Function]\n\n\n# [/DEF:SupersetChartsMixin:Class]\n# [/DEF:SupersetChartsMixin:Module]\n" - }, - { - "contract_id": "SupersetClientGetChart", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_charts.py", - "start_line": 22, - "end_line": 31, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches a single chart by ID." - }, - "relations": [ - { - "source_id": "SupersetClientGetChart", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetChart:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches a single chart by ID.\n # @RELATION: CALLS -> [APIClient]\n def get_chart(self, chart_id: int) -> Dict:\n with belief_scope(\"SupersetClient.get_chart\", f\"id={chart_id}\"):\n response = self.network.request(method=\"GET\", endpoint=f\"/chart/{chart_id}\")\n return cast(Dict, response)\n\n # [/DEF:SupersetClientGetChart:Function]\n" - }, - { - "contract_id": "SupersetClientGetCharts", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_charts.py", - "start_line": 33, - "end_line": 52, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches all charts with pagination support." - }, - "relations": [ - { - "source_id": "SupersetClientGetCharts", - "relation_type": "CALLS", - "target_id": "SupersetClientFetchAllPages", - "target_ref": "[SupersetClientFetchAllPages]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetCharts:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches all charts with pagination support.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_charts(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_charts\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\"id\", \"uuid\", \"slice_name\", \"viz_type\"]\n\n paginated_data = self._fetch_all_pages(\n endpoint=\"/chart/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n return len(paginated_data), paginated_data\n\n # [/DEF:SupersetClientGetCharts:Function]\n" - }, - { - "contract_id": "SupersetClientExtractChartIdsFromLayout", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_charts.py", - "start_line": 54, - "end_line": 86, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Traverses dashboard layout metadata and extracts chart IDs from common keys." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientExtractChartIdsFromLayout:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Traverses dashboard layout metadata and extracts chart IDs from common keys.\n def _extract_chart_ids_from_layout(\n self, payload: Any\n ) -> set:\n with belief_scope(\"_extract_chart_ids_from_layout\"):\n found = set()\n\n def walk(node):\n if isinstance(node, dict):\n for key, value in node.items():\n if key in (\"chartId\", \"chart_id\", \"slice_id\", \"sliceId\"):\n try:\n found.add(int(value))\n except (TypeError, ValueError):\n pass\n if key == \"id\" and isinstance(value, str):\n match = re.match(r\"^CHART-(\\d+)$\", value)\n if match:\n try:\n found.add(int(match.group(1)))\n except ValueError:\n pass\n walk(value)\n elif isinstance(node, list):\n for item in node:\n walk(item)\n\n walk(payload)\n return found\n\n # [/DEF:SupersetClientExtractChartIdsFromLayout:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetChartsMixin [C:3] [TYPE Module] [SEMANTICS superset, chart, query, list, extract]\n# @LAYER: Infra\n# @BRIEF Chart domain mixin for SupersetClient — list, get, extract IDs from layout.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nimport re\nfrom typing import Any, cast\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# #region SupersetChartsMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing all chart-related Superset API operations.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nclass SupersetChartsMixin:\n # #region SupersetClientGetChart [TYPE Function] [C:3]\n # @PURPOSE: Fetches a single chart by ID.\n # @RELATION: CALLS -> [APIClient]\n def get_chart(self, chart_id: int) -> dict:\n with belief_scope(\"SupersetClient.get_chart\", f\"id={chart_id}\"):\n response = self.network.request(method=\"GET\", endpoint=f\"/chart/{chart_id}\")\n return cast(dict, response)\n # #endregion SupersetClientGetChart\n # #region SupersetClientGetCharts [TYPE Function] [C:3]\n # @PURPOSE: Fetches all charts with pagination support.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_charts(self, query: dict | None = None) -> tuple[int, list[dict]]:\n with belief_scope(\"get_charts\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\"id\", \"uuid\", \"slice_name\", \"viz_type\"]\n paginated_data = self._fetch_all_pages(\n endpoint=\"/chart/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n return len(paginated_data), paginated_data\n # #endregion SupersetClientGetCharts\n # #region SupersetClientExtractChartIdsFromLayout [TYPE Function] [C:1]\n # @PURPOSE: Traverses dashboard layout metadata and extracts chart IDs from common keys.\n def _extract_chart_ids_from_layout(\n self, payload: Any\n ) -> set:\n with belief_scope(\"_extract_chart_ids_from_layout\"):\n found = set()\n def walk(node):\n if isinstance(node, dict):\n for key, value in node.items():\n if key in (\"chartId\", \"chart_id\", \"slice_id\", \"sliceId\"):\n try:\n found.add(int(value))\n except (TypeError, ValueError):\n pass\n if key == \"id\" and isinstance(value, str):\n match = re.match(r\"^CHART-(\\d+)$\", value)\n if match:\n try:\n found.add(int(match.group(1)))\n except ValueError:\n pass\n walk(value)\n elif isinstance(node, list):\n for item in node:\n walk(item)\n walk(payload)\n return found\n # #endregion SupersetClientExtractChartIdsFromLayout\n# #endregion SupersetChartsMixin\n# #endregion SupersetChartsMixin\n" }, { "contract_id": "SupersetDashboardsCrudMixin", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_dashboards_crud.py", "start_line": 1, - "end_line": 353, + "end_line": 344, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Dashboard CRUD mixin for SupersetClient — detail, export, import, delete.", - "SEMANTICS": [ - "superset", - "dashboards", - "crud", - "detail", - "export", - "import", - "delete" - ] + "PURPOSE": "Dashboard CRUD mixin for SupersetClient — detail, export, import, delete." }, "relations": [ { @@ -41065,183 +37459,35 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetDashboardsCrudMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, dashboards, crud, detail, export, import, delete\n# @PURPOSE: Dashboard CRUD mixin for SupersetClient — detail, export, import, delete.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n# @RELATION: DEPENDS_ON -> [SupersetDashboardsFiltersMixin]\n# @RELATION: DEPENDS_ON -> [SupersetChartsMixin]\n\nimport json\nimport re\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional, Tuple, Union, cast\nfrom requests import Response\n\nfrom ..cot_logger import MarkerLogger\nfrom ..logger import belief_scope\n\nlog = MarkerLogger(\"SupersetDashboardsCrud\")\n\n\n# [DEF:SupersetDashboardsCrudMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing dashboard detail resolution, export, import, and delete operations.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n# @RELATION: DEPENDS_ON -> [SupersetDashboardsFiltersMixin]\n# @RELATION: DEPENDS_ON -> [SupersetChartsMixin]\nclass SupersetDashboardsCrudMixin:\n # [DEF:SupersetClientGetDashboardDetail:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches detailed dashboard information including related charts and datasets.\n # @RELATION: CALLS -> [SupersetClientGetDashboard]\n # @RELATION: CALLS -> [SupersetClientGetChart]\n def get_dashboard_detail(self, dashboard_ref: Union[int, str]) -> Dict:\n with belief_scope(\n \"SupersetClient.get_dashboard_detail\", f\"ref={dashboard_ref}\"\n ):\n dashboard_response = self.get_dashboard(dashboard_ref)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n\n charts: List[Dict] = []\n datasets: List[Dict] = []\n\n # [DEF:extract_dataset_id_from_form_data:Function]\n def extract_dataset_id_from_form_data(\n form_data: Optional[Dict],\n ) -> Optional[int]:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n\n # [/DEF:extract_dataset_id_from_form_data:Function]\n\n try:\n charts_response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}/charts\"\n )\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append({\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\") or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict) else None\n ),\n \"dataset_id\": int(dataset_id) if dataset_id is not None else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (form_data.get(\"viz_type\") if isinstance(form_data, dict) else None)\n or \"Chart\",\n })\n except Exception as e:\n log.explore(\"Failed to fetch dashboard charts\", error=str(e), payload={\"dashboard_ref\": dashboard_ref})\n\n try:\n datasets_response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}/datasets\"\n )\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict) else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\") or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append({\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name or dataset_obj.get(\"database_name\") or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n })\n except Exception as e:\n log.explore(\"Failed to fetch dashboard datasets\", error=str(e), payload={\"dashboard_ref\": dashboard_ref})\n\n # Fallback: derive chart IDs from layout metadata\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n pass\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n pass\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n\n log.reason(\n \"Extracted fallback chart IDs from layout\",\n payload={\n \"chart_count\": len(chart_ids_from_position),\n \"dashboard_ref\": dashboard_ref,\n },\n )\n\n for chart_id in sorted(chart_ids_from_position):\n try:\n chart_response = self.get_chart(int(chart_id))\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append({\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\") or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\") or \"Chart\",\n })\n except Exception as e:\n log.explore(\"Failed to resolve fallback chart\", error=str(e), payload={\"chart_id\": chart_id})\n\n # Backfill datasets from chart datasource IDs.\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids: List[int] = []\n for raw_dataset_id in dataset_ids_from_charts:\n if raw_dataset_id is None or raw_dataset_id in known_dataset_ids:\n continue\n try:\n missing_dataset_ids.append(int(raw_dataset_id))\n except (TypeError, ValueError):\n continue\n\n for dataset_id in missing_dataset_ids:\n try:\n dataset_response = self.get_dataset(int(dataset_id))\n dataset_data = dataset_response.get(\"result\", dataset_response)\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict) else None\n )\n table_name = (\n dataset_data.get(\"table_name\") or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append({\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on_utc\")\n or dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n })\n except Exception as e:\n log.explore(\"Failed to resolve dataset from chart datasource\", error=str(e), payload={\"dataset_id\": dataset_id})\n\n unique_charts = {chart[\"id\"]: chart for chart in charts}\n unique_datasets = {dataset[\"id\"]: dataset for dataset in datasets}\n\n resolved_dashboard_id = dashboard_data.get(\"id\", dashboard_ref)\n return {\n \"id\": resolved_dashboard_id,\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\") or f\"Dashboard {resolved_dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\") or \"\",\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": list(unique_charts.values()),\n \"datasets\": list(unique_datasets.values()),\n \"chart_count\": len(unique_charts),\n \"dataset_count\": len(unique_datasets),\n }\n\n # [/DEF:SupersetClientGetDashboardDetail:Function]\n\n # [DEF:SupersetClientExportDashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Экспортирует дашборд в виде ZIP-архива.\n # @SIDE_EFFECT: Performs network I/O to download archive.\n # @RELATION: CALLS -> [APIClient]\n def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]:\n with belief_scope(\"export_dashboard\"):\n log.reason(\"Exporting dashboard\", payload={\"dashboard_id\": dashboard_id})\n response = self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/export/\",\n params={\"q\": json.dumps([dashboard_id])},\n stream=True,\n raw_response=True,\n )\n response = cast(Response, response)\n self._validate_export_response(response, dashboard_id)\n filename = self._resolve_export_filename(response, dashboard_id)\n log.reflect(\"Dashboard exported\", payload={\"dashboard_id\": dashboard_id, \"filename\": filename})\n return response.content, filename\n\n # [/DEF:SupersetClientExportDashboard:Function]\n\n # [DEF:SupersetClientImportDashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Импортирует дашборд из ZIP-файла.\n # @SIDE_EFFECT: Performs network I/O to upload archive.\n # @RELATION: CALLS -> [SupersetClientDoImport]\n # @RELATION: CALLS -> [APIClient]\n def import_dashboard(\n self,\n file_name: Union[str, Path],\n dash_id: Optional[int] = None,\n dash_slug: Optional[str] = None,\n ) -> Dict:\n with belief_scope(\"import_dashboard\"):\n if file_name is None:\n raise ValueError(\"file_name cannot be None\")\n file_path = str(file_name)\n self._validate_import_file(file_path)\n try:\n return self._do_import(file_path)\n except Exception as exc:\n log.explore(\"First import attempt failed\", error=str(exc), payload={\"file_name\": file_name})\n if not self.delete_before_reimport:\n raise\n\n target_id = self._resolve_target_id_for_delete(dash_id, dash_slug)\n if target_id is None:\n log.explore(\"No ID available for delete-retry during import\", error=\"Cannot delete-retry: neither dash_id nor dash_slug resolved\", payload={\"dash_id\": dash_id, \"dash_slug\": dash_slug})\n raise\n\n self.delete_dashboard(target_id)\n log.reason(\n \"Deleted dashboard, retrying import\",\n payload={\"target_id\": target_id},\n )\n return self._do_import(file_path)\n\n # [/DEF:SupersetClientImportDashboard:Function]\n\n # [DEF:SupersetClientDeleteDashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Удаляет дашборд по его ID или slug.\n # @SIDE_EFFECT: Deletes resource from upstream Superset environment.\n # @RELATION: CALLS -> [APIClient]\n def delete_dashboard(self, dashboard_id: Union[int, str]) -> None:\n with belief_scope(\"delete_dashboard\"):\n log.reason(\"Deleting dashboard\", payload={\"dashboard_id\": dashboard_id})\n response = self.network.request(\n method=\"DELETE\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n response = cast(Dict, response)\n if response.get(\"result\", True) is not False:\n log.reflect(\"Dashboard deleted\", payload={\"dashboard_id\": dashboard_id})\n else:\n log.explore(\"Unexpected response while deleting dashboard\", error=f\"Unexpected API response: {response}\", payload={\"dashboard_id\": dashboard_id, \"response\": response})\n\n # [/DEF:SupersetClientDeleteDashboard:Function]\n\n\n# [/DEF:SupersetDashboardsCrudMixin:Class]\n# [/DEF:SupersetDashboardsCrudMixin:Module]\n" - }, - { - "contract_id": "SupersetClientGetDashboardDetail", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_crud.py", - "start_line": 29, - "end_line": 269, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches detailed dashboard information including related charts and datasets." - }, - "relations": [ - { - "source_id": "SupersetClientGetDashboardDetail", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDashboard", - "target_ref": "[SupersetClientGetDashboard]" - }, - { - "source_id": "SupersetClientGetDashboardDetail", - "relation_type": "CALLS", - "target_id": "SupersetClientGetChart", - "target_ref": "[SupersetClientGetChart]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDashboardDetail:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches detailed dashboard information including related charts and datasets.\n # @RELATION: CALLS -> [SupersetClientGetDashboard]\n # @RELATION: CALLS -> [SupersetClientGetChart]\n def get_dashboard_detail(self, dashboard_ref: Union[int, str]) -> Dict:\n with belief_scope(\n \"SupersetClient.get_dashboard_detail\", f\"ref={dashboard_ref}\"\n ):\n dashboard_response = self.get_dashboard(dashboard_ref)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n\n charts: List[Dict] = []\n datasets: List[Dict] = []\n\n # [DEF:extract_dataset_id_from_form_data:Function]\n def extract_dataset_id_from_form_data(\n form_data: Optional[Dict],\n ) -> Optional[int]:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n\n # [/DEF:extract_dataset_id_from_form_data:Function]\n\n try:\n charts_response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}/charts\"\n )\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append({\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\") or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict) else None\n ),\n \"dataset_id\": int(dataset_id) if dataset_id is not None else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (form_data.get(\"viz_type\") if isinstance(form_data, dict) else None)\n or \"Chart\",\n })\n except Exception as e:\n log.explore(\"Failed to fetch dashboard charts\", error=str(e), payload={\"dashboard_ref\": dashboard_ref})\n\n try:\n datasets_response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}/datasets\"\n )\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict) else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\") or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append({\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name or dataset_obj.get(\"database_name\") or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n })\n except Exception as e:\n log.explore(\"Failed to fetch dashboard datasets\", error=str(e), payload={\"dashboard_ref\": dashboard_ref})\n\n # Fallback: derive chart IDs from layout metadata\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n pass\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n pass\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n\n log.reason(\n \"Extracted fallback chart IDs from layout\",\n payload={\n \"chart_count\": len(chart_ids_from_position),\n \"dashboard_ref\": dashboard_ref,\n },\n )\n\n for chart_id in sorted(chart_ids_from_position):\n try:\n chart_response = self.get_chart(int(chart_id))\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append({\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\") or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\") or \"Chart\",\n })\n except Exception as e:\n log.explore(\"Failed to resolve fallback chart\", error=str(e), payload={\"chart_id\": chart_id})\n\n # Backfill datasets from chart datasource IDs.\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids: List[int] = []\n for raw_dataset_id in dataset_ids_from_charts:\n if raw_dataset_id is None or raw_dataset_id in known_dataset_ids:\n continue\n try:\n missing_dataset_ids.append(int(raw_dataset_id))\n except (TypeError, ValueError):\n continue\n\n for dataset_id in missing_dataset_ids:\n try:\n dataset_response = self.get_dataset(int(dataset_id))\n dataset_data = dataset_response.get(\"result\", dataset_response)\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict) else None\n )\n table_name = (\n dataset_data.get(\"table_name\") or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append({\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on_utc\")\n or dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n })\n except Exception as e:\n log.explore(\"Failed to resolve dataset from chart datasource\", error=str(e), payload={\"dataset_id\": dataset_id})\n\n unique_charts = {chart[\"id\"]: chart for chart in charts}\n unique_datasets = {dataset[\"id\"]: dataset for dataset in datasets}\n\n resolved_dashboard_id = dashboard_data.get(\"id\", dashboard_ref)\n return {\n \"id\": resolved_dashboard_id,\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\") or f\"Dashboard {resolved_dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\") or \"\",\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": list(unique_charts.values()),\n \"datasets\": list(unique_datasets.values()),\n \"chart_count\": len(unique_charts),\n \"dataset_count\": len(unique_datasets),\n }\n\n # [/DEF:SupersetClientGetDashboardDetail:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetDashboardsCrudMixin [C:3] [TYPE Module] [SEMANTICS superset, dashboard, crud, import, export]\n# @LAYER: Infra\n# @BRIEF Dashboard CRUD mixin for SupersetClient — detail, export, import, delete.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetDashboardsFiltersMixin]\n# @RELATION DEPENDS_ON -> [SupersetChartsMixin]\nimport json\nimport re\nfrom pathlib import Path\nfrom typing import Any, cast\n\nfrom requests import Response\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# #region SupersetDashboardsCrudMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing dashboard detail resolution, export, import, and delete operations.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetDashboardsFiltersMixin]\n# @RELATION DEPENDS_ON -> [SupersetChartsMixin]\nclass SupersetDashboardsCrudMixin:\n # #region SupersetClientGetDashboardDetail [TYPE Function] [C:3]\n # @PURPOSE: Fetches detailed dashboard information including related charts and datasets.\n # @RELATION: CALLS -> [SupersetClientGetDashboard]\n # @RELATION: CALLS -> [SupersetClientGetChart]\n def get_dashboard_detail(self, dashboard_ref: int | str) -> dict:\n with belief_scope(\n \"SupersetClient.get_dashboard_detail\", f\"ref={dashboard_ref}\"\n ):\n dashboard_response = self.get_dashboard(dashboard_ref)\n dashboard_data = dashboard_response.get(\"result\", dashboard_response)\n charts: list[dict] = []\n datasets: list[dict] = []\n # #region extract_dataset_id_from_form_data [TYPE Function]\n def extract_dataset_id_from_form_data(\n form_data: dict | None,\n ) -> int | None:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n # #endregion extract_dataset_id_from_form_data\n try:\n charts_response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}/charts\"\n )\n charts_payload = (\n charts_response.get(\"result\", [])\n if isinstance(charts_response, dict)\n else []\n )\n for chart_obj in charts_payload:\n if not isinstance(chart_obj, dict):\n continue\n chart_id = chart_obj.get(\"id\")\n if chart_id is None:\n continue\n form_data = chart_obj.get(\"form_data\")\n if isinstance(form_data, str):\n try:\n form_data = json.loads(form_data)\n except Exception:\n form_data = {}\n dataset_id = extract_dataset_id_from_form_data(\n form_data\n ) or chart_obj.get(\"datasource_id\")\n charts.append({\n \"id\": int(chart_id),\n \"title\": chart_obj.get(\"slice_name\")\n or chart_obj.get(\"name\") or f\"Chart {chart_id}\",\n \"viz_type\": (\n form_data.get(\"viz_type\")\n if isinstance(form_data, dict) else None\n ),\n \"dataset_id\": int(dataset_id) if dataset_id is not None else None,\n \"last_modified\": chart_obj.get(\"changed_on\"),\n \"overview\": chart_obj.get(\"description\")\n or (form_data.get(\"viz_type\") if isinstance(form_data, dict) else None)\n or \"Chart\",\n })\n except Exception as e:\n app_logger.warning(\n \"[get_dashboard_detail][Warning] Failed to fetch dashboard charts: %s\", e,\n )\n try:\n datasets_response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}/datasets\"\n )\n datasets_payload = (\n datasets_response.get(\"result\", [])\n if isinstance(datasets_response, dict)\n else []\n )\n for dataset_obj in datasets_payload:\n if not isinstance(dataset_obj, dict):\n continue\n dataset_id = dataset_obj.get(\"id\")\n if dataset_id is None:\n continue\n db_payload = dataset_obj.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict) else None\n )\n table_name = (\n dataset_obj.get(\"table_name\")\n or dataset_obj.get(\"datasource_name\")\n or dataset_obj.get(\"name\") or f\"Dataset {dataset_id}\"\n )\n schema = dataset_obj.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append({\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name or dataset_obj.get(\"database_name\") or \"Unknown\",\n \"last_modified\": dataset_obj.get(\"changed_on\"),\n \"overview\": fq_name,\n })\n except Exception as e:\n app_logger.warning(\n \"[get_dashboard_detail][Warning] Failed to fetch dashboard datasets: %s\", e,\n )\n # Fallback: derive chart IDs from layout metadata\n if not charts:\n raw_position_json = dashboard_data.get(\"position_json\")\n chart_ids_from_position = set()\n if isinstance(raw_position_json, str) and raw_position_json:\n try:\n parsed_position = json.loads(raw_position_json)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_position)\n )\n except Exception:\n pass\n elif isinstance(raw_position_json, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_position_json)\n )\n raw_json_metadata = dashboard_data.get(\"json_metadata\")\n if isinstance(raw_json_metadata, str) and raw_json_metadata:\n try:\n parsed_metadata = json.loads(raw_json_metadata)\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(parsed_metadata)\n )\n except Exception:\n pass\n elif isinstance(raw_json_metadata, dict):\n chart_ids_from_position.update(\n self._extract_chart_ids_from_layout(raw_json_metadata)\n )\n app_logger.info(\n \"[get_dashboard_detail][State] Extracted %s fallback chart IDs from layout (dashboard_id=%s)\",\n len(chart_ids_from_position), dashboard_ref,\n )\n for chart_id in sorted(chart_ids_from_position):\n try:\n chart_response = self.get_chart(int(chart_id))\n chart_data = chart_response.get(\"result\", chart_response)\n charts.append({\n \"id\": int(chart_id),\n \"title\": chart_data.get(\"slice_name\")\n or chart_data.get(\"name\") or f\"Chart {chart_id}\",\n \"viz_type\": chart_data.get(\"viz_type\"),\n \"dataset_id\": chart_data.get(\"datasource_id\"),\n \"last_modified\": chart_data.get(\"changed_on\"),\n \"overview\": chart_data.get(\"description\")\n or chart_data.get(\"viz_type\") or \"Chart\",\n })\n except Exception as e:\n app_logger.warning(\n \"[get_dashboard_detail][Warning] Failed to resolve fallback chart %s: %s\",\n chart_id, e,\n )\n # Backfill datasets from chart datasource IDs.\n dataset_ids_from_charts = {\n c.get(\"dataset_id\") for c in charts if c.get(\"dataset_id\") is not None\n }\n known_dataset_ids = {\n d.get(\"id\") for d in datasets if d.get(\"id\") is not None\n }\n missing_dataset_ids: list[int] = []\n for raw_dataset_id in dataset_ids_from_charts:\n if raw_dataset_id is None or raw_dataset_id in known_dataset_ids:\n continue\n try:\n missing_dataset_ids.append(int(raw_dataset_id))\n except (TypeError, ValueError):\n continue\n for dataset_id in missing_dataset_ids:\n try:\n dataset_response = self.get_dataset(int(dataset_id))\n dataset_data = dataset_response.get(\"result\", dataset_response)\n db_payload = dataset_data.get(\"database\")\n db_name = (\n db_payload.get(\"database_name\")\n if isinstance(db_payload, dict) else None\n )\n table_name = (\n dataset_data.get(\"table_name\") or f\"Dataset {dataset_id}\"\n )\n schema = dataset_data.get(\"schema\")\n fq_name = f\"{schema}.{table_name}\" if schema else table_name\n datasets.append({\n \"id\": int(dataset_id),\n \"table_name\": table_name,\n \"schema\": schema,\n \"database\": db_name or \"Unknown\",\n \"last_modified\": dataset_data.get(\"changed_on_utc\")\n or dataset_data.get(\"changed_on\"),\n \"overview\": fq_name,\n })\n except Exception as e:\n app_logger.warning(\n \"[get_dashboard_detail][Warning] Failed to resolve dataset %s: %s\",\n dataset_id, e,\n )\n unique_charts = {chart[\"id\"]: chart for chart in charts}\n unique_datasets = {dataset[\"id\"]: dataset for dataset in datasets}\n resolved_dashboard_id = dashboard_data.get(\"id\", dashboard_ref)\n return {\n \"id\": resolved_dashboard_id,\n \"title\": dashboard_data.get(\"dashboard_title\")\n or dashboard_data.get(\"title\") or f\"Dashboard {resolved_dashboard_id}\",\n \"slug\": dashboard_data.get(\"slug\"),\n \"url\": dashboard_data.get(\"url\"),\n \"description\": dashboard_data.get(\"description\") or \"\",\n \"last_modified\": dashboard_data.get(\"changed_on_utc\")\n or dashboard_data.get(\"changed_on\"),\n \"published\": dashboard_data.get(\"published\"),\n \"charts\": list(unique_charts.values()),\n \"datasets\": list(unique_datasets.values()),\n \"chart_count\": len(unique_charts),\n \"dataset_count\": len(unique_datasets),\n }\n # #endregion SupersetClientGetDashboardDetail\n # #region SupersetClientExportDashboard [TYPE Function] [C:3]\n # @PURPOSE: Экспортирует дашборд в виде ZIP-архива.\n # @SIDE_EFFECT: Performs network I/O to download archive.\n # @RELATION: CALLS -> [APIClient]\n def export_dashboard(self, dashboard_id: int) -> tuple[bytes, str]:\n with belief_scope(\"export_dashboard\"):\n app_logger.info(\n \"[export_dashboard][Enter] Exporting dashboard %s.\", dashboard_id\n )\n response = self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/export/\",\n params={\"q\": json.dumps([dashboard_id])},\n stream=True,\n raw_response=True,\n )\n response = cast(Response, response)\n self._validate_export_response(response, dashboard_id)\n filename = self._resolve_export_filename(response, dashboard_id)\n app_logger.reflect(\n f\"Exported dashboard {dashboard_id} to {filename}.\",\n extra={\"src\": \"export_dashboard\"},\n )\n return response.content, filename\n # #endregion SupersetClientExportDashboard\n # #region SupersetClientImportDashboard [TYPE Function] [C:3]\n # @PURPOSE: Импортирует дашборд из ZIP-файла.\n # @SIDE_EFFECT: Performs network I/O to upload archive.\n # @RELATION: CALLS -> [SupersetClientDoImport]\n # @RELATION: CALLS -> [APIClient]\n def import_dashboard(\n self,\n file_name: str | Path,\n dash_id: int | None = None,\n dash_slug: str | None = None,\n ) -> dict:\n with belief_scope(\"import_dashboard\"):\n if file_name is None:\n raise ValueError(\"file_name cannot be None\")\n file_path = str(file_name)\n self._validate_import_file(file_path)\n try:\n return self._do_import(file_path)\n except Exception as exc:\n app_logger.error(\n \"[import_dashboard][Failure] First import attempt failed: %s\",\n exc, exc_info=True,\n )\n if not self.delete_before_reimport:\n raise\n target_id = self._resolve_target_id_for_delete(dash_id, dash_slug)\n if target_id is None:\n app_logger.error(\n \"[import_dashboard][Failure] No ID available for delete-retry.\"\n )\n raise\n self.delete_dashboard(target_id)\n app_logger.info(\n \"[import_dashboard][State] Deleted dashboard ID %s, retrying import.\",\n target_id,\n )\n return self._do_import(file_path)\n # #endregion SupersetClientImportDashboard\n # #region SupersetClientDeleteDashboard [TYPE Function] [C:3]\n # @PURPOSE: Удаляет дашборд по его ID или slug.\n # @SIDE_EFFECT: Deletes resource from upstream Superset environment.\n # @RELATION: CALLS -> [APIClient]\n def delete_dashboard(self, dashboard_id: int | str) -> None:\n with belief_scope(\"delete_dashboard\"):\n app_logger.info(\n \"[delete_dashboard][Enter] Deleting dashboard %s.\", dashboard_id\n )\n response = self.network.request(\n method=\"DELETE\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n response = cast(dict, response)\n if response.get(\"result\", True) is not False:\n app_logger.info(\n \"[delete_dashboard][Success] Dashboard %s deleted.\", dashboard_id\n )\n else:\n app_logger.warning(\n \"[delete_dashboard][Warning] Unexpected response while deleting %s: %s\",\n dashboard_id, response,\n )\n # #endregion SupersetClientDeleteDashboard\n# #endregion SupersetDashboardsCrudMixin\n# #endregion SupersetDashboardsCrudMixin\n" }, { "contract_id": "extract_dataset_id_from_form_data", "contract_type": "Function", "file_path": "backend/src/core/superset_client/_dashboards_crud.py", - "start_line": 44, - "end_line": 70, + "start_line": 36, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:extract_dataset_id_from_form_data:Function]\n def extract_dataset_id_from_form_data(\n form_data: Optional[Dict],\n ) -> Optional[int]:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n\n # [/DEF:extract_dataset_id_from_form_data:Function]\n" - }, - { - "contract_id": "SupersetClientExportDashboard", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_crud.py", - "start_line": 271, - "end_line": 292, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Экспортирует дашборд в виде ZIP-архива.", - "SIDE_EFFECT": "Performs network I/O to download archive." - }, - "relations": [ - { - "source_id": "SupersetClientExportDashboard", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "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": "def", - "body": " # [DEF:SupersetClientExportDashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Экспортирует дашборд в виде ZIP-архива.\n # @SIDE_EFFECT: Performs network I/O to download archive.\n # @RELATION: CALLS -> [APIClient]\n def export_dashboard(self, dashboard_id: int) -> Tuple[bytes, str]:\n with belief_scope(\"export_dashboard\"):\n log.reason(\"Exporting dashboard\", payload={\"dashboard_id\": dashboard_id})\n response = self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/export/\",\n params={\"q\": json.dumps([dashboard_id])},\n stream=True,\n raw_response=True,\n )\n response = cast(Response, response)\n self._validate_export_response(response, dashboard_id)\n filename = self._resolve_export_filename(response, dashboard_id)\n log.reflect(\"Dashboard exported\", payload={\"dashboard_id\": dashboard_id, \"filename\": filename})\n return response.content, filename\n\n # [/DEF:SupersetClientExportDashboard:Function]\n" - }, - { - "contract_id": "SupersetClientImportDashboard", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_crud.py", - "start_line": 294, - "end_line": 330, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Импортирует дашборд из ZIP-файла.", - "SIDE_EFFECT": "Performs network I/O to upload archive." - }, - "relations": [ - { - "source_id": "SupersetClientImportDashboard", - "relation_type": "CALLS", - "target_id": "SupersetClientDoImport", - "target_ref": "[SupersetClientDoImport]" - }, - { - "source_id": "SupersetClientImportDashboard", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "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": "def", - "body": " # [DEF:SupersetClientImportDashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Импортирует дашборд из ZIP-файла.\n # @SIDE_EFFECT: Performs network I/O to upload archive.\n # @RELATION: CALLS -> [SupersetClientDoImport]\n # @RELATION: CALLS -> [APIClient]\n def import_dashboard(\n self,\n file_name: Union[str, Path],\n dash_id: Optional[int] = None,\n dash_slug: Optional[str] = None,\n ) -> Dict:\n with belief_scope(\"import_dashboard\"):\n if file_name is None:\n raise ValueError(\"file_name cannot be None\")\n file_path = str(file_name)\n self._validate_import_file(file_path)\n try:\n return self._do_import(file_path)\n except Exception as exc:\n log.explore(\"First import attempt failed\", error=str(exc), payload={\"file_name\": file_name})\n if not self.delete_before_reimport:\n raise\n\n target_id = self._resolve_target_id_for_delete(dash_id, dash_slug)\n if target_id is None:\n log.explore(\"No ID available for delete-retry during import\", error=\"Cannot delete-retry: neither dash_id nor dash_slug resolved\", payload={\"dash_id\": dash_id, \"dash_slug\": dash_slug})\n raise\n\n self.delete_dashboard(target_id)\n log.reason(\n \"Deleted dashboard, retrying import\",\n payload={\"target_id\": target_id},\n )\n return self._do_import(file_path)\n\n # [/DEF:SupersetClientImportDashboard:Function]\n" - }, - { - "contract_id": "SupersetClientDeleteDashboard", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_crud.py", - "start_line": 332, - "end_line": 349, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Удаляет дашборд по его ID или slug.", - "SIDE_EFFECT": "Deletes resource from upstream Superset environment." - }, - "relations": [ - { - "source_id": "SupersetClientDeleteDashboard", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "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": "def", - "body": " # [DEF:SupersetClientDeleteDashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Удаляет дашборд по его ID или slug.\n # @SIDE_EFFECT: Deletes resource from upstream Superset environment.\n # @RELATION: CALLS -> [APIClient]\n def delete_dashboard(self, dashboard_id: Union[int, str]) -> None:\n with belief_scope(\"delete_dashboard\"):\n log.reason(\"Deleting dashboard\", payload={\"dashboard_id\": dashboard_id})\n response = self.network.request(\n method=\"DELETE\", endpoint=f\"/dashboard/{dashboard_id}\"\n )\n response = cast(Dict, response)\n if response.get(\"result\", True) is not False:\n log.reflect(\"Dashboard deleted\", payload={\"dashboard_id\": dashboard_id})\n else:\n log.explore(\"Unexpected response while deleting dashboard\", error=f\"Unexpected API response: {response}\", payload={\"dashboard_id\": dashboard_id, \"response\": response})\n\n # [/DEF:SupersetClientDeleteDashboard:Function]\n" + "anchor_syntax": "region", + "body": " # #region extract_dataset_id_from_form_data [TYPE Function]\n def extract_dataset_id_from_form_data(\n form_data: dict | None,\n ) -> int | None:\n if not isinstance(form_data, dict):\n return None\n datasource = form_data.get(\"datasource\")\n if isinstance(datasource, str):\n matched = re.match(r\"^(\\d+)__\", datasource)\n if matched:\n try:\n return int(matched.group(1))\n except ValueError:\n return None\n if isinstance(datasource, dict):\n ds_id = datasource.get(\"id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n ds_id = form_data.get(\"datasource_id\")\n try:\n return int(ds_id) if ds_id is not None else None\n except (TypeError, ValueError):\n return None\n # #endregion extract_dataset_id_from_form_data\n" }, { "contract_id": "SupersetDashboardsFiltersMixin", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_dashboards_filters.py", "start_line": 1, - "end_line": 257, + "end_line": 228, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Dashboard native filter extraction mixin for SupersetClient.", - "SEMANTICS": [ - "superset", - "dashboards", - "filters", - "permalink", - "native-filters" - ] + "PURPOSE": "Dashboard native filter extraction mixin for SupersetClient." }, "relations": [ { @@ -41252,198 +37498,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetDashboardsFiltersMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, dashboards, filters, permalink, native-filters\n# @PURPOSE: Dashboard native filter extraction mixin for SupersetClient.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n\nimport json\nfrom typing import Any, Dict, Optional, Union, cast\n\nfrom ..cot_logger import MarkerLogger\nfrom ..logger import belief_scope\n\nlog = MarkerLogger(\"SupersetDashboardsFilters\")\n\n\n# [DEF:SupersetDashboardsFiltersMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing dashboard native filter extraction from permalink and URL state.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\nclass SupersetDashboardsFiltersMixin:\n # [DEF:SupersetClientGetDashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches a single dashboard by ID or slug.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboard(self, dashboard_ref: Union[int, str]) -> Dict:\n with belief_scope(\"SupersetClient.get_dashboard\", f\"ref={dashboard_ref}\"):\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}\"\n )\n return cast(Dict, response)\n\n # [/DEF:SupersetClientGetDashboard:Function]\n\n # [DEF:SupersetClientGetDashboardPermalinkState:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetches stored dashboard permalink state by permalink key.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboard_permalink_state(self, permalink_key: str) -> Dict:\n with belief_scope(\n \"SupersetClient.get_dashboard_permalink_state\", f\"key={permalink_key}\"\n ):\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(Dict, response)\n\n # [/DEF:SupersetClientGetDashboardPermalinkState:Function]\n\n # [DEF:SupersetClientGetNativeFilterState:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetches stored native filter state by filter state key.\n # @RELATION: CALLS -> [APIClient]\n def get_native_filter_state(\n self, dashboard_id: Union[int, str], filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"SupersetClient.get_native_filter_state\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(Dict, response)\n\n # [/DEF:SupersetClientGetNativeFilterState:Function]\n\n # [DEF:SupersetClientExtractNativeFiltersFromPermalink:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters dataMask from a permalink key.\n # @RELATION: CALLS -> [SupersetClientGetDashboardPermalinkState]\n def extract_native_filters_from_permalink(self, permalink_key: str) -> Dict:\n with belief_scope(\n \"SupersetClient.extract_native_filters_from_permalink\",\n f\"key={permalink_key}\",\n ):\n permalink_response = self.get_dashboard_permalink_state(permalink_key)\n\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n\n # [/DEF:SupersetClientExtractNativeFiltersFromPermalink:Function]\n\n # [DEF:SupersetClientExtractNativeFiltersFromKey:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter.\n # @RELATION: CALLS -> [SupersetClientGetNativeFilterState]\n def extract_native_filters_from_key(\n self, dashboard_id: Union[int, str], filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"SupersetClient.extract_native_filters_from_key\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = self.get_native_filter_state(\n dashboard_id, filter_state_key\n )\n\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n log.explore(\"Failed to parse filter state JSON\", error=str(e))\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n\n extracted_filters = {}\n\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n\n # [/DEF:SupersetClientExtractNativeFiltersFromKey:Function]\n\n # [DEF:SupersetClientParseDashboardUrlForFilters:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state if present.\n # @RELATION: CALLS -> [SupersetClientExtractNativeFiltersFromPermalink]\n # @RELATION: CALLS -> [SupersetClientExtractNativeFiltersFromKey]\n def parse_dashboard_url_for_filters(self, url: str) -> Dict:\n with belief_scope(\n \"SupersetClient.parse_dashboard_url_for_filters\", f\"url={url}\"\n ):\n import urllib.parse\n\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n\n # Check for permalink URL: /dashboard/p/{key}/ or /superset/dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = self.extract_native_filters_from_permalink(\n permalink_key\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n pass\n\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n pass\n\n if dashboard_ref:\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = self.get_dashboard(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n log.explore(\"Failed to resolve dashboard slug to ID\", error=str(e), payload={\"slug\": dashboard_ref})\n\n if resolved_id is not None:\n filter_data = self.extract_native_filters_from_key(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n log.explore(\"Could not resolve dashboard_id from URL for native_filters_key\", error=\"Dashboard ID could not be resolved from URL\", payload={\"dashboard_ref\": dashboard_ref})\n\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n log.explore(\"Failed to parse native_filters JSON\", error=str(e))\n\n return result\n\n # [/DEF:SupersetClientParseDashboardUrlForFilters:Function]\n\n\n# [/DEF:SupersetDashboardsFiltersMixin:Class]\n# [/DEF:SupersetDashboardsFiltersMixin:Module]\n" - }, - { - "contract_id": "SupersetClientGetDashboard", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_filters.py", - "start_line": 22, - "end_line": 33, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches a single dashboard by ID or slug." - }, - "relations": [ - { - "source_id": "SupersetClientGetDashboard", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches a single dashboard by ID or slug.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboard(self, dashboard_ref: Union[int, str]) -> Dict:\n with belief_scope(\"SupersetClient.get_dashboard\", f\"ref={dashboard_ref}\"):\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}\"\n )\n return cast(Dict, response)\n\n # [/DEF:SupersetClientGetDashboard:Function]\n" - }, - { - "contract_id": "SupersetClientGetDashboardPermalinkState", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_filters.py", - "start_line": 35, - "end_line": 48, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Fetches stored dashboard permalink state by permalink key." - }, - "relations": [ - { - "source_id": "SupersetClientGetDashboardPermalinkState", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "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": "def", - "body": " # [DEF:SupersetClientGetDashboardPermalinkState:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetches stored dashboard permalink state by permalink key.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboard_permalink_state(self, permalink_key: str) -> Dict:\n with belief_scope(\n \"SupersetClient.get_dashboard_permalink_state\", f\"key={permalink_key}\"\n ):\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(Dict, response)\n\n # [/DEF:SupersetClientGetDashboardPermalinkState:Function]\n" - }, - { - "contract_id": "SupersetClientGetNativeFilterState", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_filters.py", - "start_line": 50, - "end_line": 67, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Fetches stored native filter state by filter state key." - }, - "relations": [ - { - "source_id": "SupersetClientGetNativeFilterState", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "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": "def", - "body": " # [DEF:SupersetClientGetNativeFilterState:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Fetches stored native filter state by filter state key.\n # @RELATION: CALLS -> [APIClient]\n def get_native_filter_state(\n self, dashboard_id: Union[int, str], filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"SupersetClient.get_native_filter_state\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(Dict, response)\n\n # [/DEF:SupersetClientGetNativeFilterState:Function]\n" - }, - { - "contract_id": "SupersetClientExtractNativeFiltersFromPermalink", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_filters.py", - "start_line": 69, - "end_line": 102, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Extract native filters dataMask from a permalink key." - }, - "relations": [ - { - "source_id": "SupersetClientExtractNativeFiltersFromPermalink", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDashboardPermalinkState", - "target_ref": "[SupersetClientGetDashboardPermalinkState]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientExtractNativeFiltersFromPermalink:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters dataMask from a permalink key.\n # @RELATION: CALLS -> [SupersetClientGetDashboardPermalinkState]\n def extract_native_filters_from_permalink(self, permalink_key: str) -> Dict:\n with belief_scope(\n \"SupersetClient.extract_native_filters_from_permalink\",\n f\"key={permalink_key}\",\n ):\n permalink_response = self.get_dashboard_permalink_state(permalink_key)\n\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n\n # [/DEF:SupersetClientExtractNativeFiltersFromPermalink:Function]\n" - }, - { - "contract_id": "SupersetClientExtractNativeFiltersFromKey", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_filters.py", - "start_line": 104, - "end_line": 158, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Extract native filters from a native_filters_key URL parameter." - }, - "relations": [ - { - "source_id": "SupersetClientExtractNativeFiltersFromKey", - "relation_type": "CALLS", - "target_id": "SupersetClientGetNativeFilterState", - "target_ref": "[SupersetClientGetNativeFilterState]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientExtractNativeFiltersFromKey:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter.\n # @RELATION: CALLS -> [SupersetClientGetNativeFilterState]\n def extract_native_filters_from_key(\n self, dashboard_id: Union[int, str], filter_state_key: str\n ) -> Dict:\n with belief_scope(\n \"SupersetClient.extract_native_filters_from_key\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = self.get_native_filter_state(\n dashboard_id, filter_state_key\n )\n\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n log.explore(\"Failed to parse filter state JSON\", error=str(e))\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n\n extracted_filters = {}\n\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n\n # [/DEF:SupersetClientExtractNativeFiltersFromKey:Function]\n" - }, - { - "contract_id": "SupersetClientParseDashboardUrlForFilters", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_filters.py", - "start_line": 160, - "end_line": 253, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Parse a Superset dashboard URL and extract native filter state if present." - }, - "relations": [ - { - "source_id": "SupersetClientParseDashboardUrlForFilters", - "relation_type": "CALLS", - "target_id": "SupersetClientExtractNativeFiltersFromPermalink", - "target_ref": "[SupersetClientExtractNativeFiltersFromPermalink]" - }, - { - "source_id": "SupersetClientParseDashboardUrlForFilters", - "relation_type": "CALLS", - "target_id": "SupersetClientExtractNativeFiltersFromKey", - "target_ref": "[SupersetClientExtractNativeFiltersFromKey]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientParseDashboardUrlForFilters:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state if present.\n # @RELATION: CALLS -> [SupersetClientExtractNativeFiltersFromPermalink]\n # @RELATION: CALLS -> [SupersetClientExtractNativeFiltersFromKey]\n def parse_dashboard_url_for_filters(self, url: str) -> Dict:\n with belief_scope(\n \"SupersetClient.parse_dashboard_url_for_filters\", f\"url={url}\"\n ):\n import urllib.parse\n\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n\n # Check for permalink URL: /dashboard/p/{key}/ or /superset/dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = self.extract_native_filters_from_permalink(\n permalink_key\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n pass\n\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n pass\n\n if dashboard_ref:\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = self.get_dashboard(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n log.explore(\"Failed to resolve dashboard slug to ID\", error=str(e), payload={\"slug\": dashboard_ref})\n\n if resolved_id is not None:\n filter_data = self.extract_native_filters_from_key(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n log.explore(\"Could not resolve dashboard_id from URL for native_filters_key\", error=\"Dashboard ID could not be resolved from URL\", payload={\"dashboard_ref\": dashboard_ref})\n\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n log.explore(\"Failed to parse native_filters JSON\", error=str(e))\n\n return result\n\n # [/DEF:SupersetClientParseDashboardUrlForFilters:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetDashboardsFiltersMixin [C:3] [TYPE Module] [SEMANTICS superset, dashboard, filter, native, advanced]\n# @LAYER: Infra\n# @BRIEF Dashboard native filter extraction mixin for SupersetClient.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nimport json\nfrom typing import Any, cast\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# #region SupersetDashboardsFiltersMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing dashboard native filter extraction from permalink and URL state.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nclass SupersetDashboardsFiltersMixin:\n # #region SupersetClientGetDashboard [TYPE Function] [C:3]\n # @PURPOSE: Fetches a single dashboard by ID or slug.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboard(self, dashboard_ref: int | str) -> dict:\n with belief_scope(\"SupersetClient.get_dashboard\", f\"ref={dashboard_ref}\"):\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/{dashboard_ref}\"\n )\n return cast(dict, response)\n # #endregion SupersetClientGetDashboard\n # #region SupersetClientGetDashboardPermalinkState [TYPE Function] [C:2]\n # @PURPOSE: Fetches stored dashboard permalink state by permalink key.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboard_permalink_state(self, permalink_key: str) -> dict:\n with belief_scope(\n \"SupersetClient.get_dashboard_permalink_state\", f\"key={permalink_key}\"\n ):\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dashboard/permalink/{permalink_key}\"\n )\n return cast(dict, response)\n # #endregion SupersetClientGetDashboardPermalinkState\n # #region SupersetClientGetNativeFilterState [TYPE Function] [C:2]\n # @PURPOSE: Fetches stored native filter state by filter state key.\n # @RELATION: CALLS -> [APIClient]\n def get_native_filter_state(\n self, dashboard_id: int | str, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"SupersetClient.get_native_filter_state\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n response = self.network.request(\n method=\"GET\",\n endpoint=f\"/dashboard/{dashboard_id}/filter_state/{filter_state_key}\",\n )\n return cast(dict, response)\n # #endregion SupersetClientGetNativeFilterState\n # #region SupersetClientExtractNativeFiltersFromPermalink [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters dataMask from a permalink key.\n # @RELATION: CALLS -> [SupersetClientGetDashboardPermalinkState]\n def extract_native_filters_from_permalink(self, permalink_key: str) -> dict:\n with belief_scope(\n \"SupersetClient.extract_native_filters_from_permalink\",\n f\"key={permalink_key}\",\n ):\n permalink_response = self.get_dashboard_permalink_state(permalink_key)\n result = permalink_response.get(\"result\", permalink_response)\n state = result.get(\"state\", result)\n data_mask = state.get(\"dataMask\", {})\n extracted_filters = {}\n for filter_id, filter_data in data_mask.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"activeTabs\": state.get(\"activeTabs\", []),\n \"anchor\": state.get(\"anchor\"),\n \"chartStates\": state.get(\"chartStates\", {}),\n \"permalink_key\": permalink_key,\n }\n # #endregion SupersetClientExtractNativeFiltersFromPermalink\n # #region SupersetClientExtractNativeFiltersFromKey [TYPE Function] [C:3]\n # @PURPOSE: Extract native filters from a native_filters_key URL parameter.\n # @RELATION: CALLS -> [SupersetClientGetNativeFilterState]\n def extract_native_filters_from_key(\n self, dashboard_id: int | str, filter_state_key: str\n ) -> dict:\n with belief_scope(\n \"SupersetClient.extract_native_filters_from_key\",\n f\"dashboard={dashboard_id}, key={filter_state_key}\",\n ):\n filter_response = self.get_native_filter_state(\n dashboard_id, filter_state_key\n )\n result = filter_response.get(\"result\", filter_response)\n value = result.get(\"value\")\n if isinstance(value, str):\n try:\n parsed_value = json.loads(value)\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[extract_native_filters_from_key][Warning] Failed to parse filter state JSON: %s\",\n e,\n )\n parsed_value = {}\n elif isinstance(value, dict):\n parsed_value = value\n else:\n parsed_value = {}\n extracted_filters = {}\n if \"id\" in parsed_value and \"extraFormData\" in parsed_value:\n filter_id = parsed_value.get(\"id\", filter_state_key)\n extracted_filters[filter_id] = {\n \"extraFormData\": parsed_value.get(\"extraFormData\", {}),\n \"filterState\": parsed_value.get(\"filterState\", {}),\n \"ownState\": parsed_value.get(\"ownState\", {}),\n }\n else:\n for filter_id, filter_data in parsed_value.items():\n if not isinstance(filter_data, dict):\n continue\n extracted_filters[filter_id] = {\n \"extraFormData\": filter_data.get(\"extraFormData\", {}),\n \"filterState\": filter_data.get(\"filterState\", {}),\n \"ownState\": filter_data.get(\"ownState\", {}),\n }\n return {\n \"dataMask\": extracted_filters,\n \"dashboard_id\": dashboard_id,\n \"filter_state_key\": filter_state_key,\n }\n # #endregion SupersetClientExtractNativeFiltersFromKey\n # #region SupersetClientParseDashboardUrlForFilters [TYPE Function] [C:3]\n # @PURPOSE: Parse a Superset dashboard URL and extract native filter state if present.\n # @RELATION: CALLS -> [SupersetClientExtractNativeFiltersFromPermalink]\n # @RELATION: CALLS -> [SupersetClientExtractNativeFiltersFromKey]\n def parse_dashboard_url_for_filters(self, url: str) -> dict:\n with belief_scope(\n \"SupersetClient.parse_dashboard_url_for_filters\", f\"url={url}\"\n ):\n import urllib.parse\n parsed_url = urllib.parse.urlparse(url)\n query_params = urllib.parse.parse_qs(parsed_url.query)\n path_parts = parsed_url.path.rstrip(\"/\").split(\"/\")\n result = {\n \"url\": url,\n \"dashboard_id\": None,\n \"filter_type\": None,\n \"filters\": {},\n }\n # Check for permalink URL: /dashboard/p/{key}/ or /superset/dashboard/p/{key}/\n if \"p\" in path_parts:\n try:\n p_index = path_parts.index(\"p\")\n if p_index + 1 < len(path_parts):\n permalink_key = path_parts[p_index + 1]\n filter_data = self.extract_native_filters_from_permalink(\n permalink_key\n )\n result[\"filter_type\"] = \"permalink\"\n result[\"filters\"] = filter_data\n return result\n except ValueError:\n pass\n # Check for native_filters_key in query params\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key:\n dashboard_ref = None\n if \"dashboard\" in path_parts:\n try:\n dash_index = path_parts.index(\"dashboard\")\n if dash_index + 1 < len(path_parts):\n potential_id = path_parts[dash_index + 1]\n if potential_id not in (\"p\", \"list\", \"new\"):\n dashboard_ref = potential_id\n except ValueError:\n pass\n if dashboard_ref:\n resolved_id = None\n try:\n resolved_id = int(dashboard_ref)\n except (ValueError, TypeError):\n try:\n dash_resp = self.get_dashboard(dashboard_ref)\n dash_data = (\n dash_resp.get(\"result\", dash_resp)\n if isinstance(dash_resp, dict)\n else {}\n )\n raw_id = dash_data.get(\"id\")\n if raw_id is not None:\n resolved_id = int(raw_id)\n except Exception as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters][Warning] Failed to resolve dashboard slug '%s' to ID: %s\",\n dashboard_ref,\n e,\n )\n if resolved_id is not None:\n filter_data = self.extract_native_filters_from_key(\n resolved_id, native_filters_key\n )\n result[\"filter_type\"] = \"native_filters_key\"\n result[\"dashboard_id\"] = resolved_id\n result[\"filters\"] = filter_data\n return result\n else:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters][Warning] Could not resolve dashboard_id from URL for native_filters_key\"\n )\n # Check for native_filters in query params (direct filter values)\n native_filters = query_params.get(\"native_filters\", [None])[0]\n if native_filters:\n try:\n parsed_filters = json.loads(native_filters)\n result[\"filter_type\"] = \"native_filters\"\n result[\"filters\"] = {\"dataMask\": parsed_filters}\n return result\n except json.JSONDecodeError as e:\n app_logger.warning(\n \"[parse_dashboard_url_for_filters][Warning] Failed to parse native_filters JSON: %s\",\n e,\n )\n return result\n # #endregion SupersetClientParseDashboardUrlForFilters\n# #endregion SupersetDashboardsFiltersMixin\n# #endregion SupersetDashboardsFiltersMixin\n" }, { "contract_id": "SupersetDashboardsListMixin", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_dashboards_list.py", "start_line": 1, - "end_line": 218, + "end_line": 171, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Dashboard listing mixin for SupersetClient — paginated list, summary projection.", - "SEMANTICS": [ - "superset", - "dashboards", - "list", - "pagination", - "summary" - ] + "PURPOSE": "Dashboard listing mixin for SupersetClient — paginated list, summary projection." }, "relations": [ { @@ -41460,125 +37529,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetDashboardsListMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, dashboards, list, pagination, summary\n# @PURPOSE: Dashboard listing mixin for SupersetClient — paginated list, summary projection.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n# @RELATION: DEPENDS_ON -> [SupersetUserProjectionMixin]\n\nimport json\nfrom typing import Any, Dict, List, Optional, Tuple, cast\n\nfrom ..cot_logger import MarkerLogger\nfrom ..logger import belief_scope\n\nlog = MarkerLogger(\"SupersetDashboardsList\")\n\n\n# [DEF:SupersetDashboardsListMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing dashboard listing and summary projection operations.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n# @RELATION: DEPENDS_ON -> [SupersetUserProjectionMixin]\nclass SupersetDashboardsListMixin:\n # [DEF:SupersetClientGetDashboards:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает полный список дашбордов, автоматически обрабатывая пагинацию.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_dashboards\"):\n log.reason(\"Fetching dashboards\", payload={\"has_query\": query is not None})\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\", \"id\", \"url\", \"changed_on_utc\", \"dashboard_title\",\n \"published\", \"created_by\", \"changed_by\", \"changed_by_name\", \"owners\",\n ]\n\n paginated_data = self._fetch_all_pages(\n endpoint=\"/dashboard/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n log.reflect(\"Dashboards fetched\", payload={\"total_count\": total_count})\n return total_count, paginated_data\n\n # [/DEF:SupersetClientGetDashboards:Function]\n\n # [DEF:SupersetClientGetDashboardsPage:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches a single dashboards page from Superset without iterating all pages.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboards_page(\n self, query: Optional[Dict] = None\n ) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_dashboards_page\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\", \"id\", \"url\", \"changed_on_utc\", \"dashboard_title\",\n \"published\", \"created_by\", \"changed_by\", \"changed_by_name\", \"owners\",\n ]\n\n response_json = cast(\n Dict[str, Any],\n self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n\n # [/DEF:SupersetClientGetDashboardsPage:Function]\n\n # [DEF:SupersetClientGetDashboardsSummary:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches dashboard metadata optimized for the grid.\n # @RELATION: CALLS -> [SupersetClientGetDashboards]\n def get_dashboards_summary(self, require_slug: bool = False) -> List[Dict]:\n with belief_scope(\"SupersetClient.get_dashboards_summary\"):\n query: Dict[str, Any] = {}\n if require_slug:\n query[\"filters\"] = [{\"col\": \"slug\", \"opr\": \"neq\", \"value\": \"\"}]\n _, dashboards = self.get_dashboards(query=query)\n\n result = []\n max_debug_samples = 12\n for index, dash in enumerate(dashboards):\n raw_owners = dash.get(\"owners\")\n raw_created_by = dash.get(\"created_by\")\n raw_changed_by = dash.get(\"changed_by\")\n raw_changed_by_name = dash.get(\"changed_by_name\")\n\n owners = self._extract_owner_labels(raw_owners)\n if not owners:\n owners = self._extract_owner_labels([raw_created_by, raw_changed_by])\n\n projected_created_by = self._extract_user_display(None, raw_created_by)\n projected_modified_by = self._extract_user_display(\n raw_changed_by_name, raw_changed_by,\n )\n\n raw_owner_usernames: List[str] = []\n if isinstance(raw_owners, list):\n for owner_payload in raw_owners:\n if isinstance(owner_payload, dict):\n owner_username = self._sanitize_user_text(\n owner_payload.get(\"username\")\n )\n if owner_username:\n raw_owner_usernames.append(owner_username)\n\n result.append({\n \"id\": dash.get(\"id\"),\n \"slug\": dash.get(\"slug\"),\n \"title\": dash.get(\"dashboard_title\"),\n \"url\": dash.get(\"url\"),\n \"last_modified\": dash.get(\"changed_on_utc\"),\n \"status\": \"published\" if dash.get(\"published\") else \"draft\",\n \"created_by\": projected_created_by,\n \"modified_by\": projected_modified_by,\n \"owners\": owners,\n })\n\n if index < max_debug_samples:\n log.reflect(\n \"Dashboard actor projection sample\",\n payload={\n \"env\": getattr(self.env, \"id\", None),\n \"dashboard_id\": dash.get(\"id\"),\n \"raw_owners\": raw_owners,\n \"raw_owner_usernames\": raw_owner_usernames,\n \"raw_created_by\": raw_created_by,\n \"raw_changed_by\": raw_changed_by,\n \"raw_changed_by_name\": raw_changed_by_name,\n \"projected_owners\": owners,\n \"projected_created_by\": projected_created_by,\n \"projected_modified_by\": projected_modified_by,\n },\n )\n\n log.reflect(\n \"Dashboard actor projection summary\",\n payload={\n \"env\": getattr(self.env, \"id\", None),\n \"dashboards\": len(result),\n \"sampled\": min(len(result), max_debug_samples),\n },\n )\n return result\n\n # [/DEF:SupersetClientGetDashboardsSummary:Function]\n\n # [DEF:SupersetClientGetDashboardsSummaryPage:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches one page of dashboard metadata optimized for the grid.\n # @RELATION: CALLS -> [SupersetClientGetDashboardsPage]\n def get_dashboards_summary_page(\n self,\n page: int,\n page_size: int,\n search: Optional[str] = None,\n require_slug: bool = False,\n ) -> Tuple[int, List[Dict]]:\n with belief_scope(\"SupersetClient.get_dashboards_summary_page\"):\n query: Dict[str, Any] = {\n \"page\": max(page - 1, 0),\n \"page_size\": page_size,\n }\n filters: List[Dict[str, Any]] = []\n if require_slug:\n filters.append({\"col\": \"slug\", \"opr\": \"neq\", \"value\": \"\"})\n normalized_search = (search or \"\").strip()\n if normalized_search:\n filters.append({\n \"col\": \"dashboard_title\", \"opr\": \"ct\", \"value\": normalized_search,\n })\n if filters:\n query[\"filters\"] = filters\n\n total_count, dashboards = self.get_dashboards_page(query=query)\n\n result = []\n for dash in dashboards:\n owners = self._extract_owner_labels(dash.get(\"owners\"))\n if not owners:\n owners = self._extract_owner_labels(\n [dash.get(\"created_by\"), dash.get(\"changed_by\")],\n )\n\n result.append({\n \"id\": dash.get(\"id\"),\n \"slug\": dash.get(\"slug\"),\n \"title\": dash.get(\"dashboard_title\"),\n \"url\": dash.get(\"url\"),\n \"last_modified\": dash.get(\"changed_on_utc\"),\n \"status\": \"published\" if dash.get(\"published\") else \"draft\",\n \"created_by\": self._extract_user_display(\n None, dash.get(\"created_by\"),\n ),\n \"modified_by\": self._extract_user_display(\n dash.get(\"changed_by_name\"), dash.get(\"changed_by\"),\n ),\n \"owners\": owners,\n })\n\n return total_count, result\n\n # [/DEF:SupersetClientGetDashboardsSummaryPage:Function]\n\n\n# [/DEF:SupersetDashboardsListMixin:Class]\n# [/DEF:SupersetDashboardsListMixin:Module]\n" - }, - { - "contract_id": "SupersetClientGetDashboards", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_list.py", - "start_line": 24, - "end_line": 49, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Получает полный список дашбордов, автоматически обрабатывая пагинацию." - }, - "relations": [ - { - "source_id": "SupersetClientGetDashboards", - "relation_type": "CALLS", - "target_id": "SupersetClientFetchAllPages", - "target_ref": "[SupersetClientFetchAllPages]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDashboards:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает полный список дашбордов, автоматически обрабатывая пагинацию.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_dashboards(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_dashboards\"):\n log.reason(\"Fetching dashboards\", payload={\"has_query\": query is not None})\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\", \"id\", \"url\", \"changed_on_utc\", \"dashboard_title\",\n \"published\", \"created_by\", \"changed_by\", \"changed_by_name\", \"owners\",\n ]\n\n paginated_data = self._fetch_all_pages(\n endpoint=\"/dashboard/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n log.reflect(\"Dashboards fetched\", payload={\"total_count\": total_count})\n return total_count, paginated_data\n\n # [/DEF:SupersetClientGetDashboards:Function]\n" - }, - { - "contract_id": "SupersetClientGetDashboardsPage", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_list.py", - "start_line": 51, - "end_line": 78, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches a single dashboards page from Superset without iterating all pages." - }, - "relations": [ - { - "source_id": "SupersetClientGetDashboardsPage", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDashboardsPage:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches a single dashboards page from Superset without iterating all pages.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboards_page(\n self, query: Optional[Dict] = None\n ) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_dashboards_page\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\", \"id\", \"url\", \"changed_on_utc\", \"dashboard_title\",\n \"published\", \"created_by\", \"changed_by\", \"changed_by_name\", \"owners\",\n ]\n\n response_json = cast(\n Dict[str, Any],\n self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n\n # [/DEF:SupersetClientGetDashboardsPage:Function]\n" - }, - { - "contract_id": "SupersetClientGetDashboardsSummary", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_list.py", - "start_line": 80, - "end_line": 157, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches dashboard metadata optimized for the grid." - }, - "relations": [ - { - "source_id": "SupersetClientGetDashboardsSummary", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDashboards", - "target_ref": "[SupersetClientGetDashboards]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDashboardsSummary:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches dashboard metadata optimized for the grid.\n # @RELATION: CALLS -> [SupersetClientGetDashboards]\n def get_dashboards_summary(self, require_slug: bool = False) -> List[Dict]:\n with belief_scope(\"SupersetClient.get_dashboards_summary\"):\n query: Dict[str, Any] = {}\n if require_slug:\n query[\"filters\"] = [{\"col\": \"slug\", \"opr\": \"neq\", \"value\": \"\"}]\n _, dashboards = self.get_dashboards(query=query)\n\n result = []\n max_debug_samples = 12\n for index, dash in enumerate(dashboards):\n raw_owners = dash.get(\"owners\")\n raw_created_by = dash.get(\"created_by\")\n raw_changed_by = dash.get(\"changed_by\")\n raw_changed_by_name = dash.get(\"changed_by_name\")\n\n owners = self._extract_owner_labels(raw_owners)\n if not owners:\n owners = self._extract_owner_labels([raw_created_by, raw_changed_by])\n\n projected_created_by = self._extract_user_display(None, raw_created_by)\n projected_modified_by = self._extract_user_display(\n raw_changed_by_name, raw_changed_by,\n )\n\n raw_owner_usernames: List[str] = []\n if isinstance(raw_owners, list):\n for owner_payload in raw_owners:\n if isinstance(owner_payload, dict):\n owner_username = self._sanitize_user_text(\n owner_payload.get(\"username\")\n )\n if owner_username:\n raw_owner_usernames.append(owner_username)\n\n result.append({\n \"id\": dash.get(\"id\"),\n \"slug\": dash.get(\"slug\"),\n \"title\": dash.get(\"dashboard_title\"),\n \"url\": dash.get(\"url\"),\n \"last_modified\": dash.get(\"changed_on_utc\"),\n \"status\": \"published\" if dash.get(\"published\") else \"draft\",\n \"created_by\": projected_created_by,\n \"modified_by\": projected_modified_by,\n \"owners\": owners,\n })\n\n if index < max_debug_samples:\n log.reflect(\n \"Dashboard actor projection sample\",\n payload={\n \"env\": getattr(self.env, \"id\", None),\n \"dashboard_id\": dash.get(\"id\"),\n \"raw_owners\": raw_owners,\n \"raw_owner_usernames\": raw_owner_usernames,\n \"raw_created_by\": raw_created_by,\n \"raw_changed_by\": raw_changed_by,\n \"raw_changed_by_name\": raw_changed_by_name,\n \"projected_owners\": owners,\n \"projected_created_by\": projected_created_by,\n \"projected_modified_by\": projected_modified_by,\n },\n )\n\n log.reflect(\n \"Dashboard actor projection summary\",\n payload={\n \"env\": getattr(self.env, \"id\", None),\n \"dashboards\": len(result),\n \"sampled\": min(len(result), max_debug_samples),\n },\n )\n return result\n\n # [/DEF:SupersetClientGetDashboardsSummary:Function]\n" - }, - { - "contract_id": "SupersetClientGetDashboardsSummaryPage", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_dashboards_list.py", - "start_line": 159, - "end_line": 214, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches one page of dashboard metadata optimized for the grid." - }, - "relations": [ - { - "source_id": "SupersetClientGetDashboardsSummaryPage", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDashboardsPage", - "target_ref": "[SupersetClientGetDashboardsPage]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDashboardsSummaryPage:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches one page of dashboard metadata optimized for the grid.\n # @RELATION: CALLS -> [SupersetClientGetDashboardsPage]\n def get_dashboards_summary_page(\n self,\n page: int,\n page_size: int,\n search: Optional[str] = None,\n require_slug: bool = False,\n ) -> Tuple[int, List[Dict]]:\n with belief_scope(\"SupersetClient.get_dashboards_summary_page\"):\n query: Dict[str, Any] = {\n \"page\": max(page - 1, 0),\n \"page_size\": page_size,\n }\n filters: List[Dict[str, Any]] = []\n if require_slug:\n filters.append({\"col\": \"slug\", \"opr\": \"neq\", \"value\": \"\"})\n normalized_search = (search or \"\").strip()\n if normalized_search:\n filters.append({\n \"col\": \"dashboard_title\", \"opr\": \"ct\", \"value\": normalized_search,\n })\n if filters:\n query[\"filters\"] = filters\n\n total_count, dashboards = self.get_dashboards_page(query=query)\n\n result = []\n for dash in dashboards:\n owners = self._extract_owner_labels(dash.get(\"owners\"))\n if not owners:\n owners = self._extract_owner_labels(\n [dash.get(\"created_by\"), dash.get(\"changed_by\")],\n )\n\n result.append({\n \"id\": dash.get(\"id\"),\n \"slug\": dash.get(\"slug\"),\n \"title\": dash.get(\"dashboard_title\"),\n \"url\": dash.get(\"url\"),\n \"last_modified\": dash.get(\"changed_on_utc\"),\n \"status\": \"published\" if dash.get(\"published\") else \"draft\",\n \"created_by\": self._extract_user_display(\n None, dash.get(\"created_by\"),\n ),\n \"modified_by\": self._extract_user_display(\n dash.get(\"changed_by_name\"), dash.get(\"changed_by\"),\n ),\n \"owners\": owners,\n })\n\n return total_count, result\n\n # [/DEF:SupersetClientGetDashboardsSummaryPage:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetDashboardsListMixin [C:3] [TYPE Module] [SEMANTICS superset, dashboard, list, search, filter]\n# @LAYER: Infra\n# @BRIEF Dashboard listing mixin for SupersetClient — paginated list, summary projection.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetUserProjectionMixin]\nimport json\nfrom typing import Any, cast\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# #region SupersetDashboardsListMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing dashboard listing and summary projection operations.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetUserProjectionMixin]\nclass SupersetDashboardsListMixin:\n # #region SupersetClientGetDashboards [TYPE Function] [C:3]\n # @PURPOSE: Получает полный список дашбордов, автоматически обрабатывая пагинацию.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_dashboards(self, query: dict | None = None) -> tuple[int, list[dict]]:\n with belief_scope(\"get_dashboards\"):\n app_logger.info(\"[get_dashboards][Enter] Fetching dashboards.\")\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\", \"id\", \"url\", \"changed_on_utc\", \"dashboard_title\",\n \"published\", \"created_by\", \"changed_by\", \"changed_by_name\", \"owners\",\n ]\n paginated_data = self._fetch_all_pages(\n endpoint=\"/dashboard/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n app_logger.reflect(f\"Found {total_count} dashboards.\", extra={\"src\": \"get_dashboards\"})\n return total_count, paginated_data\n # #endregion SupersetClientGetDashboards\n # #region SupersetClientGetDashboardsPage [TYPE Function] [C:3]\n # @PURPOSE: Fetches a single dashboards page from Superset without iterating all pages.\n # @RELATION: CALLS -> [APIClient]\n def get_dashboards_page(\n self, query: dict | None = None\n ) -> tuple[int, list[dict]]:\n with belief_scope(\"get_dashboards_page\"):\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = [\n \"slug\", \"id\", \"url\", \"changed_on_utc\", \"dashboard_title\",\n \"published\", \"created_by\", \"changed_by\", \"changed_by_name\", \"owners\",\n ]\n response_json = cast(\n dict[str, Any],\n self.network.request(\n method=\"GET\",\n endpoint=\"/dashboard/\",\n params={\"q\": json.dumps(validated_query)},\n ),\n )\n result = response_json.get(\"result\", [])\n total_count = response_json.get(\"count\", len(result))\n return total_count, result\n # #endregion SupersetClientGetDashboardsPage\n # #region SupersetClientGetDashboardsSummary [TYPE Function] [C:3]\n # @PURPOSE: Fetches dashboard metadata optimized for the grid.\n # @RELATION: CALLS -> [SupersetClientGetDashboards]\n def get_dashboards_summary(self, require_slug: bool = False) -> list[dict]:\n with belief_scope(\"SupersetClient.get_dashboards_summary\"):\n query: dict[str, Any] = {}\n if require_slug:\n query[\"filters\"] = [{\"col\": \"slug\", \"opr\": \"neq\", \"value\": \"\"}]\n _, dashboards = self.get_dashboards(query=query)\n result = []\n max_debug_samples = 12\n for index, dash in enumerate(dashboards):\n raw_owners = dash.get(\"owners\")\n raw_created_by = dash.get(\"created_by\")\n raw_changed_by = dash.get(\"changed_by\")\n raw_changed_by_name = dash.get(\"changed_by_name\")\n owners = self._extract_owner_labels(raw_owners)\n if not owners:\n owners = self._extract_owner_labels([raw_created_by, raw_changed_by])\n projected_created_by = self._extract_user_display(None, raw_created_by)\n projected_modified_by = self._extract_user_display(\n raw_changed_by_name, raw_changed_by,\n )\n raw_owner_usernames: list[str] = []\n if isinstance(raw_owners, list):\n for owner_payload in raw_owners:\n if isinstance(owner_payload, dict):\n owner_username = self._sanitize_user_text(\n owner_payload.get(\"username\")\n )\n if owner_username:\n raw_owner_usernames.append(owner_username)\n result.append({\n \"id\": dash.get(\"id\"),\n \"slug\": dash.get(\"slug\"),\n \"title\": dash.get(\"dashboard_title\"),\n \"url\": dash.get(\"url\"),\n \"last_modified\": dash.get(\"changed_on_utc\"),\n \"status\": \"published\" if dash.get(\"published\") else \"draft\",\n \"created_by\": projected_created_by,\n \"modified_by\": projected_modified_by,\n \"owners\": owners,\n })\n if index < max_debug_samples:\n app_logger.reflect(\n \"Dashboard actor projection sample\",\n extra={\"src\": \"get_dashboards_summary\", \"payload\": {\"env\": getattr(self.env, 'id', None), \"dashboard_id\": dash.get('id'), \"raw_owners\": raw_owners, \"raw_owner_usernames\": raw_owner_usernames, \"raw_created_by\": raw_created_by, \"raw_changed_by\": raw_changed_by, \"raw_changed_by_name\": raw_changed_by_name, \"projected_owners\": owners, \"projected_created_by\": projected_created_by, \"projected_modified_by\": projected_modified_by}},\n )\n app_logger.reflect(\n \"Dashboard actor projection summary\",\n extra={\"src\": \"get_dashboards_summary\", \"payload\": {\"env\": getattr(self.env, 'id', None), \"dashboards\": len(result), \"sampled\": min(len(result), max_debug_samples)}},\n )\n return result\n # #endregion SupersetClientGetDashboardsSummary\n # #region SupersetClientGetDashboardsSummaryPage [TYPE Function] [C:3]\n # @PURPOSE: Fetches one page of dashboard metadata optimized for the grid.\n # @RELATION: CALLS -> [SupersetClientGetDashboardsPage]\n def get_dashboards_summary_page(\n self,\n page: int,\n page_size: int,\n search: str | None = None,\n require_slug: bool = False,\n ) -> tuple[int, list[dict]]:\n with belief_scope(\"SupersetClient.get_dashboards_summary_page\"):\n query: dict[str, Any] = {\n \"page\": max(page - 1, 0),\n \"page_size\": page_size,\n }\n filters: list[dict[str, Any]] = []\n if require_slug:\n filters.append({\"col\": \"slug\", \"opr\": \"neq\", \"value\": \"\"})\n normalized_search = (search or \"\").strip()\n if normalized_search:\n filters.append({\n \"col\": \"dashboard_title\", \"opr\": \"ct\", \"value\": normalized_search,\n })\n if filters:\n query[\"filters\"] = filters\n total_count, dashboards = self.get_dashboards_page(query=query)\n result = []\n for dash in dashboards:\n owners = self._extract_owner_labels(dash.get(\"owners\"))\n if not owners:\n owners = self._extract_owner_labels(\n [dash.get(\"created_by\"), dash.get(\"changed_by\")],\n )\n result.append({\n \"id\": dash.get(\"id\"),\n \"slug\": dash.get(\"slug\"),\n \"title\": dash.get(\"dashboard_title\"),\n \"url\": dash.get(\"url\"),\n \"last_modified\": dash.get(\"changed_on_utc\"),\n \"status\": \"published\" if dash.get(\"published\") else \"draft\",\n \"created_by\": self._extract_user_display(\n None, dash.get(\"created_by\"),\n ),\n \"modified_by\": self._extract_user_display(\n dash.get(\"changed_by_name\"), dash.get(\"changed_by\"),\n ),\n \"owners\": owners,\n })\n return total_count, result\n # #endregion SupersetClientGetDashboardsSummaryPage\n# #endregion SupersetDashboardsListMixin\n# #endregion SupersetDashboardsListMixin\n" }, { "contract_id": "SupersetDatabasesMixin", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_databases.py", "start_line": 1, - "end_line": 92, + "end_line": 70, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Database domain mixin for SupersetClient — list, get, summary, by_uuid.", - "SEMANTICS": [ - "superset", - "databases", - "list", - "get", - "summary", - "uuid" - ] + "PURPOSE": "Database domain mixin for SupersetClient — list, get, summary, by_uuid." }, "relations": [ { @@ -41589,125 +37554,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetDatabasesMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, databases, list, get, summary, uuid\n# @PURPOSE: Database domain mixin for SupersetClient — list, get, summary, by_uuid.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n\nfrom typing import Any, Dict, List, Optional, Tuple, cast\n\nfrom ..cot_logger import MarkerLogger\nfrom ..logger import belief_scope\n\nlog = MarkerLogger(\"SupersetDatabases\")\n\n\n# [DEF:SupersetDatabasesMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing all database-related Superset API operations.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\nclass SupersetDatabasesMixin:\n # [DEF:SupersetClientGetDatabases:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает полный список баз данных.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_databases(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_databases\"):\n log.reason(\"Fetching databases\", payload={\"has_query\": query is not None})\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = []\n\n paginated_data = self._fetch_all_pages(\n endpoint=\"/database/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n log.reflect(\"Databases fetched\", payload={\"total_count\": total_count})\n return total_count, paginated_data\n\n # [/DEF:SupersetClientGetDatabases:Function]\n\n # [DEF:SupersetClientGetDatabase:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает информацию о конкретной базе данных по её ID.\n # @RELATION: CALLS -> [APIClient]\n def get_database(self, database_id: int) -> Dict:\n with belief_scope(\"get_database\"):\n log.reason(\"Fetching database\", payload={\"database_id\": database_id})\n response = self.network.request(\n method=\"GET\", endpoint=f\"/database/{database_id}\"\n )\n response = cast(Dict, response)\n log.reflect(\"Database fetched\", payload={\"database_id\": database_id})\n return response\n\n # [/DEF:SupersetClientGetDatabase:Function]\n\n # [DEF:SupersetClientGetDatabasesSummary:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch a summary of databases including uuid, name, and engine.\n # @RELATION: CALLS -> [SupersetClientGetDatabases]\n def get_databases_summary(self) -> List[Dict]:\n with belief_scope(\"SupersetClient.get_databases_summary\"):\n query = {\"columns\": [\"id\", \"uuid\", \"database_name\", \"backend\"]}\n _, databases = self.get_databases(query=query)\n\n # Map 'backend' to 'engine' for consistency with contracts\n for db in databases:\n db[\"engine\"] = db.pop(\"backend\", None)\n\n return databases\n\n # [/DEF:SupersetClientGetDatabasesSummary:Function]\n\n # [DEF:SupersetClientGetDatabaseByUuid:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Find a database by its UUID.\n # @RELATION: CALLS -> [SupersetClientGetDatabases]\n def get_database_by_uuid(self, db_uuid: str) -> Optional[Dict]:\n with belief_scope(\"SupersetClient.get_database_by_uuid\", f\"uuid={db_uuid}\"):\n query = {\"filters\": [{\"col\": \"uuid\", \"op\": \"eq\", \"value\": db_uuid}]}\n _, databases = self.get_databases(query=query)\n return databases[0] if databases else None\n\n # [/DEF:SupersetClientGetDatabaseByUuid:Function]\n\n\n# [/DEF:SupersetDatabasesMixin:Class]\n# [/DEF:SupersetDatabasesMixin:Module]\n" - }, - { - "contract_id": "SupersetClientGetDatabases", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_databases.py", - "start_line": 21, - "end_line": 43, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Получает полный список баз данных." - }, - "relations": [ - { - "source_id": "SupersetClientGetDatabases", - "relation_type": "CALLS", - "target_id": "SupersetClientFetchAllPages", - "target_ref": "[SupersetClientFetchAllPages]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDatabases:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает полный список баз данных.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_databases(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_databases\"):\n log.reason(\"Fetching databases\", payload={\"has_query\": query is not None})\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = []\n\n paginated_data = self._fetch_all_pages(\n endpoint=\"/database/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n log.reflect(\"Databases fetched\", payload={\"total_count\": total_count})\n return total_count, paginated_data\n\n # [/DEF:SupersetClientGetDatabases:Function]\n" - }, - { - "contract_id": "SupersetClientGetDatabase", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_databases.py", - "start_line": 45, - "end_line": 59, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Получает информацию о конкретной базе данных по её ID." - }, - "relations": [ - { - "source_id": "SupersetClientGetDatabase", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDatabase:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает информацию о конкретной базе данных по её ID.\n # @RELATION: CALLS -> [APIClient]\n def get_database(self, database_id: int) -> Dict:\n with belief_scope(\"get_database\"):\n log.reason(\"Fetching database\", payload={\"database_id\": database_id})\n response = self.network.request(\n method=\"GET\", endpoint=f\"/database/{database_id}\"\n )\n response = cast(Dict, response)\n log.reflect(\"Database fetched\", payload={\"database_id\": database_id})\n return response\n\n # [/DEF:SupersetClientGetDatabase:Function]\n" - }, - { - "contract_id": "SupersetClientGetDatabasesSummary", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_databases.py", - "start_line": 61, - "end_line": 76, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetch a summary of databases including uuid, name, and engine." - }, - "relations": [ - { - "source_id": "SupersetClientGetDatabasesSummary", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDatabases", - "target_ref": "[SupersetClientGetDatabases]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDatabasesSummary:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetch a summary of databases including uuid, name, and engine.\n # @RELATION: CALLS -> [SupersetClientGetDatabases]\n def get_databases_summary(self) -> List[Dict]:\n with belief_scope(\"SupersetClient.get_databases_summary\"):\n query = {\"columns\": [\"id\", \"uuid\", \"database_name\", \"backend\"]}\n _, databases = self.get_databases(query=query)\n\n # Map 'backend' to 'engine' for consistency with contracts\n for db in databases:\n db[\"engine\"] = db.pop(\"backend\", None)\n\n return databases\n\n # [/DEF:SupersetClientGetDatabasesSummary:Function]\n" - }, - { - "contract_id": "SupersetClientGetDatabaseByUuid", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_databases.py", - "start_line": 78, - "end_line": 88, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Find a database by its UUID." - }, - "relations": [ - { - "source_id": "SupersetClientGetDatabaseByUuid", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDatabases", - "target_ref": "[SupersetClientGetDatabases]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDatabaseByUuid:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Find a database by its UUID.\n # @RELATION: CALLS -> [SupersetClientGetDatabases]\n def get_database_by_uuid(self, db_uuid: str) -> Optional[Dict]:\n with belief_scope(\"SupersetClient.get_database_by_uuid\", f\"uuid={db_uuid}\"):\n query = {\"filters\": [{\"col\": \"uuid\", \"op\": \"eq\", \"value\": db_uuid}]}\n _, databases = self.get_databases(query=query)\n return databases[0] if databases else None\n\n # [/DEF:SupersetClientGetDatabaseByUuid:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetDatabasesMixin [C:3] [TYPE Module] [SEMANTICS superset, search, superset-databases-mixin]\n# @LAYER: Infra\n# @BRIEF Database domain mixin for SupersetClient — list, get, summary, by_uuid.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nfrom typing import Any, cast\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# #region SupersetDatabasesMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing all database-related Superset API operations.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nclass SupersetDatabasesMixin:\n # #region SupersetClientGetDatabases [TYPE Function] [C:3]\n # @PURPOSE: Получает полный список баз данных.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_databases(self, query: dict | None = None) -> tuple[int, list[dict]]:\n with belief_scope(\"get_databases\"):\n app_logger.info(\"[get_databases][Enter] Fetching databases.\")\n validated_query = self._validate_query_params(query or {})\n if \"columns\" not in validated_query:\n validated_query[\"columns\"] = []\n paginated_data = self._fetch_all_pages(\n endpoint=\"/database/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n app_logger.reflect(f\"Found {total_count} databases.\", extra={\"src\": \"get_databases\"})\n return total_count, paginated_data\n # #endregion SupersetClientGetDatabases\n # #region SupersetClientGetDatabase [TYPE Function] [C:3]\n # @PURPOSE: Получает информацию о конкретной базе данных по её ID.\n # @RELATION: CALLS -> [APIClient]\n def get_database(self, database_id: int) -> dict:\n with belief_scope(\"get_database\"):\n app_logger.info(\"[get_database][Enter] Fetching database %s.\", database_id)\n response = self.network.request(\n method=\"GET\", endpoint=f\"/database/{database_id}\"\n )\n response = cast(dict, response)\n app_logger.reflect(f\"Got database {database_id}.\", extra={\"src\": \"get_database\"})\n return response\n # #endregion SupersetClientGetDatabase\n # #region SupersetClientGetDatabasesSummary [TYPE Function] [C:3]\n # @PURPOSE: Fetch a summary of databases including uuid, name, and engine.\n # @RELATION: CALLS -> [SupersetClientGetDatabases]\n def get_databases_summary(self) -> list[dict]:\n with belief_scope(\"SupersetClient.get_databases_summary\"):\n query = {\"columns\": [\"id\", \"uuid\", \"database_name\", \"backend\"]}\n _, databases = self.get_databases(query=query)\n # Map 'backend' to 'engine' for consistency with contracts\n for db in databases:\n db[\"engine\"] = db.pop(\"backend\", None)\n return databases\n # #endregion SupersetClientGetDatabasesSummary\n # #region SupersetClientGetDatabaseByUuid [TYPE Function] [C:3]\n # @PURPOSE: Find a database by its UUID.\n # @RELATION: CALLS -> [SupersetClientGetDatabases]\n def get_database_by_uuid(self, db_uuid: str) -> dict | None:\n with belief_scope(\"SupersetClient.get_database_by_uuid\", f\"uuid={db_uuid}\"):\n query = {\"filters\": [{\"col\": \"uuid\", \"op\": \"eq\", \"value\": db_uuid}]}\n _, databases = self.get_databases(query=query)\n return databases[0] if databases else None\n # #endregion SupersetClientGetDatabaseByUuid\n# #endregion SupersetDatabasesMixin\n# #endregion SupersetDatabasesMixin\n" }, { "contract_id": "SupersetDatasetsMixin", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_datasets.py", "start_line": 1, - "end_line": 223, + "end_line": 187, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Dataset domain mixin for SupersetClient — list, get, detail, update.", - "SEMANTICS": [ - "superset", - "datasets", - "list", - "get", - "detail", - "update" - ] + "PURPOSE": "Dataset domain mixin for SupersetClient — list, get, detail, update." }, "relations": [ { @@ -41718,166 +37579,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetDatasetsMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, datasets, list, get, detail, update\n# @PURPOSE: Dataset domain mixin for SupersetClient — list, get, detail, update.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n\nimport json\nfrom typing import Any, Dict, List, Optional, Tuple, cast\n\nfrom ..cot_logger import MarkerLogger\nfrom ..logger import belief_scope\n\nlog = MarkerLogger(\"SupersetDatasets\")\n\n\n# [DEF:SupersetDatasetsMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing basic dataset CRUD operations (list, get, detail, update).\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\nclass SupersetDatasetsMixin:\n # [DEF:SupersetClientGetDatasets:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает полный список датасетов, автоматически обрабатывая пагинацию.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_datasets\"):\n log.reason(\"Fetching datasets\")\n validated_query = self._validate_query_params(query)\n\n paginated_data = self._fetch_all_pages(\n endpoint=\"/dataset/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n log.reflect(\"Datasets fetched\", payload={\"total_count\": total_count})\n return total_count, paginated_data\n\n # [/DEF:SupersetClientGetDatasets:Function]\n\n # [DEF:SupersetClientGetDatasetsSummary:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches dataset metadata optimized for the Dataset Hub grid.\n # @RELATION: CALLS -> [SupersetClientGetDatasets]\n def get_datasets_summary(self) -> List[Dict]:\n with belief_scope(\"SupersetClient.get_datasets_summary\"):\n query = {\"columns\": [\"id\", \"table_name\", \"schema\", \"database\"]}\n _, datasets = self.get_datasets(query=query)\n\n result = []\n for ds in datasets:\n result.append(\n {\n \"id\": ds.get(\"id\"),\n \"table_name\": ds.get(\"table_name\"),\n \"schema\": ds.get(\"schema\"),\n \"database\": ds.get(\"database\", {}).get(\n \"database_name\", \"Unknown\"\n ),\n }\n )\n return result\n\n # [/DEF:SupersetClientGetDatasetsSummary:Function]\n\n # [DEF:SupersetClientGetDatasetDetail:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches detailed dataset information including columns and linked dashboards.\n # @RELATION: CALLS -> [SupersetClientGetDataset]\n # @RELATION: CALLS -> [APIClient]\n def get_dataset_detail(self, dataset_id: int) -> Dict:\n with belief_scope(\"SupersetClient.get_dataset_detail\", f\"id={dataset_id}\"):\n\n def as_bool(value, default=False):\n if value is None:\n return default\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in (\"1\", \"true\", \"yes\", \"y\", \"on\")\n return bool(value)\n\n response = self.get_dataset(dataset_id)\n\n if isinstance(response, dict) and \"result\" in response:\n dataset = response[\"result\"]\n else:\n dataset = response\n\n columns = dataset.get(\"columns\", [])\n column_info = []\n for col in columns:\n col_id = col.get(\"id\")\n if col_id is None:\n continue\n column_info.append(\n {\n \"id\": int(col_id),\n \"name\": col.get(\"column_name\"),\n \"type\": col.get(\"type\"),\n \"is_dttm\": as_bool(col.get(\"is_dttm\"), default=False),\n \"is_active\": as_bool(col.get(\"is_active\"), default=True),\n \"description\": col.get(\"description\", \"\"),\n }\n )\n\n linked_dashboards = []\n try:\n related_objects = self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}/related_objects\"\n )\n\n if isinstance(related_objects, dict):\n if \"dashboards\" in related_objects:\n dashboards_data = related_objects[\"dashboards\"]\n elif \"result\" in related_objects and isinstance(\n related_objects[\"result\"], dict\n ):\n dashboards_data = related_objects[\"result\"].get(\"dashboards\", [])\n else:\n dashboards_data = []\n\n for dash in dashboards_data:\n if isinstance(dash, dict):\n dash_id = dash.get(\"id\")\n if dash_id is None:\n continue\n linked_dashboards.append(\n {\n \"id\": int(dash_id),\n \"title\": dash.get(\"dashboard_title\")\n or dash.get(\"title\", f\"Dashboard {dash_id}\"),\n \"slug\": dash.get(\"slug\"),\n }\n )\n else:\n try:\n dash_id = int(dash)\n except (TypeError, ValueError):\n continue\n linked_dashboards.append(\n {\"id\": dash_id, \"title\": f\"Dashboard {dash_id}\", \"slug\": None}\n )\n except Exception as e:\n log.explore(\"Failed to fetch related dashboards\", error=str(e), payload={\"dataset_id\": dataset_id})\n linked_dashboards = []\n\n database_obj = dataset.get(\"database\")\n database_dict = database_obj if isinstance(database_obj, dict) else {}\n sql = dataset.get(\"sql\", \"\")\n\n result = {\n \"id\": dataset.get(\"id\"),\n \"table_name\": dataset.get(\"table_name\"),\n \"schema\": dataset.get(\"schema\"),\n \"database\": database_dict,\n \"database_id\": dataset.get(\"database_id\", database_dict.get(\"id\")),\n \"database_backend\": database_dict.get(\"backend\") or database_dict.get(\"engine\") or \"\",\n \"description\": dataset.get(\"description\", \"\"),\n \"columns\": column_info,\n \"column_count\": len(column_info),\n \"sql\": sql,\n \"linked_dashboards\": linked_dashboards,\n \"linked_dashboard_count\": len(linked_dashboards),\n \"is_sqllab_view\": as_bool(dataset.get(\"is_sqllab_view\"), default=False),\n \"created_on\": dataset.get(\"created_on\"),\n \"changed_on\": dataset.get(\"changed_on\"),\n }\n\n log.reflect(\n \"Dataset detail fetched\",\n payload={\n \"dataset_id\": dataset_id,\n \"column_count\": len(column_info),\n \"linked_dashboard_count\": len(linked_dashboards),\n },\n )\n return result\n\n # [/DEF:SupersetClientGetDatasetDetail:Function]\n\n # [DEF:SupersetClientGetDataset:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает информацию о конкретном датасете по его ID.\n # @RELATION: CALLS -> [APIClient]\n def get_dataset(self, dataset_id: int) -> Dict:\n with belief_scope(\"SupersetClient.get_dataset\", f\"id={dataset_id}\"):\n log.reason(\"Fetching dataset\", payload={\"dataset_id\": dataset_id})\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n response = cast(Dict, response)\n log.reflect(\"Dataset fetched\", payload={\"dataset_id\": dataset_id})\n return response\n\n # [/DEF:SupersetClientGetDataset:Function]\n\n # [DEF:SupersetClientUpdateDataset:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Обновляет данные датасета по его ID.\n # @SIDE_EFFECT: Modifies resource in upstream Superset environment.\n # @RELATION: CALLS -> [APIClient]\n def update_dataset(self, dataset_id: int, data: Dict) -> Dict:\n with belief_scope(\"SupersetClient.update_dataset\", f\"id={dataset_id}\"):\n log.reason(\"Updating dataset\", payload={\"dataset_id\": dataset_id})\n response = self.network.request(\n method=\"PUT\",\n endpoint=f\"/dataset/{dataset_id}\",\n data=json.dumps(data),\n headers={\"Content-Type\": \"application/json\"},\n )\n response = cast(Dict, response)\n log.reflect(\"Dataset updated\", payload={\"dataset_id\": dataset_id})\n return response\n\n # [/DEF:SupersetClientUpdateDataset:Function]\n\n\n# [/DEF:SupersetDatasetsMixin:Class]\n# [/DEF:SupersetDatasetsMixin:Module]\n" - }, - { - "contract_id": "SupersetClientGetDatasets", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets.py", - "start_line": 22, - "end_line": 42, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Получает полный список датасетов, автоматически обрабатывая пагинацию." - }, - "relations": [ - { - "source_id": "SupersetClientGetDatasets", - "relation_type": "CALLS", - "target_id": "SupersetClientFetchAllPages", - "target_ref": "[SupersetClientFetchAllPages]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDatasets:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает полный список датасетов, автоматически обрабатывая пагинацию.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_datasets(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:\n with belief_scope(\"get_datasets\"):\n log.reason(\"Fetching datasets\")\n validated_query = self._validate_query_params(query)\n\n paginated_data = self._fetch_all_pages(\n endpoint=\"/dataset/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n log.reflect(\"Datasets fetched\", payload={\"total_count\": total_count})\n return total_count, paginated_data\n\n # [/DEF:SupersetClientGetDatasets:Function]\n" - }, - { - "contract_id": "SupersetClientGetDatasetsSummary", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets.py", - "start_line": 44, - "end_line": 67, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches dataset metadata optimized for the Dataset Hub grid." - }, - "relations": [ - { - "source_id": "SupersetClientGetDatasetsSummary", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDatasets", - "target_ref": "[SupersetClientGetDatasets]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDatasetsSummary:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches dataset metadata optimized for the Dataset Hub grid.\n # @RELATION: CALLS -> [SupersetClientGetDatasets]\n def get_datasets_summary(self) -> List[Dict]:\n with belief_scope(\"SupersetClient.get_datasets_summary\"):\n query = {\"columns\": [\"id\", \"table_name\", \"schema\", \"database\"]}\n _, datasets = self.get_datasets(query=query)\n\n result = []\n for ds in datasets:\n result.append(\n {\n \"id\": ds.get(\"id\"),\n \"table_name\": ds.get(\"table_name\"),\n \"schema\": ds.get(\"schema\"),\n \"database\": ds.get(\"database\", {}).get(\n \"database_name\", \"Unknown\"\n ),\n }\n )\n return result\n\n # [/DEF:SupersetClientGetDatasetsSummary:Function]\n" - }, - { - "contract_id": "SupersetClientGetDatasetDetail", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets.py", - "start_line": 69, - "end_line": 183, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Fetches detailed dataset information including columns and linked dashboards." - }, - "relations": [ - { - "source_id": "SupersetClientGetDatasetDetail", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDataset", - "target_ref": "[SupersetClientGetDataset]" - }, - { - "source_id": "SupersetClientGetDatasetDetail", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDatasetDetail:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Fetches detailed dataset information including columns and linked dashboards.\n # @RELATION: CALLS -> [SupersetClientGetDataset]\n # @RELATION: CALLS -> [APIClient]\n def get_dataset_detail(self, dataset_id: int) -> Dict:\n with belief_scope(\"SupersetClient.get_dataset_detail\", f\"id={dataset_id}\"):\n\n def as_bool(value, default=False):\n if value is None:\n return default\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in (\"1\", \"true\", \"yes\", \"y\", \"on\")\n return bool(value)\n\n response = self.get_dataset(dataset_id)\n\n if isinstance(response, dict) and \"result\" in response:\n dataset = response[\"result\"]\n else:\n dataset = response\n\n columns = dataset.get(\"columns\", [])\n column_info = []\n for col in columns:\n col_id = col.get(\"id\")\n if col_id is None:\n continue\n column_info.append(\n {\n \"id\": int(col_id),\n \"name\": col.get(\"column_name\"),\n \"type\": col.get(\"type\"),\n \"is_dttm\": as_bool(col.get(\"is_dttm\"), default=False),\n \"is_active\": as_bool(col.get(\"is_active\"), default=True),\n \"description\": col.get(\"description\", \"\"),\n }\n )\n\n linked_dashboards = []\n try:\n related_objects = self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}/related_objects\"\n )\n\n if isinstance(related_objects, dict):\n if \"dashboards\" in related_objects:\n dashboards_data = related_objects[\"dashboards\"]\n elif \"result\" in related_objects and isinstance(\n related_objects[\"result\"], dict\n ):\n dashboards_data = related_objects[\"result\"].get(\"dashboards\", [])\n else:\n dashboards_data = []\n\n for dash in dashboards_data:\n if isinstance(dash, dict):\n dash_id = dash.get(\"id\")\n if dash_id is None:\n continue\n linked_dashboards.append(\n {\n \"id\": int(dash_id),\n \"title\": dash.get(\"dashboard_title\")\n or dash.get(\"title\", f\"Dashboard {dash_id}\"),\n \"slug\": dash.get(\"slug\"),\n }\n )\n else:\n try:\n dash_id = int(dash)\n except (TypeError, ValueError):\n continue\n linked_dashboards.append(\n {\"id\": dash_id, \"title\": f\"Dashboard {dash_id}\", \"slug\": None}\n )\n except Exception as e:\n log.explore(\"Failed to fetch related dashboards\", error=str(e), payload={\"dataset_id\": dataset_id})\n linked_dashboards = []\n\n database_obj = dataset.get(\"database\")\n database_dict = database_obj if isinstance(database_obj, dict) else {}\n sql = dataset.get(\"sql\", \"\")\n\n result = {\n \"id\": dataset.get(\"id\"),\n \"table_name\": dataset.get(\"table_name\"),\n \"schema\": dataset.get(\"schema\"),\n \"database\": database_dict,\n \"database_id\": dataset.get(\"database_id\", database_dict.get(\"id\")),\n \"database_backend\": database_dict.get(\"backend\") or database_dict.get(\"engine\") or \"\",\n \"description\": dataset.get(\"description\", \"\"),\n \"columns\": column_info,\n \"column_count\": len(column_info),\n \"sql\": sql,\n \"linked_dashboards\": linked_dashboards,\n \"linked_dashboard_count\": len(linked_dashboards),\n \"is_sqllab_view\": as_bool(dataset.get(\"is_sqllab_view\"), default=False),\n \"created_on\": dataset.get(\"created_on\"),\n \"changed_on\": dataset.get(\"changed_on\"),\n }\n\n log.reflect(\n \"Dataset detail fetched\",\n payload={\n \"dataset_id\": dataset_id,\n \"column_count\": len(column_info),\n \"linked_dashboard_count\": len(linked_dashboards),\n },\n )\n return result\n\n # [/DEF:SupersetClientGetDatasetDetail:Function]\n" - }, - { - "contract_id": "SupersetClientGetDataset", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets.py", - "start_line": 185, - "end_line": 199, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Получает информацию о конкретном датасете по его ID." - }, - "relations": [ - { - "source_id": "SupersetClientGetDataset", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientGetDataset:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Получает информацию о конкретном датасете по его ID.\n # @RELATION: CALLS -> [APIClient]\n def get_dataset(self, dataset_id: int) -> Dict:\n with belief_scope(\"SupersetClient.get_dataset\", f\"id={dataset_id}\"):\n log.reason(\"Fetching dataset\", payload={\"dataset_id\": dataset_id})\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n response = cast(Dict, response)\n log.reflect(\"Dataset fetched\", payload={\"dataset_id\": dataset_id})\n return response\n\n # [/DEF:SupersetClientGetDataset:Function]\n" - }, - { - "contract_id": "SupersetClientUpdateDataset", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets.py", - "start_line": 201, - "end_line": 219, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Обновляет данные датасета по его ID.", - "SIDE_EFFECT": "Modifies resource in upstream Superset environment." - }, - "relations": [ - { - "source_id": "SupersetClientUpdateDataset", - "relation_type": "CALLS", - "target_id": "APIClient", - "target_ref": "[APIClient]" - } - ], - "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": "def", - "body": " # [DEF:SupersetClientUpdateDataset:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Обновляет данные датасета по его ID.\n # @SIDE_EFFECT: Modifies resource in upstream Superset environment.\n # @RELATION: CALLS -> [APIClient]\n def update_dataset(self, dataset_id: int, data: Dict) -> Dict:\n with belief_scope(\"SupersetClient.update_dataset\", f\"id={dataset_id}\"):\n log.reason(\"Updating dataset\", payload={\"dataset_id\": dataset_id})\n response = self.network.request(\n method=\"PUT\",\n endpoint=f\"/dataset/{dataset_id}\",\n data=json.dumps(data),\n headers={\"Content-Type\": \"application/json\"},\n )\n response = cast(Dict, response)\n log.reflect(\"Dataset updated\", payload={\"dataset_id\": dataset_id})\n return response\n\n # [/DEF:SupersetClientUpdateDataset:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetDatasetsMixin [C:3] [TYPE Module] [SEMANTICS dataset, superset, search, superset-datasets-mixin]\n# @LAYER: Infra\n# @BRIEF Dataset domain mixin for SupersetClient — list, get, detail, update.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nimport json\nfrom typing import Any, cast\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# #region SupersetDatasetsMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing basic dataset CRUD operations (list, get, detail, update).\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nclass SupersetDatasetsMixin:\n # #region SupersetClientGetDatasets [TYPE Function] [C:3]\n # @PURPOSE: Получает полный список датасетов, автоматически обрабатывая пагинацию.\n # @RELATION: CALLS -> [SupersetClientFetchAllPages]\n def get_datasets(self, query: dict | None = None) -> tuple[int, list[dict]]:\n with belief_scope(\"get_datasets\"):\n app_logger.info(\"[get_datasets][Enter] Fetching datasets.\")\n validated_query = self._validate_query_params(query)\n paginated_data = self._fetch_all_pages(\n endpoint=\"/dataset/\",\n pagination_options={\n \"base_query\": validated_query,\n \"results_field\": \"result\",\n },\n )\n total_count = len(paginated_data)\n app_logger.reflect(f\"Found {total_count} datasets.\", extra={\"src\": \"get_datasets\"})\n return total_count, paginated_data\n # #endregion SupersetClientGetDatasets\n # #region SupersetClientGetDatasetsSummary [TYPE Function] [C:3]\n # @PURPOSE: Fetches dataset metadata optimized for the Dataset Hub grid.\n # @RELATION: CALLS -> [SupersetClientGetDatasets]\n def get_datasets_summary(self) -> list[dict]:\n with belief_scope(\"SupersetClient.get_datasets_summary\"):\n query = {\"columns\": [\"id\", \"table_name\", \"schema\", \"database\"]}\n _, datasets = self.get_datasets(query=query)\n result = []\n for ds in datasets:\n result.append(\n {\n \"id\": ds.get(\"id\"),\n \"table_name\": ds.get(\"table_name\"),\n \"schema\": ds.get(\"schema\"),\n \"database\": ds.get(\"database\", {}).get(\n \"database_name\", \"Unknown\"\n ),\n }\n )\n return result\n # #endregion SupersetClientGetDatasetsSummary\n # #region SupersetClientGetDatasetDetail [TYPE Function] [C:3]\n # @PURPOSE: Fetches detailed dataset information including columns and linked dashboards.\n # @RELATION: CALLS -> [SupersetClientGetDataset]\n # @RELATION: CALLS -> [APIClient]\n def get_dataset_detail(self, dataset_id: int) -> dict:\n with belief_scope(\"SupersetClient.get_dataset_detail\", f\"id={dataset_id}\"):\n def as_bool(value, default=False):\n if value is None:\n return default\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in (\"1\", \"true\", \"yes\", \"y\", \"on\")\n return bool(value)\n response = self.get_dataset(dataset_id)\n if isinstance(response, dict) and \"result\" in response:\n dataset = response[\"result\"]\n else:\n dataset = response\n columns = dataset.get(\"columns\", [])\n column_info = []\n for col in columns:\n col_id = col.get(\"id\")\n if col_id is None:\n continue\n column_info.append(\n {\n \"id\": int(col_id),\n \"name\": col.get(\"column_name\"),\n \"type\": col.get(\"type\"),\n \"is_dttm\": as_bool(col.get(\"is_dttm\"), default=False),\n \"is_active\": as_bool(col.get(\"is_active\"), default=True),\n \"description\": col.get(\"description\", \"\"),\n }\n )\n linked_dashboards = []\n try:\n related_objects = self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}/related_objects\"\n )\n if isinstance(related_objects, dict):\n if \"dashboards\" in related_objects:\n dashboards_data = related_objects[\"dashboards\"]\n elif \"result\" in related_objects and isinstance(\n related_objects[\"result\"], dict\n ):\n dashboards_data = related_objects[\"result\"].get(\"dashboards\", [])\n else:\n dashboards_data = []\n for dash in dashboards_data:\n if isinstance(dash, dict):\n dash_id = dash.get(\"id\")\n if dash_id is None:\n continue\n linked_dashboards.append(\n {\n \"id\": int(dash_id),\n \"title\": dash.get(\"dashboard_title\")\n or dash.get(\"title\", f\"Dashboard {dash_id}\"),\n \"slug\": dash.get(\"slug\"),\n }\n )\n else:\n try:\n dash_id = int(dash)\n except (TypeError, ValueError):\n continue\n linked_dashboards.append(\n {\"id\": dash_id, \"title\": f\"Dashboard {dash_id}\", \"slug\": None}\n )\n except Exception as e:\n app_logger.warning(\n f\"[get_dataset_detail][Warning] Failed to fetch related dashboards: {e}\"\n )\n linked_dashboards = []\n database_obj = dataset.get(\"database\")\n database_dict = database_obj if isinstance(database_obj, dict) else {}\n sql = dataset.get(\"sql\", \"\")\n result = {\n \"id\": dataset.get(\"id\"),\n \"table_name\": dataset.get(\"table_name\"),\n \"schema\": dataset.get(\"schema\"),\n \"database\": database_dict,\n \"database_id\": dataset.get(\"database_id\", database_dict.get(\"id\")),\n \"database_backend\": database_dict.get(\"backend\") or database_dict.get(\"engine\") or \"\",\n \"description\": dataset.get(\"description\", \"\"),\n \"columns\": column_info,\n \"column_count\": len(column_info),\n \"sql\": sql,\n \"linked_dashboards\": linked_dashboards,\n \"linked_dashboard_count\": len(linked_dashboards),\n \"is_sqllab_view\": as_bool(dataset.get(\"is_sqllab_view\"), default=False),\n \"created_on\": dataset.get(\"created_on\"),\n \"changed_on\": dataset.get(\"changed_on\"),\n }\n app_logger.reflect(\n f\"Got dataset {dataset_id} with {len(column_info)} columns and {len(linked_dashboards)} linked dashboards\",\n extra={\"src\": \"get_dataset_detail\"},\n )\n return result\n # #endregion SupersetClientGetDatasetDetail\n # #region SupersetClientGetDataset [TYPE Function] [C:3]\n # @PURPOSE: Получает информацию о конкретном датасете по его ID.\n # @RELATION: CALLS -> [APIClient]\n def get_dataset(self, dataset_id: int) -> dict:\n with belief_scope(\"SupersetClient.get_dataset\", f\"id={dataset_id}\"):\n app_logger.info(\"[get_dataset][Enter] Fetching dataset %s.\", dataset_id)\n response = self.network.request(\n method=\"GET\", endpoint=f\"/dataset/{dataset_id}\"\n )\n response = cast(dict, response)\n app_logger.reflect(f\"Got dataset {dataset_id}.\", extra={\"src\": \"get_dataset\"})\n return response\n # #endregion SupersetClientGetDataset\n # #region SupersetClientUpdateDataset [TYPE Function] [C:3]\n # @PURPOSE: Обновляет данные датасета по его ID.\n # @SIDE_EFFECT: Modifies resource in upstream Superset environment.\n # @RELATION: CALLS -> [APIClient]\n def update_dataset(self, dataset_id: int, data: dict) -> dict:\n with belief_scope(\"SupersetClient.update_dataset\", f\"id={dataset_id}\"):\n app_logger.info(\"[update_dataset][Enter] Updating dataset %s.\", dataset_id)\n response = self.network.request(\n method=\"PUT\",\n endpoint=f\"/dataset/{dataset_id}\",\n data=json.dumps(data),\n headers={\"Content-Type\": \"application/json\"},\n )\n response = cast(dict, response)\n app_logger.reflect(f\"Updated dataset {dataset_id}.\", extra={\"src\": \"update_dataset\"})\n return response\n # #endregion SupersetClientUpdateDataset\n# #endregion SupersetDatasetsMixin\n# #endregion SupersetDatasetsMixin\n" }, { "contract_id": "SupersetDatasetsPreviewMixin", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_datasets_preview.py", "start_line": 1, - "end_line": 231, + "end_line": 214, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "LAYER": "Infra", - "PURPOSE": "Dataset preview compilation mixin for SupersetClient — build query context, compile SQL.", - "SEMANTICS": [ - "superset", - "datasets", - "preview", - "sql", - "compilation", - "filters" - ] + "PURPOSE": "Dataset preview compilation mixin for SupersetClient — build query context, compile SQL." }, "relations": [ { @@ -41922,192 +37638,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetDatasetsPreviewMixin:Module]\n# @COMPLEXITY: 4\n# @LAYER: Infra\n# @SEMANTICS: superset, datasets, preview, sql, compilation, filters\n# @PURPOSE: Dataset preview compilation mixin for SupersetClient — build query context, compile SQL.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n# @RELATION: DEPENDS_ON -> [SupersetDatasetsMixin]\n\nimport json\nfrom copy import deepcopy\nfrom typing import Any, Dict, List, Optional\n\nfrom ..cot_logger import MarkerLogger\nfrom ..logger import belief_scope\nfrom ..utils.network import SupersetAPIError\n\nlog = MarkerLogger(\"SupersetDatasetsPreview\")\n\n# [DEF:SupersetDatasetsPreviewMixin:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Mixin providing dataset preview compilation and query context building.\nclass SupersetDatasetsPreviewMixin:\n # [DEF:SupersetClientCompileDatasetPreview:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Compile dataset preview SQL through the strongest supported Superset preview endpoint family and return normalized SQL output.\n def compile_dataset_preview(self, dataset_id: int, template_params: Optional[Dict[str, Any]] = None, effective_filters: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:\n with belief_scope('SupersetClientCompileDatasetPreview'):\n log.reason('Compiling dataset preview SQL', payload={\"dataset_id\": dataset_id})\n dataset_response = self.get_dataset(dataset_id)\n dataset_record = dataset_response.get('result', dataset_response) if isinstance(dataset_response, dict) else {}\n query_context = self.build_dataset_preview_query_context(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params or {}, effective_filters=effective_filters or [])\n legacy_form_data = self.build_dataset_preview_legacy_form_data(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params or {}, effective_filters=effective_filters or [])\n legacy_form_data_payload = json.dumps(legacy_form_data, sort_keys=True, default=str)\n request_payload = json.dumps(query_context)\n strategy_attempts: List[Dict[str, Any]] = []\n strategy_candidates: List[Dict[str, Any]] = [{'endpoint_kind': 'legacy_explore_form_data', 'endpoint': '/explore_json/form_data', 'request_transport': 'query_param_form_data', 'params': {'form_data': legacy_form_data_payload}}, {'endpoint_kind': 'legacy_data_form_data', 'endpoint': '/data', 'request_transport': 'query_param_form_data', 'params': {'form_data': legacy_form_data_payload}}, {'endpoint_kind': 'v1_chart_data', 'endpoint': '/chart/data', 'request_transport': 'json_body', 'data': request_payload, 'headers': {'Content-Type': 'application/json'}}]\n for candidate in strategy_candidates:\n endpoint_kind = candidate['endpoint_kind']\n endpoint_path = candidate['endpoint']\n request_transport = candidate['request_transport']\n request_params = deepcopy(candidate.get('params') or {})\n request_body = candidate.get('data')\n request_headers = deepcopy(candidate.get('headers') or {})\n request_param_keys = sorted(request_params.keys())\n request_payload_keys: List[str] = []\n if isinstance(request_body, str):\n try:\n decoded_request_body = json.loads(request_body)\n if isinstance(decoded_request_body, dict):\n request_payload_keys = sorted(decoded_request_body.keys())\n except json.JSONDecodeError:\n request_payload_keys = []\n elif isinstance(request_body, dict):\n request_payload_keys = sorted(request_body.keys())\n strategy_diagnostics = {'endpoint': endpoint_path, 'endpoint_kind': endpoint_kind, 'request_transport': request_transport, 'contains_root_datasource': endpoint_kind == 'v1_chart_data' and 'datasource' in query_context, 'contains_form_datasource': endpoint_kind.startswith('legacy_') and 'datasource' in legacy_form_data, 'contains_query_object_datasource': bool(query_context.get('queries')) and isinstance(query_context['queries'][0], dict) and ('datasource' in query_context['queries'][0]), 'request_param_keys': request_param_keys, 'request_payload_keys': request_payload_keys}\n log.reason('Attempting dataset preview compilation strategy', payload={'dataset_id': dataset_id, **strategy_diagnostics, 'request_params': request_params, 'request_payload': request_body, 'legacy_form_data': legacy_form_data if endpoint_kind.startswith('legacy_') else None, 'query_context': query_context if endpoint_kind == 'v1_chart_data' else None, 'template_param_count': len(template_params or {}), 'filter_count': len(effective_filters or [])})\n try:\n response = self.network.request(method='POST', endpoint=endpoint_path, params=request_params or None, data=request_body, headers=request_headers or None)\n normalized = self._extract_compiled_sql_from_preview_response(response)\n normalized['query_context'] = query_context\n normalized['legacy_form_data'] = legacy_form_data\n normalized['endpoint'] = endpoint_path\n normalized['endpoint_kind'] = endpoint_kind\n normalized['dataset_id'] = dataset_id\n normalized['strategy_attempts'] = strategy_attempts + [{**strategy_diagnostics, 'success': True}]\n log.reflect('Dataset preview compilation returned normalized SQL payload', payload={'dataset_id': dataset_id, **strategy_diagnostics, 'success': True, 'compiled_sql_length': len(str(normalized.get('compiled_sql') or '')), 'response_diagnostics': normalized.get('response_diagnostics')})\n return normalized\n except Exception as exc:\n failure_diagnostics = {**strategy_diagnostics, 'success': False, 'error': str(exc)}\n strategy_attempts.append(failure_diagnostics)\n log.explore('Dataset preview compilation strategy failed', payload={'dataset_id': dataset_id, **failure_diagnostics, 'request_params': request_params, 'request_payload': request_body}, error=str(exc))\n raise SupersetAPIError(f'Superset preview compilation failed for all known strategies (attempts={strategy_attempts!r})')\n # [/DEF:SupersetClientCompileDatasetPreview:Function]\n\n # [DEF:SupersetClientBuildDatasetPreviewLegacyFormData:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Build browser-style legacy form_data payload for Superset preview endpoints inferred from observed deployment traffic.\n def build_dataset_preview_legacy_form_data(self, dataset_id: int, dataset_record: Dict[str, Any], template_params: Dict[str, Any], effective_filters: List[Dict[str, Any]]) -> Dict[str, Any]:\n with belief_scope('SupersetClientBuildDatasetPreviewLegacyFormData'):\n log.reason('Belief protocol reasoning checkpoint for SupersetClientBuildDatasetPreviewLegacyFormData')\n query_context = self.build_dataset_preview_query_context(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params, effective_filters=effective_filters)\n query_object = deepcopy(query_context.get('queries', [{}])[0] if query_context.get('queries') else {})\n legacy_form_data = deepcopy(query_context.get('form_data', {}))\n legacy_form_data.pop('datasource', None)\n legacy_form_data['metrics'] = deepcopy(query_object.get('metrics', ['count']))\n legacy_form_data['columns'] = deepcopy(query_object.get('columns', []))\n legacy_form_data['orderby'] = deepcopy(query_object.get('orderby', []))\n legacy_form_data['annotation_layers'] = deepcopy(query_object.get('annotation_layers', []))\n legacy_form_data['row_limit'] = query_object.get('row_limit', 1000)\n legacy_form_data['series_limit'] = query_object.get('series_limit', 0)\n legacy_form_data['url_params'] = deepcopy(query_object.get('url_params', template_params))\n legacy_form_data['applied_time_extras'] = deepcopy(query_object.get('applied_time_extras', {}))\n legacy_form_data['result_format'] = query_context.get('result_format', 'json')\n legacy_form_data['result_type'] = query_context.get('result_type', 'query')\n legacy_form_data['force'] = bool(query_context.get('force', True))\n extras = query_object.get('extras')\n if isinstance(extras, dict):\n legacy_form_data['extras'] = deepcopy(extras)\n time_range = query_object.get('time_range')\n if time_range:\n legacy_form_data['time_range'] = time_range\n log.reflect('Built Superset legacy preview form_data payload from browser-observed request shape', payload={'dataset_id': dataset_id, 'legacy_endpoint_inference': 'POST /explore_json/form_data?form_data=... primary, POST /data?form_data=... fallback, based on observed browser traffic', 'contains_form_datasource': 'datasource' in legacy_form_data, 'legacy_form_data_keys': sorted(legacy_form_data.keys()), 'legacy_extra_filters': legacy_form_data.get('extra_filters', []), 'legacy_extra_form_data': legacy_form_data.get('extra_form_data', {})})\n log.reflect('Belief protocol postcondition checkpoint for SupersetClientBuildDatasetPreviewLegacyFormData')\n return legacy_form_data\n # [/DEF:SupersetClientBuildDatasetPreviewLegacyFormData:Function]\n\n # [DEF:SupersetClientBuildDatasetPreviewQueryContext:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Build a reduced-scope chart-data query context for deterministic dataset preview compilation.\n def build_dataset_preview_query_context(\n self,\n dataset_id: int,\n dataset_record: Dict[str, Any],\n template_params: Dict[str, Any],\n effective_filters: List[Dict[str, Any]],\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetClientBuildDatasetPreviewQueryContext\"):\n log.reason(\n \"Building Superset dataset preview query context\",\n payload={\"dataset_id\": dataset_id, \"filter_count\": len(effective_filters or [])},\n )\n normalized_template_params = deepcopy(template_params or {})\n normalized_filter_payload = (\n self._normalize_effective_filters_for_query_context(\n effective_filters or []\n )\n )\n normalized_filters = normalized_filter_payload[\"filters\"]\n normalized_extra_form_data = normalized_filter_payload[\"extra_form_data\"]\n\n datasource_payload: Dict[str, Any] = {\n \"id\": dataset_id,\n \"type\": \"table\",\n }\n datasource = dataset_record.get(\"datasource\")\n if isinstance(datasource, dict):\n datasource_id = datasource.get(\"id\")\n datasource_type = datasource.get(\"type\")\n if datasource_id is not None:\n datasource_payload[\"id\"] = datasource_id\n if datasource_type:\n datasource_payload[\"type\"] = datasource_type\n\n serialized_dataset_template_params = dataset_record.get(\"template_params\")\n if (\n isinstance(serialized_dataset_template_params, str)\n and serialized_dataset_template_params.strip()\n ):\n try:\n parsed_dataset_template_params = json.loads(\n serialized_dataset_template_params\n )\n if isinstance(parsed_dataset_template_params, dict):\n for key, value in parsed_dataset_template_params.items():\n normalized_template_params.setdefault(str(key), value)\n except json.JSONDecodeError:\n log.explore(\"Dataset template_params could not be parsed while building preview query context\", error=\"Failed to parse template_params JSON\", payload={\"dataset_id\": dataset_id})\n\n extra_form_data: Dict[str, Any] = deepcopy(normalized_extra_form_data)\n if normalized_filters:\n extra_form_data[\"filters\"] = deepcopy(normalized_filters)\n\n query_object: Dict[str, Any] = {\n \"filters\": normalized_filters,\n \"extras\": {\"where\": \"\"},\n \"columns\": [],\n \"metrics\": [\"count\"],\n \"orderby\": [],\n \"annotation_layers\": [],\n \"row_limit\": 1000,\n \"series_limit\": 0,\n \"url_params\": normalized_template_params,\n \"applied_time_extras\": {},\n \"result_type\": \"query\",\n }\n\n schema = dataset_record.get(\"schema\")\n if schema:\n query_object[\"schema\"] = schema\n\n time_range = extra_form_data.get(\"time_range\") or dataset_record.get(\n \"default_time_range\"\n )\n if time_range:\n query_object[\"time_range\"] = time_range\n extra_form_data[\"time_range\"] = time_range\n\n result_format = dataset_record.get(\"result_format\") or \"json\"\n result_type = \"query\"\n\n form_data: Dict[str, Any] = {\n \"datasource\": f\"{datasource_payload['id']}__{datasource_payload['type']}\",\n \"datasource_id\": datasource_payload[\"id\"],\n \"datasource_type\": datasource_payload[\"type\"],\n \"viz_type\": \"table\",\n \"slice_id\": None,\n \"query_mode\": \"raw\",\n \"url_params\": normalized_template_params,\n \"extra_filters\": deepcopy(normalized_filters),\n \"adhoc_filters\": [],\n }\n if extra_form_data:\n form_data[\"extra_form_data\"] = extra_form_data\n\n payload = {\n \"datasource\": datasource_payload,\n \"queries\": [query_object],\n \"form_data\": form_data,\n \"result_format\": result_format,\n \"result_type\": result_type,\n \"force\": True,\n }\n log.reflect(\n \"Built Superset dataset preview query context\",\n payload={\n \"dataset_id\": dataset_id,\n \"datasource\": datasource_payload,\n \"normalized_effective_filters\": normalized_filters,\n \"normalized_filter_diagnostics\": normalized_filter_payload[\n \"diagnostics\"\n ],\n \"result_type\": result_type,\n \"result_format\": result_format,\n },\n )\n return payload\n\n # [/DEF:SupersetClientBuildDatasetPreviewQueryContext:Function]\n\n# [/DEF:SupersetDatasetsPreviewMixin:Class]\n# [/DEF:SupersetDatasetsPreviewMixin:Module]\n" - }, - { - "contract_id": "SupersetClientCompileDatasetPreview", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets_preview.py", - "start_line": 23, - "end_line": 73, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "PURPOSE": "Compile dataset preview SQL through the strongest supported Superset preview endpoint family and return normalized SQL output." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "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": "def", - "body": " # [DEF:SupersetClientCompileDatasetPreview:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Compile dataset preview SQL through the strongest supported Superset preview endpoint family and return normalized SQL output.\n def compile_dataset_preview(self, dataset_id: int, template_params: Optional[Dict[str, Any]] = None, effective_filters: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:\n with belief_scope('SupersetClientCompileDatasetPreview'):\n log.reason('Compiling dataset preview SQL', payload={\"dataset_id\": dataset_id})\n dataset_response = self.get_dataset(dataset_id)\n dataset_record = dataset_response.get('result', dataset_response) if isinstance(dataset_response, dict) else {}\n query_context = self.build_dataset_preview_query_context(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params or {}, effective_filters=effective_filters or [])\n legacy_form_data = self.build_dataset_preview_legacy_form_data(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params or {}, effective_filters=effective_filters or [])\n legacy_form_data_payload = json.dumps(legacy_form_data, sort_keys=True, default=str)\n request_payload = json.dumps(query_context)\n strategy_attempts: List[Dict[str, Any]] = []\n strategy_candidates: List[Dict[str, Any]] = [{'endpoint_kind': 'legacy_explore_form_data', 'endpoint': '/explore_json/form_data', 'request_transport': 'query_param_form_data', 'params': {'form_data': legacy_form_data_payload}}, {'endpoint_kind': 'legacy_data_form_data', 'endpoint': '/data', 'request_transport': 'query_param_form_data', 'params': {'form_data': legacy_form_data_payload}}, {'endpoint_kind': 'v1_chart_data', 'endpoint': '/chart/data', 'request_transport': 'json_body', 'data': request_payload, 'headers': {'Content-Type': 'application/json'}}]\n for candidate in strategy_candidates:\n endpoint_kind = candidate['endpoint_kind']\n endpoint_path = candidate['endpoint']\n request_transport = candidate['request_transport']\n request_params = deepcopy(candidate.get('params') or {})\n request_body = candidate.get('data')\n request_headers = deepcopy(candidate.get('headers') or {})\n request_param_keys = sorted(request_params.keys())\n request_payload_keys: List[str] = []\n if isinstance(request_body, str):\n try:\n decoded_request_body = json.loads(request_body)\n if isinstance(decoded_request_body, dict):\n request_payload_keys = sorted(decoded_request_body.keys())\n except json.JSONDecodeError:\n request_payload_keys = []\n elif isinstance(request_body, dict):\n request_payload_keys = sorted(request_body.keys())\n strategy_diagnostics = {'endpoint': endpoint_path, 'endpoint_kind': endpoint_kind, 'request_transport': request_transport, 'contains_root_datasource': endpoint_kind == 'v1_chart_data' and 'datasource' in query_context, 'contains_form_datasource': endpoint_kind.startswith('legacy_') and 'datasource' in legacy_form_data, 'contains_query_object_datasource': bool(query_context.get('queries')) and isinstance(query_context['queries'][0], dict) and ('datasource' in query_context['queries'][0]), 'request_param_keys': request_param_keys, 'request_payload_keys': request_payload_keys}\n log.reason('Attempting dataset preview compilation strategy', payload={'dataset_id': dataset_id, **strategy_diagnostics, 'request_params': request_params, 'request_payload': request_body, 'legacy_form_data': legacy_form_data if endpoint_kind.startswith('legacy_') else None, 'query_context': query_context if endpoint_kind == 'v1_chart_data' else None, 'template_param_count': len(template_params or {}), 'filter_count': len(effective_filters or [])})\n try:\n response = self.network.request(method='POST', endpoint=endpoint_path, params=request_params or None, data=request_body, headers=request_headers or None)\n normalized = self._extract_compiled_sql_from_preview_response(response)\n normalized['query_context'] = query_context\n normalized['legacy_form_data'] = legacy_form_data\n normalized['endpoint'] = endpoint_path\n normalized['endpoint_kind'] = endpoint_kind\n normalized['dataset_id'] = dataset_id\n normalized['strategy_attempts'] = strategy_attempts + [{**strategy_diagnostics, 'success': True}]\n log.reflect('Dataset preview compilation returned normalized SQL payload', payload={'dataset_id': dataset_id, **strategy_diagnostics, 'success': True, 'compiled_sql_length': len(str(normalized.get('compiled_sql') or '')), 'response_diagnostics': normalized.get('response_diagnostics')})\n return normalized\n except Exception as exc:\n failure_diagnostics = {**strategy_diagnostics, 'success': False, 'error': str(exc)}\n strategy_attempts.append(failure_diagnostics)\n log.explore('Dataset preview compilation strategy failed', payload={'dataset_id': dataset_id, **failure_diagnostics, 'request_params': request_params, 'request_payload': request_body}, error=str(exc))\n raise SupersetAPIError(f'Superset preview compilation failed for all known strategies (attempts={strategy_attempts!r})')\n # [/DEF:SupersetClientCompileDatasetPreview:Function]\n" - }, - { - "contract_id": "SupersetClientBuildDatasetPreviewLegacyFormData", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets_preview.py", - "start_line": 75, - "end_line": 105, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "PURPOSE": "Build browser-style legacy form_data payload for Superset preview endpoints inferred from observed deployment traffic." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "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": "def", - "body": " # [DEF:SupersetClientBuildDatasetPreviewLegacyFormData:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Build browser-style legacy form_data payload for Superset preview endpoints inferred from observed deployment traffic.\n def build_dataset_preview_legacy_form_data(self, dataset_id: int, dataset_record: Dict[str, Any], template_params: Dict[str, Any], effective_filters: List[Dict[str, Any]]) -> Dict[str, Any]:\n with belief_scope('SupersetClientBuildDatasetPreviewLegacyFormData'):\n log.reason('Belief protocol reasoning checkpoint for SupersetClientBuildDatasetPreviewLegacyFormData')\n query_context = self.build_dataset_preview_query_context(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params, effective_filters=effective_filters)\n query_object = deepcopy(query_context.get('queries', [{}])[0] if query_context.get('queries') else {})\n legacy_form_data = deepcopy(query_context.get('form_data', {}))\n legacy_form_data.pop('datasource', None)\n legacy_form_data['metrics'] = deepcopy(query_object.get('metrics', ['count']))\n legacy_form_data['columns'] = deepcopy(query_object.get('columns', []))\n legacy_form_data['orderby'] = deepcopy(query_object.get('orderby', []))\n legacy_form_data['annotation_layers'] = deepcopy(query_object.get('annotation_layers', []))\n legacy_form_data['row_limit'] = query_object.get('row_limit', 1000)\n legacy_form_data['series_limit'] = query_object.get('series_limit', 0)\n legacy_form_data['url_params'] = deepcopy(query_object.get('url_params', template_params))\n legacy_form_data['applied_time_extras'] = deepcopy(query_object.get('applied_time_extras', {}))\n legacy_form_data['result_format'] = query_context.get('result_format', 'json')\n legacy_form_data['result_type'] = query_context.get('result_type', 'query')\n legacy_form_data['force'] = bool(query_context.get('force', True))\n extras = query_object.get('extras')\n if isinstance(extras, dict):\n legacy_form_data['extras'] = deepcopy(extras)\n time_range = query_object.get('time_range')\n if time_range:\n legacy_form_data['time_range'] = time_range\n log.reflect('Built Superset legacy preview form_data payload from browser-observed request shape', payload={'dataset_id': dataset_id, 'legacy_endpoint_inference': 'POST /explore_json/form_data?form_data=... primary, POST /data?form_data=... fallback, based on observed browser traffic', 'contains_form_datasource': 'datasource' in legacy_form_data, 'legacy_form_data_keys': sorted(legacy_form_data.keys()), 'legacy_extra_filters': legacy_form_data.get('extra_filters', []), 'legacy_extra_form_data': legacy_form_data.get('extra_form_data', {})})\n log.reflect('Belief protocol postcondition checkpoint for SupersetClientBuildDatasetPreviewLegacyFormData')\n return legacy_form_data\n # [/DEF:SupersetClientBuildDatasetPreviewLegacyFormData:Function]\n" - }, - { - "contract_id": "SupersetClientBuildDatasetPreviewQueryContext", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets_preview.py", - "start_line": 107, - "end_line": 228, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "PURPOSE": "Build a reduced-scope chart-data query context for deterministic dataset preview compilation." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "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": "def", - "body": " # [DEF:SupersetClientBuildDatasetPreviewQueryContext:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Build a reduced-scope chart-data query context for deterministic dataset preview compilation.\n def build_dataset_preview_query_context(\n self,\n dataset_id: int,\n dataset_record: Dict[str, Any],\n template_params: Dict[str, Any],\n effective_filters: List[Dict[str, Any]],\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetClientBuildDatasetPreviewQueryContext\"):\n log.reason(\n \"Building Superset dataset preview query context\",\n payload={\"dataset_id\": dataset_id, \"filter_count\": len(effective_filters or [])},\n )\n normalized_template_params = deepcopy(template_params or {})\n normalized_filter_payload = (\n self._normalize_effective_filters_for_query_context(\n effective_filters or []\n )\n )\n normalized_filters = normalized_filter_payload[\"filters\"]\n normalized_extra_form_data = normalized_filter_payload[\"extra_form_data\"]\n\n datasource_payload: Dict[str, Any] = {\n \"id\": dataset_id,\n \"type\": \"table\",\n }\n datasource = dataset_record.get(\"datasource\")\n if isinstance(datasource, dict):\n datasource_id = datasource.get(\"id\")\n datasource_type = datasource.get(\"type\")\n if datasource_id is not None:\n datasource_payload[\"id\"] = datasource_id\n if datasource_type:\n datasource_payload[\"type\"] = datasource_type\n\n serialized_dataset_template_params = dataset_record.get(\"template_params\")\n if (\n isinstance(serialized_dataset_template_params, str)\n and serialized_dataset_template_params.strip()\n ):\n try:\n parsed_dataset_template_params = json.loads(\n serialized_dataset_template_params\n )\n if isinstance(parsed_dataset_template_params, dict):\n for key, value in parsed_dataset_template_params.items():\n normalized_template_params.setdefault(str(key), value)\n except json.JSONDecodeError:\n log.explore(\"Dataset template_params could not be parsed while building preview query context\", error=\"Failed to parse template_params JSON\", payload={\"dataset_id\": dataset_id})\n\n extra_form_data: Dict[str, Any] = deepcopy(normalized_extra_form_data)\n if normalized_filters:\n extra_form_data[\"filters\"] = deepcopy(normalized_filters)\n\n query_object: Dict[str, Any] = {\n \"filters\": normalized_filters,\n \"extras\": {\"where\": \"\"},\n \"columns\": [],\n \"metrics\": [\"count\"],\n \"orderby\": [],\n \"annotation_layers\": [],\n \"row_limit\": 1000,\n \"series_limit\": 0,\n \"url_params\": normalized_template_params,\n \"applied_time_extras\": {},\n \"result_type\": \"query\",\n }\n\n schema = dataset_record.get(\"schema\")\n if schema:\n query_object[\"schema\"] = schema\n\n time_range = extra_form_data.get(\"time_range\") or dataset_record.get(\n \"default_time_range\"\n )\n if time_range:\n query_object[\"time_range\"] = time_range\n extra_form_data[\"time_range\"] = time_range\n\n result_format = dataset_record.get(\"result_format\") or \"json\"\n result_type = \"query\"\n\n form_data: Dict[str, Any] = {\n \"datasource\": f\"{datasource_payload['id']}__{datasource_payload['type']}\",\n \"datasource_id\": datasource_payload[\"id\"],\n \"datasource_type\": datasource_payload[\"type\"],\n \"viz_type\": \"table\",\n \"slice_id\": None,\n \"query_mode\": \"raw\",\n \"url_params\": normalized_template_params,\n \"extra_filters\": deepcopy(normalized_filters),\n \"adhoc_filters\": [],\n }\n if extra_form_data:\n form_data[\"extra_form_data\"] = extra_form_data\n\n payload = {\n \"datasource\": datasource_payload,\n \"queries\": [query_object],\n \"form_data\": form_data,\n \"result_format\": result_format,\n \"result_type\": result_type,\n \"force\": True,\n }\n log.reflect(\n \"Built Superset dataset preview query context\",\n payload={\n \"dataset_id\": dataset_id,\n \"datasource\": datasource_payload,\n \"normalized_effective_filters\": normalized_filters,\n \"normalized_filter_diagnostics\": normalized_filter_payload[\n \"diagnostics\"\n ],\n \"result_type\": result_type,\n \"result_format\": result_format,\n },\n )\n return payload\n\n # [/DEF:SupersetClientBuildDatasetPreviewQueryContext:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetDatasetsPreviewMixin [C:4] [TYPE Module] [SEMANTICS superset, dataset, preview, sample, review]\n# @LAYER: Infra\n# @BRIEF Dataset preview compilation mixin for SupersetClient — build query context, compile SQL.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetDatasetsMixin]\nimport json\nfrom copy import deepcopy\nfrom typing import Any\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\nfrom ..utils.network import SupersetAPIError\n\n\n# #region SupersetDatasetsPreviewMixin [C:4] [TYPE Class]\n# @BRIEF Mixin providing dataset preview compilation and query context building.\nclass SupersetDatasetsPreviewMixin:\n # #region SupersetClientCompileDatasetPreview [TYPE Function] [C:4]\n # @PURPOSE: Compile dataset preview SQL through the strongest supported Superset preview endpoint family and return normalized SQL output.\n def compile_dataset_preview(self, dataset_id: int, template_params: dict[str, Any] | None = None, effective_filters: list[dict[str, Any]] | None = None) -> dict[str, Any]:\n with belief_scope('SupersetClientCompileDatasetPreview'):\n app_logger.reason('Belief protocol reasoning checkpoint for SupersetClientCompileDatasetPreview')\n dataset_response = self.get_dataset(dataset_id)\n dataset_record = dataset_response.get('result', dataset_response) if isinstance(dataset_response, dict) else {}\n query_context = self.build_dataset_preview_query_context(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params or {}, effective_filters=effective_filters or [])\n legacy_form_data = self.build_dataset_preview_legacy_form_data(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params or {}, effective_filters=effective_filters or [])\n legacy_form_data_payload = json.dumps(legacy_form_data, sort_keys=True, default=str)\n request_payload = json.dumps(query_context)\n strategy_attempts: list[dict[str, Any]] = []\n strategy_candidates: list[dict[str, Any]] = [{'endpoint_kind': 'legacy_explore_form_data', 'endpoint': '/explore_json/form_data', 'request_transport': 'query_param_form_data', 'params': {'form_data': legacy_form_data_payload}}, {'endpoint_kind': 'legacy_data_form_data', 'endpoint': '/data', 'request_transport': 'query_param_form_data', 'params': {'form_data': legacy_form_data_payload}}, {'endpoint_kind': 'v1_chart_data', 'endpoint': '/chart/data', 'request_transport': 'json_body', 'data': request_payload, 'headers': {'Content-Type': 'application/json'}}]\n for candidate in strategy_candidates:\n endpoint_kind = candidate['endpoint_kind']\n endpoint_path = candidate['endpoint']\n request_transport = candidate['request_transport']\n request_params = deepcopy(candidate.get('params') or {})\n request_body = candidate.get('data')\n request_headers = deepcopy(candidate.get('headers') or {})\n request_param_keys = sorted(request_params.keys())\n request_payload_keys: list[str] = []\n if isinstance(request_body, str):\n try:\n decoded_request_body = json.loads(request_body)\n if isinstance(decoded_request_body, dict):\n request_payload_keys = sorted(decoded_request_body.keys())\n except json.JSONDecodeError:\n request_payload_keys = []\n elif isinstance(request_body, dict):\n request_payload_keys = sorted(request_body.keys())\n strategy_diagnostics = {'endpoint': endpoint_path, 'endpoint_kind': endpoint_kind, 'request_transport': request_transport, 'contains_root_datasource': endpoint_kind == 'v1_chart_data' and 'datasource' in query_context, 'contains_form_datasource': endpoint_kind.startswith('legacy_') and 'datasource' in legacy_form_data, 'contains_query_object_datasource': bool(query_context.get('queries')) and isinstance(query_context['queries'][0], dict) and ('datasource' in query_context['queries'][0]), 'request_param_keys': request_param_keys, 'request_payload_keys': request_payload_keys}\n app_logger.reason('Attempting Superset dataset preview compilation strategy', extra={'dataset_id': dataset_id, **strategy_diagnostics, 'request_params': request_params, 'request_payload': request_body, 'legacy_form_data': legacy_form_data if endpoint_kind.startswith('legacy_') else None, 'query_context': query_context if endpoint_kind == 'v1_chart_data' else None, 'template_param_count': len(template_params or {}), 'filter_count': len(effective_filters or [])})\n try:\n response = self.network.request(method='POST', endpoint=endpoint_path, params=request_params or None, data=request_body, headers=request_headers or None)\n normalized = self._extract_compiled_sql_from_preview_response(response)\n normalized['query_context'] = query_context\n normalized['legacy_form_data'] = legacy_form_data\n normalized['endpoint'] = endpoint_path\n normalized['endpoint_kind'] = endpoint_kind\n normalized['dataset_id'] = dataset_id\n normalized['strategy_attempts'] = strategy_attempts + [{**strategy_diagnostics, 'success': True}]\n app_logger.reflect('Dataset preview compilation returned normalized SQL payload', extra={'dataset_id': dataset_id, **strategy_diagnostics, 'success': True, 'compiled_sql_length': len(str(normalized.get('compiled_sql') or '')), 'response_diagnostics': normalized.get('response_diagnostics')})\n app_logger.reflect('Belief protocol postcondition checkpoint for SupersetClientCompileDatasetPreview')\n return normalized\n except Exception as exc:\n failure_diagnostics = {**strategy_diagnostics, 'success': False, 'error': str(exc)}\n strategy_attempts.append(failure_diagnostics)\n app_logger.explore('Superset dataset preview compilation strategy failed', extra={'dataset_id': dataset_id, **failure_diagnostics, 'request_params': request_params, 'request_payload': request_body})\n raise SupersetAPIError(f'Superset preview compilation failed for all known strategies (attempts={strategy_attempts!r})')\n # #endregion SupersetClientCompileDatasetPreview\n # #region SupersetClientBuildDatasetPreviewLegacyFormData [TYPE Function] [C:4]\n # @PURPOSE: Build browser-style legacy form_data payload for Superset preview endpoints inferred from observed deployment traffic.\n def build_dataset_preview_legacy_form_data(self, dataset_id: int, dataset_record: dict[str, Any], template_params: dict[str, Any], effective_filters: list[dict[str, Any]]) -> dict[str, Any]:\n with belief_scope('SupersetClientBuildDatasetPreviewLegacyFormData'):\n app_logger.reason('Belief protocol reasoning checkpoint for SupersetClientBuildDatasetPreviewLegacyFormData')\n query_context = self.build_dataset_preview_query_context(dataset_id=dataset_id, dataset_record=dataset_record, template_params=template_params, effective_filters=effective_filters)\n query_object = deepcopy(query_context.get('queries', [{}])[0] if query_context.get('queries') else {})\n legacy_form_data = deepcopy(query_context.get('form_data', {}))\n legacy_form_data.pop('datasource', None)\n legacy_form_data['metrics'] = deepcopy(query_object.get('metrics', ['count']))\n legacy_form_data['columns'] = deepcopy(query_object.get('columns', []))\n legacy_form_data['orderby'] = deepcopy(query_object.get('orderby', []))\n legacy_form_data['annotation_layers'] = deepcopy(query_object.get('annotation_layers', []))\n legacy_form_data['row_limit'] = query_object.get('row_limit', 1000)\n legacy_form_data['series_limit'] = query_object.get('series_limit', 0)\n legacy_form_data['url_params'] = deepcopy(query_object.get('url_params', template_params))\n legacy_form_data['applied_time_extras'] = deepcopy(query_object.get('applied_time_extras', {}))\n legacy_form_data['result_format'] = query_context.get('result_format', 'json')\n legacy_form_data['result_type'] = query_context.get('result_type', 'query')\n legacy_form_data['force'] = bool(query_context.get('force', True))\n extras = query_object.get('extras')\n if isinstance(extras, dict):\n legacy_form_data['extras'] = deepcopy(extras)\n time_range = query_object.get('time_range')\n if time_range:\n legacy_form_data['time_range'] = time_range\n app_logger.reflect('Built Superset legacy preview form_data payload from browser-observed request shape', extra={'dataset_id': dataset_id, 'legacy_endpoint_inference': 'POST /explore_json/form_data?form_data=... primary, POST /data?form_data=... fallback, based on observed browser traffic', 'contains_form_datasource': 'datasource' in legacy_form_data, 'legacy_form_data_keys': sorted(legacy_form_data.keys()), 'legacy_extra_filters': legacy_form_data.get('extra_filters', []), 'legacy_extra_form_data': legacy_form_data.get('extra_form_data', {})})\n app_logger.reflect('Belief protocol postcondition checkpoint for SupersetClientBuildDatasetPreviewLegacyFormData')\n return legacy_form_data\n # #endregion SupersetClientBuildDatasetPreviewLegacyFormData\n # #region SupersetClientBuildDatasetPreviewQueryContext [TYPE Function] [C:4]\n # @PURPOSE: Build a reduced-scope chart-data query context for deterministic dataset preview compilation.\n def build_dataset_preview_query_context(\n self,\n dataset_id: int,\n dataset_record: dict[str, Any],\n template_params: dict[str, Any],\n effective_filters: list[dict[str, Any]],\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetClientBuildDatasetPreviewQueryContext\"):\n app_logger.reason(\n \"Building Superset dataset preview query context\",\n extra={\"dataset_id\": dataset_id, \"filter_count\": len(effective_filters or [])},\n )\n normalized_template_params = deepcopy(template_params or {})\n normalized_filter_payload = (\n self._normalize_effective_filters_for_query_context(\n effective_filters or []\n )\n )\n normalized_filters = normalized_filter_payload[\"filters\"]\n normalized_extra_form_data = normalized_filter_payload[\"extra_form_data\"]\n datasource_payload: dict[str, Any] = {\n \"id\": dataset_id,\n \"type\": \"table\",\n }\n datasource = dataset_record.get(\"datasource\")\n if isinstance(datasource, dict):\n datasource_id = datasource.get(\"id\")\n datasource_type = datasource.get(\"type\")\n if datasource_id is not None:\n datasource_payload[\"id\"] = datasource_id\n if datasource_type:\n datasource_payload[\"type\"] = datasource_type\n serialized_dataset_template_params = dataset_record.get(\"template_params\")\n if (\n isinstance(serialized_dataset_template_params, str)\n and serialized_dataset_template_params.strip()\n ):\n try:\n parsed_dataset_template_params = json.loads(\n serialized_dataset_template_params\n )\n if isinstance(parsed_dataset_template_params, dict):\n for key, value in parsed_dataset_template_params.items():\n normalized_template_params.setdefault(str(key), value)\n except json.JSONDecodeError:\n app_logger.explore(\n \"Dataset template_params could not be parsed while building preview query context\",\n extra={\"dataset_id\": dataset_id},\n )\n extra_form_data: dict[str, Any] = deepcopy(normalized_extra_form_data)\n if normalized_filters:\n extra_form_data[\"filters\"] = deepcopy(normalized_filters)\n query_object: dict[str, Any] = {\n \"filters\": normalized_filters,\n \"extras\": {\"where\": \"\"},\n \"columns\": [],\n \"metrics\": [\"count\"],\n \"orderby\": [],\n \"annotation_layers\": [],\n \"row_limit\": 1000,\n \"series_limit\": 0,\n \"url_params\": normalized_template_params,\n \"applied_time_extras\": {},\n \"result_type\": \"query\",\n }\n schema = dataset_record.get(\"schema\")\n if schema:\n query_object[\"schema\"] = schema\n time_range = extra_form_data.get(\"time_range\") or dataset_record.get(\n \"default_time_range\"\n )\n if time_range:\n query_object[\"time_range\"] = time_range\n extra_form_data[\"time_range\"] = time_range\n result_format = dataset_record.get(\"result_format\") or \"json\"\n result_type = \"query\"\n form_data: dict[str, Any] = {\n \"datasource\": f\"{datasource_payload['id']}__{datasource_payload['type']}\",\n \"datasource_id\": datasource_payload[\"id\"],\n \"datasource_type\": datasource_payload[\"type\"],\n \"viz_type\": \"table\",\n \"slice_id\": None,\n \"query_mode\": \"raw\",\n \"url_params\": normalized_template_params,\n \"extra_filters\": deepcopy(normalized_filters),\n \"adhoc_filters\": [],\n }\n if extra_form_data:\n form_data[\"extra_form_data\"] = extra_form_data\n payload = {\n \"datasource\": datasource_payload,\n \"queries\": [query_object],\n \"form_data\": form_data,\n \"result_format\": result_format,\n \"result_type\": result_type,\n \"force\": True,\n }\n app_logger.reflect(\n \"Built Superset dataset preview query context\",\n extra={\n \"dataset_id\": dataset_id,\n \"datasource\": datasource_payload,\n \"normalized_effective_filters\": normalized_filters,\n \"normalized_filter_diagnostics\": normalized_filter_payload[\n \"diagnostics\"\n ],\n \"result_type\": result_type,\n \"result_format\": result_format,\n },\n )\n return payload\n # #endregion SupersetClientBuildDatasetPreviewQueryContext\n# #endregion SupersetDatasetsPreviewMixin\n# #endregion SupersetDatasetsPreviewMixin\n" }, { "contract_id": "SupersetDatasetsPreviewFiltersMixin", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_datasets_preview_filters.py", "start_line": 1, - "end_line": 203, + "end_line": 180, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Filter normalization and SQL extraction helpers for dataset preview compilation.", - "SEMANTICS": [ - "superset", - "datasets", - "preview", - "filters", - "normalization", - "sql", - "extraction" - ] + "PURPOSE": "Filter normalization and SQL extraction helpers for dataset preview compilation." }, "relations": [ { @@ -42124,82 +37669,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetDatasetsPreviewFiltersMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, datasets, preview, filters, normalization, sql, extraction\n# @PURPOSE: Filter normalization and SQL extraction helpers for dataset preview compilation.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n# @RELATION: DEPENDS_ON -> [SupersetDatasetsPreviewMixin]\n\nfrom copy import deepcopy\nfrom typing import Any, Dict, List, Optional, Tuple\n\nfrom ..cot_logger import MarkerLogger\nfrom ..logger import belief_scope\nfrom ..utils.network import SupersetAPIError\n\nlog = MarkerLogger(\"SupersetDatasetsPreviewFilters\")\n\n# [DEF:SupersetDatasetsPreviewFiltersMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing filter normalization and compiled-SQL extraction for dataset preview operations.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n# @RELATION: DEPENDS_ON -> [SupersetDatasetsPreviewMixin]\nclass SupersetDatasetsPreviewFiltersMixin:\n # [DEF:SupersetClientNormalizeEffectiveFiltersForQueryContext:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Convert execution mappings into Superset chart-data filter objects.\n def _normalize_effective_filters_for_query_context(\n self,\n effective_filters: List[Dict[str, Any]],\n ) -> Dict[str, Any]:\n with belief_scope(\n \"SupersetClient._normalize_effective_filters_for_query_context\"\n ):\n normalized_filters: List[Dict[str, Any]] = []\n merged_extra_form_data: Dict[str, Any] = {}\n diagnostics: List[Dict[str, Any]] = []\n\n for item in effective_filters:\n if not isinstance(item, dict):\n continue\n\n display_name = str(\n item.get(\"display_name\")\n or item.get(\"filter_name\")\n or item.get(\"variable_name\")\n or \"unresolved_filter\"\n ).strip()\n value = item.get(\"effective_value\")\n normalized_payload = item.get(\"normalized_filter_payload\")\n preserved_clauses: List[Dict[str, Any]] = []\n preserved_extra_form_data: Dict[str, Any] = {}\n used_preserved_clauses = False\n\n if isinstance(normalized_payload, dict):\n raw_clauses = normalized_payload.get(\"filter_clauses\")\n if isinstance(raw_clauses, list):\n preserved_clauses = [\n deepcopy(clause)\n for clause in raw_clauses\n if isinstance(clause, dict)\n ]\n raw_extra_form_data = normalized_payload.get(\"extra_form_data\")\n if isinstance(raw_extra_form_data, dict):\n preserved_extra_form_data = deepcopy(raw_extra_form_data)\n\n if isinstance(preserved_extra_form_data, dict):\n for key, extra_value in preserved_extra_form_data.items():\n if key == \"filters\":\n continue\n merged_extra_form_data[key] = deepcopy(extra_value)\n\n outgoing_clauses: List[Dict[str, Any]] = []\n if preserved_clauses:\n for clause in preserved_clauses:\n clause_copy = deepcopy(clause)\n if \"val\" not in clause_copy and value is not None:\n clause_copy[\"val\"] = deepcopy(value)\n outgoing_clauses.append(clause_copy)\n used_preserved_clauses = True\n elif preserved_extra_form_data:\n outgoing_clauses = []\n else:\n column = str(\n item.get(\"variable_name\") or item.get(\"filter_name\") or \"\"\n ).strip()\n if column and value is not None:\n operator = \"IN\" if isinstance(value, list) else \"==\"\n outgoing_clauses.append(\n {\"col\": column, \"op\": operator, \"val\": value}\n )\n\n normalized_filters.extend(outgoing_clauses)\n diagnostics.append(\n {\n \"filter_name\": display_name,\n \"value_origin\": (\n normalized_payload.get(\"value_origin\")\n if isinstance(normalized_payload, dict)\n else None\n ),\n \"used_preserved_clauses\": used_preserved_clauses,\n \"outgoing_clauses\": deepcopy(outgoing_clauses),\n }\n )\n log.reason(\n \"Normalized effective preview filter for Superset query context\",\n payload={\n \"filter_name\": display_name,\n \"used_preserved_clauses\": used_preserved_clauses,\n \"outgoing_clauses\": outgoing_clauses,\n \"value_origin\": (\n normalized_payload.get(\"value_origin\")\n if isinstance(normalized_payload, dict)\n else \"heuristic_reconstruction\"\n ),\n },\n )\n\n return {\n \"filters\": normalized_filters,\n \"extra_form_data\": merged_extra_form_data,\n \"diagnostics\": diagnostics,\n }\n\n # [/DEF:SupersetClientNormalizeEffectiveFiltersForQueryContext:Function]\n\n # [DEF:SupersetClientExtractCompiledSqlFromPreviewResponse:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Normalize compiled SQL from either chart-data or legacy form_data preview responses.\n def _extract_compiled_sql_from_preview_response(\n self, response: Any\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetClient._extract_compiled_sql_from_preview_response\"):\n if not isinstance(response, dict):\n raise SupersetAPIError(\n \"Superset preview response was not a JSON object\"\n )\n\n response_diagnostics: List[Dict[str, Any]] = []\n result_payload = response.get(\"result\")\n if isinstance(result_payload, list):\n for index, item in enumerate(result_payload):\n if not isinstance(item, dict):\n continue\n compiled_sql = str(\n item.get(\"query\")\n or item.get(\"sql\")\n or item.get(\"compiled_sql\")\n or \"\"\n ).strip()\n response_diagnostics.append(\n {\n \"index\": index,\n \"status\": item.get(\"status\"),\n \"applied_filters\": item.get(\"applied_filters\"),\n \"rejected_filters\": item.get(\"rejected_filters\"),\n \"has_query\": bool(compiled_sql),\n \"source\": \"result_list\",\n }\n )\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n \"response_diagnostics\": response_diagnostics,\n }\n\n top_level_candidates: List[Tuple[str, Any]] = [\n (\"query\", response.get(\"query\")),\n (\"sql\", response.get(\"sql\")),\n (\"compiled_sql\", response.get(\"compiled_sql\")),\n ]\n if isinstance(result_payload, dict):\n top_level_candidates.extend(\n [\n (\"result.query\", result_payload.get(\"query\")),\n (\"result.sql\", result_payload.get(\"sql\")),\n (\"result.compiled_sql\", result_payload.get(\"compiled_sql\")),\n ]\n )\n\n for source, candidate in top_level_candidates:\n compiled_sql = str(candidate or \"\").strip()\n response_diagnostics.append(\n {\"source\": source, \"has_query\": bool(compiled_sql)}\n )\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n \"response_diagnostics\": response_diagnostics,\n }\n\n raise SupersetAPIError(\n \"Superset preview response did not expose compiled SQL \"\n f\"(diagnostics={response_diagnostics!r})\"\n )\n\n # [/DEF:SupersetClientExtractCompiledSqlFromPreviewResponse:Function]\n\n\n# [/DEF:SupersetDatasetsPreviewFiltersMixin:Class]\n# [/DEF:SupersetDatasetsPreviewFiltersMixin:Module]\n" - }, - { - "contract_id": "SupersetClientNormalizeEffectiveFiltersForQueryContext", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets_preview_filters.py", - "start_line": 24, - "end_line": 125, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Convert execution mappings into Superset chart-data filter objects." - }, - "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": "def", - "body": " # [DEF:SupersetClientNormalizeEffectiveFiltersForQueryContext:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Convert execution mappings into Superset chart-data filter objects.\n def _normalize_effective_filters_for_query_context(\n self,\n effective_filters: List[Dict[str, Any]],\n ) -> Dict[str, Any]:\n with belief_scope(\n \"SupersetClient._normalize_effective_filters_for_query_context\"\n ):\n normalized_filters: List[Dict[str, Any]] = []\n merged_extra_form_data: Dict[str, Any] = {}\n diagnostics: List[Dict[str, Any]] = []\n\n for item in effective_filters:\n if not isinstance(item, dict):\n continue\n\n display_name = str(\n item.get(\"display_name\")\n or item.get(\"filter_name\")\n or item.get(\"variable_name\")\n or \"unresolved_filter\"\n ).strip()\n value = item.get(\"effective_value\")\n normalized_payload = item.get(\"normalized_filter_payload\")\n preserved_clauses: List[Dict[str, Any]] = []\n preserved_extra_form_data: Dict[str, Any] = {}\n used_preserved_clauses = False\n\n if isinstance(normalized_payload, dict):\n raw_clauses = normalized_payload.get(\"filter_clauses\")\n if isinstance(raw_clauses, list):\n preserved_clauses = [\n deepcopy(clause)\n for clause in raw_clauses\n if isinstance(clause, dict)\n ]\n raw_extra_form_data = normalized_payload.get(\"extra_form_data\")\n if isinstance(raw_extra_form_data, dict):\n preserved_extra_form_data = deepcopy(raw_extra_form_data)\n\n if isinstance(preserved_extra_form_data, dict):\n for key, extra_value in preserved_extra_form_data.items():\n if key == \"filters\":\n continue\n merged_extra_form_data[key] = deepcopy(extra_value)\n\n outgoing_clauses: List[Dict[str, Any]] = []\n if preserved_clauses:\n for clause in preserved_clauses:\n clause_copy = deepcopy(clause)\n if \"val\" not in clause_copy and value is not None:\n clause_copy[\"val\"] = deepcopy(value)\n outgoing_clauses.append(clause_copy)\n used_preserved_clauses = True\n elif preserved_extra_form_data:\n outgoing_clauses = []\n else:\n column = str(\n item.get(\"variable_name\") or item.get(\"filter_name\") or \"\"\n ).strip()\n if column and value is not None:\n operator = \"IN\" if isinstance(value, list) else \"==\"\n outgoing_clauses.append(\n {\"col\": column, \"op\": operator, \"val\": value}\n )\n\n normalized_filters.extend(outgoing_clauses)\n diagnostics.append(\n {\n \"filter_name\": display_name,\n \"value_origin\": (\n normalized_payload.get(\"value_origin\")\n if isinstance(normalized_payload, dict)\n else None\n ),\n \"used_preserved_clauses\": used_preserved_clauses,\n \"outgoing_clauses\": deepcopy(outgoing_clauses),\n }\n )\n log.reason(\n \"Normalized effective preview filter for Superset query context\",\n payload={\n \"filter_name\": display_name,\n \"used_preserved_clauses\": used_preserved_clauses,\n \"outgoing_clauses\": outgoing_clauses,\n \"value_origin\": (\n normalized_payload.get(\"value_origin\")\n if isinstance(normalized_payload, dict)\n else \"heuristic_reconstruction\"\n ),\n },\n )\n\n return {\n \"filters\": normalized_filters,\n \"extra_form_data\": merged_extra_form_data,\n \"diagnostics\": diagnostics,\n }\n\n # [/DEF:SupersetClientNormalizeEffectiveFiltersForQueryContext:Function]\n" - }, - { - "contract_id": "SupersetClientExtractCompiledSqlFromPreviewResponse", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_datasets_preview_filters.py", - "start_line": 127, - "end_line": 199, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Normalize compiled SQL from either chart-data or legacy form_data preview responses." - }, - "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": "def", - "body": " # [DEF:SupersetClientExtractCompiledSqlFromPreviewResponse:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Normalize compiled SQL from either chart-data or legacy form_data preview responses.\n def _extract_compiled_sql_from_preview_response(\n self, response: Any\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetClient._extract_compiled_sql_from_preview_response\"):\n if not isinstance(response, dict):\n raise SupersetAPIError(\n \"Superset preview response was not a JSON object\"\n )\n\n response_diagnostics: List[Dict[str, Any]] = []\n result_payload = response.get(\"result\")\n if isinstance(result_payload, list):\n for index, item in enumerate(result_payload):\n if not isinstance(item, dict):\n continue\n compiled_sql = str(\n item.get(\"query\")\n or item.get(\"sql\")\n or item.get(\"compiled_sql\")\n or \"\"\n ).strip()\n response_diagnostics.append(\n {\n \"index\": index,\n \"status\": item.get(\"status\"),\n \"applied_filters\": item.get(\"applied_filters\"),\n \"rejected_filters\": item.get(\"rejected_filters\"),\n \"has_query\": bool(compiled_sql),\n \"source\": \"result_list\",\n }\n )\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n \"response_diagnostics\": response_diagnostics,\n }\n\n top_level_candidates: List[Tuple[str, Any]] = [\n (\"query\", response.get(\"query\")),\n (\"sql\", response.get(\"sql\")),\n (\"compiled_sql\", response.get(\"compiled_sql\")),\n ]\n if isinstance(result_payload, dict):\n top_level_candidates.extend(\n [\n (\"result.query\", result_payload.get(\"query\")),\n (\"result.sql\", result_payload.get(\"sql\")),\n (\"result.compiled_sql\", result_payload.get(\"compiled_sql\")),\n ]\n )\n\n for source, candidate in top_level_candidates:\n compiled_sql = str(candidate or \"\").strip()\n response_diagnostics.append(\n {\"source\": source, \"has_query\": bool(compiled_sql)}\n )\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n \"response_diagnostics\": response_diagnostics,\n }\n\n raise SupersetAPIError(\n \"Superset preview response did not expose compiled SQL \"\n f\"(diagnostics={response_diagnostics!r})\"\n )\n\n # [/DEF:SupersetClientExtractCompiledSqlFromPreviewResponse:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetDatasetsPreviewFiltersMixin [C:3] [TYPE Module] [SEMANTICS superset, dataset, preview, filter, advanced]\n# @LAYER: Infra\n# @BRIEF Filter normalization and SQL extraction helpers for dataset preview compilation.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetDatasetsPreviewMixin]\nfrom copy import deepcopy\nfrom typing import Any\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\nfrom ..utils.network import SupersetAPIError\n\n\n# #region SupersetDatasetsPreviewFiltersMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing filter normalization and compiled-SQL extraction for dataset preview operations.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\n# @RELATION DEPENDS_ON -> [SupersetDatasetsPreviewMixin]\nclass SupersetDatasetsPreviewFiltersMixin:\n # #region SupersetClientNormalizeEffectiveFiltersForQueryContext [TYPE Function] [C:3]\n # @PURPOSE: Convert execution mappings into Superset chart-data filter objects.\n def _normalize_effective_filters_for_query_context(\n self,\n effective_filters: list[dict[str, Any]],\n ) -> dict[str, Any]:\n with belief_scope(\n \"SupersetClient._normalize_effective_filters_for_query_context\"\n ):\n normalized_filters: list[dict[str, Any]] = []\n merged_extra_form_data: dict[str, Any] = {}\n diagnostics: list[dict[str, Any]] = []\n for item in effective_filters:\n if not isinstance(item, dict):\n continue\n display_name = str(\n item.get(\"display_name\")\n or item.get(\"filter_name\")\n or item.get(\"variable_name\")\n or \"unresolved_filter\"\n ).strip()\n value = item.get(\"effective_value\")\n normalized_payload = item.get(\"normalized_filter_payload\")\n preserved_clauses: list[dict[str, Any]] = []\n preserved_extra_form_data: dict[str, Any] = {}\n used_preserved_clauses = False\n if isinstance(normalized_payload, dict):\n raw_clauses = normalized_payload.get(\"filter_clauses\")\n if isinstance(raw_clauses, list):\n preserved_clauses = [\n deepcopy(clause)\n for clause in raw_clauses\n if isinstance(clause, dict)\n ]\n raw_extra_form_data = normalized_payload.get(\"extra_form_data\")\n if isinstance(raw_extra_form_data, dict):\n preserved_extra_form_data = deepcopy(raw_extra_form_data)\n if isinstance(preserved_extra_form_data, dict):\n for key, extra_value in preserved_extra_form_data.items():\n if key == \"filters\":\n continue\n merged_extra_form_data[key] = deepcopy(extra_value)\n outgoing_clauses: list[dict[str, Any]] = []\n if preserved_clauses:\n for clause in preserved_clauses:\n clause_copy = deepcopy(clause)\n if \"val\" not in clause_copy and value is not None:\n clause_copy[\"val\"] = deepcopy(value)\n outgoing_clauses.append(clause_copy)\n used_preserved_clauses = True\n elif preserved_extra_form_data:\n outgoing_clauses = []\n else:\n column = str(\n item.get(\"variable_name\") or item.get(\"filter_name\") or \"\"\n ).strip()\n if column and value is not None:\n operator = \"IN\" if isinstance(value, list) else \"==\"\n outgoing_clauses.append(\n {\"col\": column, \"op\": operator, \"val\": value}\n )\n normalized_filters.extend(outgoing_clauses)\n diagnostics.append(\n {\n \"filter_name\": display_name,\n \"value_origin\": (\n normalized_payload.get(\"value_origin\")\n if isinstance(normalized_payload, dict)\n else None\n ),\n \"used_preserved_clauses\": used_preserved_clauses,\n \"outgoing_clauses\": deepcopy(outgoing_clauses),\n }\n )\n app_logger.reason(\n \"Normalized effective preview filter for Superset query context\",\n extra={\n \"filter_name\": display_name,\n \"used_preserved_clauses\": used_preserved_clauses,\n \"outgoing_clauses\": outgoing_clauses,\n \"value_origin\": (\n normalized_payload.get(\"value_origin\")\n if isinstance(normalized_payload, dict)\n else \"heuristic_reconstruction\"\n ),\n },\n )\n return {\n \"filters\": normalized_filters,\n \"extra_form_data\": merged_extra_form_data,\n \"diagnostics\": diagnostics,\n }\n # #endregion SupersetClientNormalizeEffectiveFiltersForQueryContext\n # #region SupersetClientExtractCompiledSqlFromPreviewResponse [TYPE Function] [C:3]\n # @PURPOSE: Normalize compiled SQL from either chart-data or legacy form_data preview responses.\n def _extract_compiled_sql_from_preview_response(\n self, response: Any\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetClient._extract_compiled_sql_from_preview_response\"):\n if not isinstance(response, dict):\n raise SupersetAPIError(\n \"Superset preview response was not a JSON object\"\n )\n response_diagnostics: list[dict[str, Any]] = []\n result_payload = response.get(\"result\")\n if isinstance(result_payload, list):\n for index, item in enumerate(result_payload):\n if not isinstance(item, dict):\n continue\n compiled_sql = str(\n item.get(\"query\")\n or item.get(\"sql\")\n or item.get(\"compiled_sql\")\n or \"\"\n ).strip()\n response_diagnostics.append(\n {\n \"index\": index,\n \"status\": item.get(\"status\"),\n \"applied_filters\": item.get(\"applied_filters\"),\n \"rejected_filters\": item.get(\"rejected_filters\"),\n \"has_query\": bool(compiled_sql),\n \"source\": \"result_list\",\n }\n )\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n \"response_diagnostics\": response_diagnostics,\n }\n top_level_candidates: list[tuple[str, Any]] = [\n (\"query\", response.get(\"query\")),\n (\"sql\", response.get(\"sql\")),\n (\"compiled_sql\", response.get(\"compiled_sql\")),\n ]\n if isinstance(result_payload, dict):\n top_level_candidates.extend(\n [\n (\"result.query\", result_payload.get(\"query\")),\n (\"result.sql\", result_payload.get(\"sql\")),\n (\"result.compiled_sql\", result_payload.get(\"compiled_sql\")),\n ]\n )\n for source, candidate in top_level_candidates:\n compiled_sql = str(candidate or \"\").strip()\n response_diagnostics.append(\n {\"source\": source, \"has_query\": bool(compiled_sql)}\n )\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n \"response_diagnostics\": response_diagnostics,\n }\n raise SupersetAPIError(\n \"Superset preview response did not expose compiled SQL \"\n f\"(diagnostics={response_diagnostics!r})\"\n )\n # #endregion SupersetClientExtractCompiledSqlFromPreviewResponse\n# #endregion SupersetDatasetsPreviewFiltersMixin\n# #endregion SupersetDatasetsPreviewFiltersMixin\n" }, { "contract_id": "SupersetUserProjection", "contract_type": "Module", "file_path": "backend/src/core/superset_client/_user_projection.py", "start_line": 1, - "end_line": 88, + "end_line": 71, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "Infra", - "PURPOSE": "User/owner payload normalization helpers for Superset client responses.", - "SEMANTICS": [ - "superset", - "users", - "owners", - "projection", - "normalization" - ] + "PURPOSE": "User/owner payload normalization helpers for Superset client responses." }, "relations": [ { @@ -42220,19 +37704,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetUserProjection:Module]\n# @COMPLEXITY: 2\n# @LAYER: Infra\n# @SEMANTICS: superset, users, owners, projection, normalization\n# @PURPOSE: User/owner payload normalization helpers for Superset client responses.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\n\nfrom typing import Any, Dict, List, Optional, Union\n\n\n# [DEF:SupersetUserProjectionMixin:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Mixin providing user/owner payload normalization for Superset API responses.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\nclass SupersetUserProjectionMixin:\n # [DEF:SupersetClientExtractOwnerLabels:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize dashboard owners payload to stable display labels.\n def _extract_owner_labels(self, owners_payload: Any) -> List[str]:\n if owners_payload is None:\n return []\n\n owners_list: List[Any]\n if isinstance(owners_payload, list):\n owners_list = owners_payload\n else:\n owners_list = [owners_payload]\n\n normalized: List[str] = []\n for owner in owners_list:\n label: Optional[str] = None\n if isinstance(owner, dict):\n label = self._extract_user_display(None, owner)\n else:\n label = self._sanitize_user_text(owner)\n if label and label not in normalized:\n normalized.append(label)\n return normalized\n\n # [/DEF:SupersetClientExtractOwnerLabels:Function]\n\n # [DEF:SupersetClientExtractUserDisplay:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize user payload to a stable display name.\n def _extract_user_display(\n self, preferred_value: Optional[str], user_payload: Optional[Dict]\n ) -> Optional[str]:\n preferred = self._sanitize_user_text(preferred_value)\n if preferred:\n return preferred\n\n if isinstance(user_payload, dict):\n full_name = self._sanitize_user_text(user_payload.get(\"full_name\"))\n if full_name:\n return full_name\n first_name = self._sanitize_user_text(user_payload.get(\"first_name\")) or \"\"\n last_name = self._sanitize_user_text(user_payload.get(\"last_name\")) or \"\"\n combined = \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n if combined:\n return combined\n username = self._sanitize_user_text(user_payload.get(\"username\"))\n if username:\n return username\n email = self._sanitize_user_text(user_payload.get(\"email\"))\n if email:\n return email\n return None\n\n # [/DEF:SupersetClientExtractUserDisplay:Function]\n\n # [DEF:SupersetClientSanitizeUserText:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Convert scalar value to non-empty user-facing text.\n def _sanitize_user_text(self, value: Optional[Union[str, int]]) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:SupersetClientSanitizeUserText:Function]\n\n\n# [/DEF:SupersetUserProjectionMixin:Class]\n# [/DEF:SupersetUserProjection:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetUserProjection [C:2] [TYPE Module] [SEMANTICS superset, user, profile, lookup, transform]\n# @LAYER: Infra\n# @BRIEF User/owner payload normalization helpers for Superset client responses.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nfrom typing import Any\n\n\n# #region SupersetUserProjectionMixin [C:2] [TYPE Class]\n# @BRIEF Mixin providing user/owner payload normalization for Superset API responses.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nclass SupersetUserProjectionMixin:\n # #region SupersetClientExtractOwnerLabels [TYPE Function] [C:1]\n # @PURPOSE: Normalize dashboard owners payload to stable display labels.\n def _extract_owner_labels(self, owners_payload: Any) -> list[str]:\n if owners_payload is None:\n return []\n owners_list: list[Any]\n if isinstance(owners_payload, list):\n owners_list = owners_payload\n else:\n owners_list = [owners_payload]\n normalized: list[str] = []\n for owner in owners_list:\n label: str | None = None\n if isinstance(owner, dict):\n label = self._extract_user_display(None, owner)\n else:\n label = self._sanitize_user_text(owner)\n if label and label not in normalized:\n normalized.append(label)\n return normalized\n # #endregion SupersetClientExtractOwnerLabels\n # #region SupersetClientExtractUserDisplay [TYPE Function] [C:1]\n # @PURPOSE: Normalize user payload to a stable display name.\n def _extract_user_display(\n self, preferred_value: str | None, user_payload: dict | None\n ) -> str | None:\n preferred = self._sanitize_user_text(preferred_value)\n if preferred:\n return preferred\n if isinstance(user_payload, dict):\n full_name = self._sanitize_user_text(user_payload.get(\"full_name\"))\n if full_name:\n return full_name\n first_name = self._sanitize_user_text(user_payload.get(\"first_name\")) or \"\"\n last_name = self._sanitize_user_text(user_payload.get(\"last_name\")) or \"\"\n combined = \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n if combined:\n return combined\n username = self._sanitize_user_text(user_payload.get(\"username\"))\n if username:\n return username\n email = self._sanitize_user_text(user_payload.get(\"email\"))\n if email:\n return email\n return None\n # #endregion SupersetClientExtractUserDisplay\n # #region SupersetClientSanitizeUserText [TYPE Function] [C:1]\n # @PURPOSE: Convert scalar value to non-empty user-facing text.\n def _sanitize_user_text(self, value: str | int | 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 SupersetClientSanitizeUserText\n# #endregion SupersetUserProjectionMixin\n# #endregion SupersetUserProjection\n" }, { "contract_id": "SupersetUserProjectionMixin", "contract_type": "Class", "file_path": "backend/src/core/superset_client/_user_projection.py", - "start_line": 11, - "end_line": 87, + "start_line": 8, + "end_line": 70, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Mixin providing user/owner payload normalization for Superset API responses." }, "relations": [ @@ -42254,128 +37738,39 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetUserProjectionMixin:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Mixin providing user/owner payload normalization for Superset API responses.\n# @RELATION: DEPENDS_ON -> [SupersetClientBase]\nclass SupersetUserProjectionMixin:\n # [DEF:SupersetClientExtractOwnerLabels:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize dashboard owners payload to stable display labels.\n def _extract_owner_labels(self, owners_payload: Any) -> List[str]:\n if owners_payload is None:\n return []\n\n owners_list: List[Any]\n if isinstance(owners_payload, list):\n owners_list = owners_payload\n else:\n owners_list = [owners_payload]\n\n normalized: List[str] = []\n for owner in owners_list:\n label: Optional[str] = None\n if isinstance(owner, dict):\n label = self._extract_user_display(None, owner)\n else:\n label = self._sanitize_user_text(owner)\n if label and label not in normalized:\n normalized.append(label)\n return normalized\n\n # [/DEF:SupersetClientExtractOwnerLabels:Function]\n\n # [DEF:SupersetClientExtractUserDisplay:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize user payload to a stable display name.\n def _extract_user_display(\n self, preferred_value: Optional[str], user_payload: Optional[Dict]\n ) -> Optional[str]:\n preferred = self._sanitize_user_text(preferred_value)\n if preferred:\n return preferred\n\n if isinstance(user_payload, dict):\n full_name = self._sanitize_user_text(user_payload.get(\"full_name\"))\n if full_name:\n return full_name\n first_name = self._sanitize_user_text(user_payload.get(\"first_name\")) or \"\"\n last_name = self._sanitize_user_text(user_payload.get(\"last_name\")) or \"\"\n combined = \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n if combined:\n return combined\n username = self._sanitize_user_text(user_payload.get(\"username\"))\n if username:\n return username\n email = self._sanitize_user_text(user_payload.get(\"email\"))\n if email:\n return email\n return None\n\n # [/DEF:SupersetClientExtractUserDisplay:Function]\n\n # [DEF:SupersetClientSanitizeUserText:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Convert scalar value to non-empty user-facing text.\n def _sanitize_user_text(self, value: Optional[Union[str, int]]) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:SupersetClientSanitizeUserText:Function]\n\n\n# [/DEF:SupersetUserProjectionMixin:Class]\n" - }, - { - "contract_id": "SupersetClientExtractOwnerLabels", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_user_projection.py", - "start_line": 16, - "end_line": 40, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Normalize dashboard owners payload to stable display labels." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientExtractOwnerLabels:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize dashboard owners payload to stable display labels.\n def _extract_owner_labels(self, owners_payload: Any) -> List[str]:\n if owners_payload is None:\n return []\n\n owners_list: List[Any]\n if isinstance(owners_payload, list):\n owners_list = owners_payload\n else:\n owners_list = [owners_payload]\n\n normalized: List[str] = []\n for owner in owners_list:\n label: Optional[str] = None\n if isinstance(owner, dict):\n label = self._extract_user_display(None, owner)\n else:\n label = self._sanitize_user_text(owner)\n if label and label not in normalized:\n normalized.append(label)\n return normalized\n\n # [/DEF:SupersetClientExtractOwnerLabels:Function]\n" - }, - { - "contract_id": "SupersetClientExtractUserDisplay", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_user_projection.py", - "start_line": 42, - "end_line": 71, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Normalize user payload to a stable display name." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientExtractUserDisplay:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize user payload to a stable display name.\n def _extract_user_display(\n self, preferred_value: Optional[str], user_payload: Optional[Dict]\n ) -> Optional[str]:\n preferred = self._sanitize_user_text(preferred_value)\n if preferred:\n return preferred\n\n if isinstance(user_payload, dict):\n full_name = self._sanitize_user_text(user_payload.get(\"full_name\"))\n if full_name:\n return full_name\n first_name = self._sanitize_user_text(user_payload.get(\"first_name\")) or \"\"\n last_name = self._sanitize_user_text(user_payload.get(\"last_name\")) or \"\"\n combined = \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n if combined:\n return combined\n username = self._sanitize_user_text(user_payload.get(\"username\"))\n if username:\n return username\n email = self._sanitize_user_text(user_payload.get(\"email\"))\n if email:\n return email\n return None\n\n # [/DEF:SupersetClientExtractUserDisplay:Function]\n" - }, - { - "contract_id": "SupersetClientSanitizeUserText", - "contract_type": "Function", - "file_path": "backend/src/core/superset_client/_user_projection.py", - "start_line": 73, - "end_line": 84, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Convert scalar value to non-empty user-facing text." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetClientSanitizeUserText:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Convert scalar value to non-empty user-facing text.\n def _sanitize_user_text(self, value: Optional[Union[str, int]]) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:SupersetClientSanitizeUserText:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetUserProjectionMixin [C:2] [TYPE Class]\n# @BRIEF Mixin providing user/owner payload normalization for Superset API responses.\n# @RELATION DEPENDS_ON -> [SupersetClientBase]\nclass SupersetUserProjectionMixin:\n # #region SupersetClientExtractOwnerLabels [TYPE Function] [C:1]\n # @PURPOSE: Normalize dashboard owners payload to stable display labels.\n def _extract_owner_labels(self, owners_payload: Any) -> list[str]:\n if owners_payload is None:\n return []\n owners_list: list[Any]\n if isinstance(owners_payload, list):\n owners_list = owners_payload\n else:\n owners_list = [owners_payload]\n normalized: list[str] = []\n for owner in owners_list:\n label: str | None = None\n if isinstance(owner, dict):\n label = self._extract_user_display(None, owner)\n else:\n label = self._sanitize_user_text(owner)\n if label and label not in normalized:\n normalized.append(label)\n return normalized\n # #endregion SupersetClientExtractOwnerLabels\n # #region SupersetClientExtractUserDisplay [TYPE Function] [C:1]\n # @PURPOSE: Normalize user payload to a stable display name.\n def _extract_user_display(\n self, preferred_value: str | None, user_payload: dict | None\n ) -> str | None:\n preferred = self._sanitize_user_text(preferred_value)\n if preferred:\n return preferred\n if isinstance(user_payload, dict):\n full_name = self._sanitize_user_text(user_payload.get(\"full_name\"))\n if full_name:\n return full_name\n first_name = self._sanitize_user_text(user_payload.get(\"first_name\")) or \"\"\n last_name = self._sanitize_user_text(user_payload.get(\"last_name\")) or \"\"\n combined = \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n if combined:\n return combined\n username = self._sanitize_user_text(user_payload.get(\"username\"))\n if username:\n return username\n email = self._sanitize_user_text(user_payload.get(\"email\"))\n if email:\n return email\n return None\n # #endregion SupersetClientExtractUserDisplay\n # #region SupersetClientSanitizeUserText [TYPE Function] [C:1]\n # @PURPOSE: Convert scalar value to non-empty user-facing text.\n def _sanitize_user_text(self, value: str | int | 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 SupersetClientSanitizeUserText\n# #endregion SupersetUserProjectionMixin\n" }, { "contract_id": "SupersetProfileLookup", "contract_type": "Module", "file_path": "backend/src/core/superset_profile_lookup.py", "start_line": 1, - "end_line": 241, + "end_line": 208, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Adapter never leaks raw upstream payload shape to API consumers.", "LAYER": "Core", - "PURPOSE": "Provides environment-scoped Superset account lookup adapter with stable normalized output.", - "SEMANTICS": [ - "superset", - "users", - "lookup", - "profile", - "pagination", - "normalization" - ] + "PURPOSE": "Provides environment-scoped Superset account lookup adapter with stable normalized output." }, "relations": [ { "source_id": "SupersetProfileLookup", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "APIClient", "target_ref": "[APIClient]" }, { "source_id": "SupersetProfileLookup", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetAPIError", "target_ref": "[SupersetAPIError]" }, { "source_id": "SupersetProfileLookup", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetAccountLookupAdapter", "target_ref": "[SupersetAccountLookupAdapter]" } @@ -42389,138 +37784,47 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetProfileLookup:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: superset, users, lookup, profile, pagination, normalization\n# @PURPOSE: 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.\n\n# [SECTION: IMPORTS]\nimport json\nfrom typing import Any, Dict, List, Optional\n\nfrom .logger import logger, belief_scope\nfrom .utils.network import APIClient, AuthenticationError, SupersetAPIError\n# [/SECTION]\n\n\n# [DEF:SupersetAccountLookupAdapter:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Lookup Superset users and normalize candidates for profile binding.\n# @RELATION: [DEPENDS_ON] ->[APIClient]\n# @RELATION: [DEPENDS_ON] ->[SupersetProfileLookup]\nclass SupersetAccountLookupAdapter:\n # [DEF:__init__: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 # [/DEF:__init__:Function]\n\n # [DEF:get_users_page: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: Optional[str] = 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\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\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\n normalized_search = str(search or \"\").strip()\n if normalized_search:\n query[\"filters\"] = [{\"col\": \"username\", \"opr\": \"ct\", \"value\": normalized_search}]\n\n logger.reason(\n \"[REASON] Lookup Superset users \"\n f\"(env={self.environment_id}, page={normalized_page_index}, page_size={normalized_page_size})\"\n )\n logger.reflect(\n \"[REFLECT] Prepared Superset users lookup query \"\n f\"(env={self.environment_id}, order_column={normalized_sort_column}, \"\n f\"normalized_sort_order={normalized_sort_order}, \"\n f\"payload_order_direction={query.get('order_direction')})\"\n )\n\n primary_error: Optional[Exception] = None\n last_error: Optional[Exception] = None\n for attempt_index, endpoint in enumerate((\"/security/users/\", \"/security/users\"), start=1):\n try:\n logger.reason(\n \"[REASON] Users lookup request attempt \"\n f\"(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 \"[REFLECT] Users lookup endpoint succeeded \"\n f\"(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 \"[EXPLORE] Users lookup endpoint failed \"\n f\"(env={self.environment_id}, attempt={attempt_index}, endpoint={endpoint}, \"\n f\"error_type={type(exc).__name__}, status_code={status_code}, \"\n f\"payload_order_direction={query.get('order_direction')}): {exc}\"\n )\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 \"[REFLECT] Preserving primary lookup failure over fallback auth error \"\n f\"(env={self.environment_id}, primary_error_type={type(primary_error).__name__}, \"\n f\"fallback_error_type={type(last_error).__name__})\"\n )\n\n logger.explore(\n \"[EXPLORE] All Superset users lookup endpoints failed \"\n f\"(env={self.environment_id}, payload_order_direction={query.get('order_direction')}, \"\n f\"selected_error_type={type(selected_error).__name__})\"\n )\n raise selected_error\n raise SupersetAPIError(\"Superset users lookup failed without explicit error\")\n # [/DEF:get_users_page:Function]\n\n # [DEF:_normalize_lookup_payload: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\n raw_items: List[Any] = []\n total = 0\n\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\n normalized_items: List[Dict[str, Any]] = []\n seen_usernames = set()\n\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\n logger.reflect(\n \"[REFLECT] Normalized lookup payload \"\n f\"(env={self.environment_id}, items={len(normalized_items)}, total={max(total, len(normalized_items))})\"\n )\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 # [/DEF:_normalize_lookup_payload:Function]\n\n # [DEF:normalize_user_payload: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\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\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\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\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 # [/DEF:normalize_user_payload:Function]\n# [/DEF:SupersetAccountLookupAdapter:Class]\n\n# [/DEF:SupersetProfileLookup:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "SupersetAccountLookupAdapter", "contract_type": "Class", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 22, - "end_line": 239, + "start_line": 17, + "end_line": 207, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Lookup Superset users and normalize candidates for profile binding." }, "relations": [ { "source_id": "SupersetAccountLookupAdapter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "APIClient", "target_ref": "[APIClient]" }, { "source_id": "SupersetAccountLookupAdapter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetProfileLookup", "target_ref": "[SupersetProfileLookup]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetAccountLookupAdapter:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Lookup Superset users and normalize candidates for profile binding.\n# @RELATION: [DEPENDS_ON] ->[APIClient]\n# @RELATION: [DEPENDS_ON] ->[SupersetProfileLookup]\nclass SupersetAccountLookupAdapter:\n # [DEF:__init__: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 # [/DEF:__init__:Function]\n\n # [DEF:get_users_page: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: Optional[str] = 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\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\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\n normalized_search = str(search or \"\").strip()\n if normalized_search:\n query[\"filters\"] = [{\"col\": \"username\", \"opr\": \"ct\", \"value\": normalized_search}]\n\n logger.reason(\n \"[REASON] Lookup Superset users \"\n f\"(env={self.environment_id}, page={normalized_page_index}, page_size={normalized_page_size})\"\n )\n logger.reflect(\n \"[REFLECT] Prepared Superset users lookup query \"\n f\"(env={self.environment_id}, order_column={normalized_sort_column}, \"\n f\"normalized_sort_order={normalized_sort_order}, \"\n f\"payload_order_direction={query.get('order_direction')})\"\n )\n\n primary_error: Optional[Exception] = None\n last_error: Optional[Exception] = None\n for attempt_index, endpoint in enumerate((\"/security/users/\", \"/security/users\"), start=1):\n try:\n logger.reason(\n \"[REASON] Users lookup request attempt \"\n f\"(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 \"[REFLECT] Users lookup endpoint succeeded \"\n f\"(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 \"[EXPLORE] Users lookup endpoint failed \"\n f\"(env={self.environment_id}, attempt={attempt_index}, endpoint={endpoint}, \"\n f\"error_type={type(exc).__name__}, status_code={status_code}, \"\n f\"payload_order_direction={query.get('order_direction')}): {exc}\"\n )\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 \"[REFLECT] Preserving primary lookup failure over fallback auth error \"\n f\"(env={self.environment_id}, primary_error_type={type(primary_error).__name__}, \"\n f\"fallback_error_type={type(last_error).__name__})\"\n )\n\n logger.explore(\n \"[EXPLORE] All Superset users lookup endpoints failed \"\n f\"(env={self.environment_id}, payload_order_direction={query.get('order_direction')}, \"\n f\"selected_error_type={type(selected_error).__name__})\"\n )\n raise selected_error\n raise SupersetAPIError(\"Superset users lookup failed without explicit error\")\n # [/DEF:get_users_page:Function]\n\n # [DEF:_normalize_lookup_payload: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\n raw_items: List[Any] = []\n total = 0\n\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\n normalized_items: List[Dict[str, Any]] = []\n seen_usernames = set()\n\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\n logger.reflect(\n \"[REFLECT] Normalized lookup payload \"\n f\"(env={self.environment_id}, items={len(normalized_items)}, total={max(total, len(normalized_items))})\"\n )\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 # [/DEF:_normalize_lookup_payload:Function]\n\n # [DEF:normalize_user_payload: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\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\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\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\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 # [/DEF:normalize_user_payload:Function]\n# [/DEF:SupersetAccountLookupAdapter:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "__init__", "contract_type": "Function", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 28, - "end_line": 35, + "start_line": 22, + "end_line": 29, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -42558,15 +37862,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:__init__: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 # [/DEF:__init__:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "get_users_page", "contract_type": "Function", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 37, - "end_line": 139, + "start_line": 30, + "end_line": 119, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -42611,15 +37915,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:get_users_page: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: Optional[str] = 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\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\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\n normalized_search = str(search or \"\").strip()\n if normalized_search:\n query[\"filters\"] = [{\"col\": \"username\", \"opr\": \"ct\", \"value\": normalized_search}]\n\n logger.reason(\n \"[REASON] Lookup Superset users \"\n f\"(env={self.environment_id}, page={normalized_page_index}, page_size={normalized_page_size})\"\n )\n logger.reflect(\n \"[REFLECT] Prepared Superset users lookup query \"\n f\"(env={self.environment_id}, order_column={normalized_sort_column}, \"\n f\"normalized_sort_order={normalized_sort_order}, \"\n f\"payload_order_direction={query.get('order_direction')})\"\n )\n\n primary_error: Optional[Exception] = None\n last_error: Optional[Exception] = None\n for attempt_index, endpoint in enumerate((\"/security/users/\", \"/security/users\"), start=1):\n try:\n logger.reason(\n \"[REASON] Users lookup request attempt \"\n f\"(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 \"[REFLECT] Users lookup endpoint succeeded \"\n f\"(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 \"[EXPLORE] Users lookup endpoint failed \"\n f\"(env={self.environment_id}, attempt={attempt_index}, endpoint={endpoint}, \"\n f\"error_type={type(exc).__name__}, status_code={status_code}, \"\n f\"payload_order_direction={query.get('order_direction')}): {exc}\"\n )\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 \"[REFLECT] Preserving primary lookup failure over fallback auth error \"\n f\"(env={self.environment_id}, primary_error_type={type(primary_error).__name__}, \"\n f\"fallback_error_type={type(last_error).__name__})\"\n )\n\n logger.explore(\n \"[EXPLORE] All Superset users lookup endpoints failed \"\n f\"(env={self.environment_id}, payload_order_direction={query.get('order_direction')}, \"\n f\"selected_error_type={type(selected_error).__name__})\"\n )\n raise selected_error\n raise SupersetAPIError(\"Superset users lookup failed without explicit error\")\n # [/DEF:get_users_page:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_normalize_lookup_payload", "contract_type": "Function", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 141, - "end_line": 200, + "start_line": 120, + "end_line": 173, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -42664,15 +37968,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_lookup_payload: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\n raw_items: List[Any] = []\n total = 0\n\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\n normalized_items: List[Dict[str, Any]] = []\n seen_usernames = set()\n\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\n logger.reflect(\n \"[REFLECT] Normalized lookup payload \"\n f\"(env={self.environment_id}, items={len(normalized_items)}, total={max(total, len(normalized_items))})\"\n )\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 # [/DEF:_normalize_lookup_payload:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "normalize_user_payload", "contract_type": "Function", "file_path": "backend/src/core/superset_profile_lookup.py", - "start_line": 202, - "end_line": 238, + "start_line": 174, + "end_line": 206, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -42717,8 +38021,8 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:normalize_user_payload: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\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\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\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\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 # [/DEF:normalize_user_payload:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "TaskManagerPackage", @@ -42731,64 +38035,15 @@ "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TaskManagerPackage:Module]\n\nfrom .models import Task, TaskStatus, LogEntry\nfrom .manager import TaskManager\n\n__all__ = [\"TaskManager\", \"Task\", \"TaskStatus\", \"LogEntry\"]\n\n# [/DEF:TaskManagerPackage:Module]\n" - }, - { - "contract_id": "TestContext", - "contract_type": "Module", - "file_path": "backend/src/core/task_manager/__tests__/test_context.py", - "start_line": 1, - "end_line": 31, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Verify TaskContext preserves optional background task scheduler across sub-context creation.", - "SEMANTICS": [ - "tests", - "task-context", - "background-tasks", - "sub-context" - ] - }, - "relations": [ - { - "source_id": "TestContext", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestContext:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, task-context, background-tasks, sub-context\n# @PURPOSE: Verify TaskContext preserves optional background task scheduler across sub-context creation.\n\nfrom unittest.mock import MagicMock\n\nfrom src.core.task_manager.context import TaskContext\n\n\n# [DEF:test_task_context_preserves_background_tasks_across_sub_context:Function]\n# @RELATION: BINDS_TO -> TestContext\n# @PURPOSE: Plugins must be able to access background_tasks from both root and sub-context loggers.\n# @PRE: TaskContext is initialized with a BackgroundTasks-like object.\n# @POST: background_tasks remains available on root and derived sub-contexts.\ndef test_task_context_preserves_background_tasks_across_sub_context():\n background_tasks = MagicMock()\n context = TaskContext(\n task_id=\"task-1\",\n add_log_fn=lambda **_kwargs: None,\n params={\"x\": 1},\n background_tasks=background_tasks,\n )\n\n sub_context = context.create_sub_context(\"llm\")\n\n assert context.background_tasks is background_tasks\n assert sub_context.background_tasks is background_tasks\n# [/DEF:test_task_context_preserves_background_tasks_across_sub_context:Function]\n# [/DEF:TestContext:Module]\n" + "anchor_syntax": "region", + "body": "# #region TaskManagerPackage [TYPE Module] [SEMANTICS task, manager, package, init]\n\nfrom .manager import TaskManager\nfrom .models import LogEntry, Task, TaskStatus\n\n__all__ = [\"LogEntry\", \"Task\", \"TaskManager\", \"TaskStatus\"]\n\n# #endregion TaskManagerPackage\n" }, { "contract_id": "test_task_context_preserves_background_tasks_across_sub_context", "contract_type": "Function", "file_path": "backend/src/core/task_manager/__tests__/test_context.py", - "start_line": 12, - "end_line": 30, + "start_line": 9, + "end_line": 25, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -42833,8 +38088,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_task_context_preserves_background_tasks_across_sub_context:Function]\n# @RELATION: BINDS_TO -> TestContext\n# @PURPOSE: Plugins must be able to access background_tasks from both root and sub-context loggers.\n# @PRE: TaskContext is initialized with a BackgroundTasks-like object.\n# @POST: background_tasks remains available on root and derived sub-contexts.\ndef test_task_context_preserves_background_tasks_across_sub_context():\n background_tasks = MagicMock()\n context = TaskContext(\n task_id=\"task-1\",\n add_log_fn=lambda **_kwargs: None,\n params={\"x\": 1},\n background_tasks=background_tasks,\n )\n\n sub_context = context.create_sub_context(\"llm\")\n\n assert context.background_tasks is background_tasks\n assert sub_context.background_tasks is background_tasks\n# [/DEF:test_task_context_preserves_background_tasks_across_sub_context:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_task_context_preserves_background_tasks_across_sub_context [TYPE Function]\n# @RELATION BINDS_TO -> TestContext\n# @BRIEF Plugins must be able to access background_tasks from both root and sub-context loggers.\n# @PRE: TaskContext is initialized with a BackgroundTasks-like object.\n# @POST: background_tasks remains available on root and derived sub-contexts.\ndef test_task_context_preserves_background_tasks_across_sub_context():\n background_tasks = MagicMock()\n context = TaskContext(\n task_id=\"task-1\",\n add_log_fn=lambda **_kwargs: None,\n params={\"x\": 1},\n background_tasks=background_tasks,\n )\n sub_context = context.create_sub_context(\"llm\")\n assert context.background_tasks is background_tasks\n assert sub_context.background_tasks is background_tasks\n# #endregion test_task_context_preserves_background_tasks_across_sub_context\n" }, { "contract_id": "__tests__/test_task_logger", @@ -42875,15 +38130,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:__tests__/test_task_logger:Module]\n# @RELATION: VERIFIES -> ../task_logger.py\n# @PURPOSE: Contract testing for TaskLogger\n# [/DEF:__tests__/test_task_logger:Module]\n" + "anchor_syntax": "region", + "body": "# #region __tests__/test_task_logger [TYPE Module] [SEMANTICS test, task, logger, contract]\n# @RELATION VERIFIES -> ../task_logger.py\n# @BRIEF Contract testing for TaskLogger\n# #endregion __tests__/test_task_logger\n" }, { "contract_id": "test_task_logger_initialization", "contract_type": "Function", "file_path": "backend/src/core/task_manager/__tests__/test_task_logger.py", "start_line": 20, - "end_line": 29, + "end_line": 28, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -42909,15 +38164,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_task_logger_initialization:Function]\n# @RELATION: BINDS_TO -> __tests__/test_task_logger\n# @PURPOSE: Verify TaskLogger initializes with correct task_id and state.\ndef test_task_logger_initialization(task_logger):\n \"\"\"Verify TaskLogger is bound to specific task_id and source.\"\"\"\n assert task_logger._task_id == \"test_123\"\n assert task_logger._default_source == \"test_plugin\"\n\n# @TEST_CONTRACT: invariants -> \"All specific log methods (info, error) delegate to _log\"\n# [/DEF:test_task_logger_initialization:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_task_logger_initialization [TYPE Function]\n# @RELATION BINDS_TO -> __tests__/test_task_logger\n# @BRIEF Verify TaskLogger initializes with correct task_id and state.\ndef test_task_logger_initialization(task_logger):\n \"\"\"Verify TaskLogger is bound to specific task_id and source.\"\"\"\n assert task_logger._task_id == \"test_123\"\n assert task_logger._default_source == \"test_plugin\"\n# @TEST_CONTRACT: invariants -> \"All specific log methods (info, error) delegate to _log\"\n# #endregion test_task_logger_initialization\n" }, { "contract_id": "test_log_methods_delegation", "contract_type": "Function", "file_path": "backend/src/core/task_manager/__tests__/test_task_logger.py", - "start_line": 31, - "end_line": 73, + "start_line": 29, + "end_line": 68, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -42943,15 +38198,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_log_methods_delegation:Function]\n# @RELATION: BINDS_TO -> __tests__/test_task_logger\n# @PURPOSE: Verify TaskLogger delegates log method calls to the underlying persistence service.\ndef test_log_methods_delegation(task_logger, mock_add_log):\n \"\"\"Verify info, error, warning, debug delegate to internal _log.\"\"\"\n task_logger.info(\"info message\", metadata={\"k\": \"v\"})\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"INFO\",\n message=\"info message\",\n source=\"test_plugin\",\n metadata={\"k\": \"v\"}\n )\n \n task_logger.error(\"error message\", source=\"override\")\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"ERROR\",\n message=\"error message\",\n source=\"override\",\n metadata=None\n )\n\n task_logger.warning(\"warning message\")\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"WARNING\",\n message=\"warning message\",\n source=\"test_plugin\",\n metadata=None\n )\n\n task_logger.debug(\"debug message\")\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"DEBUG\",\n message=\"debug message\",\n source=\"test_plugin\",\n metadata=None\n )\n\n# @TEST_CONTRACT: invariants -> \"with_source creates a new logger with the same task_id\"\n# [/DEF:test_log_methods_delegation:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_log_methods_delegation [TYPE Function]\n# @RELATION BINDS_TO -> __tests__/test_task_logger\n# @BRIEF Verify TaskLogger delegates log method calls to the underlying persistence service.\ndef test_log_methods_delegation(task_logger, mock_add_log):\n \"\"\"Verify info, error, warning, debug delegate to internal _log.\"\"\"\n task_logger.info(\"info message\", metadata={\"k\": \"v\"})\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"INFO\",\n message=\"info message\",\n source=\"test_plugin\",\n metadata={\"k\": \"v\"}\n )\n\n task_logger.error(\"error message\", source=\"override\")\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"ERROR\",\n message=\"error message\",\n source=\"override\",\n metadata=None\n )\n task_logger.warning(\"warning message\")\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"WARNING\",\n message=\"warning message\",\n source=\"test_plugin\",\n metadata=None\n )\n task_logger.debug(\"debug message\")\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"DEBUG\",\n message=\"debug message\",\n source=\"test_plugin\",\n metadata=None\n )\n# @TEST_CONTRACT: invariants -> \"with_source creates a new logger with the same task_id\"\n# #endregion test_log_methods_delegation\n" }, { "contract_id": "test_with_source", "contract_type": "Function", "file_path": "backend/src/core/task_manager/__tests__/test_task_logger.py", - "start_line": 75, - "end_line": 87, + "start_line": 69, + "end_line": 80, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -42977,15 +38232,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_with_source:Function]\n# @RELATION: BINDS_TO -> __tests__/test_task_logger\n# @PURPOSE: Verify TaskLogger.with_source returns a new logger with the correct source attribution.\ndef test_with_source(task_logger):\n \"\"\"Verify with_source returns a new instance with updated default source.\"\"\"\n new_logger = task_logger.with_source(\"new_source\")\n assert isinstance(new_logger, TaskLogger)\n assert new_logger._task_id == \"test_123\"\n assert new_logger._default_source == \"new_source\"\n assert new_logger is not task_logger\n\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# [/DEF:test_with_source:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_with_source [TYPE Function]\n# @RELATION BINDS_TO -> __tests__/test_task_logger\n# @BRIEF Verify TaskLogger.with_source returns a new logger with the correct source attribution.\ndef test_with_source(task_logger):\n \"\"\"Verify with_source returns a new instance with updated default source.\"\"\"\n new_logger = task_logger.with_source(\"new_source\")\n assert isinstance(new_logger, TaskLogger)\n assert new_logger._task_id == \"test_123\"\n assert new_logger._default_source == \"new_source\"\n assert new_logger is not task_logger\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# #endregion test_with_source\n" }, { "contract_id": "test_missing_task_id", "contract_type": "Function", "file_path": "backend/src/core/task_manager/__tests__/test_task_logger.py", - "start_line": 89, - "end_line": 98, + "start_line": 81, + "end_line": 89, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -43011,15 +38266,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_missing_task_id:Function]\n# @RELATION: BINDS_TO -> __tests__/test_task_logger\n# @PURPOSE: Verify TaskLogger raises or handles missing task_id gracefully.\ndef test_missing_task_id():\n with pytest.raises(TypeError):\n TaskLogger(add_log_fn=lambda x: x)\n\n# @TEST_EDGE: invalid_add_log_fn -> raises TypeError\n# (Python doesn't strictly enforce this at init, but let's verify it fails on call if not callable)\n# [/DEF:test_missing_task_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_missing_task_id [TYPE Function]\n# @RELATION BINDS_TO -> __tests__/test_task_logger\n# @BRIEF Verify TaskLogger raises or handles missing task_id gracefully.\ndef test_missing_task_id():\n with pytest.raises(TypeError):\n TaskLogger(add_log_fn=lambda x: x)\n# @TEST_EDGE: invalid_add_log_fn -> raises TypeError\n# (Python doesn't strictly enforce this at init, but let's verify it fails on call if not callable)\n# #endregion test_missing_task_id\n" }, { "contract_id": "test_invalid_add_log_fn", "contract_type": "Function", "file_path": "backend/src/core/task_manager/__tests__/test_task_logger.py", - "start_line": 100, - "end_line": 109, + "start_line": 90, + "end_line": 98, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -43045,15 +38300,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_invalid_add_log_fn:Function]\n# @RELATION: BINDS_TO -> __tests__/test_task_logger\n# @PURPOSE: Verify TaskLogger raises ValueError for invalid add_log_fn parameter.\ndef test_invalid_add_log_fn():\n logger = TaskLogger(task_id=\"msg\", add_log_fn=None)\n with pytest.raises(TypeError):\n logger.info(\"test\")\n\n# @TEST_INVARIANT: consistent_delegation\n# [/DEF:test_invalid_add_log_fn:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_invalid_add_log_fn [TYPE Function]\n# @RELATION BINDS_TO -> __tests__/test_task_logger\n# @BRIEF Verify TaskLogger raises ValueError for invalid add_log_fn parameter.\ndef test_invalid_add_log_fn():\n logger = TaskLogger(task_id=\"msg\", add_log_fn=None)\n with pytest.raises(TypeError):\n logger.info(\"test\")\n# @TEST_INVARIANT: consistent_delegation\n# #endregion test_invalid_add_log_fn\n" }, { "contract_id": "test_progress_log", "contract_type": "Function", "file_path": "backend/src/core/task_manager/__tests__/test_task_logger.py", - "start_line": 111, - "end_line": 131, + "start_line": 99, + "end_line": 119, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -43078,27 +38333,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_progress_log:Function]\n# @RELATION: BINDS_TO -> __tests__/test_task_logger\n# @PURPOSE: Verify TaskLogger correctly logs progress updates with percentage and message.\ndef test_progress_log(task_logger, mock_add_log):\n \"\"\"Verify progress method correctly formats metadata.\"\"\"\n task_logger.progress(\"Step 1\", 45.5)\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"INFO\",\n message=\"Step 1\",\n source=\"test_plugin\",\n metadata={\"progress\": 45.5}\n )\n \n # Boundary checks\n task_logger.progress(\"Step high\", 150)\n assert mock_add_log.call_args[1][\"metadata\"][\"progress\"] == 100\n \n task_logger.progress(\"Step low\", -10)\n assert mock_add_log.call_args[1][\"metadata\"][\"progress\"] == 0\n# [/DEF:test_progress_log:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_progress_log [TYPE Function]\n# @RELATION BINDS_TO -> __tests__/test_task_logger\n# @BRIEF Verify TaskLogger correctly logs progress updates with percentage and message.\ndef test_progress_log(task_logger, mock_add_log):\n \"\"\"Verify progress method correctly formats metadata.\"\"\"\n task_logger.progress(\"Step 1\", 45.5)\n mock_add_log.assert_called_with(\n task_id=\"test_123\",\n level=\"INFO\",\n message=\"Step 1\",\n source=\"test_plugin\",\n metadata={\"progress\": 45.5}\n )\n\n # Boundary checks\n task_logger.progress(\"Step high\", 150)\n assert mock_add_log.call_args[1][\"metadata\"][\"progress\"] == 100\n\n task_logger.progress(\"Step low\", -10)\n assert mock_add_log.call_args[1][\"metadata\"][\"progress\"] == 0\n# #endregion test_progress_log\n" }, { "contract_id": "TaskCleanupModule", "contract_type": "Module", "file_path": "backend/src/core/task_manager/cleanup.py", "start_line": 1, - "end_line": 80, + "end_line": 75, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Core", - "PURPOSE": "Implements task cleanup and retention policies, including associated logs.", - "SEMANTICS": [ - "task", - "cleanup", - "retention", - "logs" - ] + "PURPOSE": "Implements task cleanup and retention policies, including associated logs." }, "relations": [ { @@ -43121,19 +38370,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TaskCleanupModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: task, cleanup, retention, logs\n# @PURPOSE: Implements task cleanup and retention policies, including associated logs.\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION: DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\n\nfrom typing import List\nfrom .persistence import TaskPersistenceService, TaskLogPersistenceService\nfrom ..logger import logger, belief_scope\nfrom ..config_manager import ConfigManager\n\n# [DEF:TaskCleanupService:Class]\n# @PURPOSE: Provides methods to clean up old task records and their associated logs.\n# @COMPLEXITY: 3\n# @RELATION: DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION: DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\nclass TaskCleanupService:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the cleanup service with dependencies.\n # @PRE: persistence_service and config_manager are valid.\n # @POST: Cleanup service is ready.\n def __init__(\n self,\n persistence_service: TaskPersistenceService,\n log_persistence_service: TaskLogPersistenceService,\n config_manager: ConfigManager\n ):\n self.persistence_service = persistence_service\n self.log_persistence_service = log_persistence_service\n self.config_manager = config_manager\n # [/DEF:__init__:Function]\n\n # [DEF:run_cleanup:Function]\n # @PURPOSE: Deletes tasks older than the configured retention period and their logs.\n # @PRE: Config manager has valid settings.\n # @POST: Old tasks and their logs are deleted from persistence.\n def run_cleanup(self):\n with belief_scope(\"TaskCleanupService.run_cleanup\"):\n settings = self.config_manager.get_config().settings\n retention_days = settings.task_retention_days\n \n logger.info(f\"Cleaning up tasks older than {retention_days} days.\")\n \n # Load tasks to check for limit\n tasks = self.persistence_service.load_tasks(limit=1000)\n if len(tasks) > settings.task_retention_limit:\n to_delete: List[str] = [t.id for t in tasks[settings.task_retention_limit:]]\n \n # Delete logs first (before task records)\n self.log_persistence_service.delete_logs_for_tasks(to_delete)\n \n # Then delete task records\n self.persistence_service.delete_tasks(to_delete)\n \n logger.info(f\"Deleted {len(to_delete)} tasks and their logs exceeding limit of {settings.task_retention_limit}\")\n # [/DEF:run_cleanup:Function]\n \n # [DEF:delete_task_with_logs:Function]\n # @PURPOSE: Delete a single task and all its associated logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Task and all its logs are deleted.\n # @PARAM: task_id (str) - The task ID to delete.\n def delete_task_with_logs(self, task_id: str) -> None:\n \"\"\"Delete a single task and all its associated logs.\"\"\"\n with belief_scope(\"TaskCleanupService.delete_task_with_logs\", f\"task_id={task_id}\"):\n # Delete logs first\n self.log_persistence_service.delete_logs_for_task(task_id)\n \n # Then delete task record\n self.persistence_service.delete_tasks([task_id])\n \n logger.info(f\"Deleted task {task_id} and its associated logs\")\n # [/DEF:delete_task_with_logs:Function]\n\n# [/DEF:TaskCleanupService:Class]\n# [/DEF:TaskCleanupModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region TaskCleanupModule [C:3] [TYPE Module] [SEMANTICS task, logs, task-cleanup-service]\n# @BRIEF Implements task cleanup and retention policies, including associated logs.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n\nfrom ..config_manager import ConfigManager\nfrom ..logger import belief_scope, logger\nfrom .persistence import TaskLogPersistenceService, TaskPersistenceService\n\n\n# #region TaskCleanupService [C:3] [TYPE Class]\n# @BRIEF Provides methods to clean up old task records and their associated logs.\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [ConfigManager]\nclass TaskCleanupService:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the cleanup service with dependencies.\n # @PRE: persistence_service and config_manager are valid.\n # @POST: Cleanup service is ready.\n def __init__(\n self,\n persistence_service: TaskPersistenceService,\n log_persistence_service: TaskLogPersistenceService,\n config_manager: ConfigManager\n ):\n self.persistence_service = persistence_service\n self.log_persistence_service = log_persistence_service\n self.config_manager = config_manager\n # #endregion __init__\n # #region run_cleanup [TYPE Function]\n # @PURPOSE: Deletes tasks older than the configured retention period and their logs.\n # @PRE: Config manager has valid settings.\n # @POST: Old tasks and their logs are deleted from persistence.\n def run_cleanup(self):\n with belief_scope(\"TaskCleanupService.run_cleanup\"):\n settings = self.config_manager.get_config().settings\n retention_days = settings.task_retention_days\n\n logger.info(f\"Cleaning up tasks older than {retention_days} days.\")\n\n # Load tasks to check for limit\n tasks = self.persistence_service.load_tasks(limit=1000)\n if len(tasks) > settings.task_retention_limit:\n to_delete: list[str] = [t.id for t in tasks[settings.task_retention_limit:]]\n\n # Delete logs first (before task records)\n self.log_persistence_service.delete_logs_for_tasks(to_delete)\n\n # Then delete task records\n self.persistence_service.delete_tasks(to_delete)\n\n logger.info(f\"Deleted {len(to_delete)} tasks and their logs exceeding limit of {settings.task_retention_limit}\")\n # #endregion run_cleanup\n\n # #region delete_task_with_logs [TYPE Function]\n # @PURPOSE: Delete a single task and all its associated logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Task and all its logs are deleted.\n # @PARAM: task_id (str) - The task ID to delete.\n def delete_task_with_logs(self, task_id: str) -> None:\n \"\"\"Delete a single task and all its associated logs.\"\"\"\n with belief_scope(\"TaskCleanupService.delete_task_with_logs\", f\"task_id={task_id}\"):\n # Delete logs first\n self.log_persistence_service.delete_logs_for_task(task_id)\n\n # Then delete task record\n self.persistence_service.delete_tasks([task_id])\n\n logger.info(f\"Deleted task {task_id} and its associated logs\")\n # #endregion delete_task_with_logs\n# #endregion TaskCleanupService\n# #endregion TaskCleanupModule\n" }, { "contract_id": "TaskCleanupService", "contract_type": "Class", "file_path": "backend/src/core/task_manager/cleanup.py", - "start_line": 15, - "end_line": 79, + "start_line": 13, + "end_line": 74, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Provides methods to clean up old task records and their associated logs." }, "relations": [ @@ -43157,15 +38406,15 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TaskCleanupService:Class]\n# @PURPOSE: Provides methods to clean up old task records and their associated logs.\n# @COMPLEXITY: 3\n# @RELATION: DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION: DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\nclass TaskCleanupService:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the cleanup service with dependencies.\n # @PRE: persistence_service and config_manager are valid.\n # @POST: Cleanup service is ready.\n def __init__(\n self,\n persistence_service: TaskPersistenceService,\n log_persistence_service: TaskLogPersistenceService,\n config_manager: ConfigManager\n ):\n self.persistence_service = persistence_service\n self.log_persistence_service = log_persistence_service\n self.config_manager = config_manager\n # [/DEF:__init__:Function]\n\n # [DEF:run_cleanup:Function]\n # @PURPOSE: Deletes tasks older than the configured retention period and their logs.\n # @PRE: Config manager has valid settings.\n # @POST: Old tasks and their logs are deleted from persistence.\n def run_cleanup(self):\n with belief_scope(\"TaskCleanupService.run_cleanup\"):\n settings = self.config_manager.get_config().settings\n retention_days = settings.task_retention_days\n \n logger.info(f\"Cleaning up tasks older than {retention_days} days.\")\n \n # Load tasks to check for limit\n tasks = self.persistence_service.load_tasks(limit=1000)\n if len(tasks) > settings.task_retention_limit:\n to_delete: List[str] = [t.id for t in tasks[settings.task_retention_limit:]]\n \n # Delete logs first (before task records)\n self.log_persistence_service.delete_logs_for_tasks(to_delete)\n \n # Then delete task records\n self.persistence_service.delete_tasks(to_delete)\n \n logger.info(f\"Deleted {len(to_delete)} tasks and their logs exceeding limit of {settings.task_retention_limit}\")\n # [/DEF:run_cleanup:Function]\n \n # [DEF:delete_task_with_logs:Function]\n # @PURPOSE: Delete a single task and all its associated logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Task and all its logs are deleted.\n # @PARAM: task_id (str) - The task ID to delete.\n def delete_task_with_logs(self, task_id: str) -> None:\n \"\"\"Delete a single task and all its associated logs.\"\"\"\n with belief_scope(\"TaskCleanupService.delete_task_with_logs\", f\"task_id={task_id}\"):\n # Delete logs first\n self.log_persistence_service.delete_logs_for_task(task_id)\n \n # Then delete task record\n self.persistence_service.delete_tasks([task_id])\n \n logger.info(f\"Deleted task {task_id} and its associated logs\")\n # [/DEF:delete_task_with_logs:Function]\n\n# [/DEF:TaskCleanupService:Class]\n" + "anchor_syntax": "region", + "body": "# #region TaskCleanupService [C:3] [TYPE Class]\n# @BRIEF Provides methods to clean up old task records and their associated logs.\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [ConfigManager]\nclass TaskCleanupService:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the cleanup service with dependencies.\n # @PRE: persistence_service and config_manager are valid.\n # @POST: Cleanup service is ready.\n def __init__(\n self,\n persistence_service: TaskPersistenceService,\n log_persistence_service: TaskLogPersistenceService,\n config_manager: ConfigManager\n ):\n self.persistence_service = persistence_service\n self.log_persistence_service = log_persistence_service\n self.config_manager = config_manager\n # #endregion __init__\n # #region run_cleanup [TYPE Function]\n # @PURPOSE: Deletes tasks older than the configured retention period and their logs.\n # @PRE: Config manager has valid settings.\n # @POST: Old tasks and their logs are deleted from persistence.\n def run_cleanup(self):\n with belief_scope(\"TaskCleanupService.run_cleanup\"):\n settings = self.config_manager.get_config().settings\n retention_days = settings.task_retention_days\n\n logger.info(f\"Cleaning up tasks older than {retention_days} days.\")\n\n # Load tasks to check for limit\n tasks = self.persistence_service.load_tasks(limit=1000)\n if len(tasks) > settings.task_retention_limit:\n to_delete: list[str] = [t.id for t in tasks[settings.task_retention_limit:]]\n\n # Delete logs first (before task records)\n self.log_persistence_service.delete_logs_for_tasks(to_delete)\n\n # Then delete task records\n self.persistence_service.delete_tasks(to_delete)\n\n logger.info(f\"Deleted {len(to_delete)} tasks and their logs exceeding limit of {settings.task_retention_limit}\")\n # #endregion run_cleanup\n\n # #region delete_task_with_logs [TYPE Function]\n # @PURPOSE: Delete a single task and all its associated logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Task and all its logs are deleted.\n # @PARAM: task_id (str) - The task ID to delete.\n def delete_task_with_logs(self, task_id: str) -> None:\n \"\"\"Delete a single task and all its associated logs.\"\"\"\n with belief_scope(\"TaskCleanupService.delete_task_with_logs\", f\"task_id={task_id}\"):\n # Delete logs first\n self.log_persistence_service.delete_logs_for_task(task_id)\n\n # Then delete task record\n self.persistence_service.delete_tasks([task_id])\n\n logger.info(f\"Deleted task {task_id} and its associated logs\")\n # #endregion delete_task_with_logs\n# #endregion TaskCleanupService\n" }, { "contract_id": "run_cleanup", "contract_type": "Function", "file_path": "backend/src/core/task_manager/cleanup.py", - "start_line": 37, - "end_line": 60, + "start_line": 33, + "end_line": 56, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43203,15 +38452,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:run_cleanup:Function]\n # @PURPOSE: Deletes tasks older than the configured retention period and their logs.\n # @PRE: Config manager has valid settings.\n # @POST: Old tasks and their logs are deleted from persistence.\n def run_cleanup(self):\n with belief_scope(\"TaskCleanupService.run_cleanup\"):\n settings = self.config_manager.get_config().settings\n retention_days = settings.task_retention_days\n \n logger.info(f\"Cleaning up tasks older than {retention_days} days.\")\n \n # Load tasks to check for limit\n tasks = self.persistence_service.load_tasks(limit=1000)\n if len(tasks) > settings.task_retention_limit:\n to_delete: List[str] = [t.id for t in tasks[settings.task_retention_limit:]]\n \n # Delete logs first (before task records)\n self.log_persistence_service.delete_logs_for_tasks(to_delete)\n \n # Then delete task records\n self.persistence_service.delete_tasks(to_delete)\n \n logger.info(f\"Deleted {len(to_delete)} tasks and their logs exceeding limit of {settings.task_retention_limit}\")\n # [/DEF:run_cleanup:Function]\n" + "anchor_syntax": "region", + "body": " # #region run_cleanup [TYPE Function]\n # @PURPOSE: Deletes tasks older than the configured retention period and their logs.\n # @PRE: Config manager has valid settings.\n # @POST: Old tasks and their logs are deleted from persistence.\n def run_cleanup(self):\n with belief_scope(\"TaskCleanupService.run_cleanup\"):\n settings = self.config_manager.get_config().settings\n retention_days = settings.task_retention_days\n\n logger.info(f\"Cleaning up tasks older than {retention_days} days.\")\n\n # Load tasks to check for limit\n tasks = self.persistence_service.load_tasks(limit=1000)\n if len(tasks) > settings.task_retention_limit:\n to_delete: list[str] = [t.id for t in tasks[settings.task_retention_limit:]]\n\n # Delete logs first (before task records)\n self.log_persistence_service.delete_logs_for_tasks(to_delete)\n\n # Then delete task records\n self.persistence_service.delete_tasks(to_delete)\n\n logger.info(f\"Deleted {len(to_delete)} tasks and their logs exceeding limit of {settings.task_retention_limit}\")\n # #endregion run_cleanup\n" }, { "contract_id": "delete_task_with_logs", "contract_type": "Function", "file_path": "backend/src/core/task_manager/cleanup.py", - "start_line": 62, - "end_line": 77, + "start_line": 58, + "end_line": 73, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43256,32 +38505,25 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:delete_task_with_logs:Function]\n # @PURPOSE: Delete a single task and all its associated logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Task and all its logs are deleted.\n # @PARAM: task_id (str) - The task ID to delete.\n def delete_task_with_logs(self, task_id: str) -> None:\n \"\"\"Delete a single task and all its associated logs.\"\"\"\n with belief_scope(\"TaskCleanupService.delete_task_with_logs\", f\"task_id={task_id}\"):\n # Delete logs first\n self.log_persistence_service.delete_logs_for_task(task_id)\n \n # Then delete task record\n self.persistence_service.delete_tasks([task_id])\n \n logger.info(f\"Deleted task {task_id} and its associated logs\")\n # [/DEF:delete_task_with_logs:Function]\n" + "anchor_syntax": "region", + "body": " # #region delete_task_with_logs [TYPE Function]\n # @PURPOSE: Delete a single task and all its associated logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Task and all its logs are deleted.\n # @PARAM: task_id (str) - The task ID to delete.\n def delete_task_with_logs(self, task_id: str) -> None:\n \"\"\"Delete a single task and all its associated logs.\"\"\"\n with belief_scope(\"TaskCleanupService.delete_task_with_logs\", f\"task_id={task_id}\"):\n # Delete logs first\n self.log_persistence_service.delete_logs_for_task(task_id)\n\n # Then delete task record\n self.persistence_service.delete_tasks([task_id])\n\n logger.info(f\"Deleted task {task_id} and its associated logs\")\n # #endregion delete_task_with_logs\n" }, { "contract_id": "TaskContextModule", "contract_type": "Module", "file_path": "backend/src/core/task_manager/context.py", "start_line": 1, - "end_line": 167, + "end_line": 143, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]", "INVARIANT": "Each TaskContext is bound to a single task execution.", "LAYER": "Core", "POST": "Plugins receive context instances with stable logger and parameter accessors.", "PRE": "Task execution pipeline provides valid task identifiers, logging callbacks, and parameter dictionaries.", "PURPOSE": "Provides execution context passed to plugins during task execution.", - "SEMANTICS": [ - "task", - "context", - "plugin", - "execution", - "logger" - ], "SIDE_EFFECT": "Creates task-scoped logger wrappers and carries optional background task handles across sub-contexts." }, "relations": [ @@ -43318,30 +38560,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskContextModule:Module]\n# @SEMANTICS: task, context, plugin, execution, logger\n# @PURPOSE: Provides execution context passed to plugins during task execution.\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> [TaskLoggerModule]\n# @RELATION: DEPENDS_ON -> [TaskManager]\n# @COMPLEXITY: 5\n# @INVARIANT: Each TaskContext is bound to a single task execution.\n# @PRE: Task execution pipeline provides valid task identifiers, logging callbacks, and parameter dictionaries.\n# @POST: Plugins receive context instances with stable logger and parameter accessors.\n# @SIDE_EFFECT: Creates task-scoped logger wrappers and carries optional background task handles across sub-contexts.\n# @DATA_CONTRACT: Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]\n\n# [SECTION: IMPORTS]\n# [SECTION: IMPORTS]\nfrom typing import Dict, Any, Callable, Optional\nfrom .task_logger import TaskLogger\nfrom ..logger import belief_scope\n# [/SECTION]\n\n\n# [DEF:TaskContext:Class]\n# @SEMANTICS: context, task, execution, plugin\n# @PURPOSE: A container passed to plugin.execute() providing the logger and other task-specific utilities.\n# @COMPLEXITY: 5\n# @INVARIANT: logger is always a valid TaskLogger instance.\n# @PRE: Constructor receives non-empty task_id, callable add_log_fn, and params mapping.\n# @POST: Instance exposes immutable task identity with logger, params, and optional background task access.\n# @RELATION: DEPENDS_ON -> [TaskLogger]\n# @SIDE_EFFECT: Emits structured task logs through TaskLogger on plugin interactions.\n# @DATA_CONTRACT: Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]\n# @UX_STATE: Idle -> Active -> Complete\n#\n# @TEST_CONTRACT: TaskContextContract ->\n# {\n# required_fields: {task_id: str, add_log_fn: Callable, params: dict},\n# optional_fields: {default_source: str},\n# invariants: [\n# \"task_id matches initialized logger's task_id\",\n# \"logger is a valid TaskLogger instance\"\n# ]\n# }\n# @TEST_FIXTURE: valid_context -> {\"task_id\": \"123\", \"add_log_fn\": lambda *args: None, \"params\": {\"k\": \"v\"}, \"default_source\": \"plugin\"}\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# @TEST_EDGE: missing_add_log_fn -> raises TypeError\n# @TEST_INVARIANT: logger_initialized -> verifies: [valid_context]\nclass TaskContext:\n \"\"\"\n Execution context provided to plugins during task execution.\n\n Usage:\n def execute(params: dict, context: TaskContext = None):\n if context:\n context.logger.info(\"Starting process\")\n context.logger.progress(\"Processing items\", percent=50)\n # ... plugin logic\n \"\"\"\n\n # [DEF:__init__:Function]\n # @PURPOSE: Initialize the TaskContext with task-specific resources.\n # @PRE: task_id is a valid task identifier, add_log_fn is callable.\n # @POST: TaskContext is ready to be passed to plugin.execute().\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager.\n # @PARAM: params (Dict) - Task parameters.\n # @PARAM: default_source (str) - Default source for logs (default: \"plugin\").\n def __init__(\n self,\n task_id: str,\n add_log_fn: Callable,\n params: Dict[str, Any],\n default_source: str = \"plugin\",\n background_tasks: Optional[Any] = None,\n ):\n with belief_scope(\"__init__\"):\n self._task_id = task_id\n self._params = params\n self._background_tasks = background_tasks\n self._logger = TaskLogger(\n task_id=task_id, add_log_fn=add_log_fn, source=default_source\n )\n\n # [/DEF:__init__:Function]\n\n # [DEF:task_id:Function]\n # @PURPOSE: Get the task ID.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the task ID string.\n # @RETURN: str - The task ID.\n @property\n def task_id(self) -> str:\n with belief_scope(\"task_id\"):\n return self._task_id\n\n # [/DEF:task_id:Function]\n\n # [DEF:logger:Function]\n # @PURPOSE: Get the TaskLogger instance for this context.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the TaskLogger instance.\n # @RETURN: TaskLogger - The logger instance.\n @property\n def logger(self) -> TaskLogger:\n with belief_scope(\"logger\"):\n return self._logger\n\n # [/DEF:logger:Function]\n\n # [DEF:params:Function]\n # @PURPOSE: Get the task parameters.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the parameters dictionary.\n # @RETURN: Dict[str, Any] - The task parameters.\n @property\n def params(self) -> Dict[str, Any]:\n with belief_scope(\"params\"):\n return self._params\n\n # [/DEF:params:Function]\n\n # [DEF:background_tasks:Function]\n # @PURPOSE: Expose optional background task scheduler for plugins that dispatch deferred side effects.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns BackgroundTasks-like object or None.\n @property\n def background_tasks(self) -> Optional[Any]:\n with belief_scope(\"background_tasks\"):\n return self._background_tasks\n\n # [/DEF:background_tasks:Function]\n\n # [DEF:get_param:Function]\n # @PURPOSE: Get a specific parameter value with optional default.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns parameter value or default.\n # @PARAM: key (str) - Parameter key.\n # @PARAM: default (Any) - Default value if key not found.\n # @RETURN: Any - Parameter value or default.\n def get_param(self, key: str, default: Any = None) -> Any:\n with belief_scope(\"get_param\"):\n return self._params.get(key, default)\n\n # [/DEF:get_param:Function]\n\n # [DEF:create_sub_context: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\n # [/DEF:create_sub_context:Function]\n\n\n# [/DEF:TaskContext:Class]\n\n# [/DEF:TaskContextModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region TaskContextModule [C:5] [TYPE Module] [SEMANTICS task, execution, task-context]\n# @BRIEF Provides execution context passed to plugins during task execution.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [TaskLoggerModule]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @INVARIANT: Each TaskContext is bound to a single task execution.\n# @PRE: Task execution pipeline provides valid task identifiers, logging callbacks, and parameter dictionaries.\n# @POST: Plugins receive context instances with stable logger and parameter accessors.\n# @SIDE_EFFECT: Creates task-scoped logger wrappers and carries optional background task handles across sub-contexts.\n# @DATA_CONTRACT: Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom ..logger import belief_scope\nfrom .task_logger import TaskLogger\n\n\n# #region TaskContext [C:5] [TYPE Class] [SEMANTICS context, task, execution, plugin]\n# @BRIEF A container passed to plugin.execute() providing the logger and other task-specific utilities.\n# @INVARIANT: logger is always a valid TaskLogger instance.\n# @PRE: Constructor receives non-empty task_id, callable add_log_fn, and params mapping.\n# @POST: Instance exposes immutable task identity with logger, params, and optional background task access.\n# @RELATION DEPENDS_ON -> [TaskLogger]\n# @SIDE_EFFECT: Emits structured task logs through TaskLogger on plugin interactions.\n# @DATA_CONTRACT: Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]\n# @UX_STATE: Idle -> Active -> Complete\n#\n# @TEST_CONTRACT: TaskContextContract ->\n# {\n# required_fields: {task_id: str, add_log_fn: Callable, params: dict},\n# optional_fields: {default_source: str},\n# invariants: [\n# \"task_id matches initialized logger's task_id\",\n# \"logger is a valid TaskLogger instance\"\n# ]\n# }\n# @TEST_FIXTURE: valid_context -> {\"task_id\": \"123\", \"add_log_fn\": lambda *args: None, \"params\": {\"k\": \"v\"}, \"default_source\": \"plugin\"}\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# @TEST_EDGE: missing_add_log_fn -> raises TypeError\n# @TEST_INVARIANT: logger_initialized -> verifies: [valid_context]\nclass TaskContext:\n \"\"\"\n Execution context provided to plugins during task execution.\n Usage:\n def execute(params: dict, context: TaskContext = None):\n if context:\n context.logger.info(\"Starting process\")\n context.logger.progress(\"Processing items\", percent=50)\n # ... plugin logic\n \"\"\"\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initialize the TaskContext with task-specific resources.\n # @PRE: task_id is a valid task identifier, add_log_fn is callable.\n # @POST: TaskContext is ready to be passed to plugin.execute().\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager.\n # @PARAM: params (Dict) - Task parameters.\n # @PARAM: default_source (str) - Default source for logs (default: \"plugin\").\n def __init__(\n self,\n task_id: str,\n add_log_fn: Callable,\n params: dict[str, Any],\n default_source: str = \"plugin\",\n background_tasks: Any | None = None,\n ):\n with belief_scope(\"__init__\"):\n self._task_id = task_id\n self._params = params\n self._background_tasks = background_tasks\n self._logger = TaskLogger(\n task_id=task_id, add_log_fn=add_log_fn, source=default_source\n )\n # #endregion __init__\n # #region task_id [TYPE Function]\n # @PURPOSE: Get the task ID.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the task ID string.\n # @RETURN: str - The task ID.\n @property\n def task_id(self) -> str:\n with belief_scope(\"task_id\"):\n return self._task_id\n # #endregion task_id\n # #region logger [TYPE Function]\n # @PURPOSE: Get the TaskLogger instance for this context.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the TaskLogger instance.\n # @RETURN: TaskLogger - The logger instance.\n @property\n def logger(self) -> TaskLogger:\n with belief_scope(\"logger\"):\n return self._logger\n # #endregion logger\n # #region params [TYPE Function]\n # @PURPOSE: Get the task parameters.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the parameters dictionary.\n # @RETURN: Dict[str, Any] - The task parameters.\n @property\n def params(self) -> dict[str, Any]:\n with belief_scope(\"params\"):\n return self._params\n # #endregion params\n # #region background_tasks [TYPE Function]\n # @PURPOSE: Expose optional background task scheduler for plugins that dispatch deferred side effects.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns BackgroundTasks-like object or None.\n @property\n def background_tasks(self) -> Any | None:\n with belief_scope(\"background_tasks\"):\n return self._background_tasks\n # #endregion background_tasks\n # #region get_param [TYPE Function]\n # @PURPOSE: Get a specific parameter value with optional default.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns parameter value or default.\n # @PARAM: key (str) - Parameter key.\n # @PARAM: default (Any) - Default value if key not found.\n # @RETURN: Any - Parameter value or default.\n def get_param(self, key: str, default: Any = None) -> Any:\n with belief_scope(\"get_param\"):\n return self._params.get(key, default)\n # #endregion get_param\n # #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# #endregion TaskContext\n# #endregion TaskContextModule\n" }, { "contract_id": "TaskContext", "contract_type": "Class", "file_path": "backend/src/core/task_manager/context.py", - "start_line": 22, - "end_line": 165, + "start_line": 18, + "end_line": 142, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]", "INVARIANT": "logger is always a valid TaskLogger instance.", "POST": "Instance exposes immutable task identity with logger, params, and optional background task access.", "PRE": "Constructor receives non-empty task_id, callable add_log_fn, and params mapping.", "PURPOSE": "A container passed to plugin.execute() providing the logger and other task-specific utilities.", - "SEMANTICS": [ - "context", - "task", - "execution", - "plugin" - ], "SIDE_EFFECT": "Emits structured task logs through TaskLogger on plugin interactions.", "TEST_CONTRACT": "TaskContextContract ->", "UX_STATE": "Idle -> Active -> Complete" @@ -43355,29 +38591,6 @@ } ], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -43438,15 +38651,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskContext:Class]\n# @SEMANTICS: context, task, execution, plugin\n# @PURPOSE: A container passed to plugin.execute() providing the logger and other task-specific utilities.\n# @COMPLEXITY: 5\n# @INVARIANT: logger is always a valid TaskLogger instance.\n# @PRE: Constructor receives non-empty task_id, callable add_log_fn, and params mapping.\n# @POST: Instance exposes immutable task identity with logger, params, and optional background task access.\n# @RELATION: DEPENDS_ON -> [TaskLogger]\n# @SIDE_EFFECT: Emits structured task logs through TaskLogger on plugin interactions.\n# @DATA_CONTRACT: Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]\n# @UX_STATE: Idle -> Active -> Complete\n#\n# @TEST_CONTRACT: TaskContextContract ->\n# {\n# required_fields: {task_id: str, add_log_fn: Callable, params: dict},\n# optional_fields: {default_source: str},\n# invariants: [\n# \"task_id matches initialized logger's task_id\",\n# \"logger is a valid TaskLogger instance\"\n# ]\n# }\n# @TEST_FIXTURE: valid_context -> {\"task_id\": \"123\", \"add_log_fn\": lambda *args: None, \"params\": {\"k\": \"v\"}, \"default_source\": \"plugin\"}\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# @TEST_EDGE: missing_add_log_fn -> raises TypeError\n# @TEST_INVARIANT: logger_initialized -> verifies: [valid_context]\nclass TaskContext:\n \"\"\"\n Execution context provided to plugins during task execution.\n\n Usage:\n def execute(params: dict, context: TaskContext = None):\n if context:\n context.logger.info(\"Starting process\")\n context.logger.progress(\"Processing items\", percent=50)\n # ... plugin logic\n \"\"\"\n\n # [DEF:__init__:Function]\n # @PURPOSE: Initialize the TaskContext with task-specific resources.\n # @PRE: task_id is a valid task identifier, add_log_fn is callable.\n # @POST: TaskContext is ready to be passed to plugin.execute().\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager.\n # @PARAM: params (Dict) - Task parameters.\n # @PARAM: default_source (str) - Default source for logs (default: \"plugin\").\n def __init__(\n self,\n task_id: str,\n add_log_fn: Callable,\n params: Dict[str, Any],\n default_source: str = \"plugin\",\n background_tasks: Optional[Any] = None,\n ):\n with belief_scope(\"__init__\"):\n self._task_id = task_id\n self._params = params\n self._background_tasks = background_tasks\n self._logger = TaskLogger(\n task_id=task_id, add_log_fn=add_log_fn, source=default_source\n )\n\n # [/DEF:__init__:Function]\n\n # [DEF:task_id:Function]\n # @PURPOSE: Get the task ID.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the task ID string.\n # @RETURN: str - The task ID.\n @property\n def task_id(self) -> str:\n with belief_scope(\"task_id\"):\n return self._task_id\n\n # [/DEF:task_id:Function]\n\n # [DEF:logger:Function]\n # @PURPOSE: Get the TaskLogger instance for this context.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the TaskLogger instance.\n # @RETURN: TaskLogger - The logger instance.\n @property\n def logger(self) -> TaskLogger:\n with belief_scope(\"logger\"):\n return self._logger\n\n # [/DEF:logger:Function]\n\n # [DEF:params:Function]\n # @PURPOSE: Get the task parameters.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the parameters dictionary.\n # @RETURN: Dict[str, Any] - The task parameters.\n @property\n def params(self) -> Dict[str, Any]:\n with belief_scope(\"params\"):\n return self._params\n\n # [/DEF:params:Function]\n\n # [DEF:background_tasks:Function]\n # @PURPOSE: Expose optional background task scheduler for plugins that dispatch deferred side effects.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns BackgroundTasks-like object or None.\n @property\n def background_tasks(self) -> Optional[Any]:\n with belief_scope(\"background_tasks\"):\n return self._background_tasks\n\n # [/DEF:background_tasks:Function]\n\n # [DEF:get_param:Function]\n # @PURPOSE: Get a specific parameter value with optional default.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns parameter value or default.\n # @PARAM: key (str) - Parameter key.\n # @PARAM: default (Any) - Default value if key not found.\n # @RETURN: Any - Parameter value or default.\n def get_param(self, key: str, default: Any = None) -> Any:\n with belief_scope(\"get_param\"):\n return self._params.get(key, default)\n\n # [/DEF:get_param:Function]\n\n # [DEF:create_sub_context: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\n # [/DEF:create_sub_context:Function]\n\n\n# [/DEF:TaskContext:Class]\n" + "anchor_syntax": "region", + "body": "# #region TaskContext [C:5] [TYPE Class] [SEMANTICS context, task, execution, plugin]\n# @BRIEF A container passed to plugin.execute() providing the logger and other task-specific utilities.\n# @INVARIANT: logger is always a valid TaskLogger instance.\n# @PRE: Constructor receives non-empty task_id, callable add_log_fn, and params mapping.\n# @POST: Instance exposes immutable task identity with logger, params, and optional background task access.\n# @RELATION DEPENDS_ON -> [TaskLogger]\n# @SIDE_EFFECT: Emits structured task logs through TaskLogger on plugin interactions.\n# @DATA_CONTRACT: Input[task_id, add_log_fn, params, default_source, background_tasks] -> Output[TaskContext]\n# @UX_STATE: Idle -> Active -> Complete\n#\n# @TEST_CONTRACT: TaskContextContract ->\n# {\n# required_fields: {task_id: str, add_log_fn: Callable, params: dict},\n# optional_fields: {default_source: str},\n# invariants: [\n# \"task_id matches initialized logger's task_id\",\n# \"logger is a valid TaskLogger instance\"\n# ]\n# }\n# @TEST_FIXTURE: valid_context -> {\"task_id\": \"123\", \"add_log_fn\": lambda *args: None, \"params\": {\"k\": \"v\"}, \"default_source\": \"plugin\"}\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# @TEST_EDGE: missing_add_log_fn -> raises TypeError\n# @TEST_INVARIANT: logger_initialized -> verifies: [valid_context]\nclass TaskContext:\n \"\"\"\n Execution context provided to plugins during task execution.\n Usage:\n def execute(params: dict, context: TaskContext = None):\n if context:\n context.logger.info(\"Starting process\")\n context.logger.progress(\"Processing items\", percent=50)\n # ... plugin logic\n \"\"\"\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initialize the TaskContext with task-specific resources.\n # @PRE: task_id is a valid task identifier, add_log_fn is callable.\n # @POST: TaskContext is ready to be passed to plugin.execute().\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager.\n # @PARAM: params (Dict) - Task parameters.\n # @PARAM: default_source (str) - Default source for logs (default: \"plugin\").\n def __init__(\n self,\n task_id: str,\n add_log_fn: Callable,\n params: dict[str, Any],\n default_source: str = \"plugin\",\n background_tasks: Any | None = None,\n ):\n with belief_scope(\"__init__\"):\n self._task_id = task_id\n self._params = params\n self._background_tasks = background_tasks\n self._logger = TaskLogger(\n task_id=task_id, add_log_fn=add_log_fn, source=default_source\n )\n # #endregion __init__\n # #region task_id [TYPE Function]\n # @PURPOSE: Get the task ID.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the task ID string.\n # @RETURN: str - The task ID.\n @property\n def task_id(self) -> str:\n with belief_scope(\"task_id\"):\n return self._task_id\n # #endregion task_id\n # #region logger [TYPE Function]\n # @PURPOSE: Get the TaskLogger instance for this context.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the TaskLogger instance.\n # @RETURN: TaskLogger - The logger instance.\n @property\n def logger(self) -> TaskLogger:\n with belief_scope(\"logger\"):\n return self._logger\n # #endregion logger\n # #region params [TYPE Function]\n # @PURPOSE: Get the task parameters.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the parameters dictionary.\n # @RETURN: Dict[str, Any] - The task parameters.\n @property\n def params(self) -> dict[str, Any]:\n with belief_scope(\"params\"):\n return self._params\n # #endregion params\n # #region background_tasks [TYPE Function]\n # @PURPOSE: Expose optional background task scheduler for plugins that dispatch deferred side effects.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns BackgroundTasks-like object or None.\n @property\n def background_tasks(self) -> Any | None:\n with belief_scope(\"background_tasks\"):\n return self._background_tasks\n # #endregion background_tasks\n # #region get_param [TYPE Function]\n # @PURPOSE: Get a specific parameter value with optional default.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns parameter value or default.\n # @PARAM: key (str) - Parameter key.\n # @PARAM: default (Any) - Default value if key not found.\n # @RETURN: Any - Parameter value or default.\n def get_param(self, key: str, default: Any = None) -> Any:\n with belief_scope(\"get_param\"):\n return self._params.get(key, default)\n # #endregion get_param\n # #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# #endregion TaskContext\n" }, { "contract_id": "task_id", "contract_type": "Function", "file_path": "backend/src/core/task_manager/context.py", - "start_line": 85, - "end_line": 95, + "start_line": 75, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43491,15 +38704,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:task_id:Function]\n # @PURPOSE: Get the task ID.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the task ID string.\n # @RETURN: str - The task ID.\n @property\n def task_id(self) -> str:\n with belief_scope(\"task_id\"):\n return self._task_id\n\n # [/DEF:task_id:Function]\n" + "anchor_syntax": "region", + "body": " # #region task_id [TYPE Function]\n # @PURPOSE: Get the task ID.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the task ID string.\n # @RETURN: str - The task ID.\n @property\n def task_id(self) -> str:\n with belief_scope(\"task_id\"):\n return self._task_id\n # #endregion task_id\n" }, { "contract_id": "logger", "contract_type": "Function", "file_path": "backend/src/core/task_manager/context.py", - "start_line": 97, - "end_line": 107, + "start_line": 85, + "end_line": 94, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43544,15 +38757,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:logger:Function]\n # @PURPOSE: Get the TaskLogger instance for this context.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the TaskLogger instance.\n # @RETURN: TaskLogger - The logger instance.\n @property\n def logger(self) -> TaskLogger:\n with belief_scope(\"logger\"):\n return self._logger\n\n # [/DEF:logger:Function]\n" + "anchor_syntax": "region", + "body": " # #region logger [TYPE Function]\n # @PURPOSE: Get the TaskLogger instance for this context.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the TaskLogger instance.\n # @RETURN: TaskLogger - The logger instance.\n @property\n def logger(self) -> TaskLogger:\n with belief_scope(\"logger\"):\n return self._logger\n # #endregion logger\n" }, { "contract_id": "params", "contract_type": "Function", "file_path": "backend/src/core/task_manager/context.py", - "start_line": 109, - "end_line": 119, + "start_line": 95, + "end_line": 104, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43597,15 +38810,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:params:Function]\n # @PURPOSE: Get the task parameters.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the parameters dictionary.\n # @RETURN: Dict[str, Any] - The task parameters.\n @property\n def params(self) -> Dict[str, Any]:\n with belief_scope(\"params\"):\n return self._params\n\n # [/DEF:params:Function]\n" + "anchor_syntax": "region", + "body": " # #region params [TYPE Function]\n # @PURPOSE: Get the task parameters.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns the parameters dictionary.\n # @RETURN: Dict[str, Any] - The task parameters.\n @property\n def params(self) -> dict[str, Any]:\n with belief_scope(\"params\"):\n return self._params\n # #endregion params\n" }, { "contract_id": "background_tasks", "contract_type": "Function", "file_path": "backend/src/core/task_manager/context.py", - "start_line": 121, - "end_line": 130, + "start_line": 105, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43643,15 +38856,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:background_tasks:Function]\n # @PURPOSE: Expose optional background task scheduler for plugins that dispatch deferred side effects.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns BackgroundTasks-like object or None.\n @property\n def background_tasks(self) -> Optional[Any]:\n with belief_scope(\"background_tasks\"):\n return self._background_tasks\n\n # [/DEF:background_tasks:Function]\n" + "anchor_syntax": "region", + "body": " # #region background_tasks [TYPE Function]\n # @PURPOSE: Expose optional background task scheduler for plugins that dispatch deferred side effects.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns BackgroundTasks-like object or None.\n @property\n def background_tasks(self) -> Any | None:\n with belief_scope(\"background_tasks\"):\n return self._background_tasks\n # #endregion background_tasks\n" }, { "contract_id": "get_param", "contract_type": "Function", "file_path": "backend/src/core/task_manager/context.py", - "start_line": 132, - "end_line": 143, + "start_line": 114, + "end_line": 124, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43703,15 +38916,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:get_param:Function]\n # @PURPOSE: Get a specific parameter value with optional default.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns parameter value or default.\n # @PARAM: key (str) - Parameter key.\n # @PARAM: default (Any) - Default value if key not found.\n # @RETURN: Any - Parameter value or default.\n def get_param(self, key: str, default: Any = None) -> Any:\n with belief_scope(\"get_param\"):\n return self._params.get(key, default)\n\n # [/DEF:get_param:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_param [TYPE Function]\n # @PURPOSE: Get a specific parameter value with optional default.\n # @PRE: TaskContext must be initialized.\n # @POST: Returns parameter value or default.\n # @PARAM: key (str) - Parameter key.\n # @PARAM: default (Any) - Default value if key not found.\n # @RETURN: Any - Parameter value or default.\n def get_param(self, key: str, default: Any = None) -> Any:\n with belief_scope(\"get_param\"):\n return self._params.get(key, default)\n # #endregion get_param\n" }, { "contract_id": "create_sub_context", "contract_type": "Function", "file_path": "backend/src/core/task_manager/context.py", - "start_line": 145, - "end_line": 162, + "start_line": 125, + "end_line": 141, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43763,75 +38976,68 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:create_sub_context: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\n # [/DEF:create_sub_context:Function]\n" + "anchor_syntax": "region", + "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_type": "Module", "file_path": "backend/src/core/task_manager/manager.py", "start_line": 1, - "end_line": 830, + "end_line": 708, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[plugin_id, params] -> Model[Task, LogEntry]", "INVARIANT": "Task IDs are unique.", "LAYER": "Core", "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.", - "SEMANTICS": [ - "task", - "manager", - "lifecycle", - "execution", - "state" - ], "SIDE_EFFECT": "Spawns worker threads and flushes logs to DB.", "TEST_CONTRACT": "TaskManagerRuntime -> {" }, "relations": [ { "source_id": "TaskManagerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "PluginLoader", "target_ref": "[PluginLoader]" }, { "source_id": "TaskManagerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskPersistenceService", "target_ref": "[TaskPersistenceService]" }, { "source_id": "TaskManagerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskLogPersistenceService", "target_ref": "[TaskLogPersistenceService]" }, { "source_id": "TaskManagerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskContext", "target_ref": "[TaskContext]" }, { "source_id": "TaskManagerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskGraph", "target_ref": "[TaskGraph]" }, { "source_id": "TaskManagerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "JobLifecycle", "target_ref": "[JobLifecycle]" }, { "source_id": "TaskManagerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "EventBus", "target_ref": "[EventBus]" } @@ -43875,202 +39081,69 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskManagerModule:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: task, manager, lifecycle, execution, state\n# @PURPOSE: 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]\n\n# [SECTION: IMPORTS]\nimport asyncio\nimport threading\nimport inspect\nfrom concurrent.futures import ThreadPoolExecutor\nfrom datetime import datetime, timezone\nfrom typing import Dict, Any, List, Optional\n\nfrom .models import Task, TaskStatus, LogEntry, LogFilter, LogStats\nfrom .persistence import TaskPersistenceService, TaskLogPersistenceService\nfrom .context import TaskContext\nfrom ..logger import logger, belief_scope, should_log_task_level\nfrom ..cot_logger import MarkerLogger, get_trace_id, set_trace_id\n\nlog = MarkerLogger(\"TaskManager\")\n# [/SECTION]\n\n\n# [DEF:TaskManager:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: task, manager, lifecycle, execution, state\n# @PURPOSE: 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\n # Log flush interval in seconds\n LOG_FLUSH_INTERVAL = 2.0\n\n # [DEF:TaskGraph:Block]\n # @COMPLEXITY: 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 # [/DEF:TaskGraph:Block]\n\n # [DEF:EventBus:Block]\n # @COMPLEXITY: 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 # [/DEF:EventBus:Block]\n\n # [DEF:JobLifecycle:Block]\n # @COMPLEXITY: 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 # [/DEF:JobLifecycle:Block]\n\n # [DEF:__init__:Function]\n # @COMPLEXITY: 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 log.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\n # Log buffer: task_id -> List[LogEntry]\n self._log_buffer: Dict[str, List[LogEntry]] = {}\n self._log_buffer_lock = threading.Lock()\n\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\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\n # Load persisted tasks on startup\n self.load_persisted_tasks()\n log.reflect(\n \"Task manager runtime initialized\",\n payload={\"task_count\": len(self.tasks)},\n )\n\n # [/DEF:__init__:Function]\n\n # [DEF:_flusher_loop:Function]\n # @COMPLEXITY: 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 while not self._flusher_stop_event.is_set():\n self._flush_logs()\n self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)\n\n # [/DEF:_flusher_loop:Function]\n\n # [DEF:_flush_logs:Function]\n # @COMPLEXITY: 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 with self._log_buffer_lock:\n task_ids = list(self._log_buffer.keys())\n\n for task_id in task_ids:\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n log.reflect(\"Flushed logs to database\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n log.explore(\"Failed to flush logs to database\", error=str(e), payload={\"task_id\": task_id})\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\n # [/DEF:_flush_logs:Function]\n\n # [DEF:_flush_task_logs:Function]\n # @COMPLEXITY: 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\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n log.reflect(\"Flushed task logs immediately\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n log.explore(\"Failed to flush task logs immediately\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n\n # [/DEF:_flush_task_logs:Function]\n\n # [DEF:create_task:Function]\n # @COMPLEXITY: 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: Optional[str] = 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 log.explore(\"Plugin not found\", error=f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n\n self.plugin_loader.get_plugin(plugin_id)\n\n if not isinstance(params, dict):\n log.explore(\"Invalid task params type\", error=\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n\n log.reason(\"Creating task node and scheduling execution\", payload={\"plugin_id\": plugin_id})\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 trace_id = get_trace_id()\n self.loop.create_task(\n self._run_task(task.id, trace_id=trace_id)\n ) # Schedule task for execution\n log.reflect(\n \"Task creation persisted and execution scheduled\",\n payload={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n\n # [/DEF:create_task:Function]\n\n # [DEF:_run_task:Function]\n # @COMPLEXITY: 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, trace_id: str = None):\n if trace_id:\n set_trace_id(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\n log.reason(\n \"Transitioning task to running state\",\n payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\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\n try:\n # Prepare params and check if plugin supports new TaskContext\n params = {**task.params, \"_task_id\": task_id}\n\n # Check if plugin's execute method accepts 'context' parameter\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n\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\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\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 log.reflect(\"Task completed successfully\", payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id})\n except Exception as 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 log.explore(\"Task failed during execution\", error=str(e), payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id})\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 log.reflect(\n \"Task execution finished\",\n payload={\"task_id\": task_id, \"status\": str(task.status)},\n )\n\n # [/DEF:_run_task:Function]\n\n # [DEF:resolve_task:Function]\n # @COMPLEXITY: 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\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\n # Signal the future to continue\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n\n # [/DEF:resolve_task:Function]\n\n # [DEF:wait_for_resolution:Function]\n # @COMPLEXITY: 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\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n self.task_futures[task_id] = self.loop.create_future()\n\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\n # [/DEF:wait_for_resolution:Function]\n\n # [DEF:wait_for_input:Function]\n # @COMPLEXITY: 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\n # Status is already set to AWAITING_INPUT by await_input()\n self.task_futures[task_id] = self.loop.create_future()\n\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\n # [/DEF:wait_for_input:Function]\n\n # [DEF:get_task:Function]\n # @COMPLEXITY: 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) -> Optional[Task]:\n with belief_scope(\"TaskManager.get_task\", f\"task_id={task_id}\"):\n return self.tasks.get(task_id)\n\n # [/DEF:get_task:Function]\n\n # [DEF:get_all_tasks:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_all_tasks:Function]\n\n # [DEF:get_tasks:Function]\n # @COMPLEXITY: 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: Optional[TaskStatus] = None,\n plugin_ids: Optional[List[str]] = 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\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=timezone.utc).timestamp()\n return started_at.timestamp()\n\n tasks.sort(key=sort_key, reverse=True)\n return tasks[offset : offset + limit]\n\n # [/DEF:get_tasks:Function]\n\n # [DEF:get_task_logs:Function]\n # @COMPLEXITY: 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: Optional[LogFilter] = 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\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\n # For running/pending tasks, return from memory\n return task.logs if task else []\n\n # [/DEF:get_task_logs:Function]\n\n # [DEF:get_task_log_stats:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_task_log_stats:Function]\n\n # [DEF:get_task_log_sources:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_task_log_sources:Function]\n\n # [DEF:_add_log:Function]\n # @COMPLEXITY: 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: Optional[Dict[str, Any]] = None,\n context: Optional[Dict[str, Any]] = 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\n # Filter logs based on task_log_level configuration\n if not should_log_task_level(level):\n return\n\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\n # Add to in-memory logs (for backward compatibility with legacy JSON field)\n task.logs.append(log_entry)\n\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\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\n # [/DEF:_add_log:Function]\n\n # [DEF:subscribe_logs:Function]\n # @COMPLEXITY: 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\n # [/DEF:subscribe_logs:Function]\n\n # [DEF:unsubscribe_logs:Function]\n # @COMPLEXITY: 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\n # [/DEF:unsubscribe_logs:Function]\n\n # [DEF:load_persisted_tasks:Function]\n # @COMPLEXITY: 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\n # [/DEF:load_persisted_tasks:Function]\n\n # [DEF:await_input:Function]\n # @COMPLEXITY: 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\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\n # [/DEF:await_input:Function]\n\n # [DEF:resume_task_with_password:Function]\n # @COMPLEXITY: 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\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n\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\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n\n # [/DEF:resume_task_with_password:Function]\n\n # [DEF:clear_tasks:Function]\n # @COMPLEXITY: 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: Optional[TaskStatus] = 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\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\n if should_remove:\n tasks_to_remove.append(task_id)\n\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\n del self.tasks[tid]\n\n # Remove from persistence (task_records and task_logs via CASCADE)\n self.persistence_service.delete_tasks(tasks_to_remove)\n\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\n log.reflect(\"Tasks cleared from registry and database\", payload={\"count\": len(tasks_to_remove)})\n return len(tasks_to_remove)\n\n # [/DEF:clear_tasks:Function]\n\n\n# [/DEF:TaskManager:Class]\n# [/DEF:TaskManagerModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "TaskManager", "contract_type": "Class", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 48, - "end_line": 829, + "start_line": 41, + "end_line": 707, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[plugin_id, params] -> Output[Task]", "INVARIANT": "Log entries are never deleted after being added to a task.", "LAYER": "Core", "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.", - "SEMANTICS": [ - "task", - "manager", - "lifecycle", - "execution", - "state" - ], "SIDE_EFFECT": "Spawns worker threads, flushes logs to database, and mutates task states." }, "relations": [ { "source_id": "TaskManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskPersistenceService", "target_ref": "[TaskPersistenceService]" }, { "source_id": "TaskManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskLogPersistenceService", "target_ref": "[TaskLogPersistenceService]" }, { "source_id": "TaskManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "PluginLoader", "target_ref": "[PluginLoader]" }, { "source_id": "TaskManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskContext", "target_ref": "[TaskContext]" }, { "source_id": "TaskManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskGraph", "target_ref": "[TaskGraph]" }, { "source_id": "TaskManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "JobLifecycle", "target_ref": "[JobLifecycle]" }, { "source_id": "TaskManager", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "EventBus", "target_ref": "[EventBus]" } @@ -44098,29 +39171,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, { "code": "missing_required_tag", "tag": "RATIONALE", @@ -44138,2764 +39188,36 @@ "actual_complexity": 5, "contract_type": "Class" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskManager:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: task, manager, lifecycle, execution, state\n# @PURPOSE: 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\n # Log flush interval in seconds\n LOG_FLUSH_INTERVAL = 2.0\n\n # [DEF:TaskGraph:Block]\n # @COMPLEXITY: 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 # [/DEF:TaskGraph:Block]\n\n # [DEF:EventBus:Block]\n # @COMPLEXITY: 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 # [/DEF:EventBus:Block]\n\n # [DEF:JobLifecycle:Block]\n # @COMPLEXITY: 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 # [/DEF:JobLifecycle:Block]\n\n # [DEF:__init__:Function]\n # @COMPLEXITY: 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 log.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\n # Log buffer: task_id -> List[LogEntry]\n self._log_buffer: Dict[str, List[LogEntry]] = {}\n self._log_buffer_lock = threading.Lock()\n\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\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\n # Load persisted tasks on startup\n self.load_persisted_tasks()\n log.reflect(\n \"Task manager runtime initialized\",\n payload={\"task_count\": len(self.tasks)},\n )\n\n # [/DEF:__init__:Function]\n\n # [DEF:_flusher_loop:Function]\n # @COMPLEXITY: 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 while not self._flusher_stop_event.is_set():\n self._flush_logs()\n self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)\n\n # [/DEF:_flusher_loop:Function]\n\n # [DEF:_flush_logs:Function]\n # @COMPLEXITY: 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 with self._log_buffer_lock:\n task_ids = list(self._log_buffer.keys())\n\n for task_id in task_ids:\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n log.reflect(\"Flushed logs to database\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n log.explore(\"Failed to flush logs to database\", error=str(e), payload={\"task_id\": task_id})\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\n # [/DEF:_flush_logs:Function]\n\n # [DEF:_flush_task_logs:Function]\n # @COMPLEXITY: 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\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n log.reflect(\"Flushed task logs immediately\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n log.explore(\"Failed to flush task logs immediately\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n\n # [/DEF:_flush_task_logs:Function]\n\n # [DEF:create_task:Function]\n # @COMPLEXITY: 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: Optional[str] = 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 log.explore(\"Plugin not found\", error=f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n\n self.plugin_loader.get_plugin(plugin_id)\n\n if not isinstance(params, dict):\n log.explore(\"Invalid task params type\", error=\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n\n log.reason(\"Creating task node and scheduling execution\", payload={\"plugin_id\": plugin_id})\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 trace_id = get_trace_id()\n self.loop.create_task(\n self._run_task(task.id, trace_id=trace_id)\n ) # Schedule task for execution\n log.reflect(\n \"Task creation persisted and execution scheduled\",\n payload={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n\n # [/DEF:create_task:Function]\n\n # [DEF:_run_task:Function]\n # @COMPLEXITY: 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, trace_id: str = None):\n if trace_id:\n set_trace_id(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\n log.reason(\n \"Transitioning task to running state\",\n payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\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\n try:\n # Prepare params and check if plugin supports new TaskContext\n params = {**task.params, \"_task_id\": task_id}\n\n # Check if plugin's execute method accepts 'context' parameter\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n\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\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\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 log.reflect(\"Task completed successfully\", payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id})\n except Exception as 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 log.explore(\"Task failed during execution\", error=str(e), payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id})\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 log.reflect(\n \"Task execution finished\",\n payload={\"task_id\": task_id, \"status\": str(task.status)},\n )\n\n # [/DEF:_run_task:Function]\n\n # [DEF:resolve_task:Function]\n # @COMPLEXITY: 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\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\n # Signal the future to continue\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n\n # [/DEF:resolve_task:Function]\n\n # [DEF:wait_for_resolution:Function]\n # @COMPLEXITY: 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\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n self.task_futures[task_id] = self.loop.create_future()\n\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\n # [/DEF:wait_for_resolution:Function]\n\n # [DEF:wait_for_input:Function]\n # @COMPLEXITY: 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\n # Status is already set to AWAITING_INPUT by await_input()\n self.task_futures[task_id] = self.loop.create_future()\n\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\n # [/DEF:wait_for_input:Function]\n\n # [DEF:get_task:Function]\n # @COMPLEXITY: 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) -> Optional[Task]:\n with belief_scope(\"TaskManager.get_task\", f\"task_id={task_id}\"):\n return self.tasks.get(task_id)\n\n # [/DEF:get_task:Function]\n\n # [DEF:get_all_tasks:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_all_tasks:Function]\n\n # [DEF:get_tasks:Function]\n # @COMPLEXITY: 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: Optional[TaskStatus] = None,\n plugin_ids: Optional[List[str]] = 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\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=timezone.utc).timestamp()\n return started_at.timestamp()\n\n tasks.sort(key=sort_key, reverse=True)\n return tasks[offset : offset + limit]\n\n # [/DEF:get_tasks:Function]\n\n # [DEF:get_task_logs:Function]\n # @COMPLEXITY: 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: Optional[LogFilter] = 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\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\n # For running/pending tasks, return from memory\n return task.logs if task else []\n\n # [/DEF:get_task_logs:Function]\n\n # [DEF:get_task_log_stats:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_task_log_stats:Function]\n\n # [DEF:get_task_log_sources:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_task_log_sources:Function]\n\n # [DEF:_add_log:Function]\n # @COMPLEXITY: 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: Optional[Dict[str, Any]] = None,\n context: Optional[Dict[str, Any]] = 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\n # Filter logs based on task_log_level configuration\n if not should_log_task_level(level):\n return\n\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\n # Add to in-memory logs (for backward compatibility with legacy JSON field)\n task.logs.append(log_entry)\n\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\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\n # [/DEF:_add_log:Function]\n\n # [DEF:subscribe_logs:Function]\n # @COMPLEXITY: 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\n # [/DEF:subscribe_logs:Function]\n\n # [DEF:unsubscribe_logs:Function]\n # @COMPLEXITY: 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\n # [/DEF:unsubscribe_logs:Function]\n\n # [DEF:load_persisted_tasks:Function]\n # @COMPLEXITY: 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\n # [/DEF:load_persisted_tasks:Function]\n\n # [DEF:await_input:Function]\n # @COMPLEXITY: 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\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\n # [/DEF:await_input:Function]\n\n # [DEF:resume_task_with_password:Function]\n # @COMPLEXITY: 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\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n\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\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n\n # [/DEF:resume_task_with_password:Function]\n\n # [DEF:clear_tasks:Function]\n # @COMPLEXITY: 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: Optional[TaskStatus] = 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\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\n if should_remove:\n tasks_to_remove.append(task_id)\n\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\n del self.tasks[tid]\n\n # Remove from persistence (task_records and task_logs via CASCADE)\n self.persistence_service.delete_tasks(tasks_to_remove)\n\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\n log.reflect(\"Tasks cleared from registry and database\", payload={\"count\": len(tasks_to_remove)})\n return len(tasks_to_remove)\n\n # [/DEF:clear_tasks:Function]\n\n\n# [/DEF:TaskManager:Class]\n" - }, - { - "contract_id": "TaskGraph", - "contract_type": "Block", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 75, - "end_line": 85, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": "5", - "DATA_CONTRACT": "Input[Task lifecycle events] -> Output[tasks:Dict[str, Task], task_futures:Dict[str, asyncio.Future]]", - "INVARIANT": "Registry membership is keyed by unique task id and survives log streaming side channels.", - "POST": "Each live task id resolves to at most one active Task node and optional pause future.", - "PRE": "Task ids are generated before insertion and persisted tasks can be reconstructed into Task models.", - "PURPOSE": "Represents the in-memory task dependency graph spanning task registry nodes, paused futures, and persistence-backed hydration.", - "SIDE_EFFECT": "Mutates the in-memory task registry and loads persisted state during manager startup." - }, - "relations": [ - { - "source_id": "TaskGraph", - "relation_type": "[DEPENDS_ON]", - "target_id": "Task", - "target_ref": "[Task]" - }, - { - "source_id": "TaskGraph", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskPersistenceService", - "target_ref": "[TaskPersistenceService]" - } - ], - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "POST", - "message": "@POST is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PRE", - "message": "@PRE is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:TaskGraph:Block]\n # @COMPLEXITY: 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 # [/DEF:TaskGraph:Block]\n" - }, - { - "contract_id": "EventBus", - "contract_type": "Block", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 87, - "end_line": 97, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": "5", - "DATA_CONTRACT": "Input[task_id, LogEntry, Queue subscribers] -> Output[persisted log rows, streamed log events]", - "INVARIANT": "Buffered logs are retried on persistence failure and every subscriber receives only task-scoped events.", - "POST": "Accepted task logs are buffered, optionally persisted, and dispatched to active subscribers in emission order.", - "PRE": "Task log records accept structured LogEntry payloads and subscribers consume asyncio queues.", - "PURPOSE": "Coordinates task-scoped log buffering, persistence flushes, and subscriber fan-out for real-time observers.", - "SIDE_EFFECT": "Writes task logs to persistence, mutates in-memory buffers, and pushes log entries into subscriber queues." - }, - "relations": [ - { - "source_id": "EventBus", - "relation_type": "[DEPENDS_ON]", - "target_id": "LogEntry", - "target_ref": "[LogEntry]" - }, - { - "source_id": "EventBus", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskLogPersistenceService", - "target_ref": "[TaskLogPersistenceService]" - } - ], - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "POST", - "message": "@POST is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PRE", - "message": "@PRE is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:EventBus:Block]\n # @COMPLEXITY: 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 # [/DEF:EventBus:Block]\n" - }, - { - "contract_id": "JobLifecycle", - "contract_type": "Block", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 99, - "end_line": 111, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": "5", - "DATA_CONTRACT": "Input[plugin_id, params, task_id, resolution payloads] -> Output[Task status transitions, task result]", - "INVARIANT": "A task cannot be resumed from a waiting state unless a matching future exists or a new wait future is created.", - "POST": "Every scheduled task transitions through a valid lifecycle path ending in persisted terminal or waiting state.", - "PRE": "Requested plugin ids resolve to executable plugins and task state transitions target known TaskStatus values.", - "PURPOSE": "Encodes task creation, execution, pause/resume, and completion transitions for plugin-backed jobs.", - "SIDE_EFFECT": "Schedules async execution, pauses on input/mapping requests, and mutates persisted task lifecycle markers." - }, - "relations": [ - { - "source_id": "JobLifecycle", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskGraph", - "target_ref": "[TaskGraph]" - }, - { - "source_id": "JobLifecycle", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - }, - { - "source_id": "JobLifecycle", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskContext", - "target_ref": "[TaskContext]" - }, - { - "source_id": "JobLifecycle", - "relation_type": "[DEPENDS_ON]", - "target_id": "PluginLoader", - "target_ref": "[PluginLoader]" - } - ], - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "POST", - "message": "@POST is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PRE", - "message": "@PRE is not allowed for contract type 'Block'", - "detail": { - "actual_type": "Block", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Block' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Block" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:JobLifecycle:Block]\n # @COMPLEXITY: 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 # [/DEF:JobLifecycle:Block]\n" - }, - { - "contract_id": "_flusher_loop", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 161, - "end_line": 174, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Logs are batch-written to database every LOG_FLUSH_INTERVAL seconds.", - "PRE": "TaskManager is initialized.", - "PURPOSE": "Background thread that periodically flushes log buffer to database." - }, - "relations": [ - { - "source_id": "_flusher_loop", - "relation_type": "[CALLS]", - "target_id": "TaskManager._flush_logs", - "target_ref": "[TaskManager._flush_logs]" - }, - { - "source_id": "_flusher_loop", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_flusher_loop:Function]\n # @COMPLEXITY: 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 while not self._flusher_stop_event.is_set():\n self._flush_logs()\n self._flusher_stop_event.wait(self.LOG_FLUSH_INTERVAL)\n\n # [/DEF:_flusher_loop:Function]\n" - }, - { - "contract_id": "_flush_logs", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 176, - "end_line": 205, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "All buffered logs are written to task_logs table.", - "PRE": "None.", - "PURPOSE": "Flush all buffered logs to the database." - }, - "relations": [ - { - "source_id": "_flush_logs", - "relation_type": "[CALLS]", - "target_id": "TaskLogPersistenceService.add_logs", - "target_ref": "[TaskLogPersistenceService.add_logs]" - }, - { - "source_id": "_flush_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_flush_logs:Function]\n # @COMPLEXITY: 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 with self._log_buffer_lock:\n task_ids = list(self._log_buffer.keys())\n\n for task_id in task_ids:\n with self._log_buffer_lock:\n logs = self._log_buffer.pop(task_id, [])\n\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n log.reflect(\"Flushed logs to database\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n log.explore(\"Failed to flush logs to database\", error=str(e), payload={\"task_id\": task_id})\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\n # [/DEF:_flush_logs:Function]\n" - }, - { - "contract_id": "_flush_task_logs", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 207, - "end_line": 229, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - The task ID.", - "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]" - }, - { - "source_id": "_flush_task_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_flush_task_logs:Function]\n # @COMPLEXITY: 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\n if logs:\n try:\n self.log_persistence_service.add_logs(task_id, logs)\n log.reflect(\"Flushed task logs immediately\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n log.explore(\"Failed to flush task logs immediately\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to flush logs for task {task_id}: {e}\")\n\n # [/DEF:_flush_task_logs:Function]\n" - }, - { - "contract_id": "create_task", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 231, - "end_line": 272, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "user_id (Optional[str]) - ID of the user requesting the task.", - "POST": "Task is created, added to registry, and scheduled for execution.", - "PRE": "Plugin with plugin_id exists. Params are valid.", - "PURPOSE": "Creates and queues a new task for execution.", - "RETURN": "Task - The created task instance.", - "THROWS": "ValueError if plugin not found or params invalid." - }, - "relations": [ - { - "source_id": "create_task", - "relation_type": "[CALLS]", - "target_id": "TaskPersistenceService.persist_task", - "target_ref": "[TaskPersistenceService.persist_task]" - }, - { - "source_id": "create_task", - "relation_type": "[DEPENDS_ON]", - "target_id": "JobLifecycle", - "target_ref": "[JobLifecycle]" - }, - { - "source_id": "create_task", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskGraph", - "target_ref": "[TaskGraph]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "THROWS", - "message": "@THROWS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:create_task:Function]\n # @COMPLEXITY: 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: Optional[str] = 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 log.explore(\"Plugin not found\", error=f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n\n self.plugin_loader.get_plugin(plugin_id)\n\n if not isinstance(params, dict):\n log.explore(\"Invalid task params type\", error=\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n\n log.reason(\"Creating task node and scheduling execution\", payload={\"plugin_id\": plugin_id})\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 trace_id = get_trace_id()\n self.loop.create_task(\n self._run_task(task.id, trace_id=trace_id)\n ) # Schedule task for execution\n log.reflect(\n \"Task creation persisted and execution scheduled\",\n payload={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n\n # [/DEF:create_task:Function]\n" - }, - { - "contract_id": "_run_task", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 274, - "end_line": 367, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - The ID of the task to run.", - "POST": "Task is executed, status updated to SUCCESS or FAILED.", - "PRE": "Task exists in registry.", - "PURPOSE": "Internal method to execute a task with TaskContext support." - }, - "relations": [ - { - "source_id": "_run_task", - "relation_type": "[CALLS]", - "target_id": "TaskPersistenceService.persist_task", - "target_ref": "[TaskPersistenceService.persist_task]" - }, - { - "source_id": "_run_task", - "relation_type": "[DEPENDS_ON]", - "target_id": "JobLifecycle", - "target_ref": "[JobLifecycle]" - }, - { - "source_id": "_run_task", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskContext", - "target_ref": "[TaskContext]" - }, - { - "source_id": "_run_task", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_run_task:Function]\n # @COMPLEXITY: 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, trace_id: str = None):\n if trace_id:\n set_trace_id(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\n log.reason(\n \"Transitioning task to running state\",\n payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\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\n try:\n # Prepare params and check if plugin supports new TaskContext\n params = {**task.params, \"_task_id\": task_id}\n\n # Check if plugin's execute method accepts 'context' parameter\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n\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\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\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 log.reflect(\"Task completed successfully\", payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id})\n except Exception as 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 log.explore(\"Task failed during execution\", error=str(e), payload={\"task_id\": task_id, \"plugin_id\": task.plugin_id})\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 log.reflect(\n \"Task execution finished\",\n payload={\"task_id\": task_id, \"status\": str(task.status)},\n )\n\n # [/DEF:_run_task:Function]\n" - }, - { - "contract_id": "resolve_task", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 369, - "end_line": 395, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "resolution_params (Dict[str, Any]) - Params to resolve the wait.", - "POST": "Task status updated to RUNNING, params updated, execution resumed.", - "PRE": "Task exists and is in AWAITING_MAPPING state.", - "PURPOSE": "Resumes a task that is awaiting mapping.", - "THROWS": "ValueError if task not found or not awaiting mapping." - }, - "relations": [ - { - "source_id": "resolve_task", - "relation_type": "[CALLS]", - "target_id": "TaskPersistenceService.persist_task", - "target_ref": "[TaskPersistenceService.persist_task]" - }, - { - "source_id": "resolve_task", - "relation_type": "[DEPENDS_ON]", - "target_id": "JobLifecycle", - "target_ref": "[JobLifecycle]" - } - ], - "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 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": "unknown_tag", - "tag": "THROWS", - "message": "@THROWS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:resolve_task:Function]\n # @COMPLEXITY: 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\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\n # Signal the future to continue\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n\n # [/DEF:resolve_task:Function]\n" - }, - { - "contract_id": "wait_for_resolution", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 397, - "end_line": 421, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - The ID of the task.", - "POST": "Execution pauses until future is set.", - "PRE": "Task exists.", - "PURPOSE": "Pauses execution and waits for a resolution signal." - }, - "relations": [ - { - "source_id": "wait_for_resolution", - "relation_type": "[CALLS]", - "target_id": "TaskPersistenceService.persist_task", - "target_ref": "[TaskPersistenceService.persist_task]" - }, - { - "source_id": "wait_for_resolution", - "relation_type": "[DEPENDS_ON]", - "target_id": "JobLifecycle", - "target_ref": "[JobLifecycle]" - } - ], - "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 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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:wait_for_resolution:Function]\n # @COMPLEXITY: 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\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n self.task_futures[task_id] = self.loop.create_future()\n\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\n # [/DEF:wait_for_resolution:Function]\n" - }, - { - "contract_id": "wait_for_input", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 423, - "end_line": 445, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - The ID of the task.", - "POST": "Execution pauses until future is set via resume_task_with_password.", - "PRE": "Task exists.", - "PURPOSE": "Pauses execution and waits for user input." - }, - "relations": [ - { - "source_id": "wait_for_input", - "relation_type": "[DEPENDS_ON]", - "target_id": "JobLifecycle", - "target_ref": "[JobLifecycle]" - } - ], - "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 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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:wait_for_input:Function]\n # @COMPLEXITY: 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\n # Status is already set to AWAITING_INPUT by await_input()\n self.task_futures[task_id] = self.loop.create_future()\n\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\n # [/DEF:wait_for_input:Function]\n" - }, - { - "contract_id": "get_task", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 447, - "end_line": 459, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - ID of the task.", - "POST": "Returns Task object or None.", - "PRE": "task_id is a string.", - "PURPOSE": "Retrieves a task by its ID.", - "RETURN": "Optional[Task] - The task or None." - }, - "relations": [ - { - "source_id": "get_task", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskGraph", - "target_ref": "[TaskGraph]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_task:Function]\n # @COMPLEXITY: 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) -> Optional[Task]:\n with belief_scope(\"TaskManager.get_task\", f\"task_id={task_id}\"):\n return self.tasks.get(task_id)\n\n # [/DEF:get_task:Function]\n" - }, - { - "contract_id": "get_all_tasks", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 461, - "end_line": 472, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns list of all Task objects.", - "PRE": "None.", - "PURPOSE": "Retrieves all registered tasks.", - "RETURN": "List[Task] - All tasks." - }, - "relations": [ - { - "source_id": "get_all_tasks", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskGraph", - "target_ref": "[TaskGraph]" - } - ], - "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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_all_tasks:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_all_tasks:Function]\n" - }, - { - "contract_id": "get_tasks", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 474, - "end_line": 518, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "status (Optional[TaskStatus]) - Filter by task status.", - "POST": "Returns a list of tasks sorted by start_time descending.", - "PRE": "limit and offset are non-negative integers.", - "PURPOSE": "Retrieves tasks with pagination and optional status filter.", - "RETURN": "List[Task] - List of tasks matching criteria." - }, - "relations": [ - { - "source_id": "get_tasks", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskGraph", - "target_ref": "[TaskGraph]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_tasks:Function]\n # @COMPLEXITY: 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: Optional[TaskStatus] = None,\n plugin_ids: Optional[List[str]] = 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\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=timezone.utc).timestamp()\n return started_at.timestamp()\n\n tasks.sort(key=sort_key, reverse=True)\n return tasks[offset : offset + limit]\n\n # [/DEF:get_tasks:Function]\n" - }, - { - "contract_id": "get_task_logs", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 520, - "end_line": 556, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "log_filter (Optional[LogFilter]) - Filter parameters.", - "POST": "Returns list of LogEntry or TaskLog objects.", - "PRE": "task_id is a string.", - "PURPOSE": "Retrieves logs for a specific task (from memory for running, persistence for completed).", - "RETURN": "List[LogEntry] - List of log entries." - }, - "relations": [ - { - "source_id": "get_task_logs", - "relation_type": "[CALLS]", - "target_id": "TaskLogPersistenceService.get_logs", - "target_ref": "[TaskLogPersistenceService.get_logs]" - }, - { - "source_id": "get_task_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_task_logs:Function]\n # @COMPLEXITY: 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: Optional[LogFilter] = 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\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\n # For running/pending tasks, return from memory\n return task.logs if task else []\n\n # [/DEF:get_task_logs:Function]\n" - }, - { - "contract_id": "get_task_log_stats", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 558, - "end_line": 571, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - The task ID.", - "POST": "Returns LogStats with counts by level and source.", - "PRE": "task_id is a valid task ID.", - "PURPOSE": "Get statistics about logs for a task.", - "RETURN": "LogStats - Statistics about task logs." - }, - "relations": [ - { - "source_id": "get_task_log_stats", - "relation_type": "[CALLS]", - "target_id": "TaskLogPersistenceService.get_log_stats", - "target_ref": "[TaskLogPersistenceService.get_log_stats]" - }, - { - "source_id": "get_task_log_stats", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_task_log_stats:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_task_log_stats:Function]\n" - }, - { - "contract_id": "get_task_log_sources", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 573, - "end_line": 586, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - The task ID.", - "POST": "Returns list of unique source strings.", - "PRE": "task_id is a valid task ID.", - "PURPOSE": "Get unique sources for a task's logs.", - "RETURN": "List[str] - Unique source names." - }, - "relations": [ - { - "source_id": "get_task_log_sources", - "relation_type": "[CALLS]", - "target_id": "TaskLogPersistenceService.get_sources", - "target_ref": "[TaskLogPersistenceService.get_sources]" - }, - { - "source_id": "get_task_log_sources", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_task_log_sources:Function]\n # @COMPLEXITY: 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\n # [/DEF:get_task_log_sources:Function]\n" - }, - { - "contract_id": "_add_log", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 588, - "end_line": 642, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "context (Optional[Dict]) - Legacy context (for backward compatibility).", - "POST": "Log added to buffer and pushed to queues (if level meets task_log_level filter).", - "PRE": "Task exists.", - "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]" - }, - { - "source_id": "_add_log", - "relation_type": "[DISPATCHES]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [DISPATCHES] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[DISPATCHES]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_add_log:Function]\n # @COMPLEXITY: 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: Optional[Dict[str, Any]] = None,\n context: Optional[Dict[str, Any]] = 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\n # Filter logs based on task_log_level configuration\n if not should_log_task_level(level):\n return\n\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\n # Add to in-memory logs (for backward compatibility with legacy JSON field)\n task.logs.append(log_entry)\n\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\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\n # [/DEF:_add_log:Function]\n" - }, - { - "contract_id": "subscribe_logs", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 644, - "end_line": 660, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - ID of the task.", - "POST": "Returns an asyncio.Queue for log entries.", - "PRE": "task_id is a string.", - "PURPOSE": "Subscribes to real-time logs for a task.", - "RETURN": "asyncio.Queue - Queue for log entries." - }, - "relations": [ - { - "source_id": "subscribe_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:subscribe_logs:Function]\n # @COMPLEXITY: 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\n # [/DEF:subscribe_logs:Function]\n" - }, - { - "contract_id": "unsubscribe_logs", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 662, - "end_line": 678, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "queue (asyncio.Queue) - Queue to remove.", - "POST": "Queue removed from subscribers.", - "PRE": "task_id is a string, queue is asyncio.Queue.", - "PURPOSE": "Unsubscribes from real-time logs for a task." - }, - "relations": [ - { - "source_id": "unsubscribe_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:unsubscribe_logs:Function]\n # @COMPLEXITY: 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\n # [/DEF:unsubscribe_logs:Function]\n" - }, - { - "contract_id": "load_persisted_tasks", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 680, - "end_line": 694, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Persisted tasks loaded into self.tasks.", - "PRE": "None.", - "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]" - }, - { - "source_id": "load_persisted_tasks", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskGraph", - "target_ref": "[TaskGraph]" - } - ], - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:load_persisted_tasks:Function]\n # @COMPLEXITY: 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\n # [/DEF:load_persisted_tasks:Function]\n" - }, - { - "contract_id": "await_input", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 696, - "end_line": 727, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "input_request (Dict) - Details about required input.", - "POST": "Task status changed to AWAITING_INPUT, input_request set, persisted.", - "PRE": "Task exists and is in RUNNING state.", - "PURPOSE": "Transition a task to AWAITING_INPUT state with input request.", - "THROWS": "ValueError if task not found or not RUNNING." - }, - "relations": [ - { - "source_id": "await_input", - "relation_type": "[CALLS]", - "target_id": "TaskPersistenceService.persist_task", - "target_ref": "[TaskPersistenceService.persist_task]" - }, - { - "source_id": "await_input", - "relation_type": "[DEPENDS_ON]", - "target_id": "JobLifecycle", - "target_ref": "[JobLifecycle]" - } - ], - "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 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": "unknown_tag", - "tag": "THROWS", - "message": "@THROWS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:await_input:Function]\n # @COMPLEXITY: 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\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\n # [/DEF:await_input:Function]\n" - }, - { - "contract_id": "resume_task_with_password", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 729, - "end_line": 771, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "passwords (Dict[str, str]) - Mapping of database name to password.", - "POST": "Task status changed to RUNNING, passwords injected, task resumed.", - "PRE": "Task exists and is in AWAITING_INPUT state.", - "PURPOSE": "Resume a task that is awaiting input with provided passwords.", - "THROWS": "ValueError if task not found, not awaiting input, or passwords invalid." - }, - "relations": [ - { - "source_id": "resume_task_with_password", - "relation_type": "[CALLS]", - "target_id": "TaskPersistenceService.persist_task", - "target_ref": "[TaskPersistenceService.persist_task]" - }, - { - "source_id": "resume_task_with_password", - "relation_type": "[DEPENDS_ON]", - "target_id": "JobLifecycle", - "target_ref": "[JobLifecycle]" - } - ], - "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 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": "unknown_tag", - "tag": "THROWS", - "message": "@THROWS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:resume_task_with_password:Function]\n # @COMPLEXITY: 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\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n\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\n if task_id in self.task_futures:\n self.task_futures[task_id].set_result(True)\n\n # [/DEF:resume_task_with_password:Function]\n" - }, - { - "contract_id": "clear_tasks", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 773, - "end_line": 826, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "status (Optional[TaskStatus]) - Filter by task status.", - "POST": "Tasks matching filter (or all non-active) cleared from registry and database.", - "PRE": "status is Optional[TaskStatus].", - "PURPOSE": "Clears tasks based on status filter (also deletes associated logs).", - "RETURN": "int - Number of tasks cleared." - }, - "relations": [ - { - "source_id": "clear_tasks", - "relation_type": "[CALLS]", - "target_id": "TaskPersistenceService.delete_tasks", - "target_ref": "[TaskPersistenceService.delete_tasks]" - }, - { - "source_id": "clear_tasks", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskGraph", - "target_ref": "[TaskGraph]" - }, - { - "source_id": "clear_tasks", - "relation_type": "[DEPENDS_ON]", - "target_id": "EventBus", - "target_ref": "[EventBus]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:clear_tasks:Function]\n # @COMPLEXITY: 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: Optional[TaskStatus] = 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\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\n if should_remove:\n tasks_to_remove.append(task_id)\n\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\n del self.tasks[tid]\n\n # Remove from persistence (task_records and task_logs via CASCADE)\n self.persistence_service.delete_tasks(tasks_to_remove)\n\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\n log.reflect(\"Tasks cleared from registry and database\", payload={\"count\": len(tasks_to_remove)})\n return len(tasks_to_remove)\n\n # [/DEF:clear_tasks:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "TaskManagerModels", "contract_type": "Module", "file_path": "backend/src/core/task_manager/models.py", "start_line": 1, - "end_line": 154, + "end_line": 112, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "CONSTRAINT": "Must use Pydantic for data validation.", "INVARIANT": "Task IDs are immutable once created.", "LAYER": "Core", - "PURPOSE": "Defines the data models and enumerations used by the Task Manager.", - "SEMANTICS": [ - "task", - "models", - "pydantic", - "enum", - "state" - ] + "PURPOSE": "Defines the data models and enumerations used by the Task Manager." }, "relations": [ { "source_id": "TaskManagerModels", - "relation_type": "[USED_BY]", + "relation_type": "USED_BY", "target_id": "TaskManager", "target_ref": "[TaskManager]" }, { "source_id": "TaskManagerModels", - "relation_type": "[USED_BY]", + "relation_type": "USED_BY", "target_id": "TaskManagerPackage", "target_ref": "[TaskManagerPackage]" } @@ -46919,7 +39241,7 @@ { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate [USED_BY] is not in allowed_predicates", + "message": "Predicate USED_BY is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -46931,13 +39253,13 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "[USED_BY]" + "predicate": "USED_BY" } }, { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate [USED_BY] is not in allowed_predicates", + "message": "Predicate USED_BY is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -46949,149 +39271,95 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "[USED_BY]" + "predicate": "USED_BY" } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskManagerModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: task, models, pydantic, enum, state\n# @PURPOSE: 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.\n\n# [SECTION: IMPORTS]\nimport uuid\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Dict, Any, List, Optional\n\nfrom pydantic import BaseModel, Field\n# [/SECTION]\n\n\n# [DEF:TaskStatus: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\n\n# [/DEF:TaskStatus:Enum]\n\n\n# [DEF:LogLevel:Enum]\nclass LogLevel(str, Enum):\n DEBUG = \"DEBUG\"\n INFO = \"INFO\"\n WARNING = \"WARNING\"\n ERROR = \"ERROR\"\n\n\n# [/DEF:LogLevel:Enum]\n\n\n# [DEF:LogEntry:Class]\n# @PURPOSE: A Pydantic model representing a single, structured log entry associated with a task.\n# @COMPLEXITY: 2\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: Optional[Dict[str, Any]] = (\n None # Legacy field, kept for backward compatibility\n )\n metadata: Optional[Dict[str, Any]] = (\n None # Structured metadata (e.g., dashboard_id, progress)\n )\n\n\n# [/DEF:LogEntry:Class]\n\n\n# [DEF:TaskLog:Class]\n# @SEMANTICS: task, log, persistent, pydantic\n# @PURPOSE: A Pydantic model representing a persisted log entry from the database.\n# @COMPLEXITY: 3\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: Optional[Dict[str, Any]] = None\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:TaskLog:Class]\n\n\n# [DEF:LogFilter:Class]\nclass LogFilter(BaseModel):\n level: Optional[str] = None # Filter by log level\n source: Optional[str] = None # Filter by source component\n search: Optional[str] = None # Text search in message\n offset: int = Field(default=0, ge=0)\n limit: int = Field(default=100, ge=1, le=1000)\n\n\n# [/DEF:LogFilter:Class]\n\n\n# [DEF:LogStats:Class]\n# @SEMANTICS: log, stats, aggregation, pydantic\n# @PURPOSE: Statistics about log entries for a task.\n# @COMPLEXITY: 1\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\n\n# [/DEF:LogStats:Class]\n\n\n# [DEF:Task:Class]\n# @COMPLEXITY: 3\n# @SEMANTICS: task, job, execution, state, pydantic\n# @PURPOSE: 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: Optional[datetime] = None\n finished_at: Optional[datetime] = None\n user_id: Optional[str] = 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: Optional[Dict[str, Any]] = None\n # Result payload can be dict/list/scalar depending on plugin and legacy records.\n result: Optional[Any] = None\n\n # [DEF:__init__: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\n # [/DEF:__init__:Function]\n\n\n# [/DEF:Task:Class]\n\n# [/DEF:TaskManagerModels:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "TaskStatus", "contract_type": "Enum", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 21, + "start_line": 16, + "end_line": 24, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" + }, + { + "contract_id": "LogLevel", + "contract_type": "Enum", + "file_path": "backend/src/core/task_manager/models.py", + "start_line": 25, "end_line": 31, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TaskStatus: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\n\n# [/DEF:TaskStatus:Enum]\n" - }, - { - "contract_id": "LogLevel", - "contract_type": "Enum", - "file_path": "backend/src/core/task_manager/models.py", - "start_line": 34, - "end_line": 42, - "tier": "TIER_1", - "complexity": 1, - "metadata": {}, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:LogLevel:Enum]\nclass LogLevel(str, Enum):\n DEBUG = \"DEBUG\"\n INFO = \"INFO\"\n WARNING = \"WARNING\"\n ERROR = \"ERROR\"\n\n\n# [/DEF:LogLevel:Enum]\n" + "anchor_syntax": "region", + "body": "# #region LogLevel [TYPE Enum]\nclass LogLevel(str, Enum):\n DEBUG = \"DEBUG\"\n INFO = \"INFO\"\n WARNING = \"WARNING\"\n ERROR = \"ERROR\"\n# #endregion LogLevel\n" }, { "contract_id": "TaskLog", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 71, - "end_line": 89, + "start_line": 53, + "end_line": 66, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "A Pydantic model representing a persisted log entry from the database.", - "SEMANTICS": [ - "task", - "log", - "persistent", - "pydantic" - ] + "COMPLEXITY": 3, + "PURPOSE": "A Pydantic model representing a persisted log entry from the database." }, "relations": [ { "source_id": "TaskLog", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskLogRecord", "target_ref": "[TaskLogRecord]" } ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TaskLog:Class]\n# @SEMANTICS: task, log, persistent, pydantic\n# @PURPOSE: A Pydantic model representing a persisted log entry from the database.\n# @COMPLEXITY: 3\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: Optional[Dict[str, Any]] = None\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:TaskLog:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region TaskLog [C:3] [TYPE Class] [SEMANTICS task, log, persistent, pydantic]\n# @BRIEF A Pydantic model representing a persisted log entry from the database.\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\nclass TaskLog(BaseModel):\n id: int\n task_id: str\n timestamp: datetime\n level: str\n source: str\n message: str\n metadata: dict[str, Any] | None = None\n class Config:\n from_attributes = True\n# #endregion TaskLog\n" }, { "contract_id": "LogFilter", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 92, - "end_line": 101, + "start_line": 67, + "end_line": 74, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:LogFilter:Class]\nclass LogFilter(BaseModel):\n level: Optional[str] = None # Filter by log level\n source: Optional[str] = None # Filter by source component\n search: Optional[str] = None # Text search in message\n offset: int = Field(default=0, ge=0)\n limit: int = Field(default=100, ge=1, le=1000)\n\n\n# [/DEF:LogFilter:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "LogStats", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 104, - "end_line": 115, + "start_line": 75, + "end_line": 82, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Statistics about log entries for a task.", - "SEMANTICS": [ - "log", - "stats", - "aggregation", - "pydantic" - ] + "COMPLEXITY": 1, + "PURPOSE": "Statistics about log entries for a task." }, "relations": [ { "source_id": "LogStats", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskLog", "target_ref": "[TaskLog]" } @@ -47106,29 +39374,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -47137,191 +39382,81 @@ "actual_complexity": 1, "contract_type": "Class" } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:LogStats:Class]\n# @SEMANTICS: log, stats, aggregation, pydantic\n# @PURPOSE: Statistics about log entries for a task.\n# @COMPLEXITY: 1\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\n\n# [/DEF:LogStats:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "Task", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 118, - "end_line": 152, + "start_line": 83, + "end_line": 111, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs.", - "SEMANTICS": [ - "task", - "job", - "execution", - "state", - "pydantic" - ] + "COMPLEXITY": 3, + "PURPOSE": "A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs." }, "relations": [ { "source_id": "Task", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskStatus", "target_ref": "[TaskStatus]" }, { "source_id": "Task", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LogEntry", "target_ref": "[LogEntry]" }, { "source_id": "Task", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" } ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:Task:Class]\n# @COMPLEXITY: 3\n# @SEMANTICS: task, job, execution, state, pydantic\n# @PURPOSE: 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: Optional[datetime] = None\n finished_at: Optional[datetime] = None\n user_id: Optional[str] = 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: Optional[Dict[str, Any]] = None\n # Result payload can be dict/list/scalar depending on plugin and legacy records.\n result: Optional[Any] = None\n\n # [DEF:__init__: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\n # [/DEF:__init__:Function]\n\n\n# [/DEF:Task:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "TaskPersistenceModule", "contract_type": "Module", "file_path": "backend/src/core/task_manager/persistence.py", "start_line": 1, - "end_line": 655, + "end_line": 535, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[Task, LogEntry] -> Model[TaskRecord, TaskLogRecord]", "INVARIANT": "Database schema must match the TaskRecord model structure.", "LAYER": "Core", "POST": "Provides reliable storage and retrieval for task metadata and logs.", "PRE": "Tasks database must be initialized with TaskRecord and TaskLogRecord schemas.", "PURPOSE": "Handles the persistence of tasks using SQLAlchemy and the tasks.db database.", - "SEMANTICS": [ - "persistence", - "sqlite", - "sqlalchemy", - "task", - "storage" - ], "SIDE_EFFECT": "Performs database I/O on tasks.db." }, "relations": [ { "source_id": "TaskPersistenceModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" }, { "source_id": "TaskPersistenceModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskGraph", "target_ref": "[TaskGraph]" }, { "source_id": "TaskPersistenceModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TasksSessionLocal", "target_ref": "[TasksSessionLocal]" } @@ -47344,145 +39479,62 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskPersistenceModule:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: persistence, sqlite, sqlalchemy, task, storage\n# @PURPOSE: Handles the persistence of tasks using SQLAlchemy and the tasks.db database.\n# @LAYER: Core\n# @PRE: Tasks database must be initialized with TaskRecord and TaskLogRecord schemas.\n# @POST: Provides reliable storage and retrieval for task metadata and logs.\n# @SIDE_EFFECT: Performs database I/O on tasks.db.\n# @DATA_CONTRACT: Input[Task, LogEntry] -> Model[TaskRecord, TaskLogRecord]\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n# @RELATION: [DEPENDS_ON] ->[TaskGraph]\n# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal]\n# @INVARIANT: Database schema must match the TaskRecord model structure.\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom typing import List, Optional\nimport json\nimport re\n\nfrom sqlalchemy.orm import Session\nfrom ...models.task import TaskRecord, TaskLogRecord\nfrom ...models.mapping import Environment\nfrom ..database import TasksSessionLocal\nfrom .models import Task, TaskStatus, LogEntry, TaskLog, LogFilter, LogStats\nfrom ..logger import logger, belief_scope\nfrom ..cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"TaskPersistence\")\nlog_pl = MarkerLogger(\"TaskLogPersistence\")\n# [/SECTION]\n\n\n# [DEF:TaskPersistenceService:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: persistence, service, database, sqlalchemy\n# @PURPOSE: Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.\n# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, Task inputs must expose id/plugin_id/status/params/result/logs fields, and TaskRecord plus Environment schemas must be available.\n# @POST: Persist operations leave matching TaskRecord rows committed or rolled back without leaking sessions, load operations return reconstructed Task objects from stored TaskRecord rows, and delete operations remove only the addressed task rows.\n# @SIDE_EFFECT: Opens SQLAlchemy sessions, reads and writes task_records rows, resolves environment foreign keys against environments, commits or rolls back transactions, and emits error logs on persistence failures.\n# @DATA_CONTRACT: Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]\n# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal]\n# @RELATION: [DEPENDS_ON] ->[TaskRecord]\n# @RELATION: [DEPENDS_ON] ->[Environment]\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n# @RELATION: [DEPENDS_ON] ->[TaskGraph]\n# @INVARIANT: Persistence must handle potentially missing task fields natively.\n#\n# @TEST_CONTRACT: TaskPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"persist_task creates or updates a record\",\n# \"load_tasks retrieves valid Task instances\",\n# \"delete_tasks correctly removes records from the database\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_persistence -> {\"task_id\": \"123\", \"status\": \"PENDING\"}\n# @TEST_EDGE: persist_invalid_task_type -> raises Exception\n# @TEST_EDGE: load_corrupt_json_params -> handled gracefully\n# @TEST_INVARIANT: accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params]\nclass TaskPersistenceService:\n # [DEF:_json_load_if_needed:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Safely load JSON strings from DB if necessary\n # @PRE: value is an arbitrary database value\n # @POST: Returns parsed JSON object, list, string, or primitive\n @staticmethod\n def _json_load_if_needed(value):\n with belief_scope(\"TaskPersistenceService._json_load_if_needed\"):\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n stripped = value.strip()\n if stripped == \"\" or stripped.lower() == \"null\":\n return None\n try:\n return json.loads(stripped)\n except json.JSONDecodeError:\n return value\n return value\n\n # [/DEF:_json_load_if_needed:Function]\n\n # [DEF:_parse_datetime:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE: value is an ISO string or datetime object\n # @POST: Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n with belief_scope(\"TaskPersistenceService._parse_datetime\"):\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n\n # [/DEF:_parse_datetime:Function]\n\n # [DEF:_resolve_environment_id:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE: Session is active\n # @POST: Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT: Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION: [DEPENDS_ON] ->[Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: Optional[str]\n ) -> Optional[str]:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n\n return None\n\n # [/DEF:_resolve_environment_id:Function]\n\n # [DEF:__init__:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initializes the persistence service.\n # @PRE: None.\n # @POST: Service is ready.\n def __init__(self):\n with belief_scope(\"TaskPersistenceService.__init__\"):\n # We use TasksSessionLocal from database.py\n pass\n\n # [/DEF:__init__:Function]\n\n # [DEF:persist_task:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE: isinstance(task, Task)\n # @POST: Task record created or updated in database.\n # @PARAM: task (Task) - The task object to persist.\n # @SIDE_EFFECT: Writes to task_records table in tasks.db\n # @DATA_CONTRACT: Input[Task] -> Model[TaskRecord]\n # @RELATION: [CALLS] ->[_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n log.reason(\"Persisting task to database\", payload={\"task_id\": task.id})\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n with belief_scope(\"TaskPersistenceService.json_serializable\"):\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log_entry in task.logs:\n log_dict = log_entry.dict()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log_entry in reversed(task.logs):\n if log_entry.level == \"ERROR\":\n record.error = log_entry.message\n break\n\n session.commit()\n log.reflect(\"Task persisted successfully\", payload={\"task_id\": task.id})\n except Exception as e:\n session.rollback()\n log.explore(\"Failed to persist task\", error=str(e), payload={\"task_id\": task.id})\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:persist_task:Function]\n\n # [DEF:persist_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persists multiple tasks.\n # @PRE: isinstance(tasks, list)\n # @POST: All tasks in list are persisted.\n # @PARAM: tasks (List[Task]) - The list of tasks to persist.\n # @RELATION: [CALLS] ->[persist_task]\n def persist_tasks(self, tasks: List[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n log.reason(\"Persisting multiple tasks\", payload={\"count\": len(tasks)})\n for task in tasks:\n self.persist_task(task)\n log.reflect(\"All tasks persisted\", payload={\"count\": len(tasks)})\n\n # [/DEF:persist_tasks:Function]\n\n # [DEF:load_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Loads tasks from the database.\n # @PRE: limit is an integer.\n # @POST: Returns list of Task objects.\n # @PARAM: limit (int) - Max tasks to load.\n # @PARAM: status (Optional[TaskStatus]) - Filter by status.\n # @RETURN: List[Task] - The loaded tasks.\n # @DATA_CONTRACT: Model[TaskRecord] -> Output[List[Task]]\n # @RELATION: [CALLS] ->[_json_load_if_needed]\n # @RELATION: [CALLS] ->[_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: Optional[TaskStatus] = None\n ) -> List[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n log.reason(\"Loading tasks from database\", payload={\"limit\": limit, \"status\": str(status) if status else None})\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.utcnow()\n )\n logs.append(LogEntry(**log_data))\n\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n log.explore(\"Failed to reconstruct task from record\", error=str(e), payload={\"record_id\": record.id})\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n\n log.reflect(\"Tasks loaded from database\", payload={\"count\": len(loaded_tasks)})\n return loaded_tasks\n finally:\n session.close()\n\n # [/DEF:load_tasks:Function]\n\n # [DEF:delete_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE: task_ids is a list of strings.\n # @POST: Specified task records deleted from database.\n # @PARAM: task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT: Deletes rows from task_records table.\n # @RELATION: [DEPENDS_ON] ->[TaskRecord]\n def delete_tasks(self, task_ids: List[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n log.reason(\"Deleting tasks from database\", payload={\"count\": len(task_ids)})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n log.reflect(\"Tasks deleted from database\", payload={\"count\": len(task_ids)})\n except Exception as e:\n session.rollback()\n log.explore(\"Failed to delete tasks\", error=str(e))\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_tasks:Function]\n\n\n# [/DEF:TaskPersistenceService:Class]\n\n\n# [DEF:TaskLogPersistenceService:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: persistence, service, database, log, sqlalchemy\n# @PURPOSE: Provides methods to store, query, summarize, and delete task log rows in the task_logs table.\n# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, task_id inputs must identify task log rows, LogEntry batches must expose timestamp/level/source/message/metadata fields, and LogFilter inputs must provide pagination and filter attributes used by queries.\n# @POST: add_logs commits all provided log entries or rolls back on failure, query methods return TaskLog or LogStats views reconstructed from TaskLogRecord rows, and delete methods remove only log rows matching the supplied task identifiers.\n# @SIDE_EFFECT: Opens SQLAlchemy sessions, inserts, reads, aggregates, and deletes task_logs rows, serializes log metadata to JSON, commits or rolls back transactions, and emits error logs on persistence failures.\n# @DATA_CONTRACT: Input[task_id:str, logs:List[LogEntry], log_filter:LogFilter, task_ids:List[str]] -> Model[TaskLogRecord] -> Output[None | List[TaskLog] | LogStats | List[str]]\n# @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal]\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n# @RELATION: [DEPENDS_ON] ->[EventBus]\n# @INVARIANT: Log entries are batch-inserted for performance.\n#\n# @TEST_CONTRACT: TaskLogPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"add_logs efficiently saves logs to the database\",\n# \"get_logs retrieves properly filtered LogEntry objects\"\n# ]\n# }\n# @TEST_FIXTURE: valid_log_batch -> {\"task_id\": \"123\", \"logs\": [{\"level\": \"INFO\", \"message\": \"msg\"}]}\n# @TEST_EDGE: empty_log_list -> no-op behavior\n# @TEST_EDGE: add_logs_db_error -> rollback and log error\n# @TEST_INVARIANT: accurate_log_aggregation -> verifies: [valid_log_batch]\nclass TaskLogPersistenceService:\n \"\"\"\n Service for persisting and querying task logs.\n Supports batch inserts, filtering, and statistics.\n \"\"\"\n\n # [DEF:__init__:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initializes the TaskLogPersistenceService\n # @PRE: config is provided or defaults are used\n # @POST: Service is ready for log persistence\n def __init__(self, config=None):\n pass\n\n # [/DEF:__init__:Function]\n\n # [DEF:add_logs:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Batch insert log entries for a task.\n # @PRE: logs is a list of LogEntry objects.\n # @POST: All logs inserted into task_logs table.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: logs (List[LogEntry]) - Log entries to insert.\n # @SIDE_EFFECT: Writes to task_logs table.\n # @DATA_CONTRACT: Input[List[LogEntry]] -> Model[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def add_logs(self, task_id: str, logs: List[LogEntry]) -> None:\n if not logs:\n return\n with belief_scope(\"TaskLogPersistenceService.add_logs\", f\"task_id={task_id}\"):\n log_pl.reason(\"Batch inserting log entries\", payload={\"task_id\": task_id, \"count\": len(logs)})\n session: Session = TasksSessionLocal()\n try:\n for log_entry in logs:\n record = TaskLogRecord(\n task_id=task_id,\n timestamp=log_entry.timestamp,\n level=log_entry.level,\n source=log_entry.source or \"system\",\n message=log_entry.message,\n metadata_json=json.dumps(log_entry.metadata)\n if log_entry.metadata\n else None,\n )\n session.add(record)\n session.commit()\n log_pl.reflect(\"Log entries persisted\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to persist log entries\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to add logs for task {task_id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:add_logs:Function]\n\n # [DEF:get_logs:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Query logs for a task with filtering and pagination.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of TaskLog objects matching filters.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: log_filter (LogFilter) - Filter parameters.\n # @RETURN: List[TaskLog] - Filtered log entries.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[TaskLog]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogFilter]\n # @RELATION: [DEPENDS_ON] ->[TaskLog]\n def get_logs(self, task_id: str, log_filter: LogFilter) -> List[TaskLog]:\n with belief_scope(\"TaskLogPersistenceService.get_logs\", f\"task_id={task_id}\"):\n log_pl.reason(\"Querying task logs\", payload={\"task_id\": task_id, \"filter_level\": log_filter.level})\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n )\n\n # Apply filters\n if log_filter.level:\n query = query.filter(\n TaskLogRecord.level == log_filter.level.upper()\n )\n if log_filter.source:\n query = query.filter(TaskLogRecord.source == log_filter.source)\n if log_filter.search:\n search_pattern = f\"%{log_filter.search}%\"\n query = query.filter(TaskLogRecord.message.ilike(search_pattern))\n\n # Order by timestamp ascending (oldest first)\n query = query.order_by(TaskLogRecord.timestamp.asc())\n\n # Apply pagination\n records = query.offset(log_filter.offset).limit(log_filter.limit).all()\n\n logs = []\n for record in records:\n metadata = None\n if record.metadata_json:\n try:\n metadata = json.loads(record.metadata_json)\n except json.JSONDecodeError:\n metadata = None\n\n logs.append(\n TaskLog(\n id=record.id,\n task_id=record.task_id,\n timestamp=record.timestamp,\n level=record.level,\n source=record.source,\n message=record.message,\n metadata=metadata,\n )\n )\n\n log_pl.reflect(\"Task logs retrieved\", payload={\"task_id\": task_id, \"count\": len(logs)})\n return logs\n finally:\n session.close()\n\n # [/DEF:get_logs:Function]\n\n # [DEF:get_log_stats:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns LogStats with counts by level and source.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: LogStats - Statistics about task logs.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[LogStats]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogStats]\n def get_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\n \"TaskLogPersistenceService.get_log_stats\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Aggregating log statistics\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n # Get total count\n total_count = (\n session.query(TaskLogRecord)\n .filter(TaskLogRecord.task_id == task_id)\n .count()\n )\n\n # Get counts by level\n from sqlalchemy import func\n\n level_counts = (\n session.query(TaskLogRecord.level, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.level)\n .all()\n )\n\n by_level = {level: count for level, count in level_counts}\n\n # Get counts by source\n source_counts = (\n session.query(TaskLogRecord.source, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.source)\n .all()\n )\n\n by_source = {source: count for source, count in source_counts}\n\n log_pl.reflect(\"Log statistics computed\", payload={\"task_id\": task_id, \"total\": total_count})\n return LogStats(\n total_count=total_count, by_level=by_level, by_source=by_source\n )\n finally:\n session.close()\n\n # [/DEF:get_log_stats:Function]\n\n # [DEF:get_sources:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of unique source strings.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: List[str] - Unique source names.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[str]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def get_sources(self, task_id: str) -> List[str]:\n with belief_scope(\n \"TaskLogPersistenceService.get_sources\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Querying distinct log sources\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n from sqlalchemy import distinct\n\n sources = (\n session.query(distinct(TaskLogRecord.source))\n .filter(TaskLogRecord.task_id == task_id)\n .all()\n )\n result = [s[0] for s in sources]\n log_pl.reflect(\"Log sources retrieved\", payload={\"task_id\": task_id, \"sources\": result})\n return result\n finally:\n session.close()\n\n # [/DEF:get_sources:Function]\n\n # [DEF:delete_logs_for_task:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete all logs for a specific task.\n # @PRE: task_id is a valid task ID.\n # @POST: All logs for the task are deleted.\n # @PARAM: task_id (str) - The task ID.\n # @SIDE_EFFECT: Deletes from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_task(self, task_id: str) -> None:\n with belief_scope(\n \"TaskLogPersistenceService.delete_logs_for_task\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Deleting logs for task\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n ).delete(synchronize_session=False)\n session.commit()\n log_pl.reflect(\"Task logs deleted\", payload={\"task_id\": task_id})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to delete task logs\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to delete logs for task {task_id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_logs_for_task:Function]\n\n # [DEF:delete_logs_for_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete all logs for multiple tasks.\n # @PRE: task_ids is a list of task IDs.\n # @POST: All logs for the tasks are deleted.\n # @PARAM: task_ids (List[str]) - List of task IDs.\n # @SIDE_EFFECT: Deletes rows from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_tasks(self, task_ids: List[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskLogPersistenceService.delete_logs_for_tasks\"):\n log_pl.reason(\"Deleting logs for multiple tasks\", payload={\"count\": len(task_ids)})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id.in_(task_ids)\n ).delete(synchronize_session=False)\n session.commit()\n log_pl.reflect(\"Logs deleted for multiple tasks\", payload={\"count\": len(task_ids)})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to delete logs for tasks\", error=str(e))\n logger.error(f\"Failed to delete logs for tasks: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_logs_for_tasks:Function]\n\n\n# [/DEF:TaskLogPersistenceService:Class]\n# [/DEF:TaskPersistenceModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region TaskPersistenceModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, task, search, task-persistence-service]\n# @BRIEF Handles the persistence of tasks using SQLAlchemy and the tasks.db database.\n# @LAYER: Core\n# @PRE: Tasks database must be initialized with TaskRecord and TaskLogRecord schemas.\n# @POST: Provides reliable storage and retrieval for task metadata and logs.\n# @SIDE_EFFECT: Performs database I/O on tasks.db.\n# @DATA_CONTRACT: Input[Task, LogEntry] -> Model[TaskRecord, TaskLogRecord]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @INVARIANT: Database schema must match the TaskRecord model structure.\nimport json\nimport re\nfrom datetime import datetime\n\nfrom sqlalchemy.orm import Session\n\nfrom ...models.mapping import Environment\nfrom ...models.task import TaskLogRecord, TaskRecord\nfrom ..database import TasksSessionLocal\nfrom ..logger import belief_scope, logger\nfrom .models import LogEntry, LogFilter, LogStats, Task, TaskLog, TaskStatus\n\n\n# #region TaskPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, sqlalchemy]\n# @BRIEF Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.\n# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, Task inputs must expose id/plugin_id/status/params/result/logs fields, and TaskRecord plus Environment schemas must be available.\n# @POST: Persist operations leave matching TaskRecord rows committed or rolled back without leaking sessions, load operations return reconstructed Task objects from stored TaskRecord rows, and delete operations remove only the addressed task rows.\n# @SIDE_EFFECT: Opens SQLAlchemy sessions, reads and writes task_records rows, resolves environment foreign keys against environments, commits or rolls back transactions, and emits error logs on persistence failures.\n# @DATA_CONTRACT: Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskRecord]\n# @RELATION DEPENDS_ON -> [Environment]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @INVARIANT: Persistence must handle potentially missing task fields natively.\n#\n# @TEST_CONTRACT: TaskPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"persist_task creates or updates a record\",\n# \"load_tasks retrieves valid Task instances\",\n# \"delete_tasks correctly removes records from the database\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_persistence -> {\"task_id\": \"123\", \"status\": \"PENDING\"}\n# @TEST_EDGE: persist_invalid_task_type -> raises Exception\n# @TEST_EDGE: load_corrupt_json_params -> handled gracefully\n# @TEST_INVARIANT: accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params]\nclass TaskPersistenceService:\n # #region _json_load_if_needed [TYPE Function] [C:1]\n # @PURPOSE: Safely load JSON strings from DB if necessary\n # @PRE: value is an arbitrary database value\n # @POST: Returns parsed JSON object, list, string, or primitive\n @staticmethod\n def _json_load_if_needed(value):\n # Hot-path utility — no entry/exit logging to reduce noise\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n stripped = value.strip()\n if stripped == \"\" or stripped.lower() == \"null\":\n return None\n try:\n return json.loads(stripped)\n except json.JSONDecodeError:\n return value\n return value\n # #endregion _json_load_if_needed\n # #region _parse_datetime [TYPE Function] [C:1]\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE: value is an ISO string or datetime object\n # @POST: Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n # Hot-path utility — no entry/exit logging to reduce startup noise\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n # #endregion _parse_datetime\n # #region _resolve_environment_id [TYPE Function] [C:3]\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE: Session is active\n # @POST: Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT: Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION: [DEPENDS_ON] ->[Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: str | None\n ) -> str | None:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n return None\n # #endregion _resolve_environment_id\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the persistence service.\n # @PRE: None.\n # @POST: Service is ready.\n def __init__(self):\n with belief_scope(\"TaskPersistenceService.__init__\"):\n # We use TasksSessionLocal from database.py\n pass\n # #endregion __init__\n # #region persist_task [TYPE Function] [C:3]\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE: isinstance(task, Task)\n # @POST: Task record created or updated in database.\n # @PARAM: task (Task) - The task object to persist.\n # @SIDE_EFFECT: Writes to task_records table in tasks.db\n # @DATA_CONTRACT: Input[Task] -> Model[TaskRecord]\n # @RELATION: [CALLS] ->[_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n # Hot-path utility — no entry/exit logging\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log in task.logs:\n log_dict = log.dict()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log in reversed(task.logs):\n if log.level == \"ERROR\":\n record.error = log.message\n break\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n # #endregion persist_task\n # #region persist_tasks [TYPE Function] [C:3]\n # @PURPOSE: Persists multiple tasks.\n # @PRE: isinstance(tasks, list)\n # @POST: All tasks in list are persisted.\n # @PARAM: tasks (List[Task]) - The list of tasks to persist.\n # @RELATION: [CALLS] ->[persist_task]\n def persist_tasks(self, tasks: list[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n for task in tasks:\n self.persist_task(task)\n # #endregion persist_tasks\n # #region load_tasks [TYPE Function] [C:3]\n # @PURPOSE: Loads tasks from the database.\n # @PRE: limit is an integer.\n # @POST: Returns list of Task objects.\n # @PARAM: limit (int) - Max tasks to load.\n # @PARAM: status (Optional[TaskStatus]) - Filter by status.\n # @RETURN: List[Task] - The loaded tasks.\n # @DATA_CONTRACT: Model[TaskRecord] -> Output[List[Task]]\n # @RELATION: [CALLS] ->[_json_load_if_needed]\n # @RELATION: [CALLS] ->[_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: TaskStatus | None = None\n ) -> list[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.utcnow()\n )\n logs.append(LogEntry(**log_data))\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n return loaded_tasks\n finally:\n session.close()\n # #endregion load_tasks\n # #region delete_tasks [TYPE Function] [C:3]\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE: task_ids is a list of strings.\n # @POST: Specified task records deleted from database.\n # @PARAM: task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT: Deletes rows from task_records table.\n # @RELATION: [DEPENDS_ON] ->[TaskRecord]\n def delete_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_tasks\n# #endregion TaskPersistenceService\n# #region TaskLogPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, log, sqlalchemy]\n# @BRIEF Provides methods to store, query, summarize, and delete task log rows in the task_logs table.\n# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, task_id inputs must identify task log rows, LogEntry batches must expose timestamp/level/source/message/metadata fields, and LogFilter inputs must provide pagination and filter attributes used by queries.\n# @POST: add_logs commits all provided log entries or rolls back on failure, query methods return TaskLog or LogStats views reconstructed from TaskLogRecord rows, and delete methods remove only log rows matching the supplied task identifiers.\n# @SIDE_EFFECT: Opens SQLAlchemy sessions, inserts, reads, aggregates, and deletes task_logs rows, serializes log metadata to JSON, commits or rolls back transactions, and emits error logs on persistence failures.\n# @DATA_CONTRACT: Input[task_id:str, logs:List[LogEntry], log_filter:LogFilter, task_ids:List[str]] -> Model[TaskLogRecord] -> Output[None | List[TaskLog] | LogStats | List[str]]\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT: Log entries are batch-inserted for performance.\n#\n# @TEST_CONTRACT: TaskLogPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"add_logs efficiently saves logs to the database\",\n# \"get_logs retrieves properly filtered LogEntry objects\"\n# ]\n# }\n# @TEST_FIXTURE: valid_log_batch -> {\"task_id\": \"123\", \"logs\": [{\"level\": \"INFO\", \"message\": \"msg\"}]}\n# @TEST_EDGE: empty_log_list -> no-op behavior\n# @TEST_EDGE: add_logs_db_error -> rollback and log error\n# @TEST_INVARIANT: accurate_log_aggregation -> verifies: [valid_log_batch]\nclass TaskLogPersistenceService:\n \"\"\"\n Service for persisting and querying task logs.\n Supports batch inserts, filtering, and statistics.\n \"\"\"\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the TaskLogPersistenceService\n # @PRE: config is provided or defaults are used\n # @POST: Service is ready for log persistence\n def __init__(self, config=None):\n pass\n # #endregion __init__\n # #region add_logs [TYPE Function] [C:3]\n # @PURPOSE: Batch insert log entries for a task.\n # @PRE: logs is a list of LogEntry objects.\n # @POST: All logs inserted into task_logs table.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: logs (List[LogEntry]) - Log entries to insert.\n # @SIDE_EFFECT: Writes to task_logs table.\n # @DATA_CONTRACT: Input[List[LogEntry]] -> Model[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def add_logs(self, task_id: str, logs: list[LogEntry]) -> None:\n if not logs:\n return\n with belief_scope(\"TaskLogPersistenceService.add_logs\", f\"task_id={task_id}\"):\n session: Session = TasksSessionLocal()\n try:\n for log in logs:\n record = TaskLogRecord(\n task_id=task_id,\n timestamp=log.timestamp,\n level=log.level,\n source=log.source or \"system\",\n message=log.message,\n metadata_json=json.dumps(log.metadata)\n if log.metadata\n else None,\n )\n session.add(record)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to add logs for task {task_id}: {e}\")\n finally:\n session.close()\n # #endregion add_logs\n # #region get_logs [TYPE Function] [C:3]\n # @PURPOSE: Query logs for a task with filtering and pagination.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of TaskLog objects matching filters.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: log_filter (LogFilter) - Filter parameters.\n # @RETURN: List[TaskLog] - Filtered log entries.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[TaskLog]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogFilter]\n # @RELATION: [DEPENDS_ON] ->[TaskLog]\n def get_logs(self, task_id: str, log_filter: LogFilter) -> list[TaskLog]:\n with belief_scope(\"TaskLogPersistenceService.get_logs\", f\"task_id={task_id}\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n )\n # Apply filters\n if log_filter.level:\n query = query.filter(\n TaskLogRecord.level == log_filter.level.upper()\n )\n if log_filter.source:\n query = query.filter(TaskLogRecord.source == log_filter.source)\n if log_filter.search:\n search_pattern = f\"%{log_filter.search}%\"\n query = query.filter(TaskLogRecord.message.ilike(search_pattern))\n # Order by timestamp ascending (oldest first)\n query = query.order_by(TaskLogRecord.timestamp.asc())\n # Apply pagination\n records = query.offset(log_filter.offset).limit(log_filter.limit).all()\n logs = []\n for record in records:\n metadata = None\n if record.metadata_json:\n try:\n metadata = json.loads(record.metadata_json)\n except json.JSONDecodeError:\n metadata = None\n logs.append(\n TaskLog(\n id=record.id,\n task_id=record.task_id,\n timestamp=record.timestamp,\n level=record.level,\n source=record.source,\n message=record.message,\n metadata=metadata,\n )\n )\n return logs\n finally:\n session.close()\n # #endregion get_logs\n # #region get_log_stats [TYPE Function] [C:3]\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns LogStats with counts by level and source.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: LogStats - Statistics about task logs.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[LogStats]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogStats]\n def get_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\n \"TaskLogPersistenceService.get_log_stats\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n # Get total count\n total_count = (\n session.query(TaskLogRecord)\n .filter(TaskLogRecord.task_id == task_id)\n .count()\n )\n # Get counts by level\n from sqlalchemy import func\n level_counts = (\n session.query(TaskLogRecord.level, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.level)\n .all()\n )\n by_level = {level: count for level, count in level_counts}\n # Get counts by source\n source_counts = (\n session.query(TaskLogRecord.source, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.source)\n .all()\n )\n by_source = {source: count for source, count in source_counts}\n return LogStats(\n total_count=total_count, by_level=by_level, by_source=by_source\n )\n finally:\n session.close()\n # #endregion get_log_stats\n # #region get_sources [TYPE Function] [C:3]\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of unique source strings.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: List[str] - Unique source names.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[str]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def get_sources(self, task_id: str) -> list[str]:\n with belief_scope(\n \"TaskLogPersistenceService.get_sources\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n from sqlalchemy import distinct\n sources = (\n session.query(distinct(TaskLogRecord.source))\n .filter(TaskLogRecord.task_id == task_id)\n .all()\n )\n return [s[0] for s in sources]\n finally:\n session.close()\n # #endregion get_sources\n # #region delete_logs_for_task [TYPE Function] [C:3]\n # @PURPOSE: Delete all logs for a specific task.\n # @PRE: task_id is a valid task ID.\n # @POST: All logs for the task are deleted.\n # @PARAM: task_id (str) - The task ID.\n # @SIDE_EFFECT: Deletes from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_task(self, task_id: str) -> None:\n with belief_scope(\n \"TaskLogPersistenceService.delete_logs_for_task\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n ).delete(synchronize_session=False)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete logs for task {task_id}: {e}\")\n finally:\n session.close()\n # #endregion delete_logs_for_task\n # #region delete_logs_for_tasks [TYPE Function] [C:3]\n # @PURPOSE: Delete all logs for multiple tasks.\n # @PRE: task_ids is a list of task IDs.\n # @POST: All logs for the tasks are deleted.\n # @PARAM: task_ids (List[str]) - List of task IDs.\n # @SIDE_EFFECT: Deletes rows from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskLogPersistenceService.delete_logs_for_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id.in_(task_ids)\n ).delete(synchronize_session=False)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete logs for tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_logs_for_tasks\n# #endregion TaskLogPersistenceService\n# #endregion TaskPersistenceModule\n" }, { "contract_id": "TaskPersistenceService", "contract_type": "Class", "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 34, - "end_line": 358, + "start_line": 25, + "end_line": 294, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]", "INVARIANT": "Persistence must handle potentially missing task fields natively.", "POST": "Persist operations leave matching TaskRecord rows committed or rolled back without leaking sessions, load operations return reconstructed Task objects from stored TaskRecord rows, and delete operations remove only the addressed task rows.", "PRE": "TasksSessionLocal must provide an active SQLAlchemy session, Task inputs must expose id/plugin_id/status/params/result/logs fields, and TaskRecord plus Environment schemas must be available.", "PURPOSE": "Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.", - "SEMANTICS": [ - "persistence", - "service", - "database", - "sqlalchemy" - ], "SIDE_EFFECT": "Opens SQLAlchemy sessions, reads and writes task_records rows, resolves environment foreign keys against environments, commits or rolls back transactions, and emits error logs on persistence failures.", "TEST_CONTRACT": "TaskPersistenceContract ->" }, "relations": [ { "source_id": "TaskPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TasksSessionLocal", "target_ref": "[TasksSessionLocal]" }, { "source_id": "TaskPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskRecord", "target_ref": "[TaskRecord]" }, { "source_id": "TaskPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "Environment", "target_ref": "[Environment]" }, { "source_id": "TaskPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" }, { "source_id": "TaskPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskGraph", "target_ref": "[TaskGraph]" } ], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -47521,647 +39573,56 @@ "actual_complexity": 5, "contract_type": "Class" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskPersistenceService:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: persistence, service, database, sqlalchemy\n# @PURPOSE: Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.\n# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, Task inputs must expose id/plugin_id/status/params/result/logs fields, and TaskRecord plus Environment schemas must be available.\n# @POST: Persist operations leave matching TaskRecord rows committed or rolled back without leaking sessions, load operations return reconstructed Task objects from stored TaskRecord rows, and delete operations remove only the addressed task rows.\n# @SIDE_EFFECT: Opens SQLAlchemy sessions, reads and writes task_records rows, resolves environment foreign keys against environments, commits or rolls back transactions, and emits error logs on persistence failures.\n# @DATA_CONTRACT: Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]\n# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal]\n# @RELATION: [DEPENDS_ON] ->[TaskRecord]\n# @RELATION: [DEPENDS_ON] ->[Environment]\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n# @RELATION: [DEPENDS_ON] ->[TaskGraph]\n# @INVARIANT: Persistence must handle potentially missing task fields natively.\n#\n# @TEST_CONTRACT: TaskPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"persist_task creates or updates a record\",\n# \"load_tasks retrieves valid Task instances\",\n# \"delete_tasks correctly removes records from the database\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_persistence -> {\"task_id\": \"123\", \"status\": \"PENDING\"}\n# @TEST_EDGE: persist_invalid_task_type -> raises Exception\n# @TEST_EDGE: load_corrupt_json_params -> handled gracefully\n# @TEST_INVARIANT: accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params]\nclass TaskPersistenceService:\n # [DEF:_json_load_if_needed:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Safely load JSON strings from DB if necessary\n # @PRE: value is an arbitrary database value\n # @POST: Returns parsed JSON object, list, string, or primitive\n @staticmethod\n def _json_load_if_needed(value):\n with belief_scope(\"TaskPersistenceService._json_load_if_needed\"):\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n stripped = value.strip()\n if stripped == \"\" or stripped.lower() == \"null\":\n return None\n try:\n return json.loads(stripped)\n except json.JSONDecodeError:\n return value\n return value\n\n # [/DEF:_json_load_if_needed:Function]\n\n # [DEF:_parse_datetime:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE: value is an ISO string or datetime object\n # @POST: Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n with belief_scope(\"TaskPersistenceService._parse_datetime\"):\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n\n # [/DEF:_parse_datetime:Function]\n\n # [DEF:_resolve_environment_id:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE: Session is active\n # @POST: Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT: Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION: [DEPENDS_ON] ->[Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: Optional[str]\n ) -> Optional[str]:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n\n return None\n\n # [/DEF:_resolve_environment_id:Function]\n\n # [DEF:__init__:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initializes the persistence service.\n # @PRE: None.\n # @POST: Service is ready.\n def __init__(self):\n with belief_scope(\"TaskPersistenceService.__init__\"):\n # We use TasksSessionLocal from database.py\n pass\n\n # [/DEF:__init__:Function]\n\n # [DEF:persist_task:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE: isinstance(task, Task)\n # @POST: Task record created or updated in database.\n # @PARAM: task (Task) - The task object to persist.\n # @SIDE_EFFECT: Writes to task_records table in tasks.db\n # @DATA_CONTRACT: Input[Task] -> Model[TaskRecord]\n # @RELATION: [CALLS] ->[_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n log.reason(\"Persisting task to database\", payload={\"task_id\": task.id})\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n with belief_scope(\"TaskPersistenceService.json_serializable\"):\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log_entry in task.logs:\n log_dict = log_entry.dict()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log_entry in reversed(task.logs):\n if log_entry.level == \"ERROR\":\n record.error = log_entry.message\n break\n\n session.commit()\n log.reflect(\"Task persisted successfully\", payload={\"task_id\": task.id})\n except Exception as e:\n session.rollback()\n log.explore(\"Failed to persist task\", error=str(e), payload={\"task_id\": task.id})\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:persist_task:Function]\n\n # [DEF:persist_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persists multiple tasks.\n # @PRE: isinstance(tasks, list)\n # @POST: All tasks in list are persisted.\n # @PARAM: tasks (List[Task]) - The list of tasks to persist.\n # @RELATION: [CALLS] ->[persist_task]\n def persist_tasks(self, tasks: List[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n log.reason(\"Persisting multiple tasks\", payload={\"count\": len(tasks)})\n for task in tasks:\n self.persist_task(task)\n log.reflect(\"All tasks persisted\", payload={\"count\": len(tasks)})\n\n # [/DEF:persist_tasks:Function]\n\n # [DEF:load_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Loads tasks from the database.\n # @PRE: limit is an integer.\n # @POST: Returns list of Task objects.\n # @PARAM: limit (int) - Max tasks to load.\n # @PARAM: status (Optional[TaskStatus]) - Filter by status.\n # @RETURN: List[Task] - The loaded tasks.\n # @DATA_CONTRACT: Model[TaskRecord] -> Output[List[Task]]\n # @RELATION: [CALLS] ->[_json_load_if_needed]\n # @RELATION: [CALLS] ->[_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: Optional[TaskStatus] = None\n ) -> List[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n log.reason(\"Loading tasks from database\", payload={\"limit\": limit, \"status\": str(status) if status else None})\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.utcnow()\n )\n logs.append(LogEntry(**log_data))\n\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n log.explore(\"Failed to reconstruct task from record\", error=str(e), payload={\"record_id\": record.id})\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n\n log.reflect(\"Tasks loaded from database\", payload={\"count\": len(loaded_tasks)})\n return loaded_tasks\n finally:\n session.close()\n\n # [/DEF:load_tasks:Function]\n\n # [DEF:delete_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE: task_ids is a list of strings.\n # @POST: Specified task records deleted from database.\n # @PARAM: task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT: Deletes rows from task_records table.\n # @RELATION: [DEPENDS_ON] ->[TaskRecord]\n def delete_tasks(self, task_ids: List[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n log.reason(\"Deleting tasks from database\", payload={\"count\": len(task_ids)})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n log.reflect(\"Tasks deleted from database\", payload={\"count\": len(task_ids)})\n except Exception as e:\n session.rollback()\n log.explore(\"Failed to delete tasks\", error=str(e))\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_tasks:Function]\n\n\n# [/DEF:TaskPersistenceService:Class]\n" - }, - { - "contract_id": "_parse_datetime", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 87, - "end_line": 104, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns datetime object or None", - "PRE": "value is an ISO string or datetime object", - "PURPOSE": "Safely parse a datetime string from the database" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_parse_datetime:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE: value is an ISO string or datetime object\n # @POST: Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n with belief_scope(\"TaskPersistenceService._parse_datetime\"):\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n\n # [/DEF:_parse_datetime:Function]\n" - }, - { - "contract_id": "_resolve_environment_id", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 106, - "end_line": 154, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[env_id: Optional[str]] -> Output[Optional[str]]", - "POST": "Returns existing environments.id or None when unresolved.", - "PRE": "Session is active", - "PURPOSE": "Resolve environment id into existing environments.id value to satisfy FK constraints." - }, - "relations": [ - { - "source_id": "_resolve_environment_id", - "relation_type": "[DEPENDS_ON]", - "target_id": "Environment", - "target_ref": "[Environment]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_resolve_environment_id:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE: Session is active\n # @POST: Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT: Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION: [DEPENDS_ON] ->[Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: Optional[str]\n ) -> Optional[str]:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n\n return None\n\n # [/DEF:_resolve_environment_id:Function]\n" - }, - { - "contract_id": "persist_task", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 168, - "end_line": 241, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[Task] -> Model[TaskRecord]", - "PARAM": "task (Task) - The task object to persist.", - "POST": "Task record created or updated in database.", - "PRE": "isinstance(task, Task)", - "PURPOSE": "Persists or updates a single task in the database.", - "SIDE_EFFECT": "Writes to task_records table in tasks.db" - }, - "relations": [ - { - "source_id": "persist_task", - "relation_type": "[CALLS]", - "target_id": "_resolve_environment_id", - "target_ref": "[_resolve_environment_id]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:persist_task:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE: isinstance(task, Task)\n # @POST: Task record created or updated in database.\n # @PARAM: task (Task) - The task object to persist.\n # @SIDE_EFFECT: Writes to task_records table in tasks.db\n # @DATA_CONTRACT: Input[Task] -> Model[TaskRecord]\n # @RELATION: [CALLS] ->[_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n log.reason(\"Persisting task to database\", payload={\"task_id\": task.id})\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n with belief_scope(\"TaskPersistenceService.json_serializable\"):\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log_entry in task.logs:\n log_dict = log_entry.dict()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log_entry in reversed(task.logs):\n if log_entry.level == \"ERROR\":\n record.error = log_entry.message\n break\n\n session.commit()\n log.reflect(\"Task persisted successfully\", payload={\"task_id\": task.id})\n except Exception as e:\n session.rollback()\n log.explore(\"Failed to persist task\", error=str(e), payload={\"task_id\": task.id})\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:persist_task:Function]\n" - }, - { - "contract_id": "persist_tasks", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 243, - "end_line": 257, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "tasks (List[Task]) - The list of tasks to persist.", - "POST": "All tasks in list are persisted.", - "PRE": "isinstance(tasks, list)", - "PURPOSE": "Persists multiple tasks." - }, - "relations": [ - { - "source_id": "persist_tasks", - "relation_type": "[CALLS]", - "target_id": "persist_task", - "target_ref": "[persist_task]" - } - ], - "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 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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:persist_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persists multiple tasks.\n # @PRE: isinstance(tasks, list)\n # @POST: All tasks in list are persisted.\n # @PARAM: tasks (List[Task]) - The list of tasks to persist.\n # @RELATION: [CALLS] ->[persist_task]\n def persist_tasks(self, tasks: List[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n log.reason(\"Persisting multiple tasks\", payload={\"count\": len(tasks)})\n for task in tasks:\n self.persist_task(task)\n log.reflect(\"All tasks persisted\", payload={\"count\": len(tasks)})\n\n # [/DEF:persist_tasks:Function]\n" - }, - { - "contract_id": "load_tasks", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 259, - "end_line": 326, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Model[TaskRecord] -> Output[List[Task]]", - "PARAM": "status (Optional[TaskStatus]) - Filter by status.", - "POST": "Returns list of Task objects.", - "PRE": "limit is an integer.", - "PURPOSE": "Loads tasks from the database.", - "RETURN": "List[Task] - The loaded tasks." - }, - "relations": [ - { - "source_id": "load_tasks", - "relation_type": "[CALLS]", - "target_id": "_json_load_if_needed", - "target_ref": "[_json_load_if_needed]" - }, - { - "source_id": "load_tasks", - "relation_type": "[CALLS]", - "target_id": "_parse_datetime", - "target_ref": "[_parse_datetime]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:load_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Loads tasks from the database.\n # @PRE: limit is an integer.\n # @POST: Returns list of Task objects.\n # @PARAM: limit (int) - Max tasks to load.\n # @PARAM: status (Optional[TaskStatus]) - Filter by status.\n # @RETURN: List[Task] - The loaded tasks.\n # @DATA_CONTRACT: Model[TaskRecord] -> Output[List[Task]]\n # @RELATION: [CALLS] ->[_json_load_if_needed]\n # @RELATION: [CALLS] ->[_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: Optional[TaskStatus] = None\n ) -> List[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n log.reason(\"Loading tasks from database\", payload={\"limit\": limit, \"status\": str(status) if status else None})\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.utcnow()\n )\n logs.append(LogEntry(**log_data))\n\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n log.explore(\"Failed to reconstruct task from record\", error=str(e), payload={\"record_id\": record.id})\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n\n log.reflect(\"Tasks loaded from database\", payload={\"count\": len(loaded_tasks)})\n return loaded_tasks\n finally:\n session.close()\n\n # [/DEF:load_tasks:Function]\n" - }, - { - "contract_id": "delete_tasks", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 328, - "end_line": 355, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_ids (List[str]) - List of task IDs to delete.", - "POST": "Specified task records deleted from database.", - "PRE": "task_ids is a list of strings.", - "PURPOSE": "Deletes specific tasks from the database.", - "SIDE_EFFECT": "Deletes rows from task_records table." - }, - "relations": [ - { - "source_id": "delete_tasks", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskRecord", - "target_ref": "[TaskRecord]" - } - ], - "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 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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:delete_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE: task_ids is a list of strings.\n # @POST: Specified task records deleted from database.\n # @PARAM: task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT: Deletes rows from task_records table.\n # @RELATION: [DEPENDS_ON] ->[TaskRecord]\n def delete_tasks(self, task_ids: List[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n log.reason(\"Deleting tasks from database\", payload={\"count\": len(task_ids)})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n log.reflect(\"Tasks deleted from database\", payload={\"count\": len(task_ids)})\n except Exception as e:\n session.rollback()\n log.explore(\"Failed to delete tasks\", error=str(e))\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_tasks:Function]\n" + "anchor_syntax": "region", + "body": "# #region TaskPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, sqlalchemy]\n# @BRIEF Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.\n# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, Task inputs must expose id/plugin_id/status/params/result/logs fields, and TaskRecord plus Environment schemas must be available.\n# @POST: Persist operations leave matching TaskRecord rows committed or rolled back without leaking sessions, load operations return reconstructed Task objects from stored TaskRecord rows, and delete operations remove only the addressed task rows.\n# @SIDE_EFFECT: Opens SQLAlchemy sessions, reads and writes task_records rows, resolves environment foreign keys against environments, commits or rolls back transactions, and emits error logs on persistence failures.\n# @DATA_CONTRACT: Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskRecord]\n# @RELATION DEPENDS_ON -> [Environment]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @INVARIANT: Persistence must handle potentially missing task fields natively.\n#\n# @TEST_CONTRACT: TaskPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"persist_task creates or updates a record\",\n# \"load_tasks retrieves valid Task instances\",\n# \"delete_tasks correctly removes records from the database\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_persistence -> {\"task_id\": \"123\", \"status\": \"PENDING\"}\n# @TEST_EDGE: persist_invalid_task_type -> raises Exception\n# @TEST_EDGE: load_corrupt_json_params -> handled gracefully\n# @TEST_INVARIANT: accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params]\nclass TaskPersistenceService:\n # #region _json_load_if_needed [TYPE Function] [C:1]\n # @PURPOSE: Safely load JSON strings from DB if necessary\n # @PRE: value is an arbitrary database value\n # @POST: Returns parsed JSON object, list, string, or primitive\n @staticmethod\n def _json_load_if_needed(value):\n # Hot-path utility — no entry/exit logging to reduce noise\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n stripped = value.strip()\n if stripped == \"\" or stripped.lower() == \"null\":\n return None\n try:\n return json.loads(stripped)\n except json.JSONDecodeError:\n return value\n return value\n # #endregion _json_load_if_needed\n # #region _parse_datetime [TYPE Function] [C:1]\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE: value is an ISO string or datetime object\n # @POST: Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n # Hot-path utility — no entry/exit logging to reduce startup noise\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n # #endregion _parse_datetime\n # #region _resolve_environment_id [TYPE Function] [C:3]\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE: Session is active\n # @POST: Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT: Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION: [DEPENDS_ON] ->[Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: str | None\n ) -> str | None:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n return None\n # #endregion _resolve_environment_id\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the persistence service.\n # @PRE: None.\n # @POST: Service is ready.\n def __init__(self):\n with belief_scope(\"TaskPersistenceService.__init__\"):\n # We use TasksSessionLocal from database.py\n pass\n # #endregion __init__\n # #region persist_task [TYPE Function] [C:3]\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE: isinstance(task, Task)\n # @POST: Task record created or updated in database.\n # @PARAM: task (Task) - The task object to persist.\n # @SIDE_EFFECT: Writes to task_records table in tasks.db\n # @DATA_CONTRACT: Input[Task] -> Model[TaskRecord]\n # @RELATION: [CALLS] ->[_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n # Hot-path utility — no entry/exit logging\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log in task.logs:\n log_dict = log.dict()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log in reversed(task.logs):\n if log.level == \"ERROR\":\n record.error = log.message\n break\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n # #endregion persist_task\n # #region persist_tasks [TYPE Function] [C:3]\n # @PURPOSE: Persists multiple tasks.\n # @PRE: isinstance(tasks, list)\n # @POST: All tasks in list are persisted.\n # @PARAM: tasks (List[Task]) - The list of tasks to persist.\n # @RELATION: [CALLS] ->[persist_task]\n def persist_tasks(self, tasks: list[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n for task in tasks:\n self.persist_task(task)\n # #endregion persist_tasks\n # #region load_tasks [TYPE Function] [C:3]\n # @PURPOSE: Loads tasks from the database.\n # @PRE: limit is an integer.\n # @POST: Returns list of Task objects.\n # @PARAM: limit (int) - Max tasks to load.\n # @PARAM: status (Optional[TaskStatus]) - Filter by status.\n # @RETURN: List[Task] - The loaded tasks.\n # @DATA_CONTRACT: Model[TaskRecord] -> Output[List[Task]]\n # @RELATION: [CALLS] ->[_json_load_if_needed]\n # @RELATION: [CALLS] ->[_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: TaskStatus | None = None\n ) -> list[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.utcnow()\n )\n logs.append(LogEntry(**log_data))\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n return loaded_tasks\n finally:\n session.close()\n # #endregion load_tasks\n # #region delete_tasks [TYPE Function] [C:3]\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE: task_ids is a list of strings.\n # @POST: Specified task records deleted from database.\n # @PARAM: task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT: Deletes rows from task_records table.\n # @RELATION: [DEPENDS_ON] ->[TaskRecord]\n def delete_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_tasks\n# #endregion TaskPersistenceService\n" }, { "contract_id": "TaskLogPersistenceService", "contract_type": "Class", "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 361, - "end_line": 654, + "start_line": 295, + "end_line": 534, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[task_id:str, logs:List[LogEntry], log_filter:LogFilter, task_ids:List[str]] -> Model[TaskLogRecord] -> Output[None | List[TaskLog] | LogStats | List[str]]", "INVARIANT": "Log entries are batch-inserted for performance.", "POST": "add_logs commits all provided log entries or rolls back on failure, query methods return TaskLog or LogStats views reconstructed from TaskLogRecord rows, and delete methods remove only log rows matching the supplied task identifiers.", "PRE": "TasksSessionLocal must provide an active SQLAlchemy session, task_id inputs must identify task log rows, LogEntry batches must expose timestamp/level/source/message/metadata fields, and LogFilter inputs must provide pagination and filter attributes used by queries.", "PURPOSE": "Provides methods to store, query, summarize, and delete task log rows in the task_logs table.", - "SEMANTICS": [ - "persistence", - "service", - "database", - "log", - "sqlalchemy" - ], "SIDE_EFFECT": "Opens SQLAlchemy sessions, inserts, reads, aggregates, and deletes task_logs rows, serializes log metadata to JSON, commits or rolls back transactions, and emits error logs on persistence failures.", "TEST_CONTRACT": "TaskLogPersistenceContract ->" }, "relations": [ { "source_id": "TaskLogPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskLogRecord", "target_ref": "[TaskLogRecord]" }, { "source_id": "TaskLogPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TasksSessionLocal", "target_ref": "[TasksSessionLocal]" }, { "source_id": "TaskLogPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" }, { "source_id": "TaskLogPersistenceService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "EventBus", "target_ref": "[EventBus]" } ], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -48200,697 +39661,35 @@ "actual_complexity": 5, "contract_type": "Class" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskLogPersistenceService:Class]\n# @COMPLEXITY: 5\n# @SEMANTICS: persistence, service, database, log, sqlalchemy\n# @PURPOSE: Provides methods to store, query, summarize, and delete task log rows in the task_logs table.\n# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, task_id inputs must identify task log rows, LogEntry batches must expose timestamp/level/source/message/metadata fields, and LogFilter inputs must provide pagination and filter attributes used by queries.\n# @POST: add_logs commits all provided log entries or rolls back on failure, query methods return TaskLog or LogStats views reconstructed from TaskLogRecord rows, and delete methods remove only log rows matching the supplied task identifiers.\n# @SIDE_EFFECT: Opens SQLAlchemy sessions, inserts, reads, aggregates, and deletes task_logs rows, serializes log metadata to JSON, commits or rolls back transactions, and emits error logs on persistence failures.\n# @DATA_CONTRACT: Input[task_id:str, logs:List[LogEntry], log_filter:LogFilter, task_ids:List[str]] -> Model[TaskLogRecord] -> Output[None | List[TaskLog] | LogStats | List[str]]\n# @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n# @RELATION: [DEPENDS_ON] ->[TasksSessionLocal]\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n# @RELATION: [DEPENDS_ON] ->[EventBus]\n# @INVARIANT: Log entries are batch-inserted for performance.\n#\n# @TEST_CONTRACT: TaskLogPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"add_logs efficiently saves logs to the database\",\n# \"get_logs retrieves properly filtered LogEntry objects\"\n# ]\n# }\n# @TEST_FIXTURE: valid_log_batch -> {\"task_id\": \"123\", \"logs\": [{\"level\": \"INFO\", \"message\": \"msg\"}]}\n# @TEST_EDGE: empty_log_list -> no-op behavior\n# @TEST_EDGE: add_logs_db_error -> rollback and log error\n# @TEST_INVARIANT: accurate_log_aggregation -> verifies: [valid_log_batch]\nclass TaskLogPersistenceService:\n \"\"\"\n Service for persisting and querying task logs.\n Supports batch inserts, filtering, and statistics.\n \"\"\"\n\n # [DEF:__init__:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initializes the TaskLogPersistenceService\n # @PRE: config is provided or defaults are used\n # @POST: Service is ready for log persistence\n def __init__(self, config=None):\n pass\n\n # [/DEF:__init__:Function]\n\n # [DEF:add_logs:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Batch insert log entries for a task.\n # @PRE: logs is a list of LogEntry objects.\n # @POST: All logs inserted into task_logs table.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: logs (List[LogEntry]) - Log entries to insert.\n # @SIDE_EFFECT: Writes to task_logs table.\n # @DATA_CONTRACT: Input[List[LogEntry]] -> Model[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def add_logs(self, task_id: str, logs: List[LogEntry]) -> None:\n if not logs:\n return\n with belief_scope(\"TaskLogPersistenceService.add_logs\", f\"task_id={task_id}\"):\n log_pl.reason(\"Batch inserting log entries\", payload={\"task_id\": task_id, \"count\": len(logs)})\n session: Session = TasksSessionLocal()\n try:\n for log_entry in logs:\n record = TaskLogRecord(\n task_id=task_id,\n timestamp=log_entry.timestamp,\n level=log_entry.level,\n source=log_entry.source or \"system\",\n message=log_entry.message,\n metadata_json=json.dumps(log_entry.metadata)\n if log_entry.metadata\n else None,\n )\n session.add(record)\n session.commit()\n log_pl.reflect(\"Log entries persisted\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to persist log entries\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to add logs for task {task_id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:add_logs:Function]\n\n # [DEF:get_logs:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Query logs for a task with filtering and pagination.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of TaskLog objects matching filters.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: log_filter (LogFilter) - Filter parameters.\n # @RETURN: List[TaskLog] - Filtered log entries.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[TaskLog]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogFilter]\n # @RELATION: [DEPENDS_ON] ->[TaskLog]\n def get_logs(self, task_id: str, log_filter: LogFilter) -> List[TaskLog]:\n with belief_scope(\"TaskLogPersistenceService.get_logs\", f\"task_id={task_id}\"):\n log_pl.reason(\"Querying task logs\", payload={\"task_id\": task_id, \"filter_level\": log_filter.level})\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n )\n\n # Apply filters\n if log_filter.level:\n query = query.filter(\n TaskLogRecord.level == log_filter.level.upper()\n )\n if log_filter.source:\n query = query.filter(TaskLogRecord.source == log_filter.source)\n if log_filter.search:\n search_pattern = f\"%{log_filter.search}%\"\n query = query.filter(TaskLogRecord.message.ilike(search_pattern))\n\n # Order by timestamp ascending (oldest first)\n query = query.order_by(TaskLogRecord.timestamp.asc())\n\n # Apply pagination\n records = query.offset(log_filter.offset).limit(log_filter.limit).all()\n\n logs = []\n for record in records:\n metadata = None\n if record.metadata_json:\n try:\n metadata = json.loads(record.metadata_json)\n except json.JSONDecodeError:\n metadata = None\n\n logs.append(\n TaskLog(\n id=record.id,\n task_id=record.task_id,\n timestamp=record.timestamp,\n level=record.level,\n source=record.source,\n message=record.message,\n metadata=metadata,\n )\n )\n\n log_pl.reflect(\"Task logs retrieved\", payload={\"task_id\": task_id, \"count\": len(logs)})\n return logs\n finally:\n session.close()\n\n # [/DEF:get_logs:Function]\n\n # [DEF:get_log_stats:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns LogStats with counts by level and source.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: LogStats - Statistics about task logs.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[LogStats]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogStats]\n def get_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\n \"TaskLogPersistenceService.get_log_stats\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Aggregating log statistics\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n # Get total count\n total_count = (\n session.query(TaskLogRecord)\n .filter(TaskLogRecord.task_id == task_id)\n .count()\n )\n\n # Get counts by level\n from sqlalchemy import func\n\n level_counts = (\n session.query(TaskLogRecord.level, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.level)\n .all()\n )\n\n by_level = {level: count for level, count in level_counts}\n\n # Get counts by source\n source_counts = (\n session.query(TaskLogRecord.source, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.source)\n .all()\n )\n\n by_source = {source: count for source, count in source_counts}\n\n log_pl.reflect(\"Log statistics computed\", payload={\"task_id\": task_id, \"total\": total_count})\n return LogStats(\n total_count=total_count, by_level=by_level, by_source=by_source\n )\n finally:\n session.close()\n\n # [/DEF:get_log_stats:Function]\n\n # [DEF:get_sources:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of unique source strings.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: List[str] - Unique source names.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[str]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def get_sources(self, task_id: str) -> List[str]:\n with belief_scope(\n \"TaskLogPersistenceService.get_sources\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Querying distinct log sources\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n from sqlalchemy import distinct\n\n sources = (\n session.query(distinct(TaskLogRecord.source))\n .filter(TaskLogRecord.task_id == task_id)\n .all()\n )\n result = [s[0] for s in sources]\n log_pl.reflect(\"Log sources retrieved\", payload={\"task_id\": task_id, \"sources\": result})\n return result\n finally:\n session.close()\n\n # [/DEF:get_sources:Function]\n\n # [DEF:delete_logs_for_task:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete all logs for a specific task.\n # @PRE: task_id is a valid task ID.\n # @POST: All logs for the task are deleted.\n # @PARAM: task_id (str) - The task ID.\n # @SIDE_EFFECT: Deletes from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_task(self, task_id: str) -> None:\n with belief_scope(\n \"TaskLogPersistenceService.delete_logs_for_task\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Deleting logs for task\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n ).delete(synchronize_session=False)\n session.commit()\n log_pl.reflect(\"Task logs deleted\", payload={\"task_id\": task_id})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to delete task logs\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to delete logs for task {task_id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_logs_for_task:Function]\n\n # [DEF:delete_logs_for_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete all logs for multiple tasks.\n # @PRE: task_ids is a list of task IDs.\n # @POST: All logs for the tasks are deleted.\n # @PARAM: task_ids (List[str]) - List of task IDs.\n # @SIDE_EFFECT: Deletes rows from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_tasks(self, task_ids: List[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskLogPersistenceService.delete_logs_for_tasks\"):\n log_pl.reason(\"Deleting logs for multiple tasks\", payload={\"count\": len(task_ids)})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id.in_(task_ids)\n ).delete(synchronize_session=False)\n session.commit()\n log_pl.reflect(\"Logs deleted for multiple tasks\", payload={\"count\": len(task_ids)})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to delete logs for tasks\", error=str(e))\n logger.error(f\"Failed to delete logs for tasks: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_logs_for_tasks:Function]\n\n\n# [/DEF:TaskLogPersistenceService:Class]\n" - }, - { - "contract_id": "add_logs", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 403, - "end_line": 441, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[List[LogEntry]] -> Model[TaskLogRecord]", - "PARAM": "logs (List[LogEntry]) - Log entries to insert.", - "POST": "All logs inserted into task_logs table.", - "PRE": "logs is a list of LogEntry objects.", - "PURPOSE": "Batch insert log entries for a task.", - "SIDE_EFFECT": "Writes to task_logs table." - }, - "relations": [ - { - "source_id": "add_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskLogRecord", - "target_ref": "[TaskLogRecord]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:add_logs:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Batch insert log entries for a task.\n # @PRE: logs is a list of LogEntry objects.\n # @POST: All logs inserted into task_logs table.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: logs (List[LogEntry]) - Log entries to insert.\n # @SIDE_EFFECT: Writes to task_logs table.\n # @DATA_CONTRACT: Input[List[LogEntry]] -> Model[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def add_logs(self, task_id: str, logs: List[LogEntry]) -> None:\n if not logs:\n return\n with belief_scope(\"TaskLogPersistenceService.add_logs\", f\"task_id={task_id}\"):\n log_pl.reason(\"Batch inserting log entries\", payload={\"task_id\": task_id, \"count\": len(logs)})\n session: Session = TasksSessionLocal()\n try:\n for log_entry in logs:\n record = TaskLogRecord(\n task_id=task_id,\n timestamp=log_entry.timestamp,\n level=log_entry.level,\n source=log_entry.source or \"system\",\n message=log_entry.message,\n metadata_json=json.dumps(log_entry.metadata)\n if log_entry.metadata\n else None,\n )\n session.add(record)\n session.commit()\n log_pl.reflect(\"Log entries persisted\", payload={\"task_id\": task_id, \"count\": len(logs)})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to persist log entries\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to add logs for task {task_id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:add_logs:Function]\n" - }, - { - "contract_id": "get_logs", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 443, - "end_line": 507, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Model[TaskLogRecord] -> Output[List[TaskLog]]", - "PARAM": "log_filter (LogFilter) - Filter parameters.", - "POST": "Returns list of TaskLog objects matching filters.", - "PRE": "task_id is a valid task ID.", - "PURPOSE": "Query logs for a task with filtering and pagination.", - "RETURN": "List[TaskLog] - Filtered log entries." - }, - "relations": [ - { - "source_id": "get_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskLogRecord", - "target_ref": "[TaskLogRecord]" - }, - { - "source_id": "get_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "LogFilter", - "target_ref": "[LogFilter]" - }, - { - "source_id": "get_logs", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskLog", - "target_ref": "[TaskLog]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_logs:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Query logs for a task with filtering and pagination.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of TaskLog objects matching filters.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: log_filter (LogFilter) - Filter parameters.\n # @RETURN: List[TaskLog] - Filtered log entries.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[TaskLog]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogFilter]\n # @RELATION: [DEPENDS_ON] ->[TaskLog]\n def get_logs(self, task_id: str, log_filter: LogFilter) -> List[TaskLog]:\n with belief_scope(\"TaskLogPersistenceService.get_logs\", f\"task_id={task_id}\"):\n log_pl.reason(\"Querying task logs\", payload={\"task_id\": task_id, \"filter_level\": log_filter.level})\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n )\n\n # Apply filters\n if log_filter.level:\n query = query.filter(\n TaskLogRecord.level == log_filter.level.upper()\n )\n if log_filter.source:\n query = query.filter(TaskLogRecord.source == log_filter.source)\n if log_filter.search:\n search_pattern = f\"%{log_filter.search}%\"\n query = query.filter(TaskLogRecord.message.ilike(search_pattern))\n\n # Order by timestamp ascending (oldest first)\n query = query.order_by(TaskLogRecord.timestamp.asc())\n\n # Apply pagination\n records = query.offset(log_filter.offset).limit(log_filter.limit).all()\n\n logs = []\n for record in records:\n metadata = None\n if record.metadata_json:\n try:\n metadata = json.loads(record.metadata_json)\n except json.JSONDecodeError:\n metadata = None\n\n logs.append(\n TaskLog(\n id=record.id,\n task_id=record.task_id,\n timestamp=record.timestamp,\n level=record.level,\n source=record.source,\n message=record.message,\n metadata=metadata,\n )\n )\n\n log_pl.reflect(\"Task logs retrieved\", payload={\"task_id\": task_id, \"count\": len(logs)})\n return logs\n finally:\n session.close()\n\n # [/DEF:get_logs:Function]\n" - }, - { - "contract_id": "get_log_stats", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 509, - "end_line": 562, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Model[TaskLogRecord] -> Output[LogStats]", - "PARAM": "task_id (str) - The task ID.", - "POST": "Returns LogStats with counts by level and source.", - "PRE": "task_id is a valid task ID.", - "PURPOSE": "Get statistics about logs for a task.", - "RETURN": "LogStats - Statistics about task logs." - }, - "relations": [ - { - "source_id": "get_log_stats", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskLogRecord", - "target_ref": "[TaskLogRecord]" - }, - { - "source_id": "get_log_stats", - "relation_type": "[DEPENDS_ON]", - "target_id": "LogStats", - "target_ref": "[LogStats]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_log_stats:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns LogStats with counts by level and source.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: LogStats - Statistics about task logs.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[LogStats]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogStats]\n def get_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\n \"TaskLogPersistenceService.get_log_stats\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Aggregating log statistics\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n # Get total count\n total_count = (\n session.query(TaskLogRecord)\n .filter(TaskLogRecord.task_id == task_id)\n .count()\n )\n\n # Get counts by level\n from sqlalchemy import func\n\n level_counts = (\n session.query(TaskLogRecord.level, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.level)\n .all()\n )\n\n by_level = {level: count for level, count in level_counts}\n\n # Get counts by source\n source_counts = (\n session.query(TaskLogRecord.source, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.source)\n .all()\n )\n\n by_source = {source: count for source, count in source_counts}\n\n log_pl.reflect(\"Log statistics computed\", payload={\"task_id\": task_id, \"total\": total_count})\n return LogStats(\n total_count=total_count, by_level=by_level, by_source=by_source\n )\n finally:\n session.close()\n\n # [/DEF:get_log_stats:Function]\n" - }, - { - "contract_id": "get_sources", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 564, - "end_line": 593, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Model[TaskLogRecord] -> Output[List[str]]", - "PARAM": "task_id (str) - The task ID.", - "POST": "Returns list of unique source strings.", - "PRE": "task_id is a valid task ID.", - "PURPOSE": "Get unique sources for a task's logs.", - "RETURN": "List[str] - Unique source names." - }, - "relations": [ - { - "source_id": "get_sources", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskLogRecord", - "target_ref": "[TaskLogRecord]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_sources:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of unique source strings.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: List[str] - Unique source names.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[str]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def get_sources(self, task_id: str) -> List[str]:\n with belief_scope(\n \"TaskLogPersistenceService.get_sources\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Querying distinct log sources\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n from sqlalchemy import distinct\n\n sources = (\n session.query(distinct(TaskLogRecord.source))\n .filter(TaskLogRecord.task_id == task_id)\n .all()\n )\n result = [s[0] for s in sources]\n log_pl.reflect(\"Log sources retrieved\", payload={\"task_id\": task_id, \"sources\": result})\n return result\n finally:\n session.close()\n\n # [/DEF:get_sources:Function]\n" - }, - { - "contract_id": "delete_logs_for_task", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 595, - "end_line": 622, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_id (str) - The task ID.", - "POST": "All logs for the task are deleted.", - "PRE": "task_id is a valid task ID.", - "PURPOSE": "Delete all logs for a specific task.", - "SIDE_EFFECT": "Deletes from task_logs table." - }, - "relations": [ - { - "source_id": "delete_logs_for_task", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskLogRecord", - "target_ref": "[TaskLogRecord]" - } - ], - "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 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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:delete_logs_for_task:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete all logs for a specific task.\n # @PRE: task_id is a valid task ID.\n # @POST: All logs for the task are deleted.\n # @PARAM: task_id (str) - The task ID.\n # @SIDE_EFFECT: Deletes from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_task(self, task_id: str) -> None:\n with belief_scope(\n \"TaskLogPersistenceService.delete_logs_for_task\", f\"task_id={task_id}\"\n ):\n log_pl.reason(\"Deleting logs for task\", payload={\"task_id\": task_id})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n ).delete(synchronize_session=False)\n session.commit()\n log_pl.reflect(\"Task logs deleted\", payload={\"task_id\": task_id})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to delete task logs\", error=str(e), payload={\"task_id\": task_id})\n logger.error(f\"Failed to delete logs for task {task_id}: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_logs_for_task:Function]\n" - }, - { - "contract_id": "delete_logs_for_tasks", - "contract_type": "Function", - "file_path": "backend/src/core/task_manager/persistence.py", - "start_line": 624, - "end_line": 651, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task_ids (List[str]) - List of task IDs.", - "POST": "All logs for the tasks are deleted.", - "PRE": "task_ids is a list of task IDs.", - "PURPOSE": "Delete all logs for multiple tasks.", - "SIDE_EFFECT": "Deletes rows from task_logs table." - }, - "relations": [ - { - "source_id": "delete_logs_for_tasks", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskLogRecord", - "target_ref": "[TaskLogRecord]" - } - ], - "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 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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:delete_logs_for_tasks:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete all logs for multiple tasks.\n # @PRE: task_ids is a list of task IDs.\n # @POST: All logs for the tasks are deleted.\n # @PARAM: task_ids (List[str]) - List of task IDs.\n # @SIDE_EFFECT: Deletes rows from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_tasks(self, task_ids: List[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskLogPersistenceService.delete_logs_for_tasks\"):\n log_pl.reason(\"Deleting logs for multiple tasks\", payload={\"count\": len(task_ids)})\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id.in_(task_ids)\n ).delete(synchronize_session=False)\n session.commit()\n log_pl.reflect(\"Logs deleted for multiple tasks\", payload={\"count\": len(task_ids)})\n except Exception as e:\n session.rollback()\n log_pl.explore(\"Failed to delete logs for tasks\", error=str(e))\n logger.error(f\"Failed to delete logs for tasks: {e}\")\n finally:\n session.close()\n\n # [/DEF:delete_logs_for_tasks:Function]\n" + "anchor_syntax": "region", + "body": "# #region TaskLogPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, log, sqlalchemy]\n# @BRIEF Provides methods to store, query, summarize, and delete task log rows in the task_logs table.\n# @PRE: TasksSessionLocal must provide an active SQLAlchemy session, task_id inputs must identify task log rows, LogEntry batches must expose timestamp/level/source/message/metadata fields, and LogFilter inputs must provide pagination and filter attributes used by queries.\n# @POST: add_logs commits all provided log entries or rolls back on failure, query methods return TaskLog or LogStats views reconstructed from TaskLogRecord rows, and delete methods remove only log rows matching the supplied task identifiers.\n# @SIDE_EFFECT: Opens SQLAlchemy sessions, inserts, reads, aggregates, and deletes task_logs rows, serializes log metadata to JSON, commits or rolls back transactions, and emits error logs on persistence failures.\n# @DATA_CONTRACT: Input[task_id:str, logs:List[LogEntry], log_filter:LogFilter, task_ids:List[str]] -> Model[TaskLogRecord] -> Output[None | List[TaskLog] | LogStats | List[str]]\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT: Log entries are batch-inserted for performance.\n#\n# @TEST_CONTRACT: TaskLogPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"add_logs efficiently saves logs to the database\",\n# \"get_logs retrieves properly filtered LogEntry objects\"\n# ]\n# }\n# @TEST_FIXTURE: valid_log_batch -> {\"task_id\": \"123\", \"logs\": [{\"level\": \"INFO\", \"message\": \"msg\"}]}\n# @TEST_EDGE: empty_log_list -> no-op behavior\n# @TEST_EDGE: add_logs_db_error -> rollback and log error\n# @TEST_INVARIANT: accurate_log_aggregation -> verifies: [valid_log_batch]\nclass TaskLogPersistenceService:\n \"\"\"\n Service for persisting and querying task logs.\n Supports batch inserts, filtering, and statistics.\n \"\"\"\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the TaskLogPersistenceService\n # @PRE: config is provided or defaults are used\n # @POST: Service is ready for log persistence\n def __init__(self, config=None):\n pass\n # #endregion __init__\n # #region add_logs [TYPE Function] [C:3]\n # @PURPOSE: Batch insert log entries for a task.\n # @PRE: logs is a list of LogEntry objects.\n # @POST: All logs inserted into task_logs table.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: logs (List[LogEntry]) - Log entries to insert.\n # @SIDE_EFFECT: Writes to task_logs table.\n # @DATA_CONTRACT: Input[List[LogEntry]] -> Model[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def add_logs(self, task_id: str, logs: list[LogEntry]) -> None:\n if not logs:\n return\n with belief_scope(\"TaskLogPersistenceService.add_logs\", f\"task_id={task_id}\"):\n session: Session = TasksSessionLocal()\n try:\n for log in logs:\n record = TaskLogRecord(\n task_id=task_id,\n timestamp=log.timestamp,\n level=log.level,\n source=log.source or \"system\",\n message=log.message,\n metadata_json=json.dumps(log.metadata)\n if log.metadata\n else None,\n )\n session.add(record)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to add logs for task {task_id}: {e}\")\n finally:\n session.close()\n # #endregion add_logs\n # #region get_logs [TYPE Function] [C:3]\n # @PURPOSE: Query logs for a task with filtering and pagination.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of TaskLog objects matching filters.\n # @PARAM: task_id (str) - The task ID.\n # @PARAM: log_filter (LogFilter) - Filter parameters.\n # @RETURN: List[TaskLog] - Filtered log entries.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[TaskLog]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogFilter]\n # @RELATION: [DEPENDS_ON] ->[TaskLog]\n def get_logs(self, task_id: str, log_filter: LogFilter) -> list[TaskLog]:\n with belief_scope(\"TaskLogPersistenceService.get_logs\", f\"task_id={task_id}\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n )\n # Apply filters\n if log_filter.level:\n query = query.filter(\n TaskLogRecord.level == log_filter.level.upper()\n )\n if log_filter.source:\n query = query.filter(TaskLogRecord.source == log_filter.source)\n if log_filter.search:\n search_pattern = f\"%{log_filter.search}%\"\n query = query.filter(TaskLogRecord.message.ilike(search_pattern))\n # Order by timestamp ascending (oldest first)\n query = query.order_by(TaskLogRecord.timestamp.asc())\n # Apply pagination\n records = query.offset(log_filter.offset).limit(log_filter.limit).all()\n logs = []\n for record in records:\n metadata = None\n if record.metadata_json:\n try:\n metadata = json.loads(record.metadata_json)\n except json.JSONDecodeError:\n metadata = None\n logs.append(\n TaskLog(\n id=record.id,\n task_id=record.task_id,\n timestamp=record.timestamp,\n level=record.level,\n source=record.source,\n message=record.message,\n metadata=metadata,\n )\n )\n return logs\n finally:\n session.close()\n # #endregion get_logs\n # #region get_log_stats [TYPE Function] [C:3]\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns LogStats with counts by level and source.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: LogStats - Statistics about task logs.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[LogStats]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n # @RELATION: [DEPENDS_ON] ->[LogStats]\n def get_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\n \"TaskLogPersistenceService.get_log_stats\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n # Get total count\n total_count = (\n session.query(TaskLogRecord)\n .filter(TaskLogRecord.task_id == task_id)\n .count()\n )\n # Get counts by level\n from sqlalchemy import func\n level_counts = (\n session.query(TaskLogRecord.level, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.level)\n .all()\n )\n by_level = {level: count for level, count in level_counts}\n # Get counts by source\n source_counts = (\n session.query(TaskLogRecord.source, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.source)\n .all()\n )\n by_source = {source: count for source, count in source_counts}\n return LogStats(\n total_count=total_count, by_level=by_level, by_source=by_source\n )\n finally:\n session.close()\n # #endregion get_log_stats\n # #region get_sources [TYPE Function] [C:3]\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE: task_id is a valid task ID.\n # @POST: Returns list of unique source strings.\n # @PARAM: task_id (str) - The task ID.\n # @RETURN: List[str] - Unique source names.\n # @DATA_CONTRACT: Model[TaskLogRecord] -> Output[List[str]]\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def get_sources(self, task_id: str) -> list[str]:\n with belief_scope(\n \"TaskLogPersistenceService.get_sources\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n from sqlalchemy import distinct\n sources = (\n session.query(distinct(TaskLogRecord.source))\n .filter(TaskLogRecord.task_id == task_id)\n .all()\n )\n return [s[0] for s in sources]\n finally:\n session.close()\n # #endregion get_sources\n # #region delete_logs_for_task [TYPE Function] [C:3]\n # @PURPOSE: Delete all logs for a specific task.\n # @PRE: task_id is a valid task ID.\n # @POST: All logs for the task are deleted.\n # @PARAM: task_id (str) - The task ID.\n # @SIDE_EFFECT: Deletes from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_task(self, task_id: str) -> None:\n with belief_scope(\n \"TaskLogPersistenceService.delete_logs_for_task\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n ).delete(synchronize_session=False)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete logs for task {task_id}: {e}\")\n finally:\n session.close()\n # #endregion delete_logs_for_task\n # #region delete_logs_for_tasks [TYPE Function] [C:3]\n # @PURPOSE: Delete all logs for multiple tasks.\n # @PRE: task_ids is a list of task IDs.\n # @POST: All logs for the tasks are deleted.\n # @PARAM: task_ids (List[str]) - List of task IDs.\n # @SIDE_EFFECT: Deletes rows from task_logs table.\n # @RELATION: [DEPENDS_ON] ->[TaskLogRecord]\n def delete_logs_for_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskLogPersistenceService.delete_logs_for_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id.in_(task_ids)\n ).delete(synchronize_session=False)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete logs for tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_logs_for_tasks\n# #endregion TaskLogPersistenceService\n" }, { "contract_id": "TaskLoggerModule", "contract_type": "Module", "file_path": "backend/src/core/task_manager/task_logger.py", "start_line": 1, - "end_line": 194, + "end_line": 167, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "Each TaskLogger instance is bound to a specific task_id and default source.", "LAYER": "Core", - "PURPOSE": "Provides a dedicated logger for tasks with automatic source attribution.", - "SEMANTICS": [ - "task", - "logger", - "context", - "plugin", - "attribution" - ] + "PURPOSE": "Provides a dedicated logger for tasks with automatic source attribution." }, "relations": [ { "source_id": "TaskLoggerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" }, { "source_id": "TaskLoggerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "EventBus", "target_ref": "[EventBus]" } @@ -48913,84 +39712,42 @@ "actual_complexity": 2, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskLoggerModule:Module]\n# @SEMANTICS: task, logger, context, plugin, attribution\n# @PURPOSE: Provides a dedicated logger for tasks with automatic source attribution.\n# @LAYER: Core\n# @RELATION: [DEPENDS_ON] -> [TaskManager]\n# @RELATION: [DEPENDS_ON] -> [EventBus]\n# @COMPLEXITY: 2\n# @INVARIANT: Each TaskLogger instance is bound to a specific task_id and default source.\n\n# [SECTION: IMPORTS]\nfrom typing import Dict, Any, Optional, Callable\n# [/SECTION]\n\n\n# [DEF:TaskLogger:Class]\n# @SEMANTICS: logger, task, source, attribution\n# @PURPOSE: A wrapper around TaskManager._add_log that carries task_id and source context.\n# @COMPLEXITY: 2\n# @RELATION: [DEPENDS_ON] -> [TaskManager]\n# @RELATION: [DEPENDS_ON] -> [EventBus]\n# @RELATION: [USED_BY] -> [TaskContext]\n# @INVARIANT: All log calls include the task_id and source.\n# @UX_STATE: Idle -> Logging -> (system records log)\n#\n# @TEST_CONTRACT: TaskLoggerContract ->\n# {\n# required_fields: {task_id: str, add_log_fn: Callable},\n# optional_fields: {source: str},\n# invariants: [\n# \"All specific log methods (info, error) delegate to _log\",\n# \"with_source creates a new logger with the same task_id\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_logger -> {\"task_id\": \"test_123\", \"add_log_fn\": lambda *args: None, \"source\": \"test_plugin\"}\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# @TEST_EDGE: invalid_add_log_fn -> raises TypeError\n# @TEST_INVARIANT: consistent_delegation -> verifies: [valid_task_logger]\nclass TaskLogger:\n \"\"\"\n A dedicated logger for tasks that automatically tags logs with source attribution.\n\n Usage:\n logger = TaskLogger(task_id=\"abc123\", add_log_fn=task_manager._add_log, source=\"plugin\")\n logger.info(\"Starting backup process\")\n logger.error(\"Failed to connect\", metadata={\"error_code\": 500})\n\n # Create sub-logger with different source\n api_logger = logger.with_source(\"superset_api\")\n api_logger.info(\"Fetching dashboards\")\n \"\"\"\n\n # [DEF:__init__:Function]\n # @PURPOSE: Initialize the TaskLogger with task context.\n # @PRE: add_log_fn is a callable that accepts (task_id, level, message, context, source, metadata).\n # @POST: TaskLogger is ready to log messages.\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager.\n # @PARAM: source (str) - Default source for logs (default: \"plugin\").\n def __init__(self, task_id: str, add_log_fn: Callable, source: str = \"plugin\"):\n self._task_id = task_id\n self._add_log = add_log_fn\n self._default_source = source\n\n # [/DEF:__init__:Function]\n\n # [DEF:with_source:Function]\n # @PURPOSE: Create a sub-logger with a different default source.\n # @PRE: source is a non-empty string.\n # @POST: Returns new TaskLogger with the same task_id but different source.\n # @PARAM: source (str) - New default source.\n # @RETURN: TaskLogger - New logger instance.\n def with_source(self, source: str) -> \"TaskLogger\":\n \"\"\"Create a sub-logger with a different source context.\"\"\"\n return TaskLogger(\n task_id=self._task_id, add_log_fn=self._add_log, source=source\n )\n\n # [/DEF:with_source:Function]\n\n # [DEF:_log:Function]\n # @PURPOSE: Internal method to log a message at a given level.\n # @PRE: level is a valid log level string.\n # @POST: Log entry added via add_log_fn.\n # @PARAM: level (str) - Log level (DEBUG, INFO, WARNING, ERROR).\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source for this log entry.\n # @PARAM: metadata (Optional[Dict]) - Additional structured data.\n # @UX_STATE: Logging -> (writing internal log)\n def _log(\n self,\n level: str,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n \"\"\"Internal logging method.\"\"\"\n self._add_log(\n task_id=self._task_id,\n level=level,\n message=message,\n source=source or self._default_source,\n metadata=metadata,\n )\n\n # [/DEF:_log:Function]\n\n # [DEF:debug:Function]\n # @PURPOSE: Log a DEBUG level message.\n # @PRE: message is a string.\n # @POST: Log entry added via internally with DEBUG level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def debug(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"DEBUG\", message, source, metadata)\n\n # [/DEF:debug:Function]\n\n # [DEF:info:Function]\n # @PURPOSE: Log an INFO level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with INFO level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def info(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"INFO\", message, source, metadata)\n\n # [/DEF:info:Function]\n\n # [DEF:warning:Function]\n # @PURPOSE: Log a WARNING level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with WARNING level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def warning(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"WARNING\", message, source, metadata)\n\n # [/DEF:warning:Function]\n\n # [DEF:error:Function]\n # @PURPOSE: Log an ERROR level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with ERROR level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def error(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"ERROR\", message, source, metadata)\n\n # [/DEF:error:Function]\n\n # [DEF:progress:Function]\n # @PURPOSE: Log a progress update with percentage.\n # @PRE: percent is between 0 and 100.\n # @POST: Log entry with progress metadata added.\n # @PARAM: message (str) - Progress message.\n # @PARAM: percent (float) - Progress percentage (0-100).\n # @PARAM: source (Optional[str]) - Override source.\n def progress(\n self, message: str, percent: float, source: Optional[str] = None\n ) -> None:\n \"\"\"Log a progress update with percentage.\"\"\"\n metadata = {\"progress\": min(100, max(0, percent))}\n self._log(\"INFO\", message, source, metadata)\n\n # [/DEF:progress:Function]\n\n\n# [/DEF:TaskLogger:Class]\n\n# [/DEF:TaskLoggerModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region TaskLoggerModule [C:2] [TYPE Module] [SEMANTICS task, logger, log, streaming]\n# @BRIEF Provides a dedicated logger for tasks with automatic source attribution.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT: Each TaskLogger instance is bound to a specific task_id and default source.\nfrom collections.abc import Callable\nfrom typing import Any\n\n\n# #region TaskLogger [C:2] [TYPE Class] [SEMANTICS logger, task, source, attribution]\n# @BRIEF A wrapper around TaskManager._add_log that carries task_id and source context.\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @RELATION USED_BY -> [TaskContext]\n# @INVARIANT: All log calls include the task_id and source.\n# @UX_STATE: Idle -> Logging -> (system records log)\n#\n# @TEST_CONTRACT: TaskLoggerContract ->\n# {\n# required_fields: {task_id: str, add_log_fn: Callable},\n# optional_fields: {source: str},\n# invariants: [\n# \"All specific log methods (info, error) delegate to _log\",\n# \"with_source creates a new logger with the same task_id\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_logger -> {\"task_id\": \"test_123\", \"add_log_fn\": lambda *args: None, \"source\": \"test_plugin\"}\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# @TEST_EDGE: invalid_add_log_fn -> raises TypeError\n# @TEST_INVARIANT: consistent_delegation -> verifies: [valid_task_logger]\nclass TaskLogger:\n \"\"\"\n A dedicated logger for tasks that automatically tags logs with source attribution.\n Usage:\n logger = TaskLogger(task_id=\"abc123\", add_log_fn=task_manager._add_log, source=\"plugin\")\n logger.info(\"Starting backup process\")\n logger.error(\"Failed to connect\", metadata={\"error_code\": 500})\n # Create sub-logger with different source\n api_logger = logger.with_source(\"superset_api\")\n api_logger.info(\"Fetching dashboards\")\n \"\"\"\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initialize the TaskLogger with task context.\n # @PRE: add_log_fn is a callable that accepts (task_id, level, message, context, source, metadata).\n # @POST: TaskLogger is ready to log messages.\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager.\n # @PARAM: source (str) - Default source for logs (default: \"plugin\").\n def __init__(self, task_id: str, add_log_fn: Callable, source: str = \"plugin\"):\n self._task_id = task_id\n self._add_log = add_log_fn\n self._default_source = source\n # #endregion __init__\n # #region with_source [TYPE Function]\n # @PURPOSE: Create a sub-logger with a different default source.\n # @PRE: source is a non-empty string.\n # @POST: Returns new TaskLogger with the same task_id but different source.\n # @PARAM: source (str) - New default source.\n # @RETURN: TaskLogger - New logger instance.\n def with_source(self, source: str) -> \"TaskLogger\":\n \"\"\"Create a sub-logger with a different source context.\"\"\"\n return TaskLogger(\n task_id=self._task_id, add_log_fn=self._add_log, source=source\n )\n # #endregion with_source\n # #region _log [TYPE Function]\n # @PURPOSE: Internal method to log a message at a given level.\n # @PRE: level is a valid log level string.\n # @POST: Log entry added via add_log_fn.\n # @PARAM: level (str) - Log level (DEBUG, INFO, WARNING, ERROR).\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source for this log entry.\n # @PARAM: metadata (Optional[Dict]) - Additional structured data.\n # @UX_STATE: Logging -> (writing internal log)\n def _log(\n self,\n level: str,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n \"\"\"Internal logging method.\"\"\"\n self._add_log(\n task_id=self._task_id,\n level=level,\n message=message,\n source=source or self._default_source,\n metadata=metadata,\n )\n # #endregion _log\n # #region debug [TYPE Function]\n # @PURPOSE: Log a DEBUG level message.\n # @PRE: message is a string.\n # @POST: Log entry added via internally with DEBUG level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def debug(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"DEBUG\", message, source, metadata)\n # #endregion debug\n # #region info [TYPE Function]\n # @PURPOSE: Log an INFO level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with INFO level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def info(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"INFO\", message, source, metadata)\n # #endregion info\n # #region warning [TYPE Function]\n # @PURPOSE: Log a WARNING level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with WARNING level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def warning(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"WARNING\", message, source, metadata)\n # #endregion warning\n # #region error [TYPE Function]\n # @PURPOSE: Log an ERROR level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with ERROR level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def error(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"ERROR\", message, source, metadata)\n # #endregion error\n # #region progress [TYPE Function]\n # @PURPOSE: Log a progress update with percentage.\n # @PRE: percent is between 0 and 100.\n # @POST: Log entry with progress metadata added.\n # @PARAM: message (str) - Progress message.\n # @PARAM: percent (float) - Progress percentage (0-100).\n # @PARAM: source (Optional[str]) - Override source.\n def progress(\n self, message: str, percent: float, source: str | None = None\n ) -> None:\n \"\"\"Log a progress update with percentage.\"\"\"\n metadata = {\"progress\": min(100, max(0, percent))}\n self._log(\"INFO\", message, source, metadata)\n # #endregion progress\n# #endregion TaskLogger\n# #endregion TaskLoggerModule\n" }, { "contract_id": "TaskLogger", "contract_type": "Class", "file_path": "backend/src/core/task_manager/task_logger.py", - "start_line": 15, - "end_line": 192, + "start_line": 11, + "end_line": 166, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "All log calls include the task_id and source.", "PURPOSE": "A wrapper around TaskManager._add_log that carries task_id and source context.", - "SEMANTICS": [ - "logger", - "task", - "source", - "attribution" - ], "TEST_CONTRACT": "TaskLoggerContract ->", "UX_STATE": "Idle -> Logging -> (system records log)" }, "relations": [ { "source_id": "TaskLogger", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" }, { "source_id": "TaskLogger", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "EventBus", "target_ref": "[EventBus]" }, { "source_id": "TaskLogger", - "relation_type": "[USED_BY]", + "relation_type": "USED_BY", "target_id": "TaskContext", "target_ref": "[TaskContext]" } @@ -49005,29 +39762,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -49081,7 +39815,7 @@ { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate [DEPENDS_ON] is not in allowed_predicates", + "message": "Predicate USED_BY is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -49093,55 +39827,19 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "[DEPENDS_ON]" - } - }, - { - "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]" - } - }, - { - "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]" + "predicate": "USED_BY" } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskLogger:Class]\n# @SEMANTICS: logger, task, source, attribution\n# @PURPOSE: A wrapper around TaskManager._add_log that carries task_id and source context.\n# @COMPLEXITY: 2\n# @RELATION: [DEPENDS_ON] -> [TaskManager]\n# @RELATION: [DEPENDS_ON] -> [EventBus]\n# @RELATION: [USED_BY] -> [TaskContext]\n# @INVARIANT: All log calls include the task_id and source.\n# @UX_STATE: Idle -> Logging -> (system records log)\n#\n# @TEST_CONTRACT: TaskLoggerContract ->\n# {\n# required_fields: {task_id: str, add_log_fn: Callable},\n# optional_fields: {source: str},\n# invariants: [\n# \"All specific log methods (info, error) delegate to _log\",\n# \"with_source creates a new logger with the same task_id\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_logger -> {\"task_id\": \"test_123\", \"add_log_fn\": lambda *args: None, \"source\": \"test_plugin\"}\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# @TEST_EDGE: invalid_add_log_fn -> raises TypeError\n# @TEST_INVARIANT: consistent_delegation -> verifies: [valid_task_logger]\nclass TaskLogger:\n \"\"\"\n A dedicated logger for tasks that automatically tags logs with source attribution.\n\n Usage:\n logger = TaskLogger(task_id=\"abc123\", add_log_fn=task_manager._add_log, source=\"plugin\")\n logger.info(\"Starting backup process\")\n logger.error(\"Failed to connect\", metadata={\"error_code\": 500})\n\n # Create sub-logger with different source\n api_logger = logger.with_source(\"superset_api\")\n api_logger.info(\"Fetching dashboards\")\n \"\"\"\n\n # [DEF:__init__:Function]\n # @PURPOSE: Initialize the TaskLogger with task context.\n # @PRE: add_log_fn is a callable that accepts (task_id, level, message, context, source, metadata).\n # @POST: TaskLogger is ready to log messages.\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager.\n # @PARAM: source (str) - Default source for logs (default: \"plugin\").\n def __init__(self, task_id: str, add_log_fn: Callable, source: str = \"plugin\"):\n self._task_id = task_id\n self._add_log = add_log_fn\n self._default_source = source\n\n # [/DEF:__init__:Function]\n\n # [DEF:with_source:Function]\n # @PURPOSE: Create a sub-logger with a different default source.\n # @PRE: source is a non-empty string.\n # @POST: Returns new TaskLogger with the same task_id but different source.\n # @PARAM: source (str) - New default source.\n # @RETURN: TaskLogger - New logger instance.\n def with_source(self, source: str) -> \"TaskLogger\":\n \"\"\"Create a sub-logger with a different source context.\"\"\"\n return TaskLogger(\n task_id=self._task_id, add_log_fn=self._add_log, source=source\n )\n\n # [/DEF:with_source:Function]\n\n # [DEF:_log:Function]\n # @PURPOSE: Internal method to log a message at a given level.\n # @PRE: level is a valid log level string.\n # @POST: Log entry added via add_log_fn.\n # @PARAM: level (str) - Log level (DEBUG, INFO, WARNING, ERROR).\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source for this log entry.\n # @PARAM: metadata (Optional[Dict]) - Additional structured data.\n # @UX_STATE: Logging -> (writing internal log)\n def _log(\n self,\n level: str,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n \"\"\"Internal logging method.\"\"\"\n self._add_log(\n task_id=self._task_id,\n level=level,\n message=message,\n source=source or self._default_source,\n metadata=metadata,\n )\n\n # [/DEF:_log:Function]\n\n # [DEF:debug:Function]\n # @PURPOSE: Log a DEBUG level message.\n # @PRE: message is a string.\n # @POST: Log entry added via internally with DEBUG level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def debug(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"DEBUG\", message, source, metadata)\n\n # [/DEF:debug:Function]\n\n # [DEF:info:Function]\n # @PURPOSE: Log an INFO level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with INFO level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def info(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"INFO\", message, source, metadata)\n\n # [/DEF:info:Function]\n\n # [DEF:warning:Function]\n # @PURPOSE: Log a WARNING level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with WARNING level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def warning(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"WARNING\", message, source, metadata)\n\n # [/DEF:warning:Function]\n\n # [DEF:error:Function]\n # @PURPOSE: Log an ERROR level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with ERROR level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def error(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"ERROR\", message, source, metadata)\n\n # [/DEF:error:Function]\n\n # [DEF:progress:Function]\n # @PURPOSE: Log a progress update with percentage.\n # @PRE: percent is between 0 and 100.\n # @POST: Log entry with progress metadata added.\n # @PARAM: message (str) - Progress message.\n # @PARAM: percent (float) - Progress percentage (0-100).\n # @PARAM: source (Optional[str]) - Override source.\n def progress(\n self, message: str, percent: float, source: Optional[str] = None\n ) -> None:\n \"\"\"Log a progress update with percentage.\"\"\"\n metadata = {\"progress\": min(100, max(0, percent))}\n self._log(\"INFO\", message, source, metadata)\n\n # [/DEF:progress:Function]\n\n\n# [/DEF:TaskLogger:Class]\n" + "anchor_syntax": "region", + "body": "# #region TaskLogger [C:2] [TYPE Class] [SEMANTICS logger, task, source, attribution]\n# @BRIEF A wrapper around TaskManager._add_log that carries task_id and source context.\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @RELATION USED_BY -> [TaskContext]\n# @INVARIANT: All log calls include the task_id and source.\n# @UX_STATE: Idle -> Logging -> (system records log)\n#\n# @TEST_CONTRACT: TaskLoggerContract ->\n# {\n# required_fields: {task_id: str, add_log_fn: Callable},\n# optional_fields: {source: str},\n# invariants: [\n# \"All specific log methods (info, error) delegate to _log\",\n# \"with_source creates a new logger with the same task_id\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_logger -> {\"task_id\": \"test_123\", \"add_log_fn\": lambda *args: None, \"source\": \"test_plugin\"}\n# @TEST_EDGE: missing_task_id -> raises TypeError\n# @TEST_EDGE: invalid_add_log_fn -> raises TypeError\n# @TEST_INVARIANT: consistent_delegation -> verifies: [valid_task_logger]\nclass TaskLogger:\n \"\"\"\n A dedicated logger for tasks that automatically tags logs with source attribution.\n Usage:\n logger = TaskLogger(task_id=\"abc123\", add_log_fn=task_manager._add_log, source=\"plugin\")\n logger.info(\"Starting backup process\")\n logger.error(\"Failed to connect\", metadata={\"error_code\": 500})\n # Create sub-logger with different source\n api_logger = logger.with_source(\"superset_api\")\n api_logger.info(\"Fetching dashboards\")\n \"\"\"\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initialize the TaskLogger with task context.\n # @PRE: add_log_fn is a callable that accepts (task_id, level, message, context, source, metadata).\n # @POST: TaskLogger is ready to log messages.\n # @PARAM: task_id (str) - The ID of the task.\n # @PARAM: add_log_fn (Callable) - Function to add log to TaskManager.\n # @PARAM: source (str) - Default source for logs (default: \"plugin\").\n def __init__(self, task_id: str, add_log_fn: Callable, source: str = \"plugin\"):\n self._task_id = task_id\n self._add_log = add_log_fn\n self._default_source = source\n # #endregion __init__\n # #region with_source [TYPE Function]\n # @PURPOSE: Create a sub-logger with a different default source.\n # @PRE: source is a non-empty string.\n # @POST: Returns new TaskLogger with the same task_id but different source.\n # @PARAM: source (str) - New default source.\n # @RETURN: TaskLogger - New logger instance.\n def with_source(self, source: str) -> \"TaskLogger\":\n \"\"\"Create a sub-logger with a different source context.\"\"\"\n return TaskLogger(\n task_id=self._task_id, add_log_fn=self._add_log, source=source\n )\n # #endregion with_source\n # #region _log [TYPE Function]\n # @PURPOSE: Internal method to log a message at a given level.\n # @PRE: level is a valid log level string.\n # @POST: Log entry added via add_log_fn.\n # @PARAM: level (str) - Log level (DEBUG, INFO, WARNING, ERROR).\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source for this log entry.\n # @PARAM: metadata (Optional[Dict]) - Additional structured data.\n # @UX_STATE: Logging -> (writing internal log)\n def _log(\n self,\n level: str,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n \"\"\"Internal logging method.\"\"\"\n self._add_log(\n task_id=self._task_id,\n level=level,\n message=message,\n source=source or self._default_source,\n metadata=metadata,\n )\n # #endregion _log\n # #region debug [TYPE Function]\n # @PURPOSE: Log a DEBUG level message.\n # @PRE: message is a string.\n # @POST: Log entry added via internally with DEBUG level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def debug(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"DEBUG\", message, source, metadata)\n # #endregion debug\n # #region info [TYPE Function]\n # @PURPOSE: Log an INFO level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with INFO level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def info(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"INFO\", message, source, metadata)\n # #endregion info\n # #region warning [TYPE Function]\n # @PURPOSE: Log a WARNING level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with WARNING level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def warning(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"WARNING\", message, source, metadata)\n # #endregion warning\n # #region error [TYPE Function]\n # @PURPOSE: Log an ERROR level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with ERROR level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def error(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"ERROR\", message, source, metadata)\n # #endregion error\n # #region progress [TYPE Function]\n # @PURPOSE: Log a progress update with percentage.\n # @PRE: percent is between 0 and 100.\n # @POST: Log entry with progress metadata added.\n # @PARAM: message (str) - Progress message.\n # @PARAM: percent (float) - Progress percentage (0-100).\n # @PARAM: source (Optional[str]) - Override source.\n def progress(\n self, message: str, percent: float, source: str | None = None\n ) -> None:\n \"\"\"Log a progress update with percentage.\"\"\"\n metadata = {\"progress\": min(100, max(0, percent))}\n self._log(\"INFO\", message, source, metadata)\n # #endregion progress\n# #endregion TaskLogger\n" }, { "contract_id": "with_source", "contract_type": "Function", "file_path": "backend/src/core/task_manager/task_logger.py", - "start_line": 66, - "end_line": 78, + "start_line": 55, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49193,15 +39891,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:with_source:Function]\n # @PURPOSE: Create a sub-logger with a different default source.\n # @PRE: source is a non-empty string.\n # @POST: Returns new TaskLogger with the same task_id but different source.\n # @PARAM: source (str) - New default source.\n # @RETURN: TaskLogger - New logger instance.\n def with_source(self, source: str) -> \"TaskLogger\":\n \"\"\"Create a sub-logger with a different source context.\"\"\"\n return TaskLogger(\n task_id=self._task_id, add_log_fn=self._add_log, source=source\n )\n\n # [/DEF:with_source:Function]\n" + "anchor_syntax": "region", + "body": " # #region with_source [TYPE Function]\n # @PURPOSE: Create a sub-logger with a different default source.\n # @PRE: source is a non-empty string.\n # @POST: Returns new TaskLogger with the same task_id but different source.\n # @PARAM: source (str) - New default source.\n # @RETURN: TaskLogger - New logger instance.\n def with_source(self, source: str) -> \"TaskLogger\":\n \"\"\"Create a sub-logger with a different source context.\"\"\"\n return TaskLogger(\n task_id=self._task_id, add_log_fn=self._add_log, source=source\n )\n # #endregion with_source\n" }, { "contract_id": "_log", "contract_type": "Function", "file_path": "backend/src/core/task_manager/task_logger.py", - "start_line": 80, - "end_line": 105, + "start_line": 67, + "end_line": 91, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49267,15 +39965,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_log:Function]\n # @PURPOSE: Internal method to log a message at a given level.\n # @PRE: level is a valid log level string.\n # @POST: Log entry added via add_log_fn.\n # @PARAM: level (str) - Log level (DEBUG, INFO, WARNING, ERROR).\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source for this log entry.\n # @PARAM: metadata (Optional[Dict]) - Additional structured data.\n # @UX_STATE: Logging -> (writing internal log)\n def _log(\n self,\n level: str,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n \"\"\"Internal logging method.\"\"\"\n self._add_log(\n task_id=self._task_id,\n level=level,\n message=message,\n source=source or self._default_source,\n metadata=metadata,\n )\n\n # [/DEF:_log:Function]\n" + "anchor_syntax": "region", + "body": " # #region _log [TYPE Function]\n # @PURPOSE: Internal method to log a message at a given level.\n # @PRE: level is a valid log level string.\n # @POST: Log entry added via add_log_fn.\n # @PARAM: level (str) - Log level (DEBUG, INFO, WARNING, ERROR).\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source for this log entry.\n # @PARAM: metadata (Optional[Dict]) - Additional structured data.\n # @UX_STATE: Logging -> (writing internal log)\n def _log(\n self,\n level: str,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n \"\"\"Internal logging method.\"\"\"\n self._add_log(\n task_id=self._task_id,\n level=level,\n message=message,\n source=source or self._default_source,\n metadata=metadata,\n )\n # #endregion _log\n" }, { "contract_id": "debug", "contract_type": "Function", "file_path": "backend/src/core/task_manager/task_logger.py", - "start_line": 107, - "end_line": 122, + "start_line": 92, + "end_line": 106, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49320,15 +40018,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:debug:Function]\n # @PURPOSE: Log a DEBUG level message.\n # @PRE: message is a string.\n # @POST: Log entry added via internally with DEBUG level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def debug(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"DEBUG\", message, source, metadata)\n\n # [/DEF:debug:Function]\n" + "anchor_syntax": "region", + "body": " # #region debug [TYPE Function]\n # @PURPOSE: Log a DEBUG level message.\n # @PRE: message is a string.\n # @POST: Log entry added via internally with DEBUG level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def debug(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"DEBUG\", message, source, metadata)\n # #endregion debug\n" }, { "contract_id": "info", "contract_type": "Function", "file_path": "backend/src/core/task_manager/task_logger.py", - "start_line": 124, - "end_line": 139, + "start_line": 107, + "end_line": 121, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49373,15 +40071,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:info:Function]\n # @PURPOSE: Log an INFO level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with INFO level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def info(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"INFO\", message, source, metadata)\n\n # [/DEF:info:Function]\n" + "anchor_syntax": "region", + "body": " # #region info [TYPE Function]\n # @PURPOSE: Log an INFO level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with INFO level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def info(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"INFO\", message, source, metadata)\n # #endregion info\n" }, { "contract_id": "warning", "contract_type": "Function", "file_path": "backend/src/core/task_manager/task_logger.py", - "start_line": 141, - "end_line": 156, + "start_line": 122, + "end_line": 136, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49426,15 +40124,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:warning:Function]\n # @PURPOSE: Log a WARNING level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with WARNING level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def warning(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"WARNING\", message, source, metadata)\n\n # [/DEF:warning:Function]\n" + "anchor_syntax": "region", + "body": " # #region warning [TYPE Function]\n # @PURPOSE: Log a WARNING level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with WARNING level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def warning(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"WARNING\", message, source, metadata)\n # #endregion warning\n" }, { "contract_id": "error", "contract_type": "Function", "file_path": "backend/src/core/task_manager/task_logger.py", - "start_line": 158, - "end_line": 173, + "start_line": 137, + "end_line": 151, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49479,15 +40177,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:error:Function]\n # @PURPOSE: Log an ERROR level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with ERROR level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def error(\n self,\n message: str,\n source: Optional[str] = None,\n metadata: Optional[Dict[str, Any]] = None,\n ) -> None:\n self._log(\"ERROR\", message, source, metadata)\n\n # [/DEF:error:Function]\n" + "anchor_syntax": "region", + "body": " # #region error [TYPE Function]\n # @PURPOSE: Log an ERROR level message.\n # @PRE: message is a string.\n # @POST: Log entry added internally with ERROR level.\n # @PARAM: message (str) - Log message.\n # @PARAM: source (Optional[str]) - Override source.\n # @PARAM: metadata (Optional[Dict]) - Additional data.\n def error(\n self,\n message: str,\n source: str | None = None,\n metadata: dict[str, Any] | None = None,\n ) -> None:\n self._log(\"ERROR\", message, source, metadata)\n # #endregion error\n" }, { "contract_id": "progress", "contract_type": "Function", "file_path": "backend/src/core/task_manager/task_logger.py", - "start_line": 175, - "end_line": 189, + "start_line": 152, + "end_line": 165, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -49532,8 +40230,8 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:progress:Function]\n # @PURPOSE: Log a progress update with percentage.\n # @PRE: percent is between 0 and 100.\n # @POST: Log entry with progress metadata added.\n # @PARAM: message (str) - Progress message.\n # @PARAM: percent (float) - Progress percentage (0-100).\n # @PARAM: source (Optional[str]) - Override source.\n def progress(\n self, message: str, percent: float, source: Optional[str] = None\n ) -> None:\n \"\"\"Log a progress update with percentage.\"\"\"\n metadata = {\"progress\": min(100, max(0, percent))}\n self._log(\"INFO\", message, source, metadata)\n\n # [/DEF:progress:Function]\n" + "anchor_syntax": "region", + "body": " # #region progress [TYPE Function]\n # @PURPOSE: Log a progress update with percentage.\n # @PRE: percent is between 0 and 100.\n # @POST: Log entry with progress metadata added.\n # @PARAM: message (str) - Progress message.\n # @PARAM: percent (float) - Progress percentage (0-100).\n # @PARAM: source (Optional[str]) - Override source.\n def progress(\n self, message: str, percent: float, source: str | None = None\n ) -> None:\n \"\"\"Log a progress update with percentage.\"\"\"\n metadata = {\"progress\": min(100, max(0, percent))}\n self._log(\"INFO\", message, source, metadata)\n # #endregion progress\n" }, { "contract_id": "CoreUtils", @@ -49576,39 +40274,31 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CoreUtils:Package]\n# @PURPOSE: Shared utility package root.\n# [/DEF:CoreUtils:Package]\n" + "anchor_syntax": "region", + "body": "# #region CoreUtils [TYPE Package] [SEMANTICS utils, package, init]\n# @BRIEF Shared utility package root.\n# #endregion CoreUtils\n" }, { "contract_id": "AsyncNetworkModule", "contract_type": "Module", "file_path": "backend/src/core/utils/async_network.py", "start_line": 1, - "end_line": 323, + "end_line": 270, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[config: Dict[str, Any]] -> Output[authenticated async Superset HTTP interactions]", "INVARIANT": "Async client reuses cached auth tokens per environment credentials and invalidates on 401.", "LAYER": "Infra", "POST": "Async network clients reuse cached auth tokens and expose stable async request/error translation flow.", "PRE": "Config payloads contain a Superset base URL and authentication fields needed for login.", "PURPOSE": "Provides async Superset API client with shared auth-token cache to avoid per-request re-login.", - "SEMANTICS": [ - "network", - "httpx", - "async", - "superset", - "authentication", - "cache" - ], "SIDE_EFFECT": "Performs upstream HTTP I/O and mutates process-local auth cache entries." }, "relations": [ { "source_id": "AsyncNetworkModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetAuthCache", "target_ref": "[SupersetAuthCache]" } @@ -49631,969 +40321,59 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:AsyncNetworkModule:Module]\n#\n# @COMPLEXITY: 5\n# @SEMANTICS: network, httpx, async, superset, authentication, cache\n# @PURPOSE: Provides async Superset API client with shared auth-token cache to avoid per-request re-login.\n# @LAYER: Infra\n# @PRE: Config payloads contain a Superset base URL and authentication fields needed for login.\n# @POST: Async network clients reuse cached auth tokens and expose stable async request/error translation flow.\n# @SIDE_EFFECT: Performs upstream HTTP I/O and mutates process-local auth cache entries.\n# @DATA_CONTRACT: Input[config: Dict[str, Any]] -> Output[authenticated async Superset HTTP interactions]\n# @RELATION: [DEPENDS_ON] ->[SupersetAuthCache]\n# @INVARIANT: Async client reuses cached auth tokens per environment credentials and invalidates on 401.\n\n# [SECTION: IMPORTS]\nfrom typing import Optional, Dict, Any, Union\nimport asyncio\n\nimport httpx\n\nfrom ..logger import logger as app_logger, belief_scope\nfrom ..cot_logger import MarkerLogger\nfrom .network import (\n AuthenticationError,\n DashboardNotFoundError,\n NetworkError,\n PermissionDeniedError,\n SupersetAPIError,\n SupersetAuthCache,\n)\n# [/SECTION]\n\nlog = MarkerLogger(\"AsyncNetwork\")\n\n# [DEF:AsyncAPIClient:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Async Superset API client backed by httpx.AsyncClient with shared auth cache.\n# @RELATION: [DEPENDS_ON] ->[SupersetAuthCache]\n# @RELATION: [CALLS] ->[SupersetAuthCache.get]\n# @RELATION: [CALLS] ->[SupersetAuthCache.set]\nclass AsyncAPIClient:\n DEFAULT_TIMEOUT = 30\n _auth_locks: Dict[tuple[str, str, bool], asyncio.Lock] = {}\n\n # [DEF:AsyncAPIClient.__init__:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize async API client for one environment.\n # @PRE: config contains base_url and auth payload.\n # @POST: Client is ready for async request/authentication flow.\n # @DATA_CONTRACT: Input[config: Dict[str, Any]] -> self._auth_cache_key[str]\n # @RELATION: [CALLS] ->[AsyncAPIClient._normalize_base_url]\n # @RELATION: [DEPENDS_ON] ->[SupersetAuthCache]\n def __init__(self, config: Dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\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._client = httpx.AsyncClient(\n verify=verify_ssl,\n timeout=httpx.Timeout(timeout),\n follow_redirects=True,\n )\n self._tokens: Dict[str, str] = {}\n self._authenticated = False\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n\n # [/DEF:AsyncAPIClient.__init__:Function]\n\n # [DEF:AsyncAPIClient._normalize_base_url:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize base URL for Superset API root construction.\n # @POST: Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.\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 # [/DEF:AsyncAPIClient._normalize_base_url:Function]\n\n # [DEF:AsyncAPIClient._build_api_url:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Build full API URL from relative Superset endpoint.\n # @POST: Returns absolute URL for upstream request.\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 # [/DEF:AsyncAPIClient._build_api_url:Function]\n\n # [DEF:AsyncAPIClient._get_auth_lock:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Return per-cache-key async lock to serialize fresh login attempts.\n # @POST: Returns stable asyncio.Lock instance.\n @classmethod\n def _get_auth_lock(cls, cache_key: tuple[str, str, bool]) -> asyncio.Lock:\n existing_lock = cls._auth_locks.get(cache_key)\n if existing_lock is not None:\n return existing_lock\n created_lock = asyncio.Lock()\n cls._auth_locks[cache_key] = created_lock\n return created_lock\n # [/DEF:AsyncAPIClient._get_auth_lock:Function]\n\n # [DEF:AsyncAPIClient.authenticate:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Authenticate against Superset and cache access/csrf tokens.\n # @POST: Client tokens are populated and reusable across requests.\n # @SIDE_EFFECT: Performs network requests to Superset authentication endpoints.\n # @DATA_CONTRACT: None -> Output[Dict[str, str]]\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n # @RELATION: [CALLS] ->[AsyncAPIClient._get_auth_lock]\n async def authenticate(self) -> Dict[str, str]:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n # Always fetch a fresh CSRF token to establish session cookie in this httpx client.\n # The cached access_token may still be valid; we avoid a full POST login.\n try:\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = await self._client.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {self._tokens['access_token']}\"},\n )\n csrf_response.raise_for_status()\n self._tokens[\"csrf_token\"] = csrf_response.json()[\"result\"]\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n self._authenticated = True\n log.reason(\"Reused cached tokens + refreshed CSRF token\", payload={\"base_url\": self.base_url})\n return self._tokens\n except Exception as csrf_err:\n log.explore(\"CSRF refresh failed, performing full re-auth\", error=str(csrf_err))\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._tokens = {}\n self._authenticated = False\n\n auth_lock = self._get_auth_lock(self._auth_cache_key)\n async with auth_lock:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n self._authenticated = True\n log.reason(\"Reusing cached Superset auth tokens (after lock wait)\", payload={\"base_url\": self.base_url})\n return self._tokens\n\n with belief_scope(\"AsyncAPIClient.authenticate\"):\n log.reason(\"Performing full authentication\", payload={\"base_url\": self.base_url})\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n response = await self._client.post(login_url, json=self.auth)\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 = await self._client.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {access_token}\"},\n )\n csrf_response.raise_for_status()\n\n self._tokens = {\n \"access_token\": access_token,\n \"csrf_token\": csrf_response.json()[\"result\"],\n }\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n log.reflect(\"Authenticated successfully\")\n return self._tokens\n except httpx.HTTPStatusError as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = exc.response.status_code if exc.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(\n f\"Environment unavailable during authentication (Status {status_code})\",\n status_code=status_code,\n ) from exc\n raise AuthenticationError(f\"Authentication failed: {exc}\") from exc\n except (httpx.HTTPError, KeyError) as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {exc}\") from exc\n # [/DEF:AsyncAPIClient.authenticate:Function]\n\n # [DEF:AsyncAPIClient.get_headers:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Return authenticated Superset headers for async requests.\n # @POST: Headers include Authorization and CSRF tokens.\n # @RELATION: [CALLS] ->[AsyncAPIClient.authenticate]\n async def get_headers(self) -> Dict[str, str]:\n if not self._authenticated:\n await 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 # [/DEF:AsyncAPIClient.get_headers:Function]\n\n # [DEF:AsyncAPIClient.request:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Perform one authenticated async Superset API request.\n # @POST: Returns JSON payload or raw httpx.Response when raw_response=true.\n # @SIDE_EFFECT: Performs network I/O.\n # @RELATION: [CALLS] ->[AsyncAPIClient.get_headers]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_http_error]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_network_error]\n async def request(\n self,\n method: str,\n endpoint: str,\n headers: Optional[Dict[str, str]] = None,\n raw_response: bool = False,\n **kwargs,\n ) -> Union[httpx.Response, Dict[str, Any]]:\n full_url = self._build_api_url(endpoint)\n request_headers = await self.get_headers()\n if headers:\n request_headers.update(headers)\n if \"allow_redirects\" in kwargs and \"follow_redirects\" not in kwargs:\n kwargs[\"follow_redirects\"] = bool(kwargs.pop(\"allow_redirects\"))\n\n try:\n response = await self._client.request(method, full_url, headers=request_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except httpx.HTTPStatusError as exc:\n if exc.response is not None and exc.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(exc, endpoint)\n except httpx.HTTPError as exc:\n self._handle_network_error(exc, full_url)\n # [/DEF:AsyncAPIClient.request:Function]\n\n # [DEF:AsyncAPIClient._handle_http_error:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Translate upstream HTTP errors into stable domain exceptions.\n # @POST: Raises domain-specific exception for caller flow control.\n # @DATA_CONTRACT: Input[httpx.HTTPStatusError] -> Exception\n # @RELATION: [CALLS] ->[AsyncAPIClient._is_dashboard_endpoint]\n # @RELATION: [DEPENDS_ON] ->[DashboardNotFoundError]\n # @RELATION: [DEPENDS_ON] ->[SupersetAPIError]\n # @RELATION: [DEPENDS_ON] ->[PermissionDeniedError]\n # @RELATION: [DEPENDS_ON] ->[AuthenticationError]\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_http_error\"):\n status_code = exc.response.status_code\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from exc\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from exc\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 exc\n if status_code == 403:\n raise PermissionDeniedError() from exc\n if status_code == 401:\n raise AuthenticationError() from exc\n raise SupersetAPIError(f\"API Error {status_code}: {exc.response.text}\") from exc\n # [/DEF:AsyncAPIClient._handle_http_error:Function]\n\n # [DEF:AsyncAPIClient._is_dashboard_endpoint:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\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 # [/DEF:AsyncAPIClient._is_dashboard_endpoint:Function]\n\n # [DEF:AsyncAPIClient._handle_network_error:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Translate generic httpx errors into NetworkError.\n # @POST: Raises NetworkError with URL context.\n # @DATA_CONTRACT: Input[httpx.HTTPError] -> NetworkError\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_network_error\"):\n if isinstance(exc, httpx.TimeoutException):\n message = \"Request timeout\"\n elif isinstance(exc, httpx.ConnectError):\n message = \"Connection error\"\n else:\n message = f\"Unknown network error: {exc}\"\n raise NetworkError(message, url=url) from exc\n # [/DEF:AsyncAPIClient._handle_network_error:Function]\n\n # [DEF:AsyncAPIClient.aclose:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Close underlying httpx client.\n # @POST: Client resources are released.\n # @SIDE_EFFECT: Closes network connections.\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient.__init__]\n async def aclose(self) -> None:\n await self._client.aclose()\n # [/DEF:AsyncAPIClient.aclose:Function]\n# [/DEF:AsyncAPIClient:Class]\n\n# [/DEF:AsyncNetworkModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region AsyncNetworkModule [C:5] [TYPE Module] [SEMANTICS auth, superset, token, async-client]\n#\n# @BRIEF Provides async Superset API client with shared auth-token cache to avoid per-request re-login.\n# @LAYER: Infra\n# @PRE: Config payloads contain a Superset base URL and authentication fields needed for login.\n# @POST: Async network clients reuse cached auth tokens and expose stable async request/error translation flow.\n# @SIDE_EFFECT: Performs upstream HTTP I/O and mutates process-local auth cache entries.\n# @DATA_CONTRACT: Input[config: Dict[str, Any]] -> Output[authenticated async Superset HTTP interactions]\n# @RELATION DEPENDS_ON -> [SupersetAuthCache]\n# @INVARIANT: Async client reuses cached auth tokens per environment credentials and invalidates on 401.\nimport asyncio\nfrom typing import Any\n\nimport httpx\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\nfrom .network import (\n AuthenticationError,\n DashboardNotFoundError,\n NetworkError,\n PermissionDeniedError,\n SupersetAPIError,\n SupersetAuthCache,\n)\n\n\n# #region AsyncAPIClient [C:3] [TYPE Class]\n# @BRIEF Async Superset API client backed by httpx.AsyncClient with shared auth cache.\n# @RELATION DEPENDS_ON -> [SupersetAuthCache]\n# @RELATION CALLS -> [SupersetAuthCache.get]\n# @RELATION CALLS -> [SupersetAuthCache.set]\nclass AsyncAPIClient:\n DEFAULT_TIMEOUT = 30\n _auth_locks: dict[tuple[str, str, bool], asyncio.Lock] = {}\n # #region AsyncAPIClient.__init__ [TYPE Function] [C:3]\n # @PURPOSE: Initialize async API client for one environment.\n # @PRE: config contains base_url and auth payload.\n # @POST: Client is ready for async request/authentication flow.\n # @DATA_CONTRACT: Input[config: Dict[str, Any]] -> self._auth_cache_key[str]\n # @RELATION: [CALLS] ->[AsyncAPIClient._normalize_base_url]\n # @RELATION: [DEPENDS_ON] ->[SupersetAuthCache]\n def __init__(self, config: dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\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._client = httpx.AsyncClient(\n verify=verify_ssl,\n timeout=httpx.Timeout(timeout),\n follow_redirects=True,\n )\n self._tokens: dict[str, str] = {}\n self._authenticated = False\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n # #endregion AsyncAPIClient.__init__\n # #region AsyncAPIClient._normalize_base_url [TYPE Function] [C:1]\n # @PURPOSE: Normalize base URL for Superset API root construction.\n # @POST: Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.\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 AsyncAPIClient._normalize_base_url\n # #region AsyncAPIClient._build_api_url [TYPE Function] [C:1]\n # @PURPOSE: Build full API URL from relative Superset endpoint.\n # @POST: Returns absolute URL for upstream request.\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 AsyncAPIClient._build_api_url\n # #region AsyncAPIClient._get_auth_lock [TYPE Function] [C:1]\n # @PURPOSE: Return per-cache-key async lock to serialize fresh login attempts.\n # @POST: Returns stable asyncio.Lock instance.\n @classmethod\n def _get_auth_lock(cls, cache_key: tuple[str, str, bool]) -> asyncio.Lock:\n existing_lock = cls._auth_locks.get(cache_key)\n if existing_lock is not None:\n return existing_lock\n created_lock = asyncio.Lock()\n cls._auth_locks[cache_key] = created_lock\n return created_lock\n # #endregion AsyncAPIClient._get_auth_lock\n # #region AsyncAPIClient.authenticate [TYPE Function] [C:3]\n # @PURPOSE: Authenticate against Superset and cache access/csrf tokens.\n # @POST: Client tokens are populated and reusable across requests.\n # @SIDE_EFFECT: Performs network requests to Superset authentication endpoints.\n # @DATA_CONTRACT: None -> Output[Dict[str, str]]\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n # @RELATION: [CALLS] ->[AsyncAPIClient._get_auth_lock]\n async def authenticate(self) -> dict[str, str]:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n self._authenticated = True\n app_logger.info(\"[async_authenticate][CacheHit] Reusing cached Superset auth tokens for %s\", self.base_url)\n return self._tokens\n auth_lock = self._get_auth_lock(self._auth_cache_key)\n async with auth_lock:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n self._authenticated = True\n app_logger.info(\"[async_authenticate][CacheHitAfterWait] Reusing cached Superset auth tokens for %s\", self.base_url)\n return self._tokens\n with belief_scope(\"AsyncAPIClient.authenticate\"):\n app_logger.info(\"[async_authenticate][Enter] Authenticating to %s\", self.base_url)\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n response = await self._client.post(login_url, json=self.auth)\n response.raise_for_status()\n access_token = response.json()[\"access_token\"]\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = await self._client.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {access_token}\"},\n )\n csrf_response.raise_for_status()\n self._tokens = {\n \"access_token\": access_token,\n \"csrf_token\": csrf_response.json()[\"result\"],\n }\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.reflect(\"Authenticated successfully.\", extra={\"src\": \"async_authenticate\"})\n return self._tokens\n except httpx.HTTPStatusError as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = exc.response.status_code if exc.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(\n f\"Environment unavailable during authentication (Status {status_code})\",\n status_code=status_code,\n ) from exc\n raise AuthenticationError(f\"Authentication failed: {exc}\") from exc\n except (httpx.HTTPError, KeyError) as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {exc}\") from exc\n # #endregion AsyncAPIClient.authenticate\n # #region AsyncAPIClient.get_headers [TYPE Function] [C:3]\n # @PURPOSE: Return authenticated Superset headers for async requests.\n # @POST: Headers include Authorization and CSRF tokens.\n # @RELATION: [CALLS] ->[AsyncAPIClient.authenticate]\n async def get_headers(self) -> dict[str, str]:\n if not self._authenticated:\n await 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 AsyncAPIClient.get_headers\n # #region AsyncAPIClient.request [TYPE Function] [C:3]\n # @PURPOSE: Perform one authenticated async Superset API request.\n # @POST: Returns JSON payload or raw httpx.Response when raw_response=true.\n # @SIDE_EFFECT: Performs network I/O.\n # @RELATION: [CALLS] ->[AsyncAPIClient.get_headers]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_http_error]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_network_error]\n async def request(\n self,\n method: str,\n endpoint: str,\n headers: dict[str, str] | None = None,\n raw_response: bool = False,\n **kwargs,\n ) -> httpx.Response | dict[str, Any]:\n full_url = self._build_api_url(endpoint)\n request_headers = await self.get_headers()\n if headers:\n request_headers.update(headers)\n if \"allow_redirects\" in kwargs and \"follow_redirects\" not in kwargs:\n kwargs[\"follow_redirects\"] = bool(kwargs.pop(\"allow_redirects\"))\n try:\n response = await self._client.request(method, full_url, headers=request_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except httpx.HTTPStatusError as exc:\n if exc.response is not None and exc.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(exc, endpoint)\n except httpx.HTTPError as exc:\n self._handle_network_error(exc, full_url)\n # #endregion AsyncAPIClient.request\n # #region AsyncAPIClient._handle_http_error [TYPE Function] [C:3]\n # @PURPOSE: Translate upstream HTTP errors into stable domain exceptions.\n # @POST: Raises domain-specific exception for caller flow control.\n # @DATA_CONTRACT: Input[httpx.HTTPStatusError] -> Exception\n # @RELATION: [CALLS] ->[AsyncAPIClient._is_dashboard_endpoint]\n # @RELATION: [DEPENDS_ON] ->[DashboardNotFoundError]\n # @RELATION: [DEPENDS_ON] ->[SupersetAPIError]\n # @RELATION: [DEPENDS_ON] ->[PermissionDeniedError]\n # @RELATION: [DEPENDS_ON] ->[AuthenticationError]\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_http_error\"):\n status_code = exc.response.status_code\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from exc\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from exc\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 exc\n if status_code == 403:\n raise PermissionDeniedError() from exc\n if status_code == 401:\n raise AuthenticationError() from exc\n raise SupersetAPIError(f\"API Error {status_code}: {exc.response.text}\") from exc\n # #endregion AsyncAPIClient._handle_http_error\n # #region AsyncAPIClient._is_dashboard_endpoint [TYPE Function] [C:2]\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\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 AsyncAPIClient._is_dashboard_endpoint\n # #region AsyncAPIClient._handle_network_error [TYPE Function] [C:3]\n # @PURPOSE: Translate generic httpx errors into NetworkError.\n # @POST: Raises NetworkError with URL context.\n # @DATA_CONTRACT: Input[httpx.HTTPError] -> NetworkError\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_network_error\"):\n if isinstance(exc, httpx.TimeoutException):\n message = \"Request timeout\"\n elif isinstance(exc, httpx.ConnectError):\n message = \"Connection error\"\n else:\n message = f\"Unknown network error: {exc}\"\n raise NetworkError(message, url=url) from exc\n # #endregion AsyncAPIClient._handle_network_error\n # #region AsyncAPIClient.aclose [TYPE Function] [C:3]\n # @PURPOSE: Close underlying httpx client.\n # @POST: Client resources are released.\n # @SIDE_EFFECT: Closes network connections.\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient.__init__]\n async def aclose(self) -> None:\n await self._client.aclose()\n # #endregion AsyncAPIClient.aclose\n# #endregion AsyncAPIClient\n# #endregion AsyncNetworkModule\n" }, { "contract_id": "AsyncAPIClient", "contract_type": "Class", "file_path": "backend/src/core/utils/async_network.py", - "start_line": 34, - "end_line": 321, + "start_line": 28, + "end_line": 269, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Async Superset API client backed by httpx.AsyncClient with shared auth cache." }, "relations": [ { "source_id": "AsyncAPIClient", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetAuthCache", "target_ref": "[SupersetAuthCache]" }, { "source_id": "AsyncAPIClient", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "SupersetAuthCache.get", "target_ref": "[SupersetAuthCache.get]" }, { "source_id": "AsyncAPIClient", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "SupersetAuthCache.set", "target_ref": "[SupersetAuthCache.set]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:AsyncAPIClient:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Async Superset API client backed by httpx.AsyncClient with shared auth cache.\n# @RELATION: [DEPENDS_ON] ->[SupersetAuthCache]\n# @RELATION: [CALLS] ->[SupersetAuthCache.get]\n# @RELATION: [CALLS] ->[SupersetAuthCache.set]\nclass AsyncAPIClient:\n DEFAULT_TIMEOUT = 30\n _auth_locks: Dict[tuple[str, str, bool], asyncio.Lock] = {}\n\n # [DEF:AsyncAPIClient.__init__:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize async API client for one environment.\n # @PRE: config contains base_url and auth payload.\n # @POST: Client is ready for async request/authentication flow.\n # @DATA_CONTRACT: Input[config: Dict[str, Any]] -> self._auth_cache_key[str]\n # @RELATION: [CALLS] ->[AsyncAPIClient._normalize_base_url]\n # @RELATION: [DEPENDS_ON] ->[SupersetAuthCache]\n def __init__(self, config: Dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\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._client = httpx.AsyncClient(\n verify=verify_ssl,\n timeout=httpx.Timeout(timeout),\n follow_redirects=True,\n )\n self._tokens: Dict[str, str] = {}\n self._authenticated = False\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n\n # [/DEF:AsyncAPIClient.__init__:Function]\n\n # [DEF:AsyncAPIClient._normalize_base_url:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize base URL for Superset API root construction.\n # @POST: Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.\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 # [/DEF:AsyncAPIClient._normalize_base_url:Function]\n\n # [DEF:AsyncAPIClient._build_api_url:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Build full API URL from relative Superset endpoint.\n # @POST: Returns absolute URL for upstream request.\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 # [/DEF:AsyncAPIClient._build_api_url:Function]\n\n # [DEF:AsyncAPIClient._get_auth_lock:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Return per-cache-key async lock to serialize fresh login attempts.\n # @POST: Returns stable asyncio.Lock instance.\n @classmethod\n def _get_auth_lock(cls, cache_key: tuple[str, str, bool]) -> asyncio.Lock:\n existing_lock = cls._auth_locks.get(cache_key)\n if existing_lock is not None:\n return existing_lock\n created_lock = asyncio.Lock()\n cls._auth_locks[cache_key] = created_lock\n return created_lock\n # [/DEF:AsyncAPIClient._get_auth_lock:Function]\n\n # [DEF:AsyncAPIClient.authenticate:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Authenticate against Superset and cache access/csrf tokens.\n # @POST: Client tokens are populated and reusable across requests.\n # @SIDE_EFFECT: Performs network requests to Superset authentication endpoints.\n # @DATA_CONTRACT: None -> Output[Dict[str, str]]\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n # @RELATION: [CALLS] ->[AsyncAPIClient._get_auth_lock]\n async def authenticate(self) -> Dict[str, str]:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n # Always fetch a fresh CSRF token to establish session cookie in this httpx client.\n # The cached access_token may still be valid; we avoid a full POST login.\n try:\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = await self._client.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {self._tokens['access_token']}\"},\n )\n csrf_response.raise_for_status()\n self._tokens[\"csrf_token\"] = csrf_response.json()[\"result\"]\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n self._authenticated = True\n log.reason(\"Reused cached tokens + refreshed CSRF token\", payload={\"base_url\": self.base_url})\n return self._tokens\n except Exception as csrf_err:\n log.explore(\"CSRF refresh failed, performing full re-auth\", error=str(csrf_err))\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._tokens = {}\n self._authenticated = False\n\n auth_lock = self._get_auth_lock(self._auth_cache_key)\n async with auth_lock:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n self._authenticated = True\n log.reason(\"Reusing cached Superset auth tokens (after lock wait)\", payload={\"base_url\": self.base_url})\n return self._tokens\n\n with belief_scope(\"AsyncAPIClient.authenticate\"):\n log.reason(\"Performing full authentication\", payload={\"base_url\": self.base_url})\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n response = await self._client.post(login_url, json=self.auth)\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 = await self._client.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {access_token}\"},\n )\n csrf_response.raise_for_status()\n\n self._tokens = {\n \"access_token\": access_token,\n \"csrf_token\": csrf_response.json()[\"result\"],\n }\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n log.reflect(\"Authenticated successfully\")\n return self._tokens\n except httpx.HTTPStatusError as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = exc.response.status_code if exc.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(\n f\"Environment unavailable during authentication (Status {status_code})\",\n status_code=status_code,\n ) from exc\n raise AuthenticationError(f\"Authentication failed: {exc}\") from exc\n except (httpx.HTTPError, KeyError) as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {exc}\") from exc\n # [/DEF:AsyncAPIClient.authenticate:Function]\n\n # [DEF:AsyncAPIClient.get_headers:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Return authenticated Superset headers for async requests.\n # @POST: Headers include Authorization and CSRF tokens.\n # @RELATION: [CALLS] ->[AsyncAPIClient.authenticate]\n async def get_headers(self) -> Dict[str, str]:\n if not self._authenticated:\n await 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 # [/DEF:AsyncAPIClient.get_headers:Function]\n\n # [DEF:AsyncAPIClient.request:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Perform one authenticated async Superset API request.\n # @POST: Returns JSON payload or raw httpx.Response when raw_response=true.\n # @SIDE_EFFECT: Performs network I/O.\n # @RELATION: [CALLS] ->[AsyncAPIClient.get_headers]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_http_error]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_network_error]\n async def request(\n self,\n method: str,\n endpoint: str,\n headers: Optional[Dict[str, str]] = None,\n raw_response: bool = False,\n **kwargs,\n ) -> Union[httpx.Response, Dict[str, Any]]:\n full_url = self._build_api_url(endpoint)\n request_headers = await self.get_headers()\n if headers:\n request_headers.update(headers)\n if \"allow_redirects\" in kwargs and \"follow_redirects\" not in kwargs:\n kwargs[\"follow_redirects\"] = bool(kwargs.pop(\"allow_redirects\"))\n\n try:\n response = await self._client.request(method, full_url, headers=request_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except httpx.HTTPStatusError as exc:\n if exc.response is not None and exc.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(exc, endpoint)\n except httpx.HTTPError as exc:\n self._handle_network_error(exc, full_url)\n # [/DEF:AsyncAPIClient.request:Function]\n\n # [DEF:AsyncAPIClient._handle_http_error:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Translate upstream HTTP errors into stable domain exceptions.\n # @POST: Raises domain-specific exception for caller flow control.\n # @DATA_CONTRACT: Input[httpx.HTTPStatusError] -> Exception\n # @RELATION: [CALLS] ->[AsyncAPIClient._is_dashboard_endpoint]\n # @RELATION: [DEPENDS_ON] ->[DashboardNotFoundError]\n # @RELATION: [DEPENDS_ON] ->[SupersetAPIError]\n # @RELATION: [DEPENDS_ON] ->[PermissionDeniedError]\n # @RELATION: [DEPENDS_ON] ->[AuthenticationError]\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_http_error\"):\n status_code = exc.response.status_code\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from exc\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from exc\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 exc\n if status_code == 403:\n raise PermissionDeniedError() from exc\n if status_code == 401:\n raise AuthenticationError() from exc\n raise SupersetAPIError(f\"API Error {status_code}: {exc.response.text}\") from exc\n # [/DEF:AsyncAPIClient._handle_http_error:Function]\n\n # [DEF:AsyncAPIClient._is_dashboard_endpoint:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\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 # [/DEF:AsyncAPIClient._is_dashboard_endpoint:Function]\n\n # [DEF:AsyncAPIClient._handle_network_error:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Translate generic httpx errors into NetworkError.\n # @POST: Raises NetworkError with URL context.\n # @DATA_CONTRACT: Input[httpx.HTTPError] -> NetworkError\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_network_error\"):\n if isinstance(exc, httpx.TimeoutException):\n message = \"Request timeout\"\n elif isinstance(exc, httpx.ConnectError):\n message = \"Connection error\"\n else:\n message = f\"Unknown network error: {exc}\"\n raise NetworkError(message, url=url) from exc\n # [/DEF:AsyncAPIClient._handle_network_error:Function]\n\n # [DEF:AsyncAPIClient.aclose:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Close underlying httpx client.\n # @POST: Client resources are released.\n # @SIDE_EFFECT: Closes network connections.\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient.__init__]\n async def aclose(self) -> None:\n await self._client.aclose()\n # [/DEF:AsyncAPIClient.aclose:Function]\n# [/DEF:AsyncAPIClient:Class]\n" - }, - { - "contract_id": "AsyncAPIClient.__init__", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 44, - "end_line": 70, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[config: Dict[str, Any]] -> self._auth_cache_key[str]", - "POST": "Client is ready for async request/authentication flow.", - "PRE": "config contains base_url and auth payload.", - "PURPOSE": "Initialize async API client for one environment." - }, - "relations": [ - { - "source_id": "AsyncAPIClient.__init__", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient._normalize_base_url", - "target_ref": "[AsyncAPIClient._normalize_base_url]" - }, - { - "source_id": "AsyncAPIClient.__init__", - "relation_type": "[DEPENDS_ON]", - "target_id": "SupersetAuthCache", - "target_ref": "[SupersetAuthCache]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient.__init__:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize async API client for one environment.\n # @PRE: config contains base_url and auth payload.\n # @POST: Client is ready for async request/authentication flow.\n # @DATA_CONTRACT: Input[config: Dict[str, Any]] -> self._auth_cache_key[str]\n # @RELATION: [CALLS] ->[AsyncAPIClient._normalize_base_url]\n # @RELATION: [DEPENDS_ON] ->[SupersetAuthCache]\n def __init__(self, config: Dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\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._client = httpx.AsyncClient(\n verify=verify_ssl,\n timeout=httpx.Timeout(timeout),\n follow_redirects=True,\n )\n self._tokens: Dict[str, str] = {}\n self._authenticated = False\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n\n # [/DEF:AsyncAPIClient.__init__:Function]\n" - }, - { - "contract_id": "AsyncAPIClient._normalize_base_url", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 72, - "end_line": 81, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.", - "PURPOSE": "Normalize base URL for Superset API root construction." - }, - "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": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient._normalize_base_url:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Normalize base URL for Superset API root construction.\n # @POST: Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.\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 # [/DEF:AsyncAPIClient._normalize_base_url:Function]\n" - }, - { - "contract_id": "AsyncAPIClient._build_api_url", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 83, - "end_line": 96, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns absolute URL for upstream request.", - "PURPOSE": "Build full API URL from relative Superset endpoint." - }, - "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": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient._build_api_url:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Build full API URL from relative Superset endpoint.\n # @POST: Returns absolute URL for upstream request.\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 # [/DEF:AsyncAPIClient._build_api_url:Function]\n" - }, - { - "contract_id": "AsyncAPIClient._get_auth_lock", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 98, - "end_line": 110, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns stable asyncio.Lock instance.", - "PURPOSE": "Return per-cache-key async lock to serialize fresh login attempts." - }, - "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": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient._get_auth_lock:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Return per-cache-key async lock to serialize fresh login attempts.\n # @POST: Returns stable asyncio.Lock instance.\n @classmethod\n def _get_auth_lock(cls, cache_key: tuple[str, str, bool]) -> asyncio.Lock:\n existing_lock = cls._auth_locks.get(cache_key)\n if existing_lock is not None:\n return existing_lock\n created_lock = asyncio.Lock()\n cls._auth_locks[cache_key] = created_lock\n return created_lock\n # [/DEF:AsyncAPIClient._get_auth_lock:Function]\n" - }, - { - "contract_id": "AsyncAPIClient.authenticate", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 112, - "end_line": 190, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "None -> Output[Dict[str, str]]", - "POST": "Client tokens are populated and reusable across requests.", - "PURPOSE": "Authenticate against Superset and cache access/csrf tokens.", - "SIDE_EFFECT": "Performs network requests to Superset authentication endpoints." - }, - "relations": [ - { - "source_id": "AsyncAPIClient.authenticate", - "relation_type": "[CALLS]", - "target_id": "SupersetAuthCache.get", - "target_ref": "[SupersetAuthCache.get]" - }, - { - "source_id": "AsyncAPIClient.authenticate", - "relation_type": "[CALLS]", - "target_id": "SupersetAuthCache.set", - "target_ref": "[SupersetAuthCache.set]" - }, - { - "source_id": "AsyncAPIClient.authenticate", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient._get_auth_lock", - "target_ref": "[AsyncAPIClient._get_auth_lock]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient.authenticate:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Authenticate against Superset and cache access/csrf tokens.\n # @POST: Client tokens are populated and reusable across requests.\n # @SIDE_EFFECT: Performs network requests to Superset authentication endpoints.\n # @DATA_CONTRACT: None -> Output[Dict[str, str]]\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n # @RELATION: [CALLS] ->[AsyncAPIClient._get_auth_lock]\n async def authenticate(self) -> Dict[str, str]:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n # Always fetch a fresh CSRF token to establish session cookie in this httpx client.\n # The cached access_token may still be valid; we avoid a full POST login.\n try:\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = await self._client.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {self._tokens['access_token']}\"},\n )\n csrf_response.raise_for_status()\n self._tokens[\"csrf_token\"] = csrf_response.json()[\"result\"]\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n self._authenticated = True\n log.reason(\"Reused cached tokens + refreshed CSRF token\", payload={\"base_url\": self.base_url})\n return self._tokens\n except Exception as csrf_err:\n log.explore(\"CSRF refresh failed, performing full re-auth\", error=str(csrf_err))\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._tokens = {}\n self._authenticated = False\n\n auth_lock = self._get_auth_lock(self._auth_cache_key)\n async with auth_lock:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n self._authenticated = True\n log.reason(\"Reusing cached Superset auth tokens (after lock wait)\", payload={\"base_url\": self.base_url})\n return self._tokens\n\n with belief_scope(\"AsyncAPIClient.authenticate\"):\n log.reason(\"Performing full authentication\", payload={\"base_url\": self.base_url})\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n response = await self._client.post(login_url, json=self.auth)\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 = await self._client.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {access_token}\"},\n )\n csrf_response.raise_for_status()\n\n self._tokens = {\n \"access_token\": access_token,\n \"csrf_token\": csrf_response.json()[\"result\"],\n }\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n log.reflect(\"Authenticated successfully\")\n return self._tokens\n except httpx.HTTPStatusError as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = exc.response.status_code if exc.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(\n f\"Environment unavailable during authentication (Status {status_code})\",\n status_code=status_code,\n ) from exc\n raise AuthenticationError(f\"Authentication failed: {exc}\") from exc\n except (httpx.HTTPError, KeyError) as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {exc}\") from exc\n # [/DEF:AsyncAPIClient.authenticate:Function]\n" - }, - { - "contract_id": "AsyncAPIClient.get_headers", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 192, - "end_line": 206, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Headers include Authorization and CSRF tokens.", - "PURPOSE": "Return authenticated Superset headers for async requests." - }, - "relations": [ - { - "source_id": "AsyncAPIClient.get_headers", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient.authenticate", - "target_ref": "[AsyncAPIClient.authenticate]" - } - ], - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient.get_headers:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Return authenticated Superset headers for async requests.\n # @POST: Headers include Authorization and CSRF tokens.\n # @RELATION: [CALLS] ->[AsyncAPIClient.authenticate]\n async def get_headers(self) -> Dict[str, str]:\n if not self._authenticated:\n await 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 # [/DEF:AsyncAPIClient.get_headers:Function]\n" - }, - { - "contract_id": "AsyncAPIClient.request", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 208, - "end_line": 243, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns JSON payload or raw httpx.Response when raw_response=true.", - "PURPOSE": "Perform one authenticated async Superset API request.", - "SIDE_EFFECT": "Performs network I/O." - }, - "relations": [ - { - "source_id": "AsyncAPIClient.request", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient.get_headers", - "target_ref": "[AsyncAPIClient.get_headers]" - }, - { - "source_id": "AsyncAPIClient.request", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient._handle_http_error", - "target_ref": "[AsyncAPIClient._handle_http_error]" - }, - { - "source_id": "AsyncAPIClient.request", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient._handle_network_error", - "target_ref": "[AsyncAPIClient._handle_network_error]" - } - ], - "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": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient.request:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Perform one authenticated async Superset API request.\n # @POST: Returns JSON payload or raw httpx.Response when raw_response=true.\n # @SIDE_EFFECT: Performs network I/O.\n # @RELATION: [CALLS] ->[AsyncAPIClient.get_headers]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_http_error]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_network_error]\n async def request(\n self,\n method: str,\n endpoint: str,\n headers: Optional[Dict[str, str]] = None,\n raw_response: bool = False,\n **kwargs,\n ) -> Union[httpx.Response, Dict[str, Any]]:\n full_url = self._build_api_url(endpoint)\n request_headers = await self.get_headers()\n if headers:\n request_headers.update(headers)\n if \"allow_redirects\" in kwargs and \"follow_redirects\" not in kwargs:\n kwargs[\"follow_redirects\"] = bool(kwargs.pop(\"allow_redirects\"))\n\n try:\n response = await self._client.request(method, full_url, headers=request_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except httpx.HTTPStatusError as exc:\n if exc.response is not None and exc.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(exc, endpoint)\n except httpx.HTTPError as exc:\n self._handle_network_error(exc, full_url)\n # [/DEF:AsyncAPIClient.request:Function]\n" - }, - { - "contract_id": "AsyncAPIClient._handle_http_error", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 245, - "end_line": 275, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[httpx.HTTPStatusError] -> Exception", - "POST": "Raises domain-specific exception for caller flow control.", - "PURPOSE": "Translate upstream HTTP errors into stable domain exceptions." - }, - "relations": [ - { - "source_id": "AsyncAPIClient._handle_http_error", - "relation_type": "[CALLS]", - "target_id": "AsyncAPIClient._is_dashboard_endpoint", - "target_ref": "[AsyncAPIClient._is_dashboard_endpoint]" - }, - { - "source_id": "AsyncAPIClient._handle_http_error", - "relation_type": "[DEPENDS_ON]", - "target_id": "DashboardNotFoundError", - "target_ref": "[DashboardNotFoundError]" - }, - { - "source_id": "AsyncAPIClient._handle_http_error", - "relation_type": "[DEPENDS_ON]", - "target_id": "SupersetAPIError", - "target_ref": "[SupersetAPIError]" - }, - { - "source_id": "AsyncAPIClient._handle_http_error", - "relation_type": "[DEPENDS_ON]", - "target_id": "PermissionDeniedError", - "target_ref": "[PermissionDeniedError]" - }, - { - "source_id": "AsyncAPIClient._handle_http_error", - "relation_type": "[DEPENDS_ON]", - "target_id": "AuthenticationError", - "target_ref": "[AuthenticationError]" - }, - { - "source_id": "AsyncAPIClient._handle_http_error", - "relation_type": "[DEPENDS_ON]", - "target_id": "NetworkError", - "target_ref": "[NetworkError]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient._handle_http_error:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Translate upstream HTTP errors into stable domain exceptions.\n # @POST: Raises domain-specific exception for caller flow control.\n # @DATA_CONTRACT: Input[httpx.HTTPStatusError] -> Exception\n # @RELATION: [CALLS] ->[AsyncAPIClient._is_dashboard_endpoint]\n # @RELATION: [DEPENDS_ON] ->[DashboardNotFoundError]\n # @RELATION: [DEPENDS_ON] ->[SupersetAPIError]\n # @RELATION: [DEPENDS_ON] ->[PermissionDeniedError]\n # @RELATION: [DEPENDS_ON] ->[AuthenticationError]\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_http_error\"):\n status_code = exc.response.status_code\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from exc\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from exc\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 exc\n if status_code == 403:\n raise PermissionDeniedError() from exc\n if status_code == 401:\n raise AuthenticationError() from exc\n raise SupersetAPIError(f\"API Error {status_code}: {exc.response.text}\") from exc\n # [/DEF:AsyncAPIClient._handle_http_error:Function]\n" - }, - { - "contract_id": "AsyncAPIClient._is_dashboard_endpoint", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 277, - "end_line": 293, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "POST": "Returns true only for dashboard-specific endpoints.", - "PURPOSE": "Determine whether an API endpoint represents a dashboard resource for 404 translation." - }, - "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": "def", - "body": " # [DEF:AsyncAPIClient._is_dashboard_endpoint:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\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 # [/DEF:AsyncAPIClient._is_dashboard_endpoint:Function]\n" - }, - { - "contract_id": "AsyncAPIClient._handle_network_error", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 295, - "end_line": 310, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[httpx.HTTPError] -> NetworkError", - "POST": "Raises NetworkError with URL context.", - "PURPOSE": "Translate generic httpx errors into NetworkError." - }, - "relations": [ - { - "source_id": "AsyncAPIClient._handle_network_error", - "relation_type": "[DEPENDS_ON]", - "target_id": "NetworkError", - "target_ref": "[NetworkError]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient._handle_network_error:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Translate generic httpx errors into NetworkError.\n # @POST: Raises NetworkError with URL context.\n # @DATA_CONTRACT: Input[httpx.HTTPError] -> NetworkError\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_network_error\"):\n if isinstance(exc, httpx.TimeoutException):\n message = \"Request timeout\"\n elif isinstance(exc, httpx.ConnectError):\n message = \"Connection error\"\n else:\n message = f\"Unknown network error: {exc}\"\n raise NetworkError(message, url=url) from exc\n # [/DEF:AsyncAPIClient._handle_network_error:Function]\n" - }, - { - "contract_id": "AsyncAPIClient.aclose", - "contract_type": "Function", - "file_path": "backend/src/core/utils/async_network.py", - "start_line": 312, - "end_line": 320, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Client resources are released.", - "PURPOSE": "Close underlying httpx client.", - "SIDE_EFFECT": "Closes network connections." - }, - "relations": [ - { - "source_id": "AsyncAPIClient.aclose", - "relation_type": "[DEPENDS_ON]", - "target_id": "AsyncAPIClient.__init__", - "target_ref": "[AsyncAPIClient.__init__]" - } - ], - "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": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AsyncAPIClient.aclose:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Close underlying httpx client.\n # @POST: Client resources are released.\n # @SIDE_EFFECT: Closes network connections.\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient.__init__]\n async def aclose(self) -> None:\n await self._client.aclose()\n # [/DEF:AsyncAPIClient.aclose:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region AsyncAPIClient [C:3] [TYPE Class]\n# @BRIEF Async Superset API client backed by httpx.AsyncClient with shared auth cache.\n# @RELATION DEPENDS_ON -> [SupersetAuthCache]\n# @RELATION CALLS -> [SupersetAuthCache.get]\n# @RELATION CALLS -> [SupersetAuthCache.set]\nclass AsyncAPIClient:\n DEFAULT_TIMEOUT = 30\n _auth_locks: dict[tuple[str, str, bool], asyncio.Lock] = {}\n # #region AsyncAPIClient.__init__ [TYPE Function] [C:3]\n # @PURPOSE: Initialize async API client for one environment.\n # @PRE: config contains base_url and auth payload.\n # @POST: Client is ready for async request/authentication flow.\n # @DATA_CONTRACT: Input[config: Dict[str, Any]] -> self._auth_cache_key[str]\n # @RELATION: [CALLS] ->[AsyncAPIClient._normalize_base_url]\n # @RELATION: [DEPENDS_ON] ->[SupersetAuthCache]\n def __init__(self, config: dict[str, Any], verify_ssl: bool = True, timeout: int = DEFAULT_TIMEOUT):\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._client = httpx.AsyncClient(\n verify=verify_ssl,\n timeout=httpx.Timeout(timeout),\n follow_redirects=True,\n )\n self._tokens: dict[str, str] = {}\n self._authenticated = False\n self._auth_cache_key = SupersetAuthCache.build_key(\n self.base_url,\n self.auth,\n verify_ssl,\n )\n # #endregion AsyncAPIClient.__init__\n # #region AsyncAPIClient._normalize_base_url [TYPE Function] [C:1]\n # @PURPOSE: Normalize base URL for Superset API root construction.\n # @POST: Returns canonical base URL without trailing slash and duplicate /api/v1 suffix.\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 AsyncAPIClient._normalize_base_url\n # #region AsyncAPIClient._build_api_url [TYPE Function] [C:1]\n # @PURPOSE: Build full API URL from relative Superset endpoint.\n # @POST: Returns absolute URL for upstream request.\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 AsyncAPIClient._build_api_url\n # #region AsyncAPIClient._get_auth_lock [TYPE Function] [C:1]\n # @PURPOSE: Return per-cache-key async lock to serialize fresh login attempts.\n # @POST: Returns stable asyncio.Lock instance.\n @classmethod\n def _get_auth_lock(cls, cache_key: tuple[str, str, bool]) -> asyncio.Lock:\n existing_lock = cls._auth_locks.get(cache_key)\n if existing_lock is not None:\n return existing_lock\n created_lock = asyncio.Lock()\n cls._auth_locks[cache_key] = created_lock\n return created_lock\n # #endregion AsyncAPIClient._get_auth_lock\n # #region AsyncAPIClient.authenticate [TYPE Function] [C:3]\n # @PURPOSE: Authenticate against Superset and cache access/csrf tokens.\n # @POST: Client tokens are populated and reusable across requests.\n # @SIDE_EFFECT: Performs network requests to Superset authentication endpoints.\n # @DATA_CONTRACT: None -> Output[Dict[str, str]]\n # @RELATION: [CALLS] ->[SupersetAuthCache.get]\n # @RELATION: [CALLS] ->[SupersetAuthCache.set]\n # @RELATION: [CALLS] ->[AsyncAPIClient._get_auth_lock]\n async def authenticate(self) -> dict[str, str]:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n self._authenticated = True\n app_logger.info(\"[async_authenticate][CacheHit] Reusing cached Superset auth tokens for %s\", self.base_url)\n return self._tokens\n auth_lock = self._get_auth_lock(self._auth_cache_key)\n async with auth_lock:\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n self._authenticated = True\n app_logger.info(\"[async_authenticate][CacheHitAfterWait] Reusing cached Superset auth tokens for %s\", self.base_url)\n return self._tokens\n with belief_scope(\"AsyncAPIClient.authenticate\"):\n app_logger.info(\"[async_authenticate][Enter] Authenticating to %s\", self.base_url)\n try:\n login_url = f\"{self.api_base_url}/security/login\"\n response = await self._client.post(login_url, json=self.auth)\n response.raise_for_status()\n access_token = response.json()[\"access_token\"]\n csrf_url = f\"{self.api_base_url}/security/csrf_token/\"\n csrf_response = await self._client.get(\n csrf_url,\n headers={\"Authorization\": f\"Bearer {access_token}\"},\n )\n csrf_response.raise_for_status()\n self._tokens = {\n \"access_token\": access_token,\n \"csrf_token\": csrf_response.json()[\"result\"],\n }\n self._authenticated = True\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n app_logger.reflect(\"Authenticated successfully.\", extra={\"src\": \"async_authenticate\"})\n return self._tokens\n except httpx.HTTPStatusError as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n status_code = exc.response.status_code if exc.response is not None else None\n if status_code in [502, 503, 504]:\n raise NetworkError(\n f\"Environment unavailable during authentication (Status {status_code})\",\n status_code=status_code,\n ) from exc\n raise AuthenticationError(f\"Authentication failed: {exc}\") from exc\n except (httpx.HTTPError, KeyError) as exc:\n SupersetAuthCache.invalidate(self._auth_cache_key)\n raise NetworkError(f\"Network or parsing error during authentication: {exc}\") from exc\n # #endregion AsyncAPIClient.authenticate\n # #region AsyncAPIClient.get_headers [TYPE Function] [C:3]\n # @PURPOSE: Return authenticated Superset headers for async requests.\n # @POST: Headers include Authorization and CSRF tokens.\n # @RELATION: [CALLS] ->[AsyncAPIClient.authenticate]\n async def get_headers(self) -> dict[str, str]:\n if not self._authenticated:\n await 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 AsyncAPIClient.get_headers\n # #region AsyncAPIClient.request [TYPE Function] [C:3]\n # @PURPOSE: Perform one authenticated async Superset API request.\n # @POST: Returns JSON payload or raw httpx.Response when raw_response=true.\n # @SIDE_EFFECT: Performs network I/O.\n # @RELATION: [CALLS] ->[AsyncAPIClient.get_headers]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_http_error]\n # @RELATION: [CALLS] ->[AsyncAPIClient._handle_network_error]\n async def request(\n self,\n method: str,\n endpoint: str,\n headers: dict[str, str] | None = None,\n raw_response: bool = False,\n **kwargs,\n ) -> httpx.Response | dict[str, Any]:\n full_url = self._build_api_url(endpoint)\n request_headers = await self.get_headers()\n if headers:\n request_headers.update(headers)\n if \"allow_redirects\" in kwargs and \"follow_redirects\" not in kwargs:\n kwargs[\"follow_redirects\"] = bool(kwargs.pop(\"allow_redirects\"))\n try:\n response = await self._client.request(method, full_url, headers=request_headers, **kwargs)\n response.raise_for_status()\n return response if raw_response else response.json()\n except httpx.HTTPStatusError as exc:\n if exc.response is not None and exc.response.status_code == 401:\n self._authenticated = False\n self._tokens = {}\n SupersetAuthCache.invalidate(self._auth_cache_key)\n self._handle_http_error(exc, endpoint)\n except httpx.HTTPError as exc:\n self._handle_network_error(exc, full_url)\n # #endregion AsyncAPIClient.request\n # #region AsyncAPIClient._handle_http_error [TYPE Function] [C:3]\n # @PURPOSE: Translate upstream HTTP errors into stable domain exceptions.\n # @POST: Raises domain-specific exception for caller flow control.\n # @DATA_CONTRACT: Input[httpx.HTTPStatusError] -> Exception\n # @RELATION: [CALLS] ->[AsyncAPIClient._is_dashboard_endpoint]\n # @RELATION: [DEPENDS_ON] ->[DashboardNotFoundError]\n # @RELATION: [DEPENDS_ON] ->[SupersetAPIError]\n # @RELATION: [DEPENDS_ON] ->[PermissionDeniedError]\n # @RELATION: [DEPENDS_ON] ->[AuthenticationError]\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_http_error(self, exc: httpx.HTTPStatusError, endpoint: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_http_error\"):\n status_code = exc.response.status_code\n if status_code in [502, 503, 504]:\n raise NetworkError(f\"Environment unavailable (Status {status_code})\", status_code=status_code) from exc\n if status_code == 404:\n if self._is_dashboard_endpoint(endpoint):\n raise DashboardNotFoundError(endpoint) from exc\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 exc\n if status_code == 403:\n raise PermissionDeniedError() from exc\n if status_code == 401:\n raise AuthenticationError() from exc\n raise SupersetAPIError(f\"API Error {status_code}: {exc.response.text}\") from exc\n # #endregion AsyncAPIClient._handle_http_error\n # #region AsyncAPIClient._is_dashboard_endpoint [TYPE Function] [C:2]\n # @PURPOSE: Determine whether an API endpoint represents a dashboard resource for 404 translation.\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 AsyncAPIClient._is_dashboard_endpoint\n # #region AsyncAPIClient._handle_network_error [TYPE Function] [C:3]\n # @PURPOSE: Translate generic httpx errors into NetworkError.\n # @POST: Raises NetworkError with URL context.\n # @DATA_CONTRACT: Input[httpx.HTTPError] -> NetworkError\n # @RELATION: [DEPENDS_ON] ->[NetworkError]\n def _handle_network_error(self, exc: httpx.HTTPError, url: str) -> None:\n with belief_scope(\"AsyncAPIClient._handle_network_error\"):\n if isinstance(exc, httpx.TimeoutException):\n message = \"Request timeout\"\n elif isinstance(exc, httpx.ConnectError):\n message = \"Connection error\"\n else:\n message = f\"Unknown network error: {exc}\"\n raise NetworkError(message, url=url) from exc\n # #endregion AsyncAPIClient._handle_network_error\n # #region AsyncAPIClient.aclose [TYPE Function] [C:3]\n # @PURPOSE: Close underlying httpx client.\n # @POST: Client resources are released.\n # @SIDE_EFFECT: Closes network connections.\n # @RELATION: [DEPENDS_ON] ->[AsyncAPIClient.__init__]\n async def aclose(self) -> None:\n await self._client.aclose()\n # #endregion AsyncAPIClient.aclose\n# #endregion AsyncAPIClient\n" }, { "contract_id": "DatasetMapperModule", "contract_type": "Module", "file_path": "backend/src/core/utils/dataset_mapper.py", "start_line": 1, - "end_line": 242, + "end_line": 226, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "Domain", "PUBLIC_API": "DatasetMapper", - "PURPOSE": "Этот модуль отвечает за обновление метаданных (verbose_map) в датасетах Superset, извлекая их из PostgreSQL или XLSX-файлов.", - "SEMANTICS": [ - "dataset", - "mapping", - "postgresql", - "xlsx", - "superset" - ] + "PURPOSE": "Этот модуль отвечает за обновление метаданных (verbose_map) в датасетах Superset, извлекая их из PostgreSQL или XLSX-файлов." }, "relations": [ { @@ -50641,15 +40421,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetMapperModule:Module]\n#\n# @SEMANTICS: dataset, mapping, postgresql, xlsx, superset\n# @PURPOSE: Этот модуль отвечает за обновление метаданных (verbose_map) в датасетах Superset, извлекая их из PostgreSQL или XLSX-файлов.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> backend.core.superset_client\n# @RELATION: DEPENDS_ON -> pandas\n# @RELATION: DEPENDS_ON -> psycopg2\n# @PUBLIC_API: DatasetMapper\n\n# [SECTION: IMPORTS]\nimport pandas as pd # type: ignore\nimport psycopg2 # type: ignore\nfrom typing import Dict, Optional, Any\nfrom ..logger import belief_scope\nfrom ..cot_logger import MarkerLogger\n# [/SECTION]\n\nlog = MarkerLogger(\"DatasetMapper\")\n\n# [DEF:DatasetMapper:Class]\n# @PURPOSE: Класс для меппинга и обновления verbose_map в датасетах Superset.\nclass DatasetMapper:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the mapper.\n # @POST: Объект DatasetMapper инициализирован.\n def __init__(self):\n pass\n # [/DEF:__init__:Function]\n\n # [DEF:get_postgres_comments:Function]\n # @PURPOSE: Извлекает комментарии к колонкам из системного каталога PostgreSQL.\n # @PRE: db_config должен содержать валидные параметры подключения (host, port, user, password, dbname).\n # @PRE: table_name и table_schema должны быть строками.\n # @POST: Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД.\n # @THROW: Exception - При ошибках подключения или выполнения запроса к БД.\n # @PARAM: db_config (Dict) - Конфигурация для подключения к БД.\n # @PARAM: table_name (str) - Имя таблицы.\n # @PARAM: table_schema (str) - Схема таблицы.\n # @RETURN: Dict[str, str] - Словарь с комментариями к колонкам.\n def get_postgres_comments(self, db_config: Dict, table_name: str, table_schema: str) -> Dict[str, str]:\n with belief_scope(\"Fetch comments from PostgreSQL\"):\n log.reason(\"Fetching comments from PostgreSQL\", payload={\"table_schema\": table_schema, \"table_name\": table_name})\n query = f\"\"\"\n SELECT \n cols.column_name,\n CASE \n WHEN pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ) LIKE '%|%' THEN \n split_part(\n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ), \n '|', \n 1\n )\n ELSE \n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n )\n END AS column_comment\n FROM \n information_schema.columns cols \n WHERE cols.table_catalog = '{db_config.get('dbname')}' AND cols.table_name = '{table_name}' AND cols.table_schema = '{table_schema}';\n \"\"\"\n comments = {}\n try:\n with psycopg2.connect(**db_config) as conn, conn.cursor() as cursor:\n cursor.execute(query)\n for row in cursor.fetchall():\n if row[1]:\n comments[row[0]] = row[1]\n log.reflect(\"PostgreSQL comments fetched\", payload={\"count\": len(comments)})\n except Exception as e:\n log.explore(\"Failed to fetch PostgreSQL comments\", payload={\"table_schema\": table_schema, \"table_name\": table_name}, error=str(e))\n raise\n return comments\n # [/DEF:get_postgres_comments:Function]\n\n # [DEF:load_excel_mappings:Function]\n # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла.\n # @PRE: file_path должен указывать на существующий XLSX файл.\n # @POST: Возвращается словарь с меппингами из файла.\n # @THROW: Exception - При ошибках чтения файла или парсинга.\n # @PARAM: file_path (str) - Путь к XLSX файлу.\n # @RETURN: Dict[str, str] - Словарь с меппингами.\n def load_excel_mappings(self, file_path: str) -> Dict[str, str]:\n with belief_scope(\"Load mappings from Excel\"):\n log.reason(\"Loading mappings from Excel\", payload={\"file_path\": file_path})\n try:\n df = pd.read_excel(file_path)\n mappings = df.set_index('column_name')['verbose_name'].to_dict()\n log.reflect(\"Excel mappings loaded\", payload={\"count\": len(mappings)})\n return mappings\n except Exception as e:\n log.explore(\"Failed to load Excel mappings\", payload={\"file_path\": file_path}, error=str(e))\n raise\n # [/DEF:load_excel_mappings:Function]\n\n # [DEF:run_mapping:Function]\n # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset.\n # @PRE: superset_client должен быть авторизован.\n # @PRE: dataset_id должен быть существующим ID в Superset.\n # @POST: Если найдены изменения, датасет в Superset обновлен через API.\n # @RELATION: CALLS -> self.get_postgres_comments\n # @RELATION: CALLS -> self.load_excel_mappings\n # @RELATION: CALLS -> superset_client.get_dataset\n # @RELATION: CALLS -> superset_client.update_dataset\n # @PARAM: superset_client (Any) - Клиент Superset.\n # @PARAM: dataset_id (int) - ID датасета для обновления.\n # @PARAM: source (str) - Источник данных ('postgres', 'excel', 'both').\n # @PARAM: postgres_config (Optional[Dict]) - Конфигурация для подключения к PostgreSQL.\n # @PARAM: excel_path (Optional[str]) - Путь к XLSX файлу.\n # @PARAM: table_name (Optional[str]) - Имя таблицы в PostgreSQL.\n # @PARAM: table_schema (Optional[str]) - Схема таблицы в PostgreSQL.\n def run_mapping(self, superset_client: Any, dataset_id: int, source: str, postgres_config: Optional[Dict] = None, excel_path: Optional[str] = None, table_name: Optional[str] = None, table_schema: Optional[str] = None):\n with belief_scope(f\"Run dataset mapping for ID {dataset_id}\"):\n log.reason(\"Starting dataset mapping\", payload={\"dataset_id\": dataset_id, \"source\": source})\n mappings: Dict[str, str] = {}\n \n try:\n if source in ['postgres', 'both']:\n if not (postgres_config and table_name and table_schema):\n raise ValueError(\"Postgres config is required.\")\n mappings.update(self.get_postgres_comments(postgres_config, table_name, table_schema))\n if source in ['excel', 'both']:\n if not excel_path:\n raise ValueError(\"Excel path is required.\")\n mappings.update(self.load_excel_mappings(excel_path))\n if source not in ['postgres', 'excel', 'both']:\n log.explore(\"Invalid mapping source\", payload={\"source\": source}, error=f\"Invalid source: {source}\")\n return\n\n dataset_response = superset_client.get_dataset(dataset_id)\n dataset_data = dataset_response['result']\n \n original_columns = dataset_data.get('columns', [])\n updated_columns = []\n changes_made = False\n\n for column in original_columns:\n col_name = column.get('column_name')\n \n new_column = {\n \"column_name\": col_name,\n \"id\": column.get(\"id\"),\n \"advanced_data_type\": column.get(\"advanced_data_type\"),\n \"description\": column.get(\"description\"),\n \"expression\": column.get(\"expression\"),\n \"extra\": column.get(\"extra\"),\n \"filterable\": column.get(\"filterable\"),\n \"groupby\": column.get(\"groupby\"),\n \"is_active\": column.get(\"is_active\"),\n \"is_dttm\": column.get(\"is_dttm\"),\n \"python_date_format\": column.get(\"python_date_format\"),\n \"type\": column.get(\"type\"),\n \"uuid\": column.get(\"uuid\"),\n \"verbose_name\": column.get(\"verbose_name\"),\n }\n \n new_column = {k: v for k, v in new_column.items() if v is not None}\n\n if col_name in mappings:\n mapping_value = mappings[col_name]\n if isinstance(mapping_value, str) and new_column.get('verbose_name') != mapping_value:\n new_column['verbose_name'] = mapping_value\n changes_made = True\n \n updated_columns.append(new_column)\n\n updated_metrics = []\n for metric in dataset_data.get(\"metrics\", []):\n new_metric = {\n \"id\": metric.get(\"id\"),\n \"metric_name\": metric.get(\"metric_name\"),\n \"expression\": metric.get(\"expression\"),\n \"verbose_name\": metric.get(\"verbose_name\"),\n \"description\": metric.get(\"description\"),\n \"d3format\": metric.get(\"d3format\"),\n \"currency\": metric.get(\"currency\"),\n \"extra\": metric.get(\"extra\"),\n \"warning_text\": metric.get(\"warning_text\"),\n \"metric_type\": metric.get(\"metric_type\"),\n \"uuid\": metric.get(\"uuid\"),\n }\n updated_metrics.append({k: v for k, v in new_metric.items() if v is not None})\n\n if changes_made:\n payload_for_update = {\n \"database_id\": dataset_data.get(\"database\", {}).get(\"id\"),\n \"table_name\": dataset_data.get(\"table_name\"),\n \"schema\": dataset_data.get(\"schema\"),\n \"columns\": updated_columns,\n \"owners\": [owner[\"id\"] for owner in dataset_data.get(\"owners\", [])],\n \"metrics\": updated_metrics,\n \"extra\": dataset_data.get(\"extra\"),\n \"description\": dataset_data.get(\"description\"),\n \"sql\": dataset_data.get(\"sql\"),\n \"cache_timeout\": dataset_data.get(\"cache_timeout\"),\n \"catalog\": dataset_data.get(\"catalog\"),\n \"default_endpoint\": dataset_data.get(\"default_endpoint\"),\n \"external_url\": dataset_data.get(\"external_url\"),\n \"fetch_values_predicate\": dataset_data.get(\"fetch_values_predicate\"),\n \"filter_select_enabled\": dataset_data.get(\"filter_select_enabled\"),\n \"is_managed_externally\": dataset_data.get(\"is_managed_externally\"),\n \"is_sqllab_view\": dataset_data.get(\"is_sqllab_view\"),\n \"main_dttm_col\": dataset_data.get(\"main_dttm_col\"),\n \"normalize_columns\": dataset_data.get(\"normalize_columns\"),\n \"offset\": dataset_data.get(\"offset\"),\n \"template_params\": dataset_data.get(\"template_params\"),\n }\n \n payload_for_update = {k: v for k, v in payload_for_update.items() if v is not None}\n\n superset_client.update_dataset(dataset_id, payload_for_update)\n log.reflect(\"Dataset verbose_name updated\", payload={\"dataset_id\": dataset_id})\n else:\n log.reflect(\"No changes in verbose_name, skipping update\", payload={\"dataset_id\": dataset_id})\n\n except (FileNotFoundError, Exception) as e:\n log.explore(\"Dataset mapping failed\", payload={\"dataset_id\": dataset_id, \"source\": source}, error=str(e))\n return\n # [/DEF:run_mapping:Function]\n# [/DEF:DatasetMapper:Class]\n\n# [/DEF:DatasetMapperModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetMapperModule [TYPE Module] [SEMANTICS dataset, superset, dataset-mapper]\n#\n# @BRIEF Этот модуль отвечает за обновление метаданных (verbose_map) в датасетах Superset, извлекая их из PostgreSQL или XLSX-файлов.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> backend.core.superset_client\n# @RELATION DEPENDS_ON -> pandas\n# @RELATION DEPENDS_ON -> psycopg2\n# @PUBLIC_API: DatasetMapper\nfrom typing import Any\n\nimport pandas as pd # type: ignore\nimport psycopg2 # type: ignore\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\n\n# #region DatasetMapper [TYPE Class]\n# @BRIEF Класс для меппинга и обновления verbose_map в датасетах Superset.\nclass DatasetMapper:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the mapper.\n # @POST: Объект DatasetMapper инициализирован.\n def __init__(self):\n pass\n # #endregion __init__\n # #region get_postgres_comments [TYPE Function]\n # @PURPOSE: Извлекает комментарии к колонкам из системного каталога PostgreSQL.\n # @PRE: db_config должен содержать валидные параметры подключения (host, port, user, password, dbname).\n # @PRE: table_name и table_schema должны быть строками.\n # @POST: Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД.\n # @THROW: Exception - При ошибках подключения или выполнения запроса к БД.\n # @PARAM: db_config (Dict) - Конфигурация для подключения к БД.\n # @PARAM: table_name (str) - Имя таблицы.\n # @PARAM: table_schema (str) - Схема таблицы.\n # @RETURN: Dict[str, str] - Словарь с комментариями к колонкам.\n def get_postgres_comments(self, db_config: dict, table_name: str, table_schema: str) -> dict[str, str]:\n with belief_scope(\"Fetch comments from PostgreSQL\"):\n app_logger.info(\"[get_postgres_comments][Enter] Fetching comments from PostgreSQL for %s.%s.\", table_schema, table_name)\n query = f\"\"\"\n SELECT \n cols.column_name,\n CASE \n WHEN pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ) LIKE '%|%' THEN \n split_part(\n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ), \n '|', \n 1\n )\n ELSE \n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n )\n END AS column_comment\n FROM \n information_schema.columns cols \n WHERE cols.table_catalog = '{db_config.get('dbname')}' AND cols.table_name = '{table_name}' AND cols.table_schema = '{table_schema}';\n \"\"\"\n comments = {}\n try:\n with psycopg2.connect(**db_config) as conn, conn.cursor() as cursor:\n cursor.execute(query)\n for row in cursor.fetchall():\n if row[1]:\n comments[row[0]] = row[1]\n app_logger.info(\"[get_postgres_comments][Success] Fetched %d comments.\", len(comments))\n except Exception as e:\n app_logger.error(\"[get_postgres_comments][Failure] %s\", e, exc_info=True)\n raise\n return comments\n # #endregion get_postgres_comments\n # #region load_excel_mappings [TYPE Function]\n # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла.\n # @PRE: file_path должен указывать на существующий XLSX файл.\n # @POST: Возвращается словарь с меппингами из файла.\n # @THROW: Exception - При ошибках чтения файла или парсинга.\n # @PARAM: file_path (str) - Путь к XLSX файлу.\n # @RETURN: Dict[str, str] - Словарь с меппингами.\n def load_excel_mappings(self, file_path: str) -> dict[str, str]:\n with belief_scope(\"Load mappings from Excel\"):\n app_logger.info(\"[load_excel_mappings][Enter] Loading mappings from %s.\", file_path)\n try:\n df = pd.read_excel(file_path)\n mappings = df.set_index('column_name')['verbose_name'].to_dict()\n app_logger.info(\"[load_excel_mappings][Success] Loaded %d mappings.\", len(mappings))\n return mappings\n except Exception as e:\n app_logger.error(\"[load_excel_mappings][Failure] %s\", e, exc_info=True)\n raise\n # #endregion load_excel_mappings\n # #region run_mapping [TYPE Function]\n # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset.\n # @PRE: superset_client должен быть авторизован.\n # @PRE: dataset_id должен быть существующим ID в Superset.\n # @POST: Если найдены изменения, датасет в Superset обновлен через API.\n # @RELATION: CALLS -> self.get_postgres_comments\n # @RELATION: CALLS -> self.load_excel_mappings\n # @RELATION: CALLS -> superset_client.get_dataset\n # @RELATION: CALLS -> superset_client.update_dataset\n # @PARAM: superset_client (Any) - Клиент Superset.\n # @PARAM: dataset_id (int) - ID датасета для обновления.\n # @PARAM: source (str) - Источник данных ('postgres', 'excel', 'both').\n # @PARAM: postgres_config (Optional[Dict]) - Конфигурация для подключения к PostgreSQL.\n # @PARAM: excel_path (Optional[str]) - Путь к XLSX файлу.\n # @PARAM: table_name (Optional[str]) - Имя таблицы в PostgreSQL.\n # @PARAM: table_schema (Optional[str]) - Схема таблицы в PostgreSQL.\n def run_mapping(self, superset_client: Any, dataset_id: int, source: str, postgres_config: dict | None = None, excel_path: str | None = None, table_name: str | None = None, table_schema: str | None = None):\n with belief_scope(f\"Run dataset mapping for ID {dataset_id}\"):\n app_logger.info(\"[run_mapping][Enter] Starting dataset mapping for ID %d from source '%s'.\", dataset_id, source)\n mappings: dict[str, str] = {}\n\n try:\n if source in ['postgres', 'both']:\n assert postgres_config and table_name and table_schema, \"Postgres config is required.\"\n mappings.update(self.get_postgres_comments(postgres_config, table_name, table_schema))\n if source in ['excel', 'both']:\n assert excel_path, \"Excel path is required.\"\n mappings.update(self.load_excel_mappings(excel_path))\n if source not in ['postgres', 'excel', 'both']:\n app_logger.error(\"[run_mapping][Failure] Invalid source: %s.\", source)\n return\n dataset_response = superset_client.get_dataset(dataset_id)\n dataset_data = dataset_response['result']\n\n original_columns = dataset_data.get('columns', [])\n updated_columns = []\n changes_made = False\n for column in original_columns:\n col_name = column.get('column_name')\n\n new_column = {\n \"column_name\": col_name,\n \"id\": column.get(\"id\"),\n \"advanced_data_type\": column.get(\"advanced_data_type\"),\n \"description\": column.get(\"description\"),\n \"expression\": column.get(\"expression\"),\n \"extra\": column.get(\"extra\"),\n \"filterable\": column.get(\"filterable\"),\n \"groupby\": column.get(\"groupby\"),\n \"is_active\": column.get(\"is_active\"),\n \"is_dttm\": column.get(\"is_dttm\"),\n \"python_date_format\": column.get(\"python_date_format\"),\n \"type\": column.get(\"type\"),\n \"uuid\": column.get(\"uuid\"),\n \"verbose_name\": column.get(\"verbose_name\"),\n }\n\n new_column = {k: v for k, v in new_column.items() if v is not None}\n if col_name in mappings:\n mapping_value = mappings[col_name]\n if isinstance(mapping_value, str) and new_column.get('verbose_name') != mapping_value:\n new_column['verbose_name'] = mapping_value\n changes_made = True\n\n updated_columns.append(new_column)\n updated_metrics = []\n for metric in dataset_data.get(\"metrics\", []):\n new_metric = {\n \"id\": metric.get(\"id\"),\n \"metric_name\": metric.get(\"metric_name\"),\n \"expression\": metric.get(\"expression\"),\n \"verbose_name\": metric.get(\"verbose_name\"),\n \"description\": metric.get(\"description\"),\n \"d3format\": metric.get(\"d3format\"),\n \"currency\": metric.get(\"currency\"),\n \"extra\": metric.get(\"extra\"),\n \"warning_text\": metric.get(\"warning_text\"),\n \"metric_type\": metric.get(\"metric_type\"),\n \"uuid\": metric.get(\"uuid\"),\n }\n updated_metrics.append({k: v for k, v in new_metric.items() if v is not None})\n if changes_made:\n payload_for_update = {\n \"database_id\": dataset_data.get(\"database\", {}).get(\"id\"),\n \"table_name\": dataset_data.get(\"table_name\"),\n \"schema\": dataset_data.get(\"schema\"),\n \"columns\": updated_columns,\n \"owners\": [owner[\"id\"] for owner in dataset_data.get(\"owners\", [])],\n \"metrics\": updated_metrics,\n \"extra\": dataset_data.get(\"extra\"),\n \"description\": dataset_data.get(\"description\"),\n \"sql\": dataset_data.get(\"sql\"),\n \"cache_timeout\": dataset_data.get(\"cache_timeout\"),\n \"catalog\": dataset_data.get(\"catalog\"),\n \"default_endpoint\": dataset_data.get(\"default_endpoint\"),\n \"external_url\": dataset_data.get(\"external_url\"),\n \"fetch_values_predicate\": dataset_data.get(\"fetch_values_predicate\"),\n \"filter_select_enabled\": dataset_data.get(\"filter_select_enabled\"),\n \"is_managed_externally\": dataset_data.get(\"is_managed_externally\"),\n \"is_sqllab_view\": dataset_data.get(\"is_sqllab_view\"),\n \"main_dttm_col\": dataset_data.get(\"main_dttm_col\"),\n \"normalize_columns\": dataset_data.get(\"normalize_columns\"),\n \"offset\": dataset_data.get(\"offset\"),\n \"template_params\": dataset_data.get(\"template_params\"),\n }\n\n payload_for_update = {k: v for k, v in payload_for_update.items() if v is not None}\n superset_client.update_dataset(dataset_id, payload_for_update)\n app_logger.info(\"[run_mapping][Success] Dataset %d columns' verbose_name updated.\", dataset_id)\n else:\n app_logger.info(\"[run_mapping][State] No changes in columns' verbose_name, skipping update.\")\n except (AssertionError, FileNotFoundError, Exception) as e:\n app_logger.error(\"[run_mapping][Failure] %s\", e, exc_info=True)\n return\n # #endregion run_mapping\n# #endregion DatasetMapper\n# #endregion DatasetMapperModule\n" }, { "contract_id": "DatasetMapper", "contract_type": "Class", "file_path": "backend/src/core/utils/dataset_mapper.py", - "start_line": 21, - "end_line": 240, + "start_line": 18, + "end_line": 225, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50667,15 +40447,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetMapper:Class]\n# @PURPOSE: Класс для меппинга и обновления verbose_map в датасетах Superset.\nclass DatasetMapper:\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the mapper.\n # @POST: Объект DatasetMapper инициализирован.\n def __init__(self):\n pass\n # [/DEF:__init__:Function]\n\n # [DEF:get_postgres_comments:Function]\n # @PURPOSE: Извлекает комментарии к колонкам из системного каталога PostgreSQL.\n # @PRE: db_config должен содержать валидные параметры подключения (host, port, user, password, dbname).\n # @PRE: table_name и table_schema должны быть строками.\n # @POST: Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД.\n # @THROW: Exception - При ошибках подключения или выполнения запроса к БД.\n # @PARAM: db_config (Dict) - Конфигурация для подключения к БД.\n # @PARAM: table_name (str) - Имя таблицы.\n # @PARAM: table_schema (str) - Схема таблицы.\n # @RETURN: Dict[str, str] - Словарь с комментариями к колонкам.\n def get_postgres_comments(self, db_config: Dict, table_name: str, table_schema: str) -> Dict[str, str]:\n with belief_scope(\"Fetch comments from PostgreSQL\"):\n log.reason(\"Fetching comments from PostgreSQL\", payload={\"table_schema\": table_schema, \"table_name\": table_name})\n query = f\"\"\"\n SELECT \n cols.column_name,\n CASE \n WHEN pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ) LIKE '%|%' THEN \n split_part(\n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ), \n '|', \n 1\n )\n ELSE \n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n )\n END AS column_comment\n FROM \n information_schema.columns cols \n WHERE cols.table_catalog = '{db_config.get('dbname')}' AND cols.table_name = '{table_name}' AND cols.table_schema = '{table_schema}';\n \"\"\"\n comments = {}\n try:\n with psycopg2.connect(**db_config) as conn, conn.cursor() as cursor:\n cursor.execute(query)\n for row in cursor.fetchall():\n if row[1]:\n comments[row[0]] = row[1]\n log.reflect(\"PostgreSQL comments fetched\", payload={\"count\": len(comments)})\n except Exception as e:\n log.explore(\"Failed to fetch PostgreSQL comments\", payload={\"table_schema\": table_schema, \"table_name\": table_name}, error=str(e))\n raise\n return comments\n # [/DEF:get_postgres_comments:Function]\n\n # [DEF:load_excel_mappings:Function]\n # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла.\n # @PRE: file_path должен указывать на существующий XLSX файл.\n # @POST: Возвращается словарь с меппингами из файла.\n # @THROW: Exception - При ошибках чтения файла или парсинга.\n # @PARAM: file_path (str) - Путь к XLSX файлу.\n # @RETURN: Dict[str, str] - Словарь с меппингами.\n def load_excel_mappings(self, file_path: str) -> Dict[str, str]:\n with belief_scope(\"Load mappings from Excel\"):\n log.reason(\"Loading mappings from Excel\", payload={\"file_path\": file_path})\n try:\n df = pd.read_excel(file_path)\n mappings = df.set_index('column_name')['verbose_name'].to_dict()\n log.reflect(\"Excel mappings loaded\", payload={\"count\": len(mappings)})\n return mappings\n except Exception as e:\n log.explore(\"Failed to load Excel mappings\", payload={\"file_path\": file_path}, error=str(e))\n raise\n # [/DEF:load_excel_mappings:Function]\n\n # [DEF:run_mapping:Function]\n # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset.\n # @PRE: superset_client должен быть авторизован.\n # @PRE: dataset_id должен быть существующим ID в Superset.\n # @POST: Если найдены изменения, датасет в Superset обновлен через API.\n # @RELATION: CALLS -> self.get_postgres_comments\n # @RELATION: CALLS -> self.load_excel_mappings\n # @RELATION: CALLS -> superset_client.get_dataset\n # @RELATION: CALLS -> superset_client.update_dataset\n # @PARAM: superset_client (Any) - Клиент Superset.\n # @PARAM: dataset_id (int) - ID датасета для обновления.\n # @PARAM: source (str) - Источник данных ('postgres', 'excel', 'both').\n # @PARAM: postgres_config (Optional[Dict]) - Конфигурация для подключения к PostgreSQL.\n # @PARAM: excel_path (Optional[str]) - Путь к XLSX файлу.\n # @PARAM: table_name (Optional[str]) - Имя таблицы в PostgreSQL.\n # @PARAM: table_schema (Optional[str]) - Схема таблицы в PostgreSQL.\n def run_mapping(self, superset_client: Any, dataset_id: int, source: str, postgres_config: Optional[Dict] = None, excel_path: Optional[str] = None, table_name: Optional[str] = None, table_schema: Optional[str] = None):\n with belief_scope(f\"Run dataset mapping for ID {dataset_id}\"):\n log.reason(\"Starting dataset mapping\", payload={\"dataset_id\": dataset_id, \"source\": source})\n mappings: Dict[str, str] = {}\n \n try:\n if source in ['postgres', 'both']:\n if not (postgres_config and table_name and table_schema):\n raise ValueError(\"Postgres config is required.\")\n mappings.update(self.get_postgres_comments(postgres_config, table_name, table_schema))\n if source in ['excel', 'both']:\n if not excel_path:\n raise ValueError(\"Excel path is required.\")\n mappings.update(self.load_excel_mappings(excel_path))\n if source not in ['postgres', 'excel', 'both']:\n log.explore(\"Invalid mapping source\", payload={\"source\": source}, error=f\"Invalid source: {source}\")\n return\n\n dataset_response = superset_client.get_dataset(dataset_id)\n dataset_data = dataset_response['result']\n \n original_columns = dataset_data.get('columns', [])\n updated_columns = []\n changes_made = False\n\n for column in original_columns:\n col_name = column.get('column_name')\n \n new_column = {\n \"column_name\": col_name,\n \"id\": column.get(\"id\"),\n \"advanced_data_type\": column.get(\"advanced_data_type\"),\n \"description\": column.get(\"description\"),\n \"expression\": column.get(\"expression\"),\n \"extra\": column.get(\"extra\"),\n \"filterable\": column.get(\"filterable\"),\n \"groupby\": column.get(\"groupby\"),\n \"is_active\": column.get(\"is_active\"),\n \"is_dttm\": column.get(\"is_dttm\"),\n \"python_date_format\": column.get(\"python_date_format\"),\n \"type\": column.get(\"type\"),\n \"uuid\": column.get(\"uuid\"),\n \"verbose_name\": column.get(\"verbose_name\"),\n }\n \n new_column = {k: v for k, v in new_column.items() if v is not None}\n\n if col_name in mappings:\n mapping_value = mappings[col_name]\n if isinstance(mapping_value, str) and new_column.get('verbose_name') != mapping_value:\n new_column['verbose_name'] = mapping_value\n changes_made = True\n \n updated_columns.append(new_column)\n\n updated_metrics = []\n for metric in dataset_data.get(\"metrics\", []):\n new_metric = {\n \"id\": metric.get(\"id\"),\n \"metric_name\": metric.get(\"metric_name\"),\n \"expression\": metric.get(\"expression\"),\n \"verbose_name\": metric.get(\"verbose_name\"),\n \"description\": metric.get(\"description\"),\n \"d3format\": metric.get(\"d3format\"),\n \"currency\": metric.get(\"currency\"),\n \"extra\": metric.get(\"extra\"),\n \"warning_text\": metric.get(\"warning_text\"),\n \"metric_type\": metric.get(\"metric_type\"),\n \"uuid\": metric.get(\"uuid\"),\n }\n updated_metrics.append({k: v for k, v in new_metric.items() if v is not None})\n\n if changes_made:\n payload_for_update = {\n \"database_id\": dataset_data.get(\"database\", {}).get(\"id\"),\n \"table_name\": dataset_data.get(\"table_name\"),\n \"schema\": dataset_data.get(\"schema\"),\n \"columns\": updated_columns,\n \"owners\": [owner[\"id\"] for owner in dataset_data.get(\"owners\", [])],\n \"metrics\": updated_metrics,\n \"extra\": dataset_data.get(\"extra\"),\n \"description\": dataset_data.get(\"description\"),\n \"sql\": dataset_data.get(\"sql\"),\n \"cache_timeout\": dataset_data.get(\"cache_timeout\"),\n \"catalog\": dataset_data.get(\"catalog\"),\n \"default_endpoint\": dataset_data.get(\"default_endpoint\"),\n \"external_url\": dataset_data.get(\"external_url\"),\n \"fetch_values_predicate\": dataset_data.get(\"fetch_values_predicate\"),\n \"filter_select_enabled\": dataset_data.get(\"filter_select_enabled\"),\n \"is_managed_externally\": dataset_data.get(\"is_managed_externally\"),\n \"is_sqllab_view\": dataset_data.get(\"is_sqllab_view\"),\n \"main_dttm_col\": dataset_data.get(\"main_dttm_col\"),\n \"normalize_columns\": dataset_data.get(\"normalize_columns\"),\n \"offset\": dataset_data.get(\"offset\"),\n \"template_params\": dataset_data.get(\"template_params\"),\n }\n \n payload_for_update = {k: v for k, v in payload_for_update.items() if v is not None}\n\n superset_client.update_dataset(dataset_id, payload_for_update)\n log.reflect(\"Dataset verbose_name updated\", payload={\"dataset_id\": dataset_id})\n else:\n log.reflect(\"No changes in verbose_name, skipping update\", payload={\"dataset_id\": dataset_id})\n\n except (FileNotFoundError, Exception) as e:\n log.explore(\"Dataset mapping failed\", payload={\"dataset_id\": dataset_id, \"source\": source}, error=str(e))\n return\n # [/DEF:run_mapping:Function]\n# [/DEF:DatasetMapper:Class]\n" + "anchor_syntax": "region", + "body": "# #region DatasetMapper [TYPE Class]\n# @BRIEF Класс для меппинга и обновления verbose_map в датасетах Superset.\nclass DatasetMapper:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the mapper.\n # @POST: Объект DatasetMapper инициализирован.\n def __init__(self):\n pass\n # #endregion __init__\n # #region get_postgres_comments [TYPE Function]\n # @PURPOSE: Извлекает комментарии к колонкам из системного каталога PostgreSQL.\n # @PRE: db_config должен содержать валидные параметры подключения (host, port, user, password, dbname).\n # @PRE: table_name и table_schema должны быть строками.\n # @POST: Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД.\n # @THROW: Exception - При ошибках подключения или выполнения запроса к БД.\n # @PARAM: db_config (Dict) - Конфигурация для подключения к БД.\n # @PARAM: table_name (str) - Имя таблицы.\n # @PARAM: table_schema (str) - Схема таблицы.\n # @RETURN: Dict[str, str] - Словарь с комментариями к колонкам.\n def get_postgres_comments(self, db_config: dict, table_name: str, table_schema: str) -> dict[str, str]:\n with belief_scope(\"Fetch comments from PostgreSQL\"):\n app_logger.info(\"[get_postgres_comments][Enter] Fetching comments from PostgreSQL for %s.%s.\", table_schema, table_name)\n query = f\"\"\"\n SELECT \n cols.column_name,\n CASE \n WHEN pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ) LIKE '%|%' THEN \n split_part(\n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ), \n '|', \n 1\n )\n ELSE \n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n )\n END AS column_comment\n FROM \n information_schema.columns cols \n WHERE cols.table_catalog = '{db_config.get('dbname')}' AND cols.table_name = '{table_name}' AND cols.table_schema = '{table_schema}';\n \"\"\"\n comments = {}\n try:\n with psycopg2.connect(**db_config) as conn, conn.cursor() as cursor:\n cursor.execute(query)\n for row in cursor.fetchall():\n if row[1]:\n comments[row[0]] = row[1]\n app_logger.info(\"[get_postgres_comments][Success] Fetched %d comments.\", len(comments))\n except Exception as e:\n app_logger.error(\"[get_postgres_comments][Failure] %s\", e, exc_info=True)\n raise\n return comments\n # #endregion get_postgres_comments\n # #region load_excel_mappings [TYPE Function]\n # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла.\n # @PRE: file_path должен указывать на существующий XLSX файл.\n # @POST: Возвращается словарь с меппингами из файла.\n # @THROW: Exception - При ошибках чтения файла или парсинга.\n # @PARAM: file_path (str) - Путь к XLSX файлу.\n # @RETURN: Dict[str, str] - Словарь с меппингами.\n def load_excel_mappings(self, file_path: str) -> dict[str, str]:\n with belief_scope(\"Load mappings from Excel\"):\n app_logger.info(\"[load_excel_mappings][Enter] Loading mappings from %s.\", file_path)\n try:\n df = pd.read_excel(file_path)\n mappings = df.set_index('column_name')['verbose_name'].to_dict()\n app_logger.info(\"[load_excel_mappings][Success] Loaded %d mappings.\", len(mappings))\n return mappings\n except Exception as e:\n app_logger.error(\"[load_excel_mappings][Failure] %s\", e, exc_info=True)\n raise\n # #endregion load_excel_mappings\n # #region run_mapping [TYPE Function]\n # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset.\n # @PRE: superset_client должен быть авторизован.\n # @PRE: dataset_id должен быть существующим ID в Superset.\n # @POST: Если найдены изменения, датасет в Superset обновлен через API.\n # @RELATION: CALLS -> self.get_postgres_comments\n # @RELATION: CALLS -> self.load_excel_mappings\n # @RELATION: CALLS -> superset_client.get_dataset\n # @RELATION: CALLS -> superset_client.update_dataset\n # @PARAM: superset_client (Any) - Клиент Superset.\n # @PARAM: dataset_id (int) - ID датасета для обновления.\n # @PARAM: source (str) - Источник данных ('postgres', 'excel', 'both').\n # @PARAM: postgres_config (Optional[Dict]) - Конфигурация для подключения к PostgreSQL.\n # @PARAM: excel_path (Optional[str]) - Путь к XLSX файлу.\n # @PARAM: table_name (Optional[str]) - Имя таблицы в PostgreSQL.\n # @PARAM: table_schema (Optional[str]) - Схема таблицы в PostgreSQL.\n def run_mapping(self, superset_client: Any, dataset_id: int, source: str, postgres_config: dict | None = None, excel_path: str | None = None, table_name: str | None = None, table_schema: str | None = None):\n with belief_scope(f\"Run dataset mapping for ID {dataset_id}\"):\n app_logger.info(\"[run_mapping][Enter] Starting dataset mapping for ID %d from source '%s'.\", dataset_id, source)\n mappings: dict[str, str] = {}\n\n try:\n if source in ['postgres', 'both']:\n assert postgres_config and table_name and table_schema, \"Postgres config is required.\"\n mappings.update(self.get_postgres_comments(postgres_config, table_name, table_schema))\n if source in ['excel', 'both']:\n assert excel_path, \"Excel path is required.\"\n mappings.update(self.load_excel_mappings(excel_path))\n if source not in ['postgres', 'excel', 'both']:\n app_logger.error(\"[run_mapping][Failure] Invalid source: %s.\", source)\n return\n dataset_response = superset_client.get_dataset(dataset_id)\n dataset_data = dataset_response['result']\n\n original_columns = dataset_data.get('columns', [])\n updated_columns = []\n changes_made = False\n for column in original_columns:\n col_name = column.get('column_name')\n\n new_column = {\n \"column_name\": col_name,\n \"id\": column.get(\"id\"),\n \"advanced_data_type\": column.get(\"advanced_data_type\"),\n \"description\": column.get(\"description\"),\n \"expression\": column.get(\"expression\"),\n \"extra\": column.get(\"extra\"),\n \"filterable\": column.get(\"filterable\"),\n \"groupby\": column.get(\"groupby\"),\n \"is_active\": column.get(\"is_active\"),\n \"is_dttm\": column.get(\"is_dttm\"),\n \"python_date_format\": column.get(\"python_date_format\"),\n \"type\": column.get(\"type\"),\n \"uuid\": column.get(\"uuid\"),\n \"verbose_name\": column.get(\"verbose_name\"),\n }\n\n new_column = {k: v for k, v in new_column.items() if v is not None}\n if col_name in mappings:\n mapping_value = mappings[col_name]\n if isinstance(mapping_value, str) and new_column.get('verbose_name') != mapping_value:\n new_column['verbose_name'] = mapping_value\n changes_made = True\n\n updated_columns.append(new_column)\n updated_metrics = []\n for metric in dataset_data.get(\"metrics\", []):\n new_metric = {\n \"id\": metric.get(\"id\"),\n \"metric_name\": metric.get(\"metric_name\"),\n \"expression\": metric.get(\"expression\"),\n \"verbose_name\": metric.get(\"verbose_name\"),\n \"description\": metric.get(\"description\"),\n \"d3format\": metric.get(\"d3format\"),\n \"currency\": metric.get(\"currency\"),\n \"extra\": metric.get(\"extra\"),\n \"warning_text\": metric.get(\"warning_text\"),\n \"metric_type\": metric.get(\"metric_type\"),\n \"uuid\": metric.get(\"uuid\"),\n }\n updated_metrics.append({k: v for k, v in new_metric.items() if v is not None})\n if changes_made:\n payload_for_update = {\n \"database_id\": dataset_data.get(\"database\", {}).get(\"id\"),\n \"table_name\": dataset_data.get(\"table_name\"),\n \"schema\": dataset_data.get(\"schema\"),\n \"columns\": updated_columns,\n \"owners\": [owner[\"id\"] for owner in dataset_data.get(\"owners\", [])],\n \"metrics\": updated_metrics,\n \"extra\": dataset_data.get(\"extra\"),\n \"description\": dataset_data.get(\"description\"),\n \"sql\": dataset_data.get(\"sql\"),\n \"cache_timeout\": dataset_data.get(\"cache_timeout\"),\n \"catalog\": dataset_data.get(\"catalog\"),\n \"default_endpoint\": dataset_data.get(\"default_endpoint\"),\n \"external_url\": dataset_data.get(\"external_url\"),\n \"fetch_values_predicate\": dataset_data.get(\"fetch_values_predicate\"),\n \"filter_select_enabled\": dataset_data.get(\"filter_select_enabled\"),\n \"is_managed_externally\": dataset_data.get(\"is_managed_externally\"),\n \"is_sqllab_view\": dataset_data.get(\"is_sqllab_view\"),\n \"main_dttm_col\": dataset_data.get(\"main_dttm_col\"),\n \"normalize_columns\": dataset_data.get(\"normalize_columns\"),\n \"offset\": dataset_data.get(\"offset\"),\n \"template_params\": dataset_data.get(\"template_params\"),\n }\n\n payload_for_update = {k: v for k, v in payload_for_update.items() if v is not None}\n superset_client.update_dataset(dataset_id, payload_for_update)\n app_logger.info(\"[run_mapping][Success] Dataset %d columns' verbose_name updated.\", dataset_id)\n else:\n app_logger.info(\"[run_mapping][State] No changes in columns' verbose_name, skipping update.\")\n except (AssertionError, FileNotFoundError, Exception) as e:\n app_logger.error(\"[run_mapping][Failure] %s\", e, exc_info=True)\n return\n # #endregion run_mapping\n# #endregion DatasetMapper\n" }, { "contract_id": "get_postgres_comments", "contract_type": "Function", "file_path": "backend/src/core/utils/dataset_mapper.py", - "start_line": 31, - "end_line": 94, + "start_line": 27, + "end_line": 90, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50734,15 +40514,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:get_postgres_comments:Function]\n # @PURPOSE: Извлекает комментарии к колонкам из системного каталога PostgreSQL.\n # @PRE: db_config должен содержать валидные параметры подключения (host, port, user, password, dbname).\n # @PRE: table_name и table_schema должны быть строками.\n # @POST: Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД.\n # @THROW: Exception - При ошибках подключения или выполнения запроса к БД.\n # @PARAM: db_config (Dict) - Конфигурация для подключения к БД.\n # @PARAM: table_name (str) - Имя таблицы.\n # @PARAM: table_schema (str) - Схема таблицы.\n # @RETURN: Dict[str, str] - Словарь с комментариями к колонкам.\n def get_postgres_comments(self, db_config: Dict, table_name: str, table_schema: str) -> Dict[str, str]:\n with belief_scope(\"Fetch comments from PostgreSQL\"):\n log.reason(\"Fetching comments from PostgreSQL\", payload={\"table_schema\": table_schema, \"table_name\": table_name})\n query = f\"\"\"\n SELECT \n cols.column_name,\n CASE \n WHEN pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ) LIKE '%|%' THEN \n split_part(\n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ), \n '|', \n 1\n )\n ELSE \n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n )\n END AS column_comment\n FROM \n information_schema.columns cols \n WHERE cols.table_catalog = '{db_config.get('dbname')}' AND cols.table_name = '{table_name}' AND cols.table_schema = '{table_schema}';\n \"\"\"\n comments = {}\n try:\n with psycopg2.connect(**db_config) as conn, conn.cursor() as cursor:\n cursor.execute(query)\n for row in cursor.fetchall():\n if row[1]:\n comments[row[0]] = row[1]\n log.reflect(\"PostgreSQL comments fetched\", payload={\"count\": len(comments)})\n except Exception as e:\n log.explore(\"Failed to fetch PostgreSQL comments\", payload={\"table_schema\": table_schema, \"table_name\": table_name}, error=str(e))\n raise\n return comments\n # [/DEF:get_postgres_comments:Function]\n" + "anchor_syntax": "region", + "body": " # #region get_postgres_comments [TYPE Function]\n # @PURPOSE: Извлекает комментарии к колонкам из системного каталога PostgreSQL.\n # @PRE: db_config должен содержать валидные параметры подключения (host, port, user, password, dbname).\n # @PRE: table_name и table_schema должны быть строками.\n # @POST: Возвращается словарь, где ключи - имена колонок, значения - комментарии из БД.\n # @THROW: Exception - При ошибках подключения или выполнения запроса к БД.\n # @PARAM: db_config (Dict) - Конфигурация для подключения к БД.\n # @PARAM: table_name (str) - Имя таблицы.\n # @PARAM: table_schema (str) - Схема таблицы.\n # @RETURN: Dict[str, str] - Словарь с комментариями к колонкам.\n def get_postgres_comments(self, db_config: dict, table_name: str, table_schema: str) -> dict[str, str]:\n with belief_scope(\"Fetch comments from PostgreSQL\"):\n app_logger.info(\"[get_postgres_comments][Enter] Fetching comments from PostgreSQL for %s.%s.\", table_schema, table_name)\n query = f\"\"\"\n SELECT \n cols.column_name,\n CASE \n WHEN pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ) LIKE '%|%' THEN \n split_part(\n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n ), \n '|', \n 1\n )\n ELSE \n pg_catalog.col_description(\n (SELECT c.oid \n FROM pg_catalog.pg_class c \n JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \n WHERE c.relname = cols.table_name \n AND n.nspname = cols.table_schema),\n cols.ordinal_position::int\n )\n END AS column_comment\n FROM \n information_schema.columns cols \n WHERE cols.table_catalog = '{db_config.get('dbname')}' AND cols.table_name = '{table_name}' AND cols.table_schema = '{table_schema}';\n \"\"\"\n comments = {}\n try:\n with psycopg2.connect(**db_config) as conn, conn.cursor() as cursor:\n cursor.execute(query)\n for row in cursor.fetchall():\n if row[1]:\n comments[row[0]] = row[1]\n app_logger.info(\"[get_postgres_comments][Success] Fetched %d comments.\", len(comments))\n except Exception as e:\n app_logger.error(\"[get_postgres_comments][Failure] %s\", e, exc_info=True)\n raise\n return comments\n # #endregion get_postgres_comments\n" }, { "contract_id": "load_excel_mappings", "contract_type": "Function", "file_path": "backend/src/core/utils/dataset_mapper.py", - "start_line": 96, - "end_line": 114, + "start_line": 91, + "end_line": 109, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50801,15 +40581,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:load_excel_mappings:Function]\n # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла.\n # @PRE: file_path должен указывать на существующий XLSX файл.\n # @POST: Возвращается словарь с меппингами из файла.\n # @THROW: Exception - При ошибках чтения файла или парсинга.\n # @PARAM: file_path (str) - Путь к XLSX файлу.\n # @RETURN: Dict[str, str] - Словарь с меппингами.\n def load_excel_mappings(self, file_path: str) -> Dict[str, str]:\n with belief_scope(\"Load mappings from Excel\"):\n log.reason(\"Loading mappings from Excel\", payload={\"file_path\": file_path})\n try:\n df = pd.read_excel(file_path)\n mappings = df.set_index('column_name')['verbose_name'].to_dict()\n log.reflect(\"Excel mappings loaded\", payload={\"count\": len(mappings)})\n return mappings\n except Exception as e:\n log.explore(\"Failed to load Excel mappings\", payload={\"file_path\": file_path}, error=str(e))\n raise\n # [/DEF:load_excel_mappings:Function]\n" + "anchor_syntax": "region", + "body": " # #region load_excel_mappings [TYPE Function]\n # @PURPOSE: Загружает меппинги 'column_name' -> 'column_comment' из XLSX файла.\n # @PRE: file_path должен указывать на существующий XLSX файл.\n # @POST: Возвращается словарь с меппингами из файла.\n # @THROW: Exception - При ошибках чтения файла или парсинга.\n # @PARAM: file_path (str) - Путь к XLSX файлу.\n # @RETURN: Dict[str, str] - Словарь с меппингами.\n def load_excel_mappings(self, file_path: str) -> dict[str, str]:\n with belief_scope(\"Load mappings from Excel\"):\n app_logger.info(\"[load_excel_mappings][Enter] Loading mappings from %s.\", file_path)\n try:\n df = pd.read_excel(file_path)\n mappings = df.set_index('column_name')['verbose_name'].to_dict()\n app_logger.info(\"[load_excel_mappings][Success] Loaded %d mappings.\", len(mappings))\n return mappings\n except Exception as e:\n app_logger.error(\"[load_excel_mappings][Failure] %s\", e, exc_info=True)\n raise\n # #endregion load_excel_mappings\n" }, { "contract_id": "run_mapping", "contract_type": "Function", "file_path": "backend/src/core/utils/dataset_mapper.py", - "start_line": 116, - "end_line": 239, + "start_line": 110, + "end_line": 224, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50888,30 +40668,21 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:run_mapping:Function]\n # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset.\n # @PRE: superset_client должен быть авторизован.\n # @PRE: dataset_id должен быть существующим ID в Superset.\n # @POST: Если найдены изменения, датасет в Superset обновлен через API.\n # @RELATION: CALLS -> self.get_postgres_comments\n # @RELATION: CALLS -> self.load_excel_mappings\n # @RELATION: CALLS -> superset_client.get_dataset\n # @RELATION: CALLS -> superset_client.update_dataset\n # @PARAM: superset_client (Any) - Клиент Superset.\n # @PARAM: dataset_id (int) - ID датасета для обновления.\n # @PARAM: source (str) - Источник данных ('postgres', 'excel', 'both').\n # @PARAM: postgres_config (Optional[Dict]) - Конфигурация для подключения к PostgreSQL.\n # @PARAM: excel_path (Optional[str]) - Путь к XLSX файлу.\n # @PARAM: table_name (Optional[str]) - Имя таблицы в PostgreSQL.\n # @PARAM: table_schema (Optional[str]) - Схема таблицы в PostgreSQL.\n def run_mapping(self, superset_client: Any, dataset_id: int, source: str, postgres_config: Optional[Dict] = None, excel_path: Optional[str] = None, table_name: Optional[str] = None, table_schema: Optional[str] = None):\n with belief_scope(f\"Run dataset mapping for ID {dataset_id}\"):\n log.reason(\"Starting dataset mapping\", payload={\"dataset_id\": dataset_id, \"source\": source})\n mappings: Dict[str, str] = {}\n \n try:\n if source in ['postgres', 'both']:\n if not (postgres_config and table_name and table_schema):\n raise ValueError(\"Postgres config is required.\")\n mappings.update(self.get_postgres_comments(postgres_config, table_name, table_schema))\n if source in ['excel', 'both']:\n if not excel_path:\n raise ValueError(\"Excel path is required.\")\n mappings.update(self.load_excel_mappings(excel_path))\n if source not in ['postgres', 'excel', 'both']:\n log.explore(\"Invalid mapping source\", payload={\"source\": source}, error=f\"Invalid source: {source}\")\n return\n\n dataset_response = superset_client.get_dataset(dataset_id)\n dataset_data = dataset_response['result']\n \n original_columns = dataset_data.get('columns', [])\n updated_columns = []\n changes_made = False\n\n for column in original_columns:\n col_name = column.get('column_name')\n \n new_column = {\n \"column_name\": col_name,\n \"id\": column.get(\"id\"),\n \"advanced_data_type\": column.get(\"advanced_data_type\"),\n \"description\": column.get(\"description\"),\n \"expression\": column.get(\"expression\"),\n \"extra\": column.get(\"extra\"),\n \"filterable\": column.get(\"filterable\"),\n \"groupby\": column.get(\"groupby\"),\n \"is_active\": column.get(\"is_active\"),\n \"is_dttm\": column.get(\"is_dttm\"),\n \"python_date_format\": column.get(\"python_date_format\"),\n \"type\": column.get(\"type\"),\n \"uuid\": column.get(\"uuid\"),\n \"verbose_name\": column.get(\"verbose_name\"),\n }\n \n new_column = {k: v for k, v in new_column.items() if v is not None}\n\n if col_name in mappings:\n mapping_value = mappings[col_name]\n if isinstance(mapping_value, str) and new_column.get('verbose_name') != mapping_value:\n new_column['verbose_name'] = mapping_value\n changes_made = True\n \n updated_columns.append(new_column)\n\n updated_metrics = []\n for metric in dataset_data.get(\"metrics\", []):\n new_metric = {\n \"id\": metric.get(\"id\"),\n \"metric_name\": metric.get(\"metric_name\"),\n \"expression\": metric.get(\"expression\"),\n \"verbose_name\": metric.get(\"verbose_name\"),\n \"description\": metric.get(\"description\"),\n \"d3format\": metric.get(\"d3format\"),\n \"currency\": metric.get(\"currency\"),\n \"extra\": metric.get(\"extra\"),\n \"warning_text\": metric.get(\"warning_text\"),\n \"metric_type\": metric.get(\"metric_type\"),\n \"uuid\": metric.get(\"uuid\"),\n }\n updated_metrics.append({k: v for k, v in new_metric.items() if v is not None})\n\n if changes_made:\n payload_for_update = {\n \"database_id\": dataset_data.get(\"database\", {}).get(\"id\"),\n \"table_name\": dataset_data.get(\"table_name\"),\n \"schema\": dataset_data.get(\"schema\"),\n \"columns\": updated_columns,\n \"owners\": [owner[\"id\"] for owner in dataset_data.get(\"owners\", [])],\n \"metrics\": updated_metrics,\n \"extra\": dataset_data.get(\"extra\"),\n \"description\": dataset_data.get(\"description\"),\n \"sql\": dataset_data.get(\"sql\"),\n \"cache_timeout\": dataset_data.get(\"cache_timeout\"),\n \"catalog\": dataset_data.get(\"catalog\"),\n \"default_endpoint\": dataset_data.get(\"default_endpoint\"),\n \"external_url\": dataset_data.get(\"external_url\"),\n \"fetch_values_predicate\": dataset_data.get(\"fetch_values_predicate\"),\n \"filter_select_enabled\": dataset_data.get(\"filter_select_enabled\"),\n \"is_managed_externally\": dataset_data.get(\"is_managed_externally\"),\n \"is_sqllab_view\": dataset_data.get(\"is_sqllab_view\"),\n \"main_dttm_col\": dataset_data.get(\"main_dttm_col\"),\n \"normalize_columns\": dataset_data.get(\"normalize_columns\"),\n \"offset\": dataset_data.get(\"offset\"),\n \"template_params\": dataset_data.get(\"template_params\"),\n }\n \n payload_for_update = {k: v for k, v in payload_for_update.items() if v is not None}\n\n superset_client.update_dataset(dataset_id, payload_for_update)\n log.reflect(\"Dataset verbose_name updated\", payload={\"dataset_id\": dataset_id})\n else:\n log.reflect(\"No changes in verbose_name, skipping update\", payload={\"dataset_id\": dataset_id})\n\n except (FileNotFoundError, Exception) as e:\n log.explore(\"Dataset mapping failed\", payload={\"dataset_id\": dataset_id, \"source\": source}, error=str(e))\n return\n # [/DEF:run_mapping:Function]\n" + "anchor_syntax": "region", + "body": " # #region run_mapping [TYPE Function]\n # @PURPOSE: Основная функция для выполнения меппинга и обновления verbose_map датасета в Superset.\n # @PRE: superset_client должен быть авторизован.\n # @PRE: dataset_id должен быть существующим ID в Superset.\n # @POST: Если найдены изменения, датасет в Superset обновлен через API.\n # @RELATION: CALLS -> self.get_postgres_comments\n # @RELATION: CALLS -> self.load_excel_mappings\n # @RELATION: CALLS -> superset_client.get_dataset\n # @RELATION: CALLS -> superset_client.update_dataset\n # @PARAM: superset_client (Any) - Клиент Superset.\n # @PARAM: dataset_id (int) - ID датасета для обновления.\n # @PARAM: source (str) - Источник данных ('postgres', 'excel', 'both').\n # @PARAM: postgres_config (Optional[Dict]) - Конфигурация для подключения к PostgreSQL.\n # @PARAM: excel_path (Optional[str]) - Путь к XLSX файлу.\n # @PARAM: table_name (Optional[str]) - Имя таблицы в PostgreSQL.\n # @PARAM: table_schema (Optional[str]) - Схема таблицы в PostgreSQL.\n def run_mapping(self, superset_client: Any, dataset_id: int, source: str, postgres_config: dict | None = None, excel_path: str | None = None, table_name: str | None = None, table_schema: str | None = None):\n with belief_scope(f\"Run dataset mapping for ID {dataset_id}\"):\n app_logger.info(\"[run_mapping][Enter] Starting dataset mapping for ID %d from source '%s'.\", dataset_id, source)\n mappings: dict[str, str] = {}\n\n try:\n if source in ['postgres', 'both']:\n assert postgres_config and table_name and table_schema, \"Postgres config is required.\"\n mappings.update(self.get_postgres_comments(postgres_config, table_name, table_schema))\n if source in ['excel', 'both']:\n assert excel_path, \"Excel path is required.\"\n mappings.update(self.load_excel_mappings(excel_path))\n if source not in ['postgres', 'excel', 'both']:\n app_logger.error(\"[run_mapping][Failure] Invalid source: %s.\", source)\n return\n dataset_response = superset_client.get_dataset(dataset_id)\n dataset_data = dataset_response['result']\n\n original_columns = dataset_data.get('columns', [])\n updated_columns = []\n changes_made = False\n for column in original_columns:\n col_name = column.get('column_name')\n\n new_column = {\n \"column_name\": col_name,\n \"id\": column.get(\"id\"),\n \"advanced_data_type\": column.get(\"advanced_data_type\"),\n \"description\": column.get(\"description\"),\n \"expression\": column.get(\"expression\"),\n \"extra\": column.get(\"extra\"),\n \"filterable\": column.get(\"filterable\"),\n \"groupby\": column.get(\"groupby\"),\n \"is_active\": column.get(\"is_active\"),\n \"is_dttm\": column.get(\"is_dttm\"),\n \"python_date_format\": column.get(\"python_date_format\"),\n \"type\": column.get(\"type\"),\n \"uuid\": column.get(\"uuid\"),\n \"verbose_name\": column.get(\"verbose_name\"),\n }\n\n new_column = {k: v for k, v in new_column.items() if v is not None}\n if col_name in mappings:\n mapping_value = mappings[col_name]\n if isinstance(mapping_value, str) and new_column.get('verbose_name') != mapping_value:\n new_column['verbose_name'] = mapping_value\n changes_made = True\n\n updated_columns.append(new_column)\n updated_metrics = []\n for metric in dataset_data.get(\"metrics\", []):\n new_metric = {\n \"id\": metric.get(\"id\"),\n \"metric_name\": metric.get(\"metric_name\"),\n \"expression\": metric.get(\"expression\"),\n \"verbose_name\": metric.get(\"verbose_name\"),\n \"description\": metric.get(\"description\"),\n \"d3format\": metric.get(\"d3format\"),\n \"currency\": metric.get(\"currency\"),\n \"extra\": metric.get(\"extra\"),\n \"warning_text\": metric.get(\"warning_text\"),\n \"metric_type\": metric.get(\"metric_type\"),\n \"uuid\": metric.get(\"uuid\"),\n }\n updated_metrics.append({k: v for k, v in new_metric.items() if v is not None})\n if changes_made:\n payload_for_update = {\n \"database_id\": dataset_data.get(\"database\", {}).get(\"id\"),\n \"table_name\": dataset_data.get(\"table_name\"),\n \"schema\": dataset_data.get(\"schema\"),\n \"columns\": updated_columns,\n \"owners\": [owner[\"id\"] for owner in dataset_data.get(\"owners\", [])],\n \"metrics\": updated_metrics,\n \"extra\": dataset_data.get(\"extra\"),\n \"description\": dataset_data.get(\"description\"),\n \"sql\": dataset_data.get(\"sql\"),\n \"cache_timeout\": dataset_data.get(\"cache_timeout\"),\n \"catalog\": dataset_data.get(\"catalog\"),\n \"default_endpoint\": dataset_data.get(\"default_endpoint\"),\n \"external_url\": dataset_data.get(\"external_url\"),\n \"fetch_values_predicate\": dataset_data.get(\"fetch_values_predicate\"),\n \"filter_select_enabled\": dataset_data.get(\"filter_select_enabled\"),\n \"is_managed_externally\": dataset_data.get(\"is_managed_externally\"),\n \"is_sqllab_view\": dataset_data.get(\"is_sqllab_view\"),\n \"main_dttm_col\": dataset_data.get(\"main_dttm_col\"),\n \"normalize_columns\": dataset_data.get(\"normalize_columns\"),\n \"offset\": dataset_data.get(\"offset\"),\n \"template_params\": dataset_data.get(\"template_params\"),\n }\n\n payload_for_update = {k: v for k, v in payload_for_update.items() if v is not None}\n superset_client.update_dataset(dataset_id, payload_for_update)\n app_logger.info(\"[run_mapping][Success] Dataset %d columns' verbose_name updated.\", dataset_id)\n else:\n app_logger.info(\"[run_mapping][State] No changes in columns' verbose_name, skipping update.\")\n except (AssertionError, FileNotFoundError, Exception) as e:\n app_logger.error(\"[run_mapping][Failure] %s\", e, exc_info=True)\n return\n # #endregion run_mapping\n" }, { "contract_id": "FileIO", "contract_type": "Module", "file_path": "backend/src/core/utils/fileio.py", "start_line": 1, - "end_line": 489, + "end_line": 421, "tier": "STANDARD", "complexity": 1, "metadata": { "LAYER": "Infra", "PUBLIC_API": "create_temp_file, remove_empty_directories, read_dashboard_from_disk, calculate_crc32, RetentionPolicy, archive_exports, save_and_unpack_dashboard, update_yamls, create_dashboard_export, sanitize_filename, get_filename_from_headers, consolidate_archive_folders", "PURPOSE": "Предоставляет набор утилит для управления файловыми операциями, включая работу с временными файлами, архивами ZIP, файлами YAML и очистку директорий.", - "SEMANTICS": [ - "file", - "io", - "zip", - "yaml", - "temp", - "archive", - "utility" - ], "TIER": "STANDARD" }, "relations": [ @@ -50954,15 +40725,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FileIO:Module]\n#\n# @TIER: STANDARD\n# @SEMANTICS: file, io, zip, yaml, temp, archive, utility\n# @PURPOSE: Предоставляет набор утилит для управления файловыми операциями, включая работу с временными файлами, архивами ZIP, файлами YAML и очистку директорий.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> [LoggerModule]\n# @PUBLIC_API: create_temp_file, remove_empty_directories, read_dashboard_from_disk, calculate_crc32, RetentionPolicy, archive_exports, save_and_unpack_dashboard, update_yamls, create_dashboard_export, sanitize_filename, get_filename_from_headers, consolidate_archive_folders\n\n# [SECTION: IMPORTS]\nimport os\nimport re\nimport zipfile\nfrom pathlib import Path\nfrom typing import Any, Optional, Tuple, Dict, List, Union, LiteralString, Generator\nfrom contextlib import contextmanager\nimport tempfile\nfrom datetime import date, datetime\nimport shutil\nimport zlib\nfrom dataclasses import dataclass\nfrom ..logger import logger as app_logger, belief_scope\nfrom ..cot_logger import MarkerLogger\n# [/SECTION]\n\nlog = MarkerLogger(\"FileIO\")\n\n# [DEF:InvalidZipFormatError:Class]\n# @PURPOSE: Exception raised when a file is not a valid ZIP archive.\nclass InvalidZipFormatError(Exception):\n pass\n# [/DEF:InvalidZipFormatError:Class]\n\n# [DEF:create_temp_file:Function]\n# @PURPOSE: Контекстный менеджер для создания временного файла или директории с гарантированным удалением.\n# @PRE: suffix должен быть строкой, определяющей тип ресурса.\n# @POST: Временный ресурс создан и путь к нему возвращен; ресурс удален после выхода из контекста.\n# @PARAM: content (Optional[bytes]) - Бинарное содержимое для записи во временный файл.\n# @PARAM: suffix (str) - Суффикс ресурса. Если `.dir`, создается директория.\n# @PARAM: mode (str) - Режим записи в файл (e.g., 'wb').\n# @YIELDS: Path - Путь к временному ресурсу.\n# @THROW: IOError - При ошибках создания ресурса.\n@contextmanager\ndef create_temp_file(content: Optional[bytes] = None, suffix: str = \".zip\", mode: str = 'wb', dry_run = False) -> Generator[Path, None, None]:\n with belief_scope(\"Create temporary resource\"):\n resource_path = None\n is_dir = suffix.startswith('.dir')\n try:\n if is_dir:\n with tempfile.TemporaryDirectory(suffix=suffix) as temp_dir:\n resource_path = Path(temp_dir)\n log.reason(\"Created temporary directory\", payload={\"path\": str(resource_path)})\n yield resource_path\n else:\n fd, temp_path_str = tempfile.mkstemp(suffix=suffix)\n resource_path = Path(temp_path_str)\n os.close(fd)\n if content:\n resource_path.write_bytes(content)\n log.reason(\"Created temporary file\", payload={\"path\": str(resource_path)})\n yield resource_path\n finally:\n if resource_path and resource_path.exists() and not dry_run:\n try:\n if resource_path.is_dir():\n shutil.rmtree(resource_path)\n log.reflect(\"Cleaned up temporary directory\", payload={\"path\": str(resource_path)})\n else:\n resource_path.unlink()\n log.reflect(\"Cleaned up temporary file\", payload={\"path\": str(resource_path)})\n except OSError as e:\n log.explore(\"Error during temp resource cleanup\", payload={\"path\": str(resource_path)}, error=str(e))\n# [/DEF:create_temp_file:Function]\n\n# [DEF:remove_empty_directories:Function]\n# @PURPOSE: Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути.\n# @PRE: root_dir должен быть путем к существующей директории.\n# @POST: Все пустые поддиректории удалены, возвращено их количество.\n# @PARAM: root_dir (str) - Путь к корневой директории для очистки.\n# @RETURN: int - Количество удаленных директорий.\ndef remove_empty_directories(root_dir: str) -> int:\n with belief_scope(f\"Remove empty directories in {root_dir}\"):\n removed_count = 0\n if not os.path.isdir(root_dir):\n log.explore(\"Empty directory cleanup skipped - directory not found\", payload={\"root_dir\": root_dir}, error=f\"Directory not found: {root_dir}\")\n return 0\n for current_dir, _, _ in os.walk(root_dir, topdown=False):\n if not os.listdir(current_dir):\n try:\n os.rmdir(current_dir)\n removed_count += 1\n log.reason(\"Removed empty directory\", payload={\"directory\": current_dir})\n except OSError as e:\n log.explore(\"Failed to remove empty directory\", payload={\"directory\": current_dir}, error=str(e))\n log.reflect(\"Empty directory cleanup complete\", payload={\"removed_count\": removed_count})\n return removed_count\n# [/DEF:remove_empty_directories:Function]\n\n# [DEF:read_dashboard_from_disk:Function]\n# @PURPOSE: Читает бинарное содержимое файла с диска.\n# @PRE: file_path должен указывать на существующий файл.\n# @POST: Возвращает байты содержимого и имя файла.\n# @PARAM: file_path (str) - Путь к файлу.\n# @RETURN: Tuple[bytes, str] - Кортеж (содержимое, имя файла).\n# @THROW: FileNotFoundError - Если файл не найден.\ndef read_dashboard_from_disk(file_path: str) -> Tuple[bytes, str]:\n with belief_scope(f\"Read dashboard from {file_path}\"):\n path = Path(file_path)\n assert path.is_file(), f\"Файл дашборда не найден: {file_path}\"\n log.reason(\"Reading dashboard file from disk\", payload={\"file_path\": file_path})\n content = path.read_bytes()\n if not content:\n log.explore(\"Dashboard file is empty\", payload={\"file_path\": file_path}, error=\"Empty dashboard file\")\n return content, path.name\n# [/DEF:read_dashboard_from_disk:Function]\n\n# [DEF:calculate_crc32:Function]\n# @PURPOSE: Вычисляет контрольную сумму CRC32 для файла.\n# @PRE: file_path должен быть объектом Path к существующему файлу.\n# @POST: Возвращает 8-значную hex-строку CRC32.\n# @PARAM: file_path (Path) - Путь к файлу.\n# @RETURN: str - 8-значное шестнадцатеричное представление CRC32.\n# @THROW: IOError - При ошибках чтения файла.\ndef calculate_crc32(file_path: Path) -> str:\n with belief_scope(f\"Calculate CRC32 for {file_path}\"):\n with open(file_path, 'rb') as f:\n crc32_value = zlib.crc32(f.read())\n return f\"{crc32_value:08x}\"\n# [/DEF:calculate_crc32:Function]\n\n# [SECTION: DATA_CLASSES]\n# [DEF:RetentionPolicy:DataClass]\n# @PURPOSE: Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные).\n@dataclass\nclass RetentionPolicy:\n daily: int = 7\n weekly: int = 4\n monthly: int = 12\n# [/DEF:RetentionPolicy:DataClass]\n# [/SECTION]\n\n# [DEF:archive_exports:Function]\n# @PURPOSE: Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию.\n# @PRE: output_dir должен быть путем к существующей директории.\n# @POST: Старые или дублирующиеся архивы удалены согласно политике.\n# @RELATION: CALLS -> apply_retention_policy\n# @RELATION: CALLS -> calculate_crc32\n# @PARAM: output_dir (str) - Директория с архивами.\n# @PARAM: policy (RetentionPolicy) - Политика хранения.\n# @PARAM: deduplicate (bool) - Флаг для включения удаления дубликатов по CRC32.\ndef archive_exports(output_dir: str, policy: RetentionPolicy, deduplicate: bool = False) -> None:\n with belief_scope(f\"Archive exports in {output_dir}\"):\n output_path = Path(output_dir)\n if not output_path.is_dir():\n log.explore(\"Archive directory not found\", payload={\"output_dir\": output_dir}, error=f\"Archive directory not found: {output_dir}\")\n return\n\n log.reason(\"Managing archive exports\", payload={\"output_dir\": output_dir})\n \n # 1. Collect all zip files\n zip_files = list(output_path.glob(\"*.zip\"))\n if not zip_files:\n log.reason(\"No zip files found in archive directory\", payload={\"output_dir\": output_dir})\n return\n\n # 2. Deduplication\n if deduplicate:\n log.reason(\"Starting archive deduplication\")\n checksums = {}\n files_to_remove = []\n \n # Sort by modification time (newest first) to keep the latest version\n zip_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)\n \n for file_path in zip_files:\n try:\n crc = calculate_crc32(file_path)\n if crc in checksums:\n files_to_remove.append(file_path)\n log.reason(\"Duplicate archive found (same checksum)\", payload={\"file\": file_path.name, \"original\": checksums[crc].name})\n else:\n checksums[crc] = file_path\n except Exception as e:\n log.explore(\"Failed to calculate CRC32 for archive\", payload={\"file\": str(file_path)}, error=str(e))\n \n for f in files_to_remove:\n try:\n f.unlink()\n zip_files.remove(f)\n log.reason(\"Removed duplicate archive\", payload={\"file\": f.name})\n except OSError as e:\n log.explore(\"Failed to remove duplicate archive\", payload={\"file\": str(f)}, error=str(e))\n\n # 3. Retention Policy\n files_with_dates = []\n for file_path in zip_files:\n # Try to extract date from filename\n # Pattern: ..._YYYYMMDD_HHMMSS.zip or ..._YYYYMMDD.zip\n match = re.search(r'_(\\d{8})_', file_path.name)\n file_date = None\n if match:\n try:\n date_str = match.group(1)\n file_date = datetime.strptime(date_str, \"%Y%m%d\").date()\n except ValueError:\n pass\n \n if not file_date:\n # Fallback to modification time\n file_date = datetime.fromtimestamp(file_path.stat().st_mtime).date()\n \n files_with_dates.append((file_path, file_date))\n\n files_to_keep = apply_retention_policy(files_with_dates, policy)\n \n for file_path, _ in files_with_dates:\n if file_path not in files_to_keep:\n try:\n file_path.unlink()\n log.reason(\"Removed archive by retention policy\", payload={\"file\": file_path.name})\n except OSError as e:\n log.explore(\"Failed to remove archive by retention policy\", payload={\"file\": str(file_path)}, error=str(e))\n# [/DEF:archive_exports:Function]\n\n# [DEF:apply_retention_policy:Function]\n# @PURPOSE: (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить.\n# @PRE: files_with_dates is a list of (Path, date) tuples.\n# @POST: Returns a set of files to keep.\n# @PARAM: files_with_dates (List[Tuple[Path, date]]) - Список файлов с датами.\n# @PARAM: policy (RetentionPolicy) - Политика хранения.\n# @RETURN: set - Множество путей к файлам, которые должны быть сохранены.\ndef apply_retention_policy(files_with_dates: List[Tuple[Path, date]], policy: RetentionPolicy) -> set:\n with belief_scope(\"Apply retention policy\"):\n # Сортируем по дате (от новой к старой)\n sorted_files = sorted(files_with_dates, key=lambda x: x[1], reverse=True) \n # Словарь для хранения файлов по категориям \n daily_files = [] \n weekly_files = [] \n monthly_files = [] \n today = date.today() \n for file_path, file_date in sorted_files: \n # Ежедневные \n if (today - file_date).days < policy.daily: \n daily_files.append(file_path) \n # Еженедельные \n elif (today - file_date).days < policy.weekly * 7: \n weekly_files.append(file_path) \n # Ежемесячные \n elif (today - file_date).days < policy.monthly * 30: \n monthly_files.append(file_path) \n # Возвращаем множество файлов, которые нужно сохранить \n files_to_keep = set() \n files_to_keep.update(daily_files) \n files_to_keep.update(weekly_files[:policy.weekly]) \n files_to_keep.update(monthly_files[:policy.monthly]) \n log.reflect(\"Retention policy applied\", payload={\"files_to_keep\": len(files_to_keep)})\n return files_to_keep\n# [/DEF:apply_retention_policy:Function]\n\n# [DEF:save_and_unpack_dashboard:Function]\n# @PURPOSE: Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.\n# @PRE: zip_content должен быть байтами валидного ZIP-архива.\n# @POST: ZIP-файл сохранен, и если unpack=True, он распакован в output_dir.\n# @PARAM: zip_content (bytes) - Содержимое ZIP-архива.\n# @PARAM: output_dir (Union[str, Path]) - Директория для сохранения.\n# @PARAM: unpack (bool) - Флаг, нужно ли распаковывать архив.\n# @PARAM: original_filename (Optional[str]) - Исходное имя файла для сохранения.\n# @RETURN: Tuple[Path, Optional[Path]] - Путь к ZIP-файлу и, если применимо, путь к директории с распаковкой.\n# @THROW: InvalidZipFormatError - При ошибке формата ZIP.\ndef save_and_unpack_dashboard(zip_content: bytes, output_dir: Union[str, Path], unpack: bool = False, original_filename: Optional[str] = None) -> Tuple[Path, Optional[Path]]:\n with belief_scope(\"Save and unpack dashboard\"):\n log.reason(\"Processing dashboard ZIP save\", payload={\"unpack\": unpack})\n try:\n output_path = Path(output_dir)\n output_path.mkdir(parents=True, exist_ok=True)\n zip_name = sanitize_filename(original_filename) if original_filename else f\"dashboard_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip\"\n zip_path = output_path / zip_name\n zip_path.write_bytes(zip_content)\n log.reason(\"Dashboard ZIP saved to disk\", payload={\"zip_path\": str(zip_path)})\n if unpack:\n with zipfile.ZipFile(zip_path, 'r') as zip_ref:\n zip_ref.extractall(output_path)\n log.reason(\"Dashboard unpacked to directory\", payload={\"output_path\": str(output_path)})\n return zip_path, output_path\n return zip_path, None\n except zipfile.BadZipFile as e:\n log.explore(\"Invalid ZIP archive format\", payload={\"output_dir\": str(output_dir)}, error=str(e))\n raise InvalidZipFormatError(f\"Invalid ZIP file: {e}\") from e\n# [/DEF:save_and_unpack_dashboard:Function]\n\n# [DEF:update_yamls:Function]\n# @PURPOSE: Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex.\n# @PRE: path должен быть существующей директорией.\n# @POST: Все YAML файлы в директории обновлены согласно переданным параметрам.\n# @RELATION: CALLS -> _update_yaml_file\n# @THROW: FileNotFoundError - Если `path` не существует.\n# @PARAM: db_configs (Optional[List[Dict]]) - Список конфигураций для замены.\n# @PARAM: path (str) - Путь к директории с YAML файлами.\n# @PARAM: regexp_pattern (Optional[LiteralString]) - Паттерн для поиска.\n# @PARAM: replace_string (Optional[LiteralString]) - Строка для замены.\ndef update_yamls(db_configs: Optional[List[Dict[str, Any]]] = None, path: str = \"dashboards\", regexp_pattern: Optional[LiteralString] = None, replace_string: Optional[LiteralString] = None) -> None:\n with belief_scope(\"Update YAML configurations\"):\n log.reason(\"Starting YAML configuration update\", payload={\"path\": path})\n dir_path = Path(path)\n assert dir_path.is_dir(), f\"Путь {path} не существует или не является директорией\"\n \n configs: List[Dict[str, Any]] = db_configs or []\n \n for file_path in dir_path.rglob(\"*.yaml\"):\n _update_yaml_file(file_path, configs, regexp_pattern, replace_string)\n# [/DEF:update_yamls:Function]\n\n# [DEF:_update_yaml_file:Function]\n# @PURPOSE: (Helper) Обновляет один YAML файл.\n# @PRE: file_path должен быть объектом Path к существующему YAML файлу.\n# @POST: Файл обновлен согласно переданным конфигурациям или регулярному выражению.\n# @PARAM: file_path (Path) - Путь к файлу.\n# @PARAM: db_configs (List[Dict]) - Конфигурации.\n# @PARAM: regexp_pattern (Optional[str]) - Паттерн.\n# @PARAM: replace_string (Optional[str]) - Замена.\ndef _update_yaml_file(file_path: Path, db_configs: List[Dict[str, Any]], regexp_pattern: Optional[str], replace_string: Optional[str]) -> None:\n with belief_scope(f\"Update YAML file: {file_path}\"):\n # Читаем содержимое файла\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n except Exception as e:\n log.explore(\"Failed to read YAML file\", payload={\"file_path\": str(file_path)}, error=str(e))\n return\n # Если задан pattern и replace_string, применяем замену по регулярному выражению\n if regexp_pattern and replace_string:\n try:\n new_content = re.sub(regexp_pattern, replace_string, content)\n if new_content != content:\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(new_content)\n log.reason(\"Updated YAML file using regex pattern\", payload={\"file_path\": str(file_path)})\n except Exception as e:\n log.explore(\"Error applying regex to YAML file\", payload={\"file_path\": str(file_path)}, error=str(e))\n # Если заданы конфигурации, заменяем значения (поддержка old/new)\n if db_configs:\n try:\n # Прямой текстовый заменитель для старых/новых значений, чтобы сохранить структуру файла\n modified_content = content\n for cfg in db_configs:\n # Ожидаем структуру: {'old': {...}, 'new': {...}}\n old_cfg = cfg.get('old', {})\n new_cfg = cfg.get('new', {})\n for key, old_val in old_cfg.items():\n if key in new_cfg:\n new_val = new_cfg[key]\n # Заменяем только точные совпадения старого значения в тексте YAML, используя ключ для контекста\n if isinstance(old_val, str):\n # Ищем паттерн: key: \"value\" или key: value\n key_pattern = re.escape(key)\n val_pattern = re.escape(old_val)\n # Группы: 1=ключ+разделитель, 2=открывающая кавычка (опц), 3=значение, 4=закрывающая кавычка (опц)\n pattern = rf'({key_pattern}\\s*:\\s*)([\"\\']?)({val_pattern})([\"\\']?)'\n \n # [DEF:replacer:Function]\n # @PURPOSE: Функция замены, сохраняющая кавычки если они были.\n # @PRE: match должен быть объектом совпадения регулярного выражения.\n # @POST: Возвращает строку с новым значением, сохраняя префикс и кавычки.\n def replacer(match):\n prefix = match.group(1)\n quote_open = match.group(2)\n quote_close = match.group(4)\n return f\"{prefix}{quote_open}{new_val}{quote_close}\"\n # [/DEF:replacer:Function]\n\n modified_content = re.sub(pattern, replacer, modified_content)\n log.reason(\"Replaced YAML config value\", payload={\"key\": key, \"file_path\": str(file_path)})\n # Записываем обратно изменённый контент без парсинга YAML, сохраняем оригинальное форматирование\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(modified_content)\n except Exception as e:\n log.explore(\"Error performing raw replacement in YAML\", payload={\"file_path\": str(file_path)}, error=str(e))\n# [/DEF:_update_yaml_file:Function]\n\n# [DEF:create_dashboard_export:Function]\n# @PURPOSE: Создает ZIP-архив из указанных исходных путей.\n# @PRE: source_paths должен содержать существующие пути.\n# @POST: ZIP-архив создан по пути zip_path.\n# @PARAM: zip_path (Union[str, Path]) - Путь для сохранения ZIP архива.\n# @PARAM: source_paths (List[Union[str, Path]]) - Список исходных путей для архивации.\n# @PARAM: exclude_extensions (Optional[List[str]]) - Список расширений для исключения.\n# @RETURN: bool - `True` при успехе, `False` при ошибке.\ndef create_dashboard_export(zip_path: Union[str, Path], source_paths: List[Union[str, Path]], exclude_extensions: Optional[List[str]] = None) -> bool:\n with belief_scope(f\"Create dashboard export: {zip_path}\"):\n log.reason(\"Packing dashboard export\", payload={\"source_paths\": [str(p) for p in source_paths], \"zip_path\": str(zip_path)})\n try:\n exclude_ext = [ext.lower() for ext in exclude_extensions or []]\n with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n for src_path_str in source_paths:\n src_path = Path(src_path_str)\n assert src_path.exists(), f\"Путь не найден: {src_path}\"\n for item in src_path.rglob('*'):\n if item.is_file() and item.suffix.lower() not in exclude_ext:\n arcname = item.relative_to(src_path.parent)\n zipf.write(item, arcname)\n log.reflect(\"Dashboard archive created\", payload={\"zip_path\": str(zip_path)})\n return True\n except (IOError, zipfile.BadZipFile, AssertionError) as e:\n log.explore(\"Dashboard archive creation failed\", payload={\"zip_path\": str(zip_path)}, error=str(e))\n return False\n# [/DEF:create_dashboard_export:Function]\n\n# [DEF:sanitize_filename:Function]\n# @PURPOSE: Очищает строку от символов, недопустимых в именах файлов.\n# @PRE: filename должен быть строкой.\n# @POST: Возвращает строку без спецсимволов.\n# @PARAM: filename (str) - Исходное имя файла.\n# @RETURN: str - Очищенная строка.\ndef sanitize_filename(filename: str) -> str:\n with belief_scope(f\"Sanitize filename: {filename}\"):\n return re.sub(r'[\\\\/*?:\"<>|]', \"_\", filename).strip()\n# [/DEF:sanitize_filename:Function]\n\n# [DEF:get_filename_from_headers:Function]\n# @PURPOSE: Извлекает имя файла из HTTP заголовка 'Content-Disposition'.\n# @PRE: headers должен быть словарем заголовков.\n# @POST: Возвращает имя файла или None, если заголовок отсутствует.\n# @PARAM: headers (dict) - Словарь HTTP заголовков.\n# @RETURN: Optional[str] - Имя файла or `None`.\ndef get_filename_from_headers(headers: dict) -> Optional[str]:\n with belief_scope(\"Get filename from headers\"):\n content_disposition = headers.get(\"Content-Disposition\", \"\")\n if match := re.search(r'filename=\"?([^\"]+)\"?', content_disposition):\n return match.group(1).strip()\n return None\n# [/DEF:get_filename_from_headers:Function]\n\n# [DEF:consolidate_archive_folders:Function]\n# @PURPOSE: Консолидирует директории архивов на основе общего слага в имени.\n# @PRE: root_directory должен быть объектом Path к существующей директории.\n# @POST: Директории с одинаковым префиксом объединены в одну.\n# @THROW: TypeError, ValueError - Если `root_directory` невалиден.\n# @PARAM: root_directory (Path) - Корневая директория для консолидации.\ndef consolidate_archive_folders(root_directory: Path) -> None:\n with belief_scope(f\"Consolidate archives in {root_directory}\"):\n assert isinstance(root_directory, Path), \"root_directory must be a Path object.\" \n assert root_directory.is_dir(), \"root_directory must be an existing directory.\" \n \n log.reason(\"Consolidating archive folders\", payload={\"root_directory\": str(root_directory)}) \n # Собираем все директории с архивами \n archive_dirs = [] \n for item in root_directory.iterdir(): \n if item.is_dir(): \n # Проверяем, есть ли в директории ZIP-архивы \n if any(item.glob(\"*.zip\")): \n archive_dirs.append(item) \n # Группируем по слагу (части имени до первого '_') \n slug_groups = {} \n for dir_path in archive_dirs: \n dir_name = dir_path.name \n slug = dir_name.split('_')[0] if '_' in dir_name else dir_name \n if slug not in slug_groups: \n slug_groups[slug] = [] \n slug_groups[slug].append(dir_path) \n # Для каждой группы консолидируем \n for slug, dirs in slug_groups.items(): \n if len(dirs) <= 1: \n continue \n # Создаем целевую директорию \n target_dir = root_directory / slug \n target_dir.mkdir(exist_ok=True) \n log.reason(\"Consolidating archive directories\", payload={\"slug\": slug, \"dir_count\": len(dirs), \"target\": str(target_dir)}) \n # Перемещаем содержимое \n for source_dir in dirs: \n if source_dir == target_dir: \n continue \n for item in source_dir.iterdir(): \n dest_item = target_dir / item.name \n try: \n if item.is_dir(): \n shutil.move(str(item), str(dest_item)) \n else: \n shutil.move(str(item), str(dest_item)) \n except Exception as e: \n log.explore(\"Failed to move item during consolidation\", payload={\"source\": str(item), \"dest\": str(dest_item)}, error=str(e)) \n # Удаляем исходную директорию \n try: \n source_dir.rmdir() \n log.reason(\"Removed source directory after consolidation\", payload={\"directory\": str(source_dir)}) \n except Exception as e: \n log.explore(\"Failed to remove source directory after consolidation\", payload={\"directory\": str(source_dir)}, error=str(e)) \n# [/DEF:consolidate_archive_folders:Function]\n\n# [/DEF:FileIO:Module]\n" + "anchor_syntax": "region", + "body": "# #region FileIO [TYPE Module] [SEMANTICS fileio, archive, zip, yaml, utility]\n#\n# @TIER: STANDARD\n# @BRIEF Предоставляет набор утилит для управления файловыми операциями, включая работу с временными файлами, архивами ZIP, файлами YAML и очистку директорий.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PUBLIC_API: create_temp_file, remove_empty_directories, read_dashboard_from_disk, calculate_crc32, RetentionPolicy, archive_exports, save_and_unpack_dashboard, update_yamls, create_dashboard_export, sanitize_filename, get_filename_from_headers, consolidate_archive_folders\nimport os\nimport re\nimport shutil\nimport tempfile\nimport zipfile\nimport zlib\nfrom collections.abc import Generator\nfrom contextlib import contextmanager\nfrom dataclasses import dataclass\nfrom datetime import date, datetime\nfrom pathlib import Path\nfrom typing import Any, LiteralString\n\nfrom ..logger import belief_scope\nfrom ..logger import logger as app_logger\n\n\n# #region InvalidZipFormatError [TYPE Class]\n# @BRIEF Exception raised when a file is not a valid ZIP archive.\nclass InvalidZipFormatError(Exception):\n pass\n# #endregion InvalidZipFormatError\n# #region create_temp_file [TYPE Function]\n# @BRIEF Контекстный менеджер для создания временного файла или директории с гарантированным удалением.\n# @PRE: suffix должен быть строкой, определяющей тип ресурса.\n# @POST: Временный ресурс создан и путь к нему возвращен; ресурс удален после выхода из контекста.\n# @YIELDS: Path - Путь к временному ресурсу.\n@contextmanager\ndef create_temp_file(content: bytes | None = None, suffix: str = \".zip\", mode: str = 'wb', dry_run = False) -> Generator[Path]:\n with belief_scope(\"Create temporary resource\"):\n resource_path = None\n is_dir = suffix.startswith('.dir')\n try:\n if is_dir:\n with tempfile.TemporaryDirectory(suffix=suffix) as temp_dir:\n resource_path = Path(temp_dir)\n app_logger.debug(\"[create_temp_file][State] Created temporary directory: %s\", resource_path)\n yield resource_path\n else:\n fd, temp_path_str = tempfile.mkstemp(suffix=suffix)\n resource_path = Path(temp_path_str)\n os.close(fd)\n if content:\n resource_path.write_bytes(content)\n app_logger.debug(\"[create_temp_file][State] Created temporary file: %s\", resource_path)\n yield resource_path\n finally:\n if resource_path and resource_path.exists() and not dry_run:\n try:\n if resource_path.is_dir():\n shutil.rmtree(resource_path)\n app_logger.debug(\"[create_temp_file][Cleanup] Removed temporary directory: %s\", resource_path)\n else:\n resource_path.unlink()\n app_logger.debug(\"[create_temp_file][Cleanup] Removed temporary file: %s\", resource_path)\n except OSError as e:\n app_logger.error(\"[create_temp_file][Failure] Error during cleanup of %s: %s\", resource_path, e)\n# #endregion create_temp_file\n# #region remove_empty_directories [TYPE Function]\n# @BRIEF Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути.\n# @PRE: root_dir должен быть путем к существующей директории.\n# @POST: Все пустые поддиректории удалены, возвращено их количество.\ndef remove_empty_directories(root_dir: str) -> int:\n with belief_scope(f\"Remove empty directories in {root_dir}\"):\n app_logger.info(\"[remove_empty_directories][Enter] Starting cleanup of empty directories in %s\", root_dir)\n removed_count = 0\n if not os.path.isdir(root_dir):\n app_logger.error(\"[remove_empty_directories][Failure] Directory not found: %s\", root_dir)\n return 0\n for current_dir, _, _ in os.walk(root_dir, topdown=False):\n if not os.listdir(current_dir):\n try:\n os.rmdir(current_dir)\n removed_count += 1\n app_logger.info(\"[remove_empty_directories][State] Removed empty directory: %s\", current_dir)\n except OSError as e:\n app_logger.error(\"[remove_empty_directories][Failure] Failed to remove %s: %s\", current_dir, e)\n app_logger.reflect(f\"Removed {removed_count} empty directories.\", extra={\"src\": \"remove_empty_directories\"})\n return removed_count\n# #endregion remove_empty_directories\n# #region read_dashboard_from_disk [TYPE Function]\n# @BRIEF Читает бинарное содержимое файла с диска.\n# @PRE: file_path должен указывать на существующий файл.\n# @POST: Возвращает байты содержимого и имя файла.\ndef read_dashboard_from_disk(file_path: str) -> tuple[bytes, str]:\n with belief_scope(f\"Read dashboard from {file_path}\"):\n path = Path(file_path)\n assert path.is_file(), f\"Файл дашборда не найден: {file_path}\"\n app_logger.info(\"[read_dashboard_from_disk][Enter] Reading file: %s\", file_path)\n content = path.read_bytes()\n if not content:\n app_logger.warning(\"[read_dashboard_from_disk][Warning] File is empty: %s\", file_path)\n return content, path.name\n# #endregion read_dashboard_from_disk\n# #region calculate_crc32 [TYPE Function]\n# @BRIEF Вычисляет контрольную сумму CRC32 для файла.\n# @PRE: file_path должен быть объектом Path к существующему файлу.\n# @POST: Возвращает 8-значную hex-строку CRC32.\ndef calculate_crc32(file_path: Path) -> str:\n with belief_scope(f\"Calculate CRC32 for {file_path}\"), open(file_path, 'rb') as f:\n crc32_value = zlib.crc32(f.read())\n return f\"{crc32_value:08x}\"\n# #endregion calculate_crc32\n# #region RetentionPolicy [TYPE DataClass]\n# @BRIEF Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные).\n@dataclass\nclass RetentionPolicy:\n daily: int = 7\n weekly: int = 4\n monthly: int = 12\n# #endregion RetentionPolicy\n# #region archive_exports [TYPE Function]\n# @BRIEF Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию.\n# @PRE: output_dir должен быть путем к существующей директории.\n# @POST: Старые или дублирующиеся архивы удалены согласно политике.\n# @RELATION CALLS -> apply_retention_policy\n# @RELATION CALLS -> calculate_crc32\ndef archive_exports(output_dir: str, policy: RetentionPolicy, deduplicate: bool = False) -> None:\n with belief_scope(f\"Archive exports in {output_dir}\"):\n output_path = Path(output_dir)\n if not output_path.is_dir():\n app_logger.warning(\"[archive_exports][Skip] Archive directory not found: %s\", output_dir)\n return\n app_logger.info(\"[archive_exports][Enter] Managing archive in %s\", output_dir)\n\n # 1. Collect all zip files\n zip_files = list(output_path.glob(\"*.zip\"))\n if not zip_files:\n app_logger.info(\"[archive_exports][State] No zip files found in %s\", output_dir)\n return\n # 2. Deduplication\n if deduplicate:\n app_logger.info(\"[archive_exports][State] Starting deduplication...\")\n checksums = {}\n files_to_remove = []\n\n # Sort by modification time (newest first) to keep the latest version\n zip_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)\n\n for file_path in zip_files:\n try:\n crc = calculate_crc32(file_path)\n if crc in checksums:\n files_to_remove.append(file_path)\n app_logger.debug(\"[archive_exports][State] Duplicate found: %s (same as %s)\", file_path.name, checksums[crc].name)\n else:\n checksums[crc] = file_path\n except Exception as e:\n app_logger.error(\"[archive_exports][Failure] Failed to calculate CRC32 for %s: %s\", file_path, e)\n\n for f in files_to_remove:\n try:\n f.unlink()\n zip_files.remove(f)\n app_logger.info(\"[archive_exports][State] Removed duplicate: %s\", f.name)\n except OSError as e:\n app_logger.error(\"[archive_exports][Failure] Failed to remove duplicate %s: %s\", f, e)\n # 3. Retention Policy\n files_with_dates = []\n for file_path in zip_files:\n # Try to extract date from filename\n # Pattern: ..._YYYYMMDD_HHMMSS.zip or ..._YYYYMMDD.zip\n match = re.search(r'_(\\d{8})_', file_path.name)\n file_date = None\n if match:\n try:\n date_str = match.group(1)\n file_date = datetime.strptime(date_str, \"%Y%m%d\").date()\n except ValueError:\n pass\n\n if not file_date:\n # Fallback to modification time\n file_date = datetime.fromtimestamp(file_path.stat().st_mtime).date()\n\n files_with_dates.append((file_path, file_date))\n files_to_keep = apply_retention_policy(files_with_dates, policy)\n\n for file_path, _ in files_with_dates:\n if file_path not in files_to_keep:\n try:\n file_path.unlink()\n app_logger.info(\"[archive_exports][State] Removed by retention policy: %s\", file_path.name)\n except OSError as e:\n app_logger.error(\"[archive_exports][Failure] Failed to remove %s: %s\", file_path, e)\n# #endregion archive_exports\n# #region apply_retention_policy [TYPE Function]\n# @BRIEF (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить.\n# @PRE: files_with_dates is a list of (Path, date) tuples.\n# @POST: Returns a set of files to keep.\ndef apply_retention_policy(files_with_dates: list[tuple[Path, date]], policy: RetentionPolicy) -> set:\n with belief_scope(\"Apply retention policy\"):\n # Сортируем по дате (от новой к старой)\n sorted_files = sorted(files_with_dates, key=lambda x: x[1], reverse=True)\n # Словарь для хранения файлов по категориям\n daily_files = []\n weekly_files = []\n monthly_files = []\n today = date.today()\n for file_path, file_date in sorted_files:\n # Ежедневные\n if (today - file_date).days < policy.daily:\n daily_files.append(file_path)\n # Еженедельные\n elif (today - file_date).days < policy.weekly * 7:\n weekly_files.append(file_path)\n # Ежемесячные\n elif (today - file_date).days < policy.monthly * 30:\n monthly_files.append(file_path)\n # Возвращаем множество файлов, которые нужно сохранить\n files_to_keep = set()\n files_to_keep.update(daily_files)\n files_to_keep.update(weekly_files[:policy.weekly])\n files_to_keep.update(monthly_files[:policy.monthly])\n app_logger.debug(\"[apply_retention_policy][State] Keeping %d files according to retention policy\", len(files_to_keep))\n return files_to_keep\n# #endregion apply_retention_policy\n# #region save_and_unpack_dashboard [TYPE Function]\n# @BRIEF Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.\n# @PRE: zip_content должен быть байтами валидного ZIP-архива.\n# @POST: ZIP-файл сохранен, и если unpack=True, он распакован в output_dir.\ndef save_and_unpack_dashboard(zip_content: bytes, output_dir: str | Path, unpack: bool = False, original_filename: str | None = None) -> tuple[Path, Path | None]:\n with belief_scope(\"Save and unpack dashboard\"):\n app_logger.info(\"[save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: %s\", unpack)\n try:\n output_path = Path(output_dir)\n output_path.mkdir(parents=True, exist_ok=True)\n zip_name = sanitize_filename(original_filename) if original_filename else f\"dashboard_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip\"\n zip_path = output_path / zip_name\n zip_path.write_bytes(zip_content)\n app_logger.info(\"[save_and_unpack_dashboard][State] Dashboard saved to: %s\", zip_path)\n if unpack:\n with zipfile.ZipFile(zip_path, 'r') as zip_ref:\n zip_ref.extractall(output_path)\n app_logger.info(\"[save_and_unpack_dashboard][State] Dashboard unpacked to: %s\", output_path)\n return zip_path, output_path\n return zip_path, None\n except zipfile.BadZipFile as e:\n app_logger.error(\"[save_and_unpack_dashboard][Failure] Invalid ZIP archive: %s\", e)\n raise InvalidZipFormatError(f\"Invalid ZIP file: {e}\") from e\n# #endregion save_and_unpack_dashboard\n# #region update_yamls [TYPE Function]\n# @BRIEF Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex.\n# @PRE: path должен быть существующей директорией.\n# @POST: Все YAML файлы в директории обновлены согласно переданным параметрам.\n# @RELATION CALLS -> _update_yaml_file\ndef update_yamls(db_configs: list[dict[str, Any]] | None = None, path: str = \"dashboards\", regexp_pattern: LiteralString | None = None, replace_string: LiteralString | None = None) -> None:\n with belief_scope(\"Update YAML configurations\"):\n app_logger.info(\"[update_yamls][Enter] Starting YAML configuration update.\")\n dir_path = Path(path)\n assert dir_path.is_dir(), f\"Путь {path} не существует или не является директорией\"\n\n configs: list[dict[str, Any]] = db_configs or []\n\n for file_path in dir_path.rglob(\"*.yaml\"):\n _update_yaml_file(file_path, configs, regexp_pattern, replace_string)\n# #endregion update_yamls\n# #region _update_yaml_file [TYPE Function]\n# @BRIEF (Helper) Обновляет один YAML файл.\n# @PRE: file_path должен быть объектом Path к существующему YAML файлу.\n# @POST: Файл обновлен согласно переданным конфигурациям или регулярному выражению.\ndef _update_yaml_file(file_path: Path, db_configs: list[dict[str, Any]], regexp_pattern: str | None, replace_string: str | None) -> None:\n with belief_scope(f\"Update YAML file: {file_path}\"):\n # Читаем содержимое файла\n try:\n with open(file_path, encoding='utf-8') as f:\n content = f.read()\n except Exception as e:\n app_logger.error(\"[_update_yaml_file][Failure] Failed to read %s: %s\", file_path, e)\n return\n # Если задан pattern и replace_string, применяем замену по регулярному выражению\n if regexp_pattern and replace_string:\n try:\n new_content = re.sub(regexp_pattern, replace_string, content)\n if new_content != content:\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(new_content)\n app_logger.info(\"[_update_yaml_file][State] Updated %s using regex pattern\", file_path)\n except Exception as e:\n app_logger.error(\"[_update_yaml_file][Failure] Error applying regex to %s: %s\", file_path, e)\n # Если заданы конфигурации, заменяем значения (поддержка old/new)\n if db_configs:\n try:\n # Прямой текстовый заменитель для старых/новых значений, чтобы сохранить структуру файла\n modified_content = content\n for cfg in db_configs:\n # Ожидаем структуру: {'old': {...}, 'new': {...}}\n old_cfg = cfg.get('old', {})\n new_cfg = cfg.get('new', {})\n for key, old_val in old_cfg.items():\n if key in new_cfg:\n new_val = new_cfg[key]\n # Заменяем только точные совпадения старого значения в тексте YAML, используя ключ для контекста\n if isinstance(old_val, str):\n # Ищем паттерн: key: \"value\" или key: value\n key_pattern = re.escape(key)\n val_pattern = re.escape(old_val)\n # Группы: 1=ключ+разделитель, 2=открывающая кавычка (опц), 3=значение, 4=закрывающая кавычка (опц)\n pattern = rf'({key_pattern}\\s*:\\s*)([\"\\']?)({val_pattern})([\"\\']?)'\n\n # #region replacer [TYPE Function]\n # @PURPOSE: Функция замены, сохраняющая кавычки если они были.\n # @PRE: match должен быть объектом совпадения регулярного выражения.\n # @POST: Возвращает строку с новым значением, сохраняя префикс и кавычки.\n def replacer(match):\n prefix = match.group(1)\n quote_open = match.group(2)\n quote_close = match.group(4)\n return f\"{prefix}{quote_open}{new_val}{quote_close}\"\n # #endregion replacer\n modified_content = re.sub(pattern, replacer, modified_content)\n app_logger.info(\"[_update_yaml_file][State] Replaced '%s' with '%s' for key %s in %s\", old_val, new_val, key, file_path)\n # Записываем обратно изменённый контент без парсинга YAML, сохраняем оригинальное форматирование\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(modified_content)\n except Exception as e:\n app_logger.error(\"[_update_yaml_file][Failure] Error performing raw replacement in %s: %s\", file_path, e)\n# #endregion _update_yaml_file\n# #region create_dashboard_export [TYPE Function]\n# @BRIEF Создает ZIP-архив из указанных исходных путей.\n# @PRE: source_paths должен содержать существующие пути.\n# @POST: ZIP-архив создан по пути zip_path.\ndef create_dashboard_export(zip_path: str | Path, source_paths: list[str | Path], exclude_extensions: list[str] | None = None) -> bool:\n with belief_scope(f\"Create dashboard export: {zip_path}\"):\n app_logger.info(\"[create_dashboard_export][Enter] Packing dashboard: %s -> %s\", source_paths, zip_path)\n try:\n exclude_ext = [ext.lower() for ext in exclude_extensions or []]\n with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n for src_path_str in source_paths:\n src_path = Path(src_path_str)\n assert src_path.exists(), f\"Путь не найден: {src_path}\"\n for item in src_path.rglob('*'):\n if item.is_file() and item.suffix.lower() not in exclude_ext:\n arcname = item.relative_to(src_path.parent)\n zipf.write(item, arcname)\n app_logger.reflect(f\"Archive created: {zip_path}\", extra={\"src\": \"create_dashboard_export\"})\n return True\n except (OSError, zipfile.BadZipFile, AssertionError) as e:\n app_logger.error(\"[create_dashboard_export][Failure] Error: %s\", e, exc_info=True)\n return False\n# #endregion create_dashboard_export\n# #region sanitize_filename [TYPE Function]\n# @BRIEF Очищает строку от символов, недопустимых в именах файлов.\n# @PRE: filename должен быть строкой.\n# @POST: Возвращает строку без спецсимволов.\ndef sanitize_filename(filename: str) -> str:\n with belief_scope(f\"Sanitize filename: {filename}\"):\n return re.sub(r'[\\\\/*?:\"<>|]', \"_\", filename).strip()\n# #endregion sanitize_filename\n# #region get_filename_from_headers [TYPE Function]\n# @BRIEF Извлекает имя файла из HTTP заголовка 'Content-Disposition'.\n# @PRE: headers должен быть словарем заголовков.\n# @POST: Возвращает имя файла или None, если заголовок отсутствует.\ndef get_filename_from_headers(headers: dict) -> str | None:\n with belief_scope(\"Get filename from headers\"):\n content_disposition = headers.get(\"Content-Disposition\", \"\")\n if match := re.search(r'filename=\"?([^\"]+)\"?', content_disposition):\n return match.group(1).strip()\n return None\n# #endregion get_filename_from_headers\n# #region consolidate_archive_folders [TYPE Function]\n# @BRIEF Консолидирует директории архивов на основе общего слага в имени.\n# @PRE: root_directory должен быть объектом Path к существующей директории.\n# @POST: Директории с одинаковым префиксом объединены в одну.\ndef consolidate_archive_folders(root_directory: Path) -> None:\n with belief_scope(f\"Consolidate archives in {root_directory}\"):\n assert isinstance(root_directory, Path), \"root_directory must be a Path object.\"\n assert root_directory.is_dir(), \"root_directory must be an existing directory.\"\n\n app_logger.info(\"[consolidate_archive_folders][Enter] Consolidating archives in %s\", root_directory)\n # Собираем все директории с архивами\n archive_dirs = []\n for item in root_directory.iterdir():\n if item.is_dir():\n # Проверяем, есть ли в директории ZIP-архивы\n if any(item.glob(\"*.zip\")):\n archive_dirs.append(item)\n # Группируем по слагу (части имени до первого '_')\n slug_groups = {}\n for dir_path in archive_dirs:\n dir_name = dir_path.name\n slug = dir_name.split('_')[0] if '_' in dir_name else dir_name\n if slug not in slug_groups:\n slug_groups[slug] = []\n slug_groups[slug].append(dir_path)\n # Для каждой группы консолидируем\n for slug, dirs in slug_groups.items():\n if len(dirs) <= 1:\n continue\n # Создаем целевую директорию\n target_dir = root_directory / slug\n target_dir.mkdir(exist_ok=True)\n app_logger.info(\"[consolidate_archive_folders][State] Consolidating %d directories under %s\", len(dirs), target_dir)\n # Перемещаем содержимое\n for source_dir in dirs:\n if source_dir == target_dir:\n continue\n for item in source_dir.iterdir():\n dest_item = target_dir / item.name\n try:\n if item.is_dir():\n shutil.move(str(item), str(dest_item))\n else:\n shutil.move(str(item), str(dest_item))\n except Exception as e:\n app_logger.error(\"[consolidate_archive_folders][Failure] Failed to move %s to %s: %s\", item, dest_item, e)\n # Удаляем исходную директорию\n try:\n source_dir.rmdir()\n app_logger.info(\"[consolidate_archive_folders][State] Removed source directory: %s\", source_dir)\n except Exception as e:\n app_logger.error(\"[consolidate_archive_folders][Failure] Failed to remove source directory %s: %s\", source_dir, e)\n# #endregion consolidate_archive_folders\n# #endregion FileIO\n" }, { "contract_id": "InvalidZipFormatError", "contract_type": "Class", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 28, - "end_line": 32, + "start_line": 25, + "end_line": 29, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -50980,33 +40751,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:InvalidZipFormatError:Class]\n# @PURPOSE: Exception raised when a file is not a valid ZIP archive.\nclass InvalidZipFormatError(Exception):\n pass\n# [/DEF:InvalidZipFormatError:Class]\n" + "anchor_syntax": "region", + "body": "# #region InvalidZipFormatError [TYPE Class]\n# @BRIEF Exception raised when a file is not a valid ZIP archive.\nclass InvalidZipFormatError(Exception):\n pass\n# #endregion InvalidZipFormatError\n" }, { "contract_id": "create_temp_file", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 34, - "end_line": 73, + "start_line": 30, + "end_line": 65, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "mode (str) - Режим записи в файл (e.g., 'wb').", "POST": "Временный ресурс создан и путь к нему возвращен; ресурс удален после выхода из контекста.", "PRE": "suffix должен быть строкой, определяющей тип ресурса.", "PURPOSE": "Контекстный менеджер для создания временного файла или директории с гарантированным удалением.", - "THROW": "IOError - При ошибках создания ресурса.", "YIELDS": "Path - Путь к временному ресурсу." }, "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", @@ -51034,12 +40797,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "unknown_tag", "tag": "YIELDS", @@ -51047,32 +40804,24 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:create_temp_file:Function]\n# @PURPOSE: Контекстный менеджер для создания временного файла или директории с гарантированным удалением.\n# @PRE: suffix должен быть строкой, определяющей тип ресурса.\n# @POST: Временный ресурс создан и путь к нему возвращен; ресурс удален после выхода из контекста.\n# @PARAM: content (Optional[bytes]) - Бинарное содержимое для записи во временный файл.\n# @PARAM: suffix (str) - Суффикс ресурса. Если `.dir`, создается директория.\n# @PARAM: mode (str) - Режим записи в файл (e.g., 'wb').\n# @YIELDS: Path - Путь к временному ресурсу.\n# @THROW: IOError - При ошибках создания ресурса.\n@contextmanager\ndef create_temp_file(content: Optional[bytes] = None, suffix: str = \".zip\", mode: str = 'wb', dry_run = False) -> Generator[Path, None, None]:\n with belief_scope(\"Create temporary resource\"):\n resource_path = None\n is_dir = suffix.startswith('.dir')\n try:\n if is_dir:\n with tempfile.TemporaryDirectory(suffix=suffix) as temp_dir:\n resource_path = Path(temp_dir)\n log.reason(\"Created temporary directory\", payload={\"path\": str(resource_path)})\n yield resource_path\n else:\n fd, temp_path_str = tempfile.mkstemp(suffix=suffix)\n resource_path = Path(temp_path_str)\n os.close(fd)\n if content:\n resource_path.write_bytes(content)\n log.reason(\"Created temporary file\", payload={\"path\": str(resource_path)})\n yield resource_path\n finally:\n if resource_path and resource_path.exists() and not dry_run:\n try:\n if resource_path.is_dir():\n shutil.rmtree(resource_path)\n log.reflect(\"Cleaned up temporary directory\", payload={\"path\": str(resource_path)})\n else:\n resource_path.unlink()\n log.reflect(\"Cleaned up temporary file\", payload={\"path\": str(resource_path)})\n except OSError as e:\n log.explore(\"Error during temp resource cleanup\", payload={\"path\": str(resource_path)}, error=str(e))\n# [/DEF:create_temp_file:Function]\n" + "anchor_syntax": "region", + "body": "# #region create_temp_file [TYPE Function]\n# @BRIEF Контекстный менеджер для создания временного файла или директории с гарантированным удалением.\n# @PRE: suffix должен быть строкой, определяющей тип ресурса.\n# @POST: Временный ресурс создан и путь к нему возвращен; ресурс удален после выхода из контекста.\n# @YIELDS: Path - Путь к временному ресурсу.\n@contextmanager\ndef create_temp_file(content: bytes | None = None, suffix: str = \".zip\", mode: str = 'wb', dry_run = False) -> Generator[Path]:\n with belief_scope(\"Create temporary resource\"):\n resource_path = None\n is_dir = suffix.startswith('.dir')\n try:\n if is_dir:\n with tempfile.TemporaryDirectory(suffix=suffix) as temp_dir:\n resource_path = Path(temp_dir)\n app_logger.debug(\"[create_temp_file][State] Created temporary directory: %s\", resource_path)\n yield resource_path\n else:\n fd, temp_path_str = tempfile.mkstemp(suffix=suffix)\n resource_path = Path(temp_path_str)\n os.close(fd)\n if content:\n resource_path.write_bytes(content)\n app_logger.debug(\"[create_temp_file][State] Created temporary file: %s\", resource_path)\n yield resource_path\n finally:\n if resource_path and resource_path.exists() and not dry_run:\n try:\n if resource_path.is_dir():\n shutil.rmtree(resource_path)\n app_logger.debug(\"[create_temp_file][Cleanup] Removed temporary directory: %s\", resource_path)\n else:\n resource_path.unlink()\n app_logger.debug(\"[create_temp_file][Cleanup] Removed temporary file: %s\", resource_path)\n except OSError as e:\n app_logger.error(\"[create_temp_file][Failure] Error during cleanup of %s: %s\", resource_path, e)\n# #endregion create_temp_file\n" }, { "contract_id": "remove_empty_directories", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 75, - "end_line": 97, + "start_line": 66, + "end_line": 87, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "root_dir (str) - Путь к корневой директории для очистки.", "POST": "Все пустые поддиректории удалены, возвращено их количество.", "PRE": "root_dir должен быть путем к существующей директории.", - "PURPOSE": "Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути.", - "RETURN": "int - Количество удаленных директорий." + "PURPOSE": "Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути." }, "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", @@ -51099,41 +40848,26 @@ "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": "def", - "body": "# [DEF:remove_empty_directories:Function]\n# @PURPOSE: Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути.\n# @PRE: root_dir должен быть путем к существующей директории.\n# @POST: Все пустые поддиректории удалены, возвращено их количество.\n# @PARAM: root_dir (str) - Путь к корневой директории для очистки.\n# @RETURN: int - Количество удаленных директорий.\ndef remove_empty_directories(root_dir: str) -> int:\n with belief_scope(f\"Remove empty directories in {root_dir}\"):\n removed_count = 0\n if not os.path.isdir(root_dir):\n log.explore(\"Empty directory cleanup skipped - directory not found\", payload={\"root_dir\": root_dir}, error=f\"Directory not found: {root_dir}\")\n return 0\n for current_dir, _, _ in os.walk(root_dir, topdown=False):\n if not os.listdir(current_dir):\n try:\n os.rmdir(current_dir)\n removed_count += 1\n log.reason(\"Removed empty directory\", payload={\"directory\": current_dir})\n except OSError as e:\n log.explore(\"Failed to remove empty directory\", payload={\"directory\": current_dir}, error=str(e))\n log.reflect(\"Empty directory cleanup complete\", payload={\"removed_count\": removed_count})\n return removed_count\n# [/DEF:remove_empty_directories:Function]\n" + "anchor_syntax": "region", + "body": "# #region remove_empty_directories [TYPE Function]\n# @BRIEF Рекурсивно удаляет все пустые поддиректории, начиная с указанного пути.\n# @PRE: root_dir должен быть путем к существующей директории.\n# @POST: Все пустые поддиректории удалены, возвращено их количество.\ndef remove_empty_directories(root_dir: str) -> int:\n with belief_scope(f\"Remove empty directories in {root_dir}\"):\n app_logger.info(\"[remove_empty_directories][Enter] Starting cleanup of empty directories in %s\", root_dir)\n removed_count = 0\n if not os.path.isdir(root_dir):\n app_logger.error(\"[remove_empty_directories][Failure] Directory not found: %s\", root_dir)\n return 0\n for current_dir, _, _ in os.walk(root_dir, topdown=False):\n if not os.listdir(current_dir):\n try:\n os.rmdir(current_dir)\n removed_count += 1\n app_logger.info(\"[remove_empty_directories][State] Removed empty directory: %s\", current_dir)\n except OSError as e:\n app_logger.error(\"[remove_empty_directories][Failure] Failed to remove %s: %s\", current_dir, e)\n app_logger.reflect(f\"Removed {removed_count} empty directories.\", extra={\"src\": \"remove_empty_directories\"})\n return removed_count\n# #endregion remove_empty_directories\n" }, { "contract_id": "read_dashboard_from_disk", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 99, - "end_line": 115, + "start_line": 88, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "file_path (str) - Путь к файлу.", "POST": "Возвращает байты содержимого и имя файла.", "PRE": "file_path должен указывать на существующий файл.", - "PURPOSE": "Читает бинарное содержимое файла с диска.", - "RETURN": "Tuple[bytes, str] - Кортеж (содержимое, имя файла).", - "THROW": "FileNotFoundError - Если файл не найден." + "PURPOSE": "Читает бинарное содержимое файла с диска." }, "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", @@ -51160,47 +40894,26 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:read_dashboard_from_disk:Function]\n# @PURPOSE: Читает бинарное содержимое файла с диска.\n# @PRE: file_path должен указывать на существующий файл.\n# @POST: Возвращает байты содержимого и имя файла.\n# @PARAM: file_path (str) - Путь к файлу.\n# @RETURN: Tuple[bytes, str] - Кортеж (содержимое, имя файла).\n# @THROW: FileNotFoundError - Если файл не найден.\ndef read_dashboard_from_disk(file_path: str) -> Tuple[bytes, str]:\n with belief_scope(f\"Read dashboard from {file_path}\"):\n path = Path(file_path)\n assert path.is_file(), f\"Файл дашборда не найден: {file_path}\"\n log.reason(\"Reading dashboard file from disk\", payload={\"file_path\": file_path})\n content = path.read_bytes()\n if not content:\n log.explore(\"Dashboard file is empty\", payload={\"file_path\": file_path}, error=\"Empty dashboard file\")\n return content, path.name\n# [/DEF:read_dashboard_from_disk:Function]\n" + "anchor_syntax": "region", + "body": "# #region read_dashboard_from_disk [TYPE Function]\n# @BRIEF Читает бинарное содержимое файла с диска.\n# @PRE: file_path должен указывать на существующий файл.\n# @POST: Возвращает байты содержимого и имя файла.\ndef read_dashboard_from_disk(file_path: str) -> tuple[bytes, str]:\n with belief_scope(f\"Read dashboard from {file_path}\"):\n path = Path(file_path)\n assert path.is_file(), f\"Файл дашборда не найден: {file_path}\"\n app_logger.info(\"[read_dashboard_from_disk][Enter] Reading file: %s\", file_path)\n content = path.read_bytes()\n if not content:\n app_logger.warning(\"[read_dashboard_from_disk][Warning] File is empty: %s\", file_path)\n return content, path.name\n# #endregion read_dashboard_from_disk\n" }, { "contract_id": "calculate_crc32", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 117, - "end_line": 129, + "start_line": 102, + "end_line": 110, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "file_path (Path) - Путь к файлу.", "POST": "Возвращает 8-значную hex-строку CRC32.", "PRE": "file_path должен быть объектом Path к существующему файлу.", - "PURPOSE": "Вычисляет контрольную сумму CRC32 для файла.", - "RETURN": "str - 8-значное шестнадцатеричное представление CRC32.", - "THROW": "IOError - При ошибках чтения файла." + "PURPOSE": "Вычисляет контрольную сумму CRC32 для файла." }, "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", @@ -51227,29 +40940,17 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:calculate_crc32:Function]\n# @PURPOSE: Вычисляет контрольную сумму CRC32 для файла.\n# @PRE: file_path должен быть объектом Path к существующему файлу.\n# @POST: Возвращает 8-значную hex-строку CRC32.\n# @PARAM: file_path (Path) - Путь к файлу.\n# @RETURN: str - 8-значное шестнадцатеричное представление CRC32.\n# @THROW: IOError - При ошибках чтения файла.\ndef calculate_crc32(file_path: Path) -> str:\n with belief_scope(f\"Calculate CRC32 for {file_path}\"):\n with open(file_path, 'rb') as f:\n crc32_value = zlib.crc32(f.read())\n return f\"{crc32_value:08x}\"\n# [/DEF:calculate_crc32:Function]\n" + "anchor_syntax": "region", + "body": "# #region calculate_crc32 [TYPE Function]\n# @BRIEF Вычисляет контрольную сумму CRC32 для файла.\n# @PRE: file_path должен быть объектом Path к существующему файлу.\n# @POST: Возвращает 8-значную hex-строку CRC32.\ndef calculate_crc32(file_path: Path) -> str:\n with belief_scope(f\"Calculate CRC32 for {file_path}\"), open(file_path, 'rb') as f:\n crc32_value = zlib.crc32(f.read())\n return f\"{crc32_value:08x}\"\n# #endregion calculate_crc32\n" }, { "contract_id": "RetentionPolicy", "contract_type": "DataClass", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 132, - "end_line": 139, + "start_line": 111, + "end_line": 118, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -51285,19 +40986,18 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RetentionPolicy:DataClass]\n# @PURPOSE: Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные).\n@dataclass\nclass RetentionPolicy:\n daily: int = 7\n weekly: int = 4\n monthly: int = 12\n# [/DEF:RetentionPolicy:DataClass]\n" + "anchor_syntax": "region", + "body": "# #region RetentionPolicy [TYPE DataClass]\n# @BRIEF Определяет политику хранения для архивов (ежедневные, еженедельные, ежемесячные).\n@dataclass\nclass RetentionPolicy:\n daily: int = 7\n weekly: int = 4\n monthly: int = 12\n# #endregion RetentionPolicy\n" }, { "contract_id": "archive_exports", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 142, - "end_line": 223, + "start_line": 119, + "end_line": 193, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "deduplicate (bool) - Флаг для включения удаления дубликатов по CRC32.", "POST": "Старые или дублирующиеся архивы удалены согласно политике.", "PRE": "output_dir должен быть путем к существующей директории.", "PURPOSE": "Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию." @@ -51317,12 +41017,6 @@ } ], "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", @@ -51360,32 +41054,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:archive_exports:Function]\n# @PURPOSE: Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию.\n# @PRE: output_dir должен быть путем к существующей директории.\n# @POST: Старые или дублирующиеся архивы удалены согласно политике.\n# @RELATION: CALLS -> apply_retention_policy\n# @RELATION: CALLS -> calculate_crc32\n# @PARAM: output_dir (str) - Директория с архивами.\n# @PARAM: policy (RetentionPolicy) - Политика хранения.\n# @PARAM: deduplicate (bool) - Флаг для включения удаления дубликатов по CRC32.\ndef archive_exports(output_dir: str, policy: RetentionPolicy, deduplicate: bool = False) -> None:\n with belief_scope(f\"Archive exports in {output_dir}\"):\n output_path = Path(output_dir)\n if not output_path.is_dir():\n log.explore(\"Archive directory not found\", payload={\"output_dir\": output_dir}, error=f\"Archive directory not found: {output_dir}\")\n return\n\n log.reason(\"Managing archive exports\", payload={\"output_dir\": output_dir})\n \n # 1. Collect all zip files\n zip_files = list(output_path.glob(\"*.zip\"))\n if not zip_files:\n log.reason(\"No zip files found in archive directory\", payload={\"output_dir\": output_dir})\n return\n\n # 2. Deduplication\n if deduplicate:\n log.reason(\"Starting archive deduplication\")\n checksums = {}\n files_to_remove = []\n \n # Sort by modification time (newest first) to keep the latest version\n zip_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)\n \n for file_path in zip_files:\n try:\n crc = calculate_crc32(file_path)\n if crc in checksums:\n files_to_remove.append(file_path)\n log.reason(\"Duplicate archive found (same checksum)\", payload={\"file\": file_path.name, \"original\": checksums[crc].name})\n else:\n checksums[crc] = file_path\n except Exception as e:\n log.explore(\"Failed to calculate CRC32 for archive\", payload={\"file\": str(file_path)}, error=str(e))\n \n for f in files_to_remove:\n try:\n f.unlink()\n zip_files.remove(f)\n log.reason(\"Removed duplicate archive\", payload={\"file\": f.name})\n except OSError as e:\n log.explore(\"Failed to remove duplicate archive\", payload={\"file\": str(f)}, error=str(e))\n\n # 3. Retention Policy\n files_with_dates = []\n for file_path in zip_files:\n # Try to extract date from filename\n # Pattern: ..._YYYYMMDD_HHMMSS.zip or ..._YYYYMMDD.zip\n match = re.search(r'_(\\d{8})_', file_path.name)\n file_date = None\n if match:\n try:\n date_str = match.group(1)\n file_date = datetime.strptime(date_str, \"%Y%m%d\").date()\n except ValueError:\n pass\n \n if not file_date:\n # Fallback to modification time\n file_date = datetime.fromtimestamp(file_path.stat().st_mtime).date()\n \n files_with_dates.append((file_path, file_date))\n\n files_to_keep = apply_retention_policy(files_with_dates, policy)\n \n for file_path, _ in files_with_dates:\n if file_path not in files_to_keep:\n try:\n file_path.unlink()\n log.reason(\"Removed archive by retention policy\", payload={\"file\": file_path.name})\n except OSError as e:\n log.explore(\"Failed to remove archive by retention policy\", payload={\"file\": str(file_path)}, error=str(e))\n# [/DEF:archive_exports:Function]\n" + "anchor_syntax": "region", + "body": "# #region archive_exports [TYPE Function]\n# @BRIEF Управляет архивом экспортированных файлов, применяя политику хранения и дедупликацию.\n# @PRE: output_dir должен быть путем к существующей директории.\n# @POST: Старые или дублирующиеся архивы удалены согласно политике.\n# @RELATION CALLS -> apply_retention_policy\n# @RELATION CALLS -> calculate_crc32\ndef archive_exports(output_dir: str, policy: RetentionPolicy, deduplicate: bool = False) -> None:\n with belief_scope(f\"Archive exports in {output_dir}\"):\n output_path = Path(output_dir)\n if not output_path.is_dir():\n app_logger.warning(\"[archive_exports][Skip] Archive directory not found: %s\", output_dir)\n return\n app_logger.info(\"[archive_exports][Enter] Managing archive in %s\", output_dir)\n\n # 1. Collect all zip files\n zip_files = list(output_path.glob(\"*.zip\"))\n if not zip_files:\n app_logger.info(\"[archive_exports][State] No zip files found in %s\", output_dir)\n return\n # 2. Deduplication\n if deduplicate:\n app_logger.info(\"[archive_exports][State] Starting deduplication...\")\n checksums = {}\n files_to_remove = []\n\n # Sort by modification time (newest first) to keep the latest version\n zip_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)\n\n for file_path in zip_files:\n try:\n crc = calculate_crc32(file_path)\n if crc in checksums:\n files_to_remove.append(file_path)\n app_logger.debug(\"[archive_exports][State] Duplicate found: %s (same as %s)\", file_path.name, checksums[crc].name)\n else:\n checksums[crc] = file_path\n except Exception as e:\n app_logger.error(\"[archive_exports][Failure] Failed to calculate CRC32 for %s: %s\", file_path, e)\n\n for f in files_to_remove:\n try:\n f.unlink()\n zip_files.remove(f)\n app_logger.info(\"[archive_exports][State] Removed duplicate: %s\", f.name)\n except OSError as e:\n app_logger.error(\"[archive_exports][Failure] Failed to remove duplicate %s: %s\", f, e)\n # 3. Retention Policy\n files_with_dates = []\n for file_path in zip_files:\n # Try to extract date from filename\n # Pattern: ..._YYYYMMDD_HHMMSS.zip or ..._YYYYMMDD.zip\n match = re.search(r'_(\\d{8})_', file_path.name)\n file_date = None\n if match:\n try:\n date_str = match.group(1)\n file_date = datetime.strptime(date_str, \"%Y%m%d\").date()\n except ValueError:\n pass\n\n if not file_date:\n # Fallback to modification time\n file_date = datetime.fromtimestamp(file_path.stat().st_mtime).date()\n\n files_with_dates.append((file_path, file_date))\n files_to_keep = apply_retention_policy(files_with_dates, policy)\n\n for file_path, _ in files_with_dates:\n if file_path not in files_to_keep:\n try:\n file_path.unlink()\n app_logger.info(\"[archive_exports][State] Removed by retention policy: %s\", file_path.name)\n except OSError as e:\n app_logger.error(\"[archive_exports][Failure] Failed to remove %s: %s\", file_path, e)\n# #endregion archive_exports\n" }, { "contract_id": "apply_retention_policy", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 225, - "end_line": 258, + "start_line": 194, + "end_line": 224, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "policy (RetentionPolicy) - Политика хранения.", "POST": "Returns a set of files to keep.", "PRE": "files_with_dates is a list of (Path, date) tuples.", - "PURPOSE": "(Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить.", - "RETURN": "set - Множество путей к файлам, которые должны быть сохранены." + "PURPOSE": "(Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить." }, "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", @@ -51412,41 +41098,26 @@ "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": "def", - "body": "# [DEF:apply_retention_policy:Function]\n# @PURPOSE: (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить.\n# @PRE: files_with_dates is a list of (Path, date) tuples.\n# @POST: Returns a set of files to keep.\n# @PARAM: files_with_dates (List[Tuple[Path, date]]) - Список файлов с датами.\n# @PARAM: policy (RetentionPolicy) - Политика хранения.\n# @RETURN: set - Множество путей к файлам, которые должны быть сохранены.\ndef apply_retention_policy(files_with_dates: List[Tuple[Path, date]], policy: RetentionPolicy) -> set:\n with belief_scope(\"Apply retention policy\"):\n # Сортируем по дате (от новой к старой)\n sorted_files = sorted(files_with_dates, key=lambda x: x[1], reverse=True) \n # Словарь для хранения файлов по категориям \n daily_files = [] \n weekly_files = [] \n monthly_files = [] \n today = date.today() \n for file_path, file_date in sorted_files: \n # Ежедневные \n if (today - file_date).days < policy.daily: \n daily_files.append(file_path) \n # Еженедельные \n elif (today - file_date).days < policy.weekly * 7: \n weekly_files.append(file_path) \n # Ежемесячные \n elif (today - file_date).days < policy.monthly * 30: \n monthly_files.append(file_path) \n # Возвращаем множество файлов, которые нужно сохранить \n files_to_keep = set() \n files_to_keep.update(daily_files) \n files_to_keep.update(weekly_files[:policy.weekly]) \n files_to_keep.update(monthly_files[:policy.monthly]) \n log.reflect(\"Retention policy applied\", payload={\"files_to_keep\": len(files_to_keep)})\n return files_to_keep\n# [/DEF:apply_retention_policy:Function]\n" + "anchor_syntax": "region", + "body": "# #region apply_retention_policy [TYPE Function]\n# @BRIEF (Helper) Применяет политику хранения к списку файлов, возвращая те, что нужно сохранить.\n# @PRE: files_with_dates is a list of (Path, date) tuples.\n# @POST: Returns a set of files to keep.\ndef apply_retention_policy(files_with_dates: list[tuple[Path, date]], policy: RetentionPolicy) -> set:\n with belief_scope(\"Apply retention policy\"):\n # Сортируем по дате (от новой к старой)\n sorted_files = sorted(files_with_dates, key=lambda x: x[1], reverse=True)\n # Словарь для хранения файлов по категориям\n daily_files = []\n weekly_files = []\n monthly_files = []\n today = date.today()\n for file_path, file_date in sorted_files:\n # Ежедневные\n if (today - file_date).days < policy.daily:\n daily_files.append(file_path)\n # Еженедельные\n elif (today - file_date).days < policy.weekly * 7:\n weekly_files.append(file_path)\n # Ежемесячные\n elif (today - file_date).days < policy.monthly * 30:\n monthly_files.append(file_path)\n # Возвращаем множество файлов, которые нужно сохранить\n files_to_keep = set()\n files_to_keep.update(daily_files)\n files_to_keep.update(weekly_files[:policy.weekly])\n files_to_keep.update(monthly_files[:policy.monthly])\n app_logger.debug(\"[apply_retention_policy][State] Keeping %d files according to retention policy\", len(files_to_keep))\n return files_to_keep\n# #endregion apply_retention_policy\n" }, { "contract_id": "save_and_unpack_dashboard", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 260, - "end_line": 289, + "start_line": 225, + "end_line": 248, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "original_filename (Optional[str]) - Исходное имя файла для сохранения.", "POST": "ZIP-файл сохранен, и если unpack=True, он распакован в output_dir.", "PRE": "zip_content должен быть байтами валидного ZIP-архива.", - "PURPOSE": "Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.", - "RETURN": "Tuple[Path, Optional[Path]] - Путь к ZIP-файлу и, если применимо, путь к директории с распаковкой.", - "THROW": "InvalidZipFormatError - При ошибке формата ZIP." + "PURPOSE": "Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его." }, "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", @@ -51473,37 +41144,23 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:save_and_unpack_dashboard:Function]\n# @PURPOSE: Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.\n# @PRE: zip_content должен быть байтами валидного ZIP-архива.\n# @POST: ZIP-файл сохранен, и если unpack=True, он распакован в output_dir.\n# @PARAM: zip_content (bytes) - Содержимое ZIP-архива.\n# @PARAM: output_dir (Union[str, Path]) - Директория для сохранения.\n# @PARAM: unpack (bool) - Флаг, нужно ли распаковывать архив.\n# @PARAM: original_filename (Optional[str]) - Исходное имя файла для сохранения.\n# @RETURN: Tuple[Path, Optional[Path]] - Путь к ZIP-файлу и, если применимо, путь к директории с распаковкой.\n# @THROW: InvalidZipFormatError - При ошибке формата ZIP.\ndef save_and_unpack_dashboard(zip_content: bytes, output_dir: Union[str, Path], unpack: bool = False, original_filename: Optional[str] = None) -> Tuple[Path, Optional[Path]]:\n with belief_scope(\"Save and unpack dashboard\"):\n log.reason(\"Processing dashboard ZIP save\", payload={\"unpack\": unpack})\n try:\n output_path = Path(output_dir)\n output_path.mkdir(parents=True, exist_ok=True)\n zip_name = sanitize_filename(original_filename) if original_filename else f\"dashboard_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip\"\n zip_path = output_path / zip_name\n zip_path.write_bytes(zip_content)\n log.reason(\"Dashboard ZIP saved to disk\", payload={\"zip_path\": str(zip_path)})\n if unpack:\n with zipfile.ZipFile(zip_path, 'r') as zip_ref:\n zip_ref.extractall(output_path)\n log.reason(\"Dashboard unpacked to directory\", payload={\"output_path\": str(output_path)})\n return zip_path, output_path\n return zip_path, None\n except zipfile.BadZipFile as e:\n log.explore(\"Invalid ZIP archive format\", payload={\"output_dir\": str(output_dir)}, error=str(e))\n raise InvalidZipFormatError(f\"Invalid ZIP file: {e}\") from e\n# [/DEF:save_and_unpack_dashboard:Function]\n" + "anchor_syntax": "region", + "body": "# #region save_and_unpack_dashboard [TYPE Function]\n# @BRIEF Сохраняет бинарное содержимое ZIP-архива на диск и опционально распаковывает его.\n# @PRE: zip_content должен быть байтами валидного ZIP-архива.\n# @POST: ZIP-файл сохранен, и если unpack=True, он распакован в output_dir.\ndef save_and_unpack_dashboard(zip_content: bytes, output_dir: str | Path, unpack: bool = False, original_filename: str | None = None) -> tuple[Path, Path | None]:\n with belief_scope(\"Save and unpack dashboard\"):\n app_logger.info(\"[save_and_unpack_dashboard][Enter] Processing dashboard. Unpack: %s\", unpack)\n try:\n output_path = Path(output_dir)\n output_path.mkdir(parents=True, exist_ok=True)\n zip_name = sanitize_filename(original_filename) if original_filename else f\"dashboard_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip\"\n zip_path = output_path / zip_name\n zip_path.write_bytes(zip_content)\n app_logger.info(\"[save_and_unpack_dashboard][State] Dashboard saved to: %s\", zip_path)\n if unpack:\n with zipfile.ZipFile(zip_path, 'r') as zip_ref:\n zip_ref.extractall(output_path)\n app_logger.info(\"[save_and_unpack_dashboard][State] Dashboard unpacked to: %s\", output_path)\n return zip_path, output_path\n return zip_path, None\n except zipfile.BadZipFile as e:\n app_logger.error(\"[save_and_unpack_dashboard][Failure] Invalid ZIP archive: %s\", e)\n raise InvalidZipFormatError(f\"Invalid ZIP file: {e}\") from e\n# #endregion save_and_unpack_dashboard\n" }, { "contract_id": "update_yamls", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 291, - "end_line": 311, + "start_line": 249, + "end_line": 264, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "replace_string (Optional[LiteralString]) - Строка для замены.", "POST": "Все YAML файлы в директории обновлены согласно переданным параметрам.", "PRE": "path должен быть существующей директорией.", - "PURPOSE": "Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex.", - "THROW": "FileNotFoundError - Если `path` не существует." + "PURPOSE": "Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex." }, "relations": [ { @@ -51514,12 +41171,6 @@ } ], "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", @@ -51547,12 +41198,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", @@ -51563,31 +41208,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:update_yamls:Function]\n# @PURPOSE: Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex.\n# @PRE: path должен быть существующей директорией.\n# @POST: Все YAML файлы в директории обновлены согласно переданным параметрам.\n# @RELATION: CALLS -> _update_yaml_file\n# @THROW: FileNotFoundError - Если `path` не существует.\n# @PARAM: db_configs (Optional[List[Dict]]) - Список конфигураций для замены.\n# @PARAM: path (str) - Путь к директории с YAML файлами.\n# @PARAM: regexp_pattern (Optional[LiteralString]) - Паттерн для поиска.\n# @PARAM: replace_string (Optional[LiteralString]) - Строка для замены.\ndef update_yamls(db_configs: Optional[List[Dict[str, Any]]] = None, path: str = \"dashboards\", regexp_pattern: Optional[LiteralString] = None, replace_string: Optional[LiteralString] = None) -> None:\n with belief_scope(\"Update YAML configurations\"):\n log.reason(\"Starting YAML configuration update\", payload={\"path\": path})\n dir_path = Path(path)\n assert dir_path.is_dir(), f\"Путь {path} не существует или не является директорией\"\n \n configs: List[Dict[str, Any]] = db_configs or []\n \n for file_path in dir_path.rglob(\"*.yaml\"):\n _update_yaml_file(file_path, configs, regexp_pattern, replace_string)\n# [/DEF:update_yamls:Function]\n" + "anchor_syntax": "region", + "body": "# #region update_yamls [TYPE Function]\n# @BRIEF Обновляет конфигурации в YAML-файлах, заменяя значения или применяя regex.\n# @PRE: path должен быть существующей директорией.\n# @POST: Все YAML файлы в директории обновлены согласно переданным параметрам.\n# @RELATION CALLS -> _update_yaml_file\ndef update_yamls(db_configs: list[dict[str, Any]] | None = None, path: str = \"dashboards\", regexp_pattern: LiteralString | None = None, replace_string: LiteralString | None = None) -> None:\n with belief_scope(\"Update YAML configurations\"):\n app_logger.info(\"[update_yamls][Enter] Starting YAML configuration update.\")\n dir_path = Path(path)\n assert dir_path.is_dir(), f\"Путь {path} не существует или не является директорией\"\n\n configs: list[dict[str, Any]] = db_configs or []\n\n for file_path in dir_path.rglob(\"*.yaml\"):\n _update_yaml_file(file_path, configs, regexp_pattern, replace_string)\n# #endregion update_yamls\n" }, { "contract_id": "_update_yaml_file", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 313, - "end_line": 378, + "start_line": 265, + "end_line": 325, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "replace_string (Optional[str]) - Замена.", "POST": "Файл обновлен согласно переданным конфигурациям или регулярному выражению.", "PRE": "file_path должен быть объектом Path к существующему YAML файлу.", "PURPOSE": "(Helper) Обновляет один YAML файл." }, "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", @@ -51616,15 +41254,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_update_yaml_file:Function]\n# @PURPOSE: (Helper) Обновляет один YAML файл.\n# @PRE: file_path должен быть объектом Path к существующему YAML файлу.\n# @POST: Файл обновлен согласно переданным конфигурациям или регулярному выражению.\n# @PARAM: file_path (Path) - Путь к файлу.\n# @PARAM: db_configs (List[Dict]) - Конфигурации.\n# @PARAM: regexp_pattern (Optional[str]) - Паттерн.\n# @PARAM: replace_string (Optional[str]) - Замена.\ndef _update_yaml_file(file_path: Path, db_configs: List[Dict[str, Any]], regexp_pattern: Optional[str], replace_string: Optional[str]) -> None:\n with belief_scope(f\"Update YAML file: {file_path}\"):\n # Читаем содержимое файла\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n except Exception as e:\n log.explore(\"Failed to read YAML file\", payload={\"file_path\": str(file_path)}, error=str(e))\n return\n # Если задан pattern и replace_string, применяем замену по регулярному выражению\n if regexp_pattern and replace_string:\n try:\n new_content = re.sub(regexp_pattern, replace_string, content)\n if new_content != content:\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(new_content)\n log.reason(\"Updated YAML file using regex pattern\", payload={\"file_path\": str(file_path)})\n except Exception as e:\n log.explore(\"Error applying regex to YAML file\", payload={\"file_path\": str(file_path)}, error=str(e))\n # Если заданы конфигурации, заменяем значения (поддержка old/new)\n if db_configs:\n try:\n # Прямой текстовый заменитель для старых/новых значений, чтобы сохранить структуру файла\n modified_content = content\n for cfg in db_configs:\n # Ожидаем структуру: {'old': {...}, 'new': {...}}\n old_cfg = cfg.get('old', {})\n new_cfg = cfg.get('new', {})\n for key, old_val in old_cfg.items():\n if key in new_cfg:\n new_val = new_cfg[key]\n # Заменяем только точные совпадения старого значения в тексте YAML, используя ключ для контекста\n if isinstance(old_val, str):\n # Ищем паттерн: key: \"value\" или key: value\n key_pattern = re.escape(key)\n val_pattern = re.escape(old_val)\n # Группы: 1=ключ+разделитель, 2=открывающая кавычка (опц), 3=значение, 4=закрывающая кавычка (опц)\n pattern = rf'({key_pattern}\\s*:\\s*)([\"\\']?)({val_pattern})([\"\\']?)'\n \n # [DEF:replacer:Function]\n # @PURPOSE: Функция замены, сохраняющая кавычки если они были.\n # @PRE: match должен быть объектом совпадения регулярного выражения.\n # @POST: Возвращает строку с новым значением, сохраняя префикс и кавычки.\n def replacer(match):\n prefix = match.group(1)\n quote_open = match.group(2)\n quote_close = match.group(4)\n return f\"{prefix}{quote_open}{new_val}{quote_close}\"\n # [/DEF:replacer:Function]\n\n modified_content = re.sub(pattern, replacer, modified_content)\n log.reason(\"Replaced YAML config value\", payload={\"key\": key, \"file_path\": str(file_path)})\n # Записываем обратно изменённый контент без парсинга YAML, сохраняем оригинальное форматирование\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(modified_content)\n except Exception as e:\n log.explore(\"Error performing raw replacement in YAML\", payload={\"file_path\": str(file_path)}, error=str(e))\n# [/DEF:_update_yaml_file:Function]\n" + "anchor_syntax": "region", + "body": "# #region _update_yaml_file [TYPE Function]\n# @BRIEF (Helper) Обновляет один YAML файл.\n# @PRE: file_path должен быть объектом Path к существующему YAML файлу.\n# @POST: Файл обновлен согласно переданным конфигурациям или регулярному выражению.\ndef _update_yaml_file(file_path: Path, db_configs: list[dict[str, Any]], regexp_pattern: str | None, replace_string: str | None) -> None:\n with belief_scope(f\"Update YAML file: {file_path}\"):\n # Читаем содержимое файла\n try:\n with open(file_path, encoding='utf-8') as f:\n content = f.read()\n except Exception as e:\n app_logger.error(\"[_update_yaml_file][Failure] Failed to read %s: %s\", file_path, e)\n return\n # Если задан pattern и replace_string, применяем замену по регулярному выражению\n if regexp_pattern and replace_string:\n try:\n new_content = re.sub(regexp_pattern, replace_string, content)\n if new_content != content:\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(new_content)\n app_logger.info(\"[_update_yaml_file][State] Updated %s using regex pattern\", file_path)\n except Exception as e:\n app_logger.error(\"[_update_yaml_file][Failure] Error applying regex to %s: %s\", file_path, e)\n # Если заданы конфигурации, заменяем значения (поддержка old/new)\n if db_configs:\n try:\n # Прямой текстовый заменитель для старых/новых значений, чтобы сохранить структуру файла\n modified_content = content\n for cfg in db_configs:\n # Ожидаем структуру: {'old': {...}, 'new': {...}}\n old_cfg = cfg.get('old', {})\n new_cfg = cfg.get('new', {})\n for key, old_val in old_cfg.items():\n if key in new_cfg:\n new_val = new_cfg[key]\n # Заменяем только точные совпадения старого значения в тексте YAML, используя ключ для контекста\n if isinstance(old_val, str):\n # Ищем паттерн: key: \"value\" или key: value\n key_pattern = re.escape(key)\n val_pattern = re.escape(old_val)\n # Группы: 1=ключ+разделитель, 2=открывающая кавычка (опц), 3=значение, 4=закрывающая кавычка (опц)\n pattern = rf'({key_pattern}\\s*:\\s*)([\"\\']?)({val_pattern})([\"\\']?)'\n\n # #region replacer [TYPE Function]\n # @PURPOSE: Функция замены, сохраняющая кавычки если они были.\n # @PRE: match должен быть объектом совпадения регулярного выражения.\n # @POST: Возвращает строку с новым значением, сохраняя префикс и кавычки.\n def replacer(match):\n prefix = match.group(1)\n quote_open = match.group(2)\n quote_close = match.group(4)\n return f\"{prefix}{quote_open}{new_val}{quote_close}\"\n # #endregion replacer\n modified_content = re.sub(pattern, replacer, modified_content)\n app_logger.info(\"[_update_yaml_file][State] Replaced '%s' with '%s' for key %s in %s\", old_val, new_val, key, file_path)\n # Записываем обратно изменённый контент без парсинга YAML, сохраняем оригинальное форматирование\n with open(file_path, 'w', encoding='utf-8') as f:\n f.write(modified_content)\n except Exception as e:\n app_logger.error(\"[_update_yaml_file][Failure] Error performing raw replacement in %s: %s\", file_path, e)\n# #endregion _update_yaml_file\n" }, { "contract_id": "replacer", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 360, - "end_line": 369, + "start_line": 308, + "end_line": 317, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -51662,32 +41300,24 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:replacer:Function]\n # @PURPOSE: Функция замены, сохраняющая кавычки если они были.\n # @PRE: match должен быть объектом совпадения регулярного выражения.\n # @POST: Возвращает строку с новым значением, сохраняя префикс и кавычки.\n def replacer(match):\n prefix = match.group(1)\n quote_open = match.group(2)\n quote_close = match.group(4)\n return f\"{prefix}{quote_open}{new_val}{quote_close}\"\n # [/DEF:replacer:Function]\n" + "anchor_syntax": "region", + "body": " # #region replacer [TYPE Function]\n # @PURPOSE: Функция замены, сохраняющая кавычки если они были.\n # @PRE: match должен быть объектом совпадения регулярного выражения.\n # @POST: Возвращает строку с новым значением, сохраняя префикс и кавычки.\n def replacer(match):\n prefix = match.group(1)\n quote_open = match.group(2)\n quote_close = match.group(4)\n return f\"{prefix}{quote_open}{new_val}{quote_close}\"\n # #endregion replacer\n" }, { "contract_id": "create_dashboard_export", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 380, - "end_line": 406, + "start_line": 326, + "end_line": 348, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "exclude_extensions (Optional[List[str]]) - Список расширений для исключения.", "POST": "ZIP-архив создан по пути zip_path.", "PRE": "source_paths должен содержать существующие пути.", - "PURPOSE": "Создает ZIP-архив из указанных исходных путей.", - "RETURN": "bool - `True` при успехе, `False` при ошибке." + "PURPOSE": "Создает ZIP-архив из указанных исходных путей." }, "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", @@ -51714,40 +41344,26 @@ "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": "def", - "body": "# [DEF:create_dashboard_export:Function]\n# @PURPOSE: Создает ZIP-архив из указанных исходных путей.\n# @PRE: source_paths должен содержать существующие пути.\n# @POST: ZIP-архив создан по пути zip_path.\n# @PARAM: zip_path (Union[str, Path]) - Путь для сохранения ZIP архива.\n# @PARAM: source_paths (List[Union[str, Path]]) - Список исходных путей для архивации.\n# @PARAM: exclude_extensions (Optional[List[str]]) - Список расширений для исключения.\n# @RETURN: bool - `True` при успехе, `False` при ошибке.\ndef create_dashboard_export(zip_path: Union[str, Path], source_paths: List[Union[str, Path]], exclude_extensions: Optional[List[str]] = None) -> bool:\n with belief_scope(f\"Create dashboard export: {zip_path}\"):\n log.reason(\"Packing dashboard export\", payload={\"source_paths\": [str(p) for p in source_paths], \"zip_path\": str(zip_path)})\n try:\n exclude_ext = [ext.lower() for ext in exclude_extensions or []]\n with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n for src_path_str in source_paths:\n src_path = Path(src_path_str)\n assert src_path.exists(), f\"Путь не найден: {src_path}\"\n for item in src_path.rglob('*'):\n if item.is_file() and item.suffix.lower() not in exclude_ext:\n arcname = item.relative_to(src_path.parent)\n zipf.write(item, arcname)\n log.reflect(\"Dashboard archive created\", payload={\"zip_path\": str(zip_path)})\n return True\n except (IOError, zipfile.BadZipFile, AssertionError) as e:\n log.explore(\"Dashboard archive creation failed\", payload={\"zip_path\": str(zip_path)}, error=str(e))\n return False\n# [/DEF:create_dashboard_export:Function]\n" + "anchor_syntax": "region", + "body": "# #region create_dashboard_export [TYPE Function]\n# @BRIEF Создает ZIP-архив из указанных исходных путей.\n# @PRE: source_paths должен содержать существующие пути.\n# @POST: ZIP-архив создан по пути zip_path.\ndef create_dashboard_export(zip_path: str | Path, source_paths: list[str | Path], exclude_extensions: list[str] | None = None) -> bool:\n with belief_scope(f\"Create dashboard export: {zip_path}\"):\n app_logger.info(\"[create_dashboard_export][Enter] Packing dashboard: %s -> %s\", source_paths, zip_path)\n try:\n exclude_ext = [ext.lower() for ext in exclude_extensions or []]\n with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n for src_path_str in source_paths:\n src_path = Path(src_path_str)\n assert src_path.exists(), f\"Путь не найден: {src_path}\"\n for item in src_path.rglob('*'):\n if item.is_file() and item.suffix.lower() not in exclude_ext:\n arcname = item.relative_to(src_path.parent)\n zipf.write(item, arcname)\n app_logger.reflect(f\"Archive created: {zip_path}\", extra={\"src\": \"create_dashboard_export\"})\n return True\n except (OSError, zipfile.BadZipFile, AssertionError) as e:\n app_logger.error(\"[create_dashboard_export][Failure] Error: %s\", e, exc_info=True)\n return False\n# #endregion create_dashboard_export\n" }, { "contract_id": "sanitize_filename", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 408, - "end_line": 417, + "start_line": 349, + "end_line": 356, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "filename (str) - Исходное имя файла.", "POST": "Возвращает строку без спецсимволов.", "PRE": "filename должен быть строкой.", - "PURPOSE": "Очищает строку от символов, недопустимых в именах файлов.", - "RETURN": "str - Очищенная строка." + "PURPOSE": "Очищает строку от символов, недопустимых в именах файлов." }, "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", @@ -51774,40 +41390,26 @@ "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": "def", - "body": "# [DEF:sanitize_filename:Function]\n# @PURPOSE: Очищает строку от символов, недопустимых в именах файлов.\n# @PRE: filename должен быть строкой.\n# @POST: Возвращает строку без спецсимволов.\n# @PARAM: filename (str) - Исходное имя файла.\n# @RETURN: str - Очищенная строка.\ndef sanitize_filename(filename: str) -> str:\n with belief_scope(f\"Sanitize filename: {filename}\"):\n return re.sub(r'[\\\\/*?:\"<>|]', \"_\", filename).strip()\n# [/DEF:sanitize_filename:Function]\n" + "anchor_syntax": "region", + "body": "# #region sanitize_filename [TYPE Function]\n# @BRIEF Очищает строку от символов, недопустимых в именах файлов.\n# @PRE: filename должен быть строкой.\n# @POST: Возвращает строку без спецсимволов.\ndef sanitize_filename(filename: str) -> str:\n with belief_scope(f\"Sanitize filename: {filename}\"):\n return re.sub(r'[\\\\/*?:\"<>|]', \"_\", filename).strip()\n# #endregion sanitize_filename\n" }, { "contract_id": "get_filename_from_headers", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 419, - "end_line": 431, + "start_line": 357, + "end_line": 367, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "headers (dict) - Словарь HTTP заголовков.", "POST": "Возвращает имя файла или None, если заголовок отсутствует.", "PRE": "headers должен быть словарем заголовков.", - "PURPOSE": "Извлекает имя файла из HTTP заголовка 'Content-Disposition'.", - "RETURN": "Optional[str] - Имя файла or `None`." + "PURPOSE": "Извлекает имя файла из HTTP заголовка 'Content-Disposition'." }, "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", @@ -51834,40 +41436,26 @@ "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": "def", - "body": "# [DEF:get_filename_from_headers:Function]\n# @PURPOSE: Извлекает имя файла из HTTP заголовка 'Content-Disposition'.\n# @PRE: headers должен быть словарем заголовков.\n# @POST: Возвращает имя файла или None, если заголовок отсутствует.\n# @PARAM: headers (dict) - Словарь HTTP заголовков.\n# @RETURN: Optional[str] - Имя файла or `None`.\ndef get_filename_from_headers(headers: dict) -> Optional[str]:\n with belief_scope(\"Get filename from headers\"):\n content_disposition = headers.get(\"Content-Disposition\", \"\")\n if match := re.search(r'filename=\"?([^\"]+)\"?', content_disposition):\n return match.group(1).strip()\n return None\n# [/DEF:get_filename_from_headers:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_filename_from_headers [TYPE Function]\n# @BRIEF Извлекает имя файла из HTTP заголовка 'Content-Disposition'.\n# @PRE: headers должен быть словарем заголовков.\n# @POST: Возвращает имя файла или None, если заголовок отсутствует.\ndef get_filename_from_headers(headers: dict) -> str | None:\n with belief_scope(\"Get filename from headers\"):\n content_disposition = headers.get(\"Content-Disposition\", \"\")\n if match := re.search(r'filename=\"?([^\"]+)\"?', content_disposition):\n return match.group(1).strip()\n return None\n# #endregion get_filename_from_headers\n" }, { "contract_id": "consolidate_archive_folders", "contract_type": "Function", "file_path": "backend/src/core/utils/fileio.py", - "start_line": 433, - "end_line": 487, + "start_line": 368, + "end_line": 420, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "root_directory (Path) - Корневая директория для консолидации.", "POST": "Директории с одинаковым префиксом объединены в одну.", "PRE": "root_directory должен быть объектом Path к существующей директории.", - "PURPOSE": "Консолидирует директории архивов на основе общего слага в имени.", - "THROW": "TypeError, ValueError - Если `root_directory` невалиден." + "PURPOSE": "Консолидирует директории архивов на основе общего слага в имени." }, "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", @@ -51894,36 +41482,23 @@ "actual_complexity": 1, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:consolidate_archive_folders:Function]\n# @PURPOSE: Консолидирует директории архивов на основе общего слага в имени.\n# @PRE: root_directory должен быть объектом Path к существующей директории.\n# @POST: Директории с одинаковым префиксом объединены в одну.\n# @THROW: TypeError, ValueError - Если `root_directory` невалиден.\n# @PARAM: root_directory (Path) - Корневая директория для консолидации.\ndef consolidate_archive_folders(root_directory: Path) -> None:\n with belief_scope(f\"Consolidate archives in {root_directory}\"):\n assert isinstance(root_directory, Path), \"root_directory must be a Path object.\" \n assert root_directory.is_dir(), \"root_directory must be an existing directory.\" \n \n log.reason(\"Consolidating archive folders\", payload={\"root_directory\": str(root_directory)}) \n # Собираем все директории с архивами \n archive_dirs = [] \n for item in root_directory.iterdir(): \n if item.is_dir(): \n # Проверяем, есть ли в директории ZIP-архивы \n if any(item.glob(\"*.zip\")): \n archive_dirs.append(item) \n # Группируем по слагу (части имени до первого '_') \n slug_groups = {} \n for dir_path in archive_dirs: \n dir_name = dir_path.name \n slug = dir_name.split('_')[0] if '_' in dir_name else dir_name \n if slug not in slug_groups: \n slug_groups[slug] = [] \n slug_groups[slug].append(dir_path) \n # Для каждой группы консолидируем \n for slug, dirs in slug_groups.items(): \n if len(dirs) <= 1: \n continue \n # Создаем целевую директорию \n target_dir = root_directory / slug \n target_dir.mkdir(exist_ok=True) \n log.reason(\"Consolidating archive directories\", payload={\"slug\": slug, \"dir_count\": len(dirs), \"target\": str(target_dir)}) \n # Перемещаем содержимое \n for source_dir in dirs: \n if source_dir == target_dir: \n continue \n for item in source_dir.iterdir(): \n dest_item = target_dir / item.name \n try: \n if item.is_dir(): \n shutil.move(str(item), str(dest_item)) \n else: \n shutil.move(str(item), str(dest_item)) \n except Exception as e: \n log.explore(\"Failed to move item during consolidation\", payload={\"source\": str(item), \"dest\": str(dest_item)}, error=str(e)) \n # Удаляем исходную директорию \n try: \n source_dir.rmdir() \n log.reason(\"Removed source directory after consolidation\", payload={\"directory\": str(source_dir)}) \n except Exception as e: \n log.explore(\"Failed to remove source directory after consolidation\", payload={\"directory\": str(source_dir)}, error=str(e)) \n# [/DEF:consolidate_archive_folders:Function]\n" + "anchor_syntax": "region", + "body": "# #region consolidate_archive_folders [TYPE Function]\n# @BRIEF Консолидирует директории архивов на основе общего слага в имени.\n# @PRE: root_directory должен быть объектом Path к существующей директории.\n# @POST: Директории с одинаковым префиксом объединены в одну.\ndef consolidate_archive_folders(root_directory: Path) -> None:\n with belief_scope(f\"Consolidate archives in {root_directory}\"):\n assert isinstance(root_directory, Path), \"root_directory must be a Path object.\"\n assert root_directory.is_dir(), \"root_directory must be an existing directory.\"\n\n app_logger.info(\"[consolidate_archive_folders][Enter] Consolidating archives in %s\", root_directory)\n # Собираем все директории с архивами\n archive_dirs = []\n for item in root_directory.iterdir():\n if item.is_dir():\n # Проверяем, есть ли в директории ZIP-архивы\n if any(item.glob(\"*.zip\")):\n archive_dirs.append(item)\n # Группируем по слагу (части имени до первого '_')\n slug_groups = {}\n for dir_path in archive_dirs:\n dir_name = dir_path.name\n slug = dir_name.split('_')[0] if '_' in dir_name else dir_name\n if slug not in slug_groups:\n slug_groups[slug] = []\n slug_groups[slug].append(dir_path)\n # Для каждой группы консолидируем\n for slug, dirs in slug_groups.items():\n if len(dirs) <= 1:\n continue\n # Создаем целевую директорию\n target_dir = root_directory / slug\n target_dir.mkdir(exist_ok=True)\n app_logger.info(\"[consolidate_archive_folders][State] Consolidating %d directories under %s\", len(dirs), target_dir)\n # Перемещаем содержимое\n for source_dir in dirs:\n if source_dir == target_dir:\n continue\n for item in source_dir.iterdir():\n dest_item = target_dir / item.name\n try:\n if item.is_dir():\n shutil.move(str(item), str(dest_item))\n else:\n shutil.move(str(item), str(dest_item))\n except Exception as e:\n app_logger.error(\"[consolidate_archive_folders][Failure] Failed to move %s to %s: %s\", item, dest_item, e)\n # Удаляем исходную директорию\n try:\n source_dir.rmdir()\n app_logger.info(\"[consolidate_archive_folders][State] Removed source directory: %s\", source_dir)\n except Exception as e:\n app_logger.error(\"[consolidate_archive_folders][Failure] Failed to remove source directory %s: %s\", source_dir, e)\n# #endregion consolidate_archive_folders\n" }, { "contract_id": "FuzzyMatching", "contract_type": "Module", "file_path": "backend/src/core/utils/matching.py", "start_line": 1, - "end_line": 55, + "end_line": 49, "tier": "TIER_1", "complexity": 1, "metadata": { "INVARIANT": "Confidence scores are returned as floats between 0.0 and 1.0.", "LAYER": "Core", - "PURPOSE": "Provides utility functions for fuzzy matching database names.", - "SEMANTICS": [ - "fuzzy", - "matching", - "rapidfuzz", - "database", - "mapping" - ] + "PURPOSE": "Provides utility functions for fuzzy matching database names." }, "relations": [ { @@ -51962,32 +41537,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FuzzyMatching:Module]\n#\n# @SEMANTICS: fuzzy, matching, rapidfuzz, database, mapping\n# @PURPOSE: Provides utility functions for fuzzy matching database names.\n# @LAYER: Core\n# @RELATION: DEPENDS_ON -> rapidfuzz\n#\n# @INVARIANT: Confidence scores are returned as floats between 0.0 and 1.0.\n\n# [SECTION: IMPORTS]\nfrom rapidfuzz import fuzz, process\nfrom typing import List, Dict\n# [/SECTION]\n\n# [DEF:suggest_mappings:Function]\n# @PURPOSE: Suggests mappings between source and target databases using fuzzy matching.\n# @PRE: source_databases and target_databases are lists of dictionaries with 'uuid' and 'database_name'.\n# @POST: Returns a list of suggested mappings with confidence scores.\n# @PARAM: source_databases (List[Dict]) - Databases from the source environment.\n# @PARAM: target_databases (List[Dict]) - Databases from the target environment.\n# @PARAM: threshold (int) - Minimum confidence score (0-100).\n# @RETURN: List[Dict] - Suggested mappings.\ndef suggest_mappings(source_databases: List[Dict], target_databases: List[Dict], threshold: int = 60) -> List[Dict]:\n \"\"\"\n Suggest mappings between source and target databases using fuzzy matching.\n \"\"\"\n suggestions = []\n if not target_databases:\n return suggestions\n\n target_names = [db['database_name'] for db in target_databases]\n \n for s_db in source_databases:\n # Use token_sort_ratio as decided in research.md\n match = process.extractOne(\n s_db['database_name'], \n target_names, \n scorer=fuzz.token_sort_ratio\n )\n \n if match:\n name, score, index = match\n if score >= threshold:\n suggestions.append({\n \"source_db\": s_db['database_name'],\n \"target_db\": target_databases[index]['database_name'],\n \"source_db_uuid\": s_db['uuid'],\n \"target_db_uuid\": target_databases[index]['uuid'],\n \"confidence\": score / 100.0\n })\n \n return suggestions\n# [/DEF:suggest_mappings:Function]\n\n# [/DEF:FuzzyMatching:Module]\n" + "anchor_syntax": "region", + "body": "# #region FuzzyMatching [TYPE Module] [SEMANTICS fuzzy, matching, rapidfuzz, database, mapping]\n#\n# @BRIEF Provides utility functions for fuzzy matching database names.\n# @LAYER: Core\n# @RELATION DEPENDS_ON -> rapidfuzz\n#\n# @INVARIANT: Confidence scores are returned as floats between 0.0 and 1.0.\n\n\nfrom rapidfuzz import fuzz, process\n\n\n# #region suggest_mappings [TYPE Function]\n# @BRIEF Suggests mappings between source and target databases using fuzzy matching.\n# @PRE: source_databases and target_databases are lists of dictionaries with 'uuid' and 'database_name'.\n# @POST: Returns a list of suggested mappings with confidence scores.\ndef suggest_mappings(source_databases: list[dict], target_databases: list[dict], threshold: int = 60) -> list[dict]:\n \"\"\"\n Suggest mappings between source and target databases using fuzzy matching.\n \"\"\"\n suggestions = []\n if not target_databases:\n return suggestions\n\n target_names = [db['database_name'] for db in target_databases]\n\n for s_db in source_databases:\n # Use token_sort_ratio as decided in research.md\n match = process.extractOne(\n s_db['database_name'],\n target_names,\n scorer=fuzz.token_sort_ratio\n )\n\n if match:\n name, score, index = match\n if score >= threshold:\n suggestions.append({\n \"source_db\": s_db['database_name'],\n \"target_db\": target_databases[index]['database_name'],\n \"source_db_uuid\": s_db['uuid'],\n \"target_db_uuid\": target_databases[index]['uuid'],\n \"confidence\": score / 100.0\n })\n\n return suggestions\n# #endregion suggest_mappings\n\n# #endregion FuzzyMatching\n" }, { "contract_id": "suggest_mappings", "contract_type": "Function", "file_path": "backend/src/core/utils/matching.py", - "start_line": 15, - "end_line": 53, + "start_line": 13, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "threshold (int) - Minimum confidence score (0-100).", "POST": "Returns a list of suggested mappings with confidence scores.", "PRE": "source_databases and target_databases are lists of dictionaries with 'uuid' and 'database_name'.", - "PURPOSE": "Suggests mappings between source and target databases using fuzzy matching.", - "RETURN": "List[Dict] - Suggested mappings." + "PURPOSE": "Suggests mappings between source and target databases using fuzzy matching." }, "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", @@ -52014,44 +41581,29 @@ "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": "def", - "body": "# [DEF:suggest_mappings:Function]\n# @PURPOSE: Suggests mappings between source and target databases using fuzzy matching.\n# @PRE: source_databases and target_databases are lists of dictionaries with 'uuid' and 'database_name'.\n# @POST: Returns a list of suggested mappings with confidence scores.\n# @PARAM: source_databases (List[Dict]) - Databases from the source environment.\n# @PARAM: target_databases (List[Dict]) - Databases from the target environment.\n# @PARAM: threshold (int) - Minimum confidence score (0-100).\n# @RETURN: List[Dict] - Suggested mappings.\ndef suggest_mappings(source_databases: List[Dict], target_databases: List[Dict], threshold: int = 60) -> List[Dict]:\n \"\"\"\n Suggest mappings between source and target databases using fuzzy matching.\n \"\"\"\n suggestions = []\n if not target_databases:\n return suggestions\n\n target_names = [db['database_name'] for db in target_databases]\n \n for s_db in source_databases:\n # Use token_sort_ratio as decided in research.md\n match = process.extractOne(\n s_db['database_name'], \n target_names, \n scorer=fuzz.token_sort_ratio\n )\n \n if match:\n name, score, index = match\n if score >= threshold:\n suggestions.append({\n \"source_db\": s_db['database_name'],\n \"target_db\": target_databases[index]['database_name'],\n \"source_db_uuid\": s_db['uuid'],\n \"target_db_uuid\": target_databases[index]['uuid'],\n \"confidence\": score / 100.0\n })\n \n return suggestions\n# [/DEF:suggest_mappings:Function]\n" + "anchor_syntax": "region", + "body": "# #region suggest_mappings [TYPE Function]\n# @BRIEF Suggests mappings between source and target databases using fuzzy matching.\n# @PRE: source_databases and target_databases are lists of dictionaries with 'uuid' and 'database_name'.\n# @POST: Returns a list of suggested mappings with confidence scores.\ndef suggest_mappings(source_databases: list[dict], target_databases: list[dict], threshold: int = 60) -> list[dict]:\n \"\"\"\n Suggest mappings between source and target databases using fuzzy matching.\n \"\"\"\n suggestions = []\n if not target_databases:\n return suggestions\n\n target_names = [db['database_name'] for db in target_databases]\n\n for s_db in source_databases:\n # Use token_sort_ratio as decided in research.md\n match = process.extractOne(\n s_db['database_name'],\n target_names,\n scorer=fuzz.token_sort_ratio\n )\n\n if match:\n name, score, index = match\n if score >= threshold:\n suggestions.append({\n \"source_db\": s_db['database_name'],\n \"target_db\": target_databases[index]['database_name'],\n \"source_db_uuid\": s_db['uuid'],\n \"target_db_uuid\": target_databases[index]['uuid'],\n \"confidence\": score / 100.0\n })\n\n return suggestions\n# #endregion suggest_mappings\n" }, { "contract_id": "NetworkModule", "contract_type": "Module", "file_path": "backend/src/core/utils/network.py", "start_line": 1, - "end_line": 573, + "end_line": 538, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PUBLIC_API": "APIClient", - "PURPOSE": "Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок.", - "SEMANTICS": [ - "network", - "http", - "client", - "api", - "requests", - "session", - "authentication" - ] + "PURPOSE": "Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок." }, "relations": [ { "source_id": "NetworkModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LoggerModule", "target_ref": "[LoggerModule]" } @@ -52062,39 +41614,21 @@ "tag": "PUBLIC_API", "message": "@PUBLIC_API is not defined in axiom_config.yaml tags", "detail": null - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:NetworkModule:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: network, http, client, api, requests, session, authentication\n# @PURPOSE: Инкапсулирует низкоуровневую HTTP-логику для взаимодействия с Superset API, включая аутентификацию, управление сессией, retry-логику и обработку ошибок.\n# @LAYER: Infra\n# @RELATION: [DEPENDS_ON] ->[LoggerModule]\n# @PUBLIC_API: APIClient\n\n# [SECTION: IMPORTS]\nfrom typing import Optional, Dict, Any, List, Union, cast, Tuple\nimport json\nimport io\nfrom pathlib import Path\nimport threading\nimport time\nimport requests\nfrom requests.adapters import HTTPAdapter\nimport urllib3\nfrom urllib3.util.retry import Retry\nfrom ..logger import logger as app_logger, belief_scope\nfrom ..cot_logger import MarkerLogger\n# [/SECTION]\n\nlog = MarkerLogger(\"Network\")\n\n# [DEF:SupersetAPIError:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Base exception for all Superset API related errors.\nclass SupersetAPIError(Exception):\n # [DEF:__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the exception with a message and context.\n # @PRE: message is a string, context is a dict.\n # @POST: Exception is initialized with context.\n def __init__(self, message: str = \"Superset API error\", **context: Any):\n with belief_scope(\"SupersetAPIError.__init__\"):\n self.context = context\n super().__init__(f\"[API_FAILURE] {message} | Context: {self.context}\")\n # [/DEF:__init__:Function]\n# [/DEF:SupersetAPIError:Class]\n\n# [DEF:AuthenticationError:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Exception raised when authentication fails.\nclass AuthenticationError(SupersetAPIError):\n # [DEF:__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the authentication error.\n # @PRE: message is a string, context is a dict.\n # @POST: AuthenticationError is initialized.\n def __init__(self, message: str = \"Authentication failed\", **context: Any):\n with belief_scope(\"AuthenticationError.__init__\"):\n super().__init__(message, type=\"authentication\", **context)\n # [/DEF:__init__:Function]\n# [/DEF:AuthenticationError:Class]\n\n# [DEF:PermissionDeniedError:Class]\n# @PURPOSE: Exception raised when access is denied.\nclass PermissionDeniedError(AuthenticationError):\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the permission denied error.\n # @PRE: message is a string, context is a dict.\n # @POST: PermissionDeniedError is initialized.\n def __init__(self, message: str = \"Permission denied\", **context: Any):\n with belief_scope(\"PermissionDeniedError.__init__\"):\n super().__init__(message, **context)\n # [/DEF:__init__:Function]\n# [/DEF:PermissionDeniedError:Class]\n\n# [DEF:DashboardNotFoundError:Class]\n# @PURPOSE: Exception raised when a dashboard cannot be found.\nclass DashboardNotFoundError(SupersetAPIError):\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the not found error with resource ID.\n # @PRE: resource_id is provided.\n # @POST: DashboardNotFoundError is initialized.\n def __init__(self, resource_id: Union[int, str], message: str = \"Dashboard not found\", **context: Any):\n with belief_scope(\"DashboardNotFoundError.__init__\"):\n super().__init__(f\"Dashboard '{resource_id}' {message}\", subtype=\"not_found\", resource_id=resource_id, **context)\n # [/DEF:__init__:Function]\n# [/DEF:DashboardNotFoundError:Class]\n\n# [DEF:NetworkError:Class]\n# @PURPOSE: Exception raised when a network level error occurs.\nclass NetworkError(Exception):\n # [DEF:NetworkError.__init__:Function]\n # @PURPOSE: Initializes the network error.\n # @PRE: message is a string.\n # @POST: NetworkError is initialized.\n def __init__(self, message: str = \"Network connection failed\", **context: Any):\n with belief_scope(\"NetworkError.__init__\"):\n self.context = context\n super().__init__(f\"[NETWORK_FAILURE] {message} | Context: {self.context}\")\n # [/DEF:NetworkError.__init__:Function]\n# [/DEF:NetworkError:Class]\n\n\n# [DEF:SupersetAuthCache:Class]\n# @PURPOSE: 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\n _lock = threading.Lock()\n _entries: Dict[Tuple[str, str, bool], Dict[str, Any]] = {}\n\n @classmethod\n def build_key(cls, base_url: str, auth: Optional[Dict[str, Any]], 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\n @classmethod\n # [DEF:SupersetAuthCache.get:Function]\n def get(cls, key: Tuple[str, str, bool]) -> Optional[Dict[str, str]]:\n now = time.time()\n with cls._lock:\n payload = cls._entries.get(key)\n if not payload:\n return None\n expires_at = float(payload.get(\"expires_at\") or 0)\n if expires_at <= now:\n cls._entries.pop(key, None)\n return None\n tokens = payload.get(\"tokens\")\n if not isinstance(tokens, dict):\n cls._entries.pop(key, None)\n return None\n return {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n }\n # [/DEF:SupersetAuthCache.get:Function]\n\n @classmethod\n # [DEF:SupersetAuthCache.set:Function]\n def set(cls, key: Tuple[str, str, bool], tokens: Dict[str, str], ttl_seconds: Optional[int] = None) -> None:\n normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1)\n with cls._lock:\n cls._entries[key] = {\n \"tokens\": {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n },\n \"expires_at\": time.time() + normalized_ttl,\n }\n # [/DEF:SupersetAuthCache.set:Function]\n\n @classmethod\n def invalidate(cls, key: Tuple[str, str, bool]) -> None:\n with cls._lock:\n cls._entries.pop(key, None)\n# [/DEF:SupersetAuthCache:Class]\n\n# [DEF:APIClient:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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\n # [DEF:APIClient.__init__: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 log.reason(\"Initializing APIClient\")\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 log.reflect(\"APIClient initialized\")\n # [/DEF:APIClient.__init__:Function]\n\n # [DEF:_init_session: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 from urllib3.poolmanager import PoolManager\n import ssl\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 log.reflect(\"SSL verification disabled for session\", payload={\"base_url\": self.base_url})\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 # [/DEF:_init_session:Function]\n\n # [DEF:_normalize_base_url: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 # [/DEF:_normalize_base_url:Function]\n\n # [DEF:_build_api_url: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 # [/DEF:_build_api_url:Function]\n\n # [DEF:APIClient.authenticate: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 # @RATIONALE: Auth cache stores access_token and csrf_token but not the session cookie.\n # When a new APIClient instance reuses cached tokens, its fresh requests.Session\n # has no session cookie — causing superset CSRF check to fail with\n # \"CSRF session token is missing\". The fix is to always fetch a fresh CSRF token\n # (GET /security/csrf_token/) even on cache hit, which establishes a session cookie\n # in the new session while avoiding a full POST login.\n # @REJECTED: Skipping csrf_token GET on cache hit — session cookie would be missing, causing\n # all POST/PUT/DELETE requests to fail with CSRF error on new APIClient instances.\n def authenticate(self) -> Dict[str, str]:\n with belief_scope(\"authenticate\"):\n log.reason(\"Authenticating to Superset\", payload={\"base_url\": self.base_url})\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n # Always fetch a fresh CSRF token to establish session cookie in this session.\n # The cached access_token may still be valid; we avoid a full POST login.\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 {self._tokens['access_token']}\"},\n timeout=self.request_settings[\"timeout\"],\n )\n csrf_response.raise_for_status()\n # Update CSRF token (may have rotated)\n self._tokens[\"csrf_token\"] = csrf_response.json()[\"result\"]\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n log.reason(\"Reused cached tokens + refreshed CSRF token\", payload={\"base_url\": self.base_url})\n except Exception as csrf_err:\n # CSRF refresh failed — likely access_token expired, do full re-auth\n log.explore(\"CSRF refresh failed, performing full re-auth\", error=str(csrf_err))\n SupersetAuthCache.invalidate(self._auth_cache_key)\n # Fall through to full login below\n self._tokens = {}\n self._authenticated = False\n\n if not self._authenticated:\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 log.reason(\"Performing full Superset login\", payload={\"login_url\": login_url, \"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 log.explore(\"Superset login returned non-200 status\", payload={\"status_code\": response.status_code, \"response\": response.text}, error=f\"HTTP {response.status_code}: {response.text[:200]}\")\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 log.reflect(\"Authenticated successfully\")\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\n self._authenticated = True\n log.reflect(\"Authenticated successfully (from cached tokens)\")\n return self._tokens\n # [/DEF:APIClient.authenticate:Function]\n\n @property\n # [DEF:headers: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 # [/DEF:headers:Function]\n\n # [DEF:request: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: Optional[Dict] = None, raw_response: bool = False, **kwargs) -> Union[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 # [/DEF:request:Function]\n\n # [DEF:_handle_http_error: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 # [/DEF:_handle_http_error:Function]\n\n # [DEF:_is_dashboard_endpoint: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 # [/DEF:_is_dashboard_endpoint:Function]\n\n # [DEF:_handle_network_error: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 # [/DEF:_handle_network_error:Function]\n\n # [DEF:upload_file: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: Optional[Dict] = None, timeout: Optional[int] = 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 # [/DEF:upload_file:Function]\n\n # [DEF:_perform_upload: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: Optional[Dict], headers: Dict, timeout: Optional[int]) -> 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 log.explore(\"Upload response not valid JSON\", payload={\"preview\": response.text[:200]}, error=str(json_e))\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 # [/DEF:_perform_upload:Function]\n\n # [DEF:fetch_paginated_count: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 # [/DEF:fetch_paginated_count:Function]\n\n # [DEF:fetch_paginated_data: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 log.reason(\"Total count resolved from first page\", payload={\"total_count\": total_count})\n\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 # [/DEF:fetch_paginated_data:Function]\n\n# [/DEF:APIClient:Class]\n\n# [/DEF:NetworkModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "SupersetAPIError", "contract_type": "Class", "file_path": "backend/src/core/utils/network.py", - "start_line": 27, - "end_line": 41, + "start_line": 23, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Base exception for all Superset API related errors." }, "relations": [], @@ -52109,19 +41643,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetAPIError:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Base exception for all Superset API related errors.\nclass SupersetAPIError(Exception):\n # [DEF:__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the exception with a message and context.\n # @PRE: message is a string, context is a dict.\n # @POST: Exception is initialized with context.\n def __init__(self, message: str = \"Superset API error\", **context: Any):\n with belief_scope(\"SupersetAPIError.__init__\"):\n self.context = context\n super().__init__(f\"[API_FAILURE] {message} | Context: {self.context}\")\n # [/DEF:__init__:Function]\n# [/DEF:SupersetAPIError:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "AuthenticationError", "contract_type": "Class", "file_path": "backend/src/core/utils/network.py", - "start_line": 43, - "end_line": 56, + "start_line": 36, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Exception raised when authentication fails." }, "relations": [], @@ -52136,15 +41670,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthenticationError:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Exception raised when authentication fails.\nclass AuthenticationError(SupersetAPIError):\n # [DEF:__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the authentication error.\n # @PRE: message is a string, context is a dict.\n # @POST: AuthenticationError is initialized.\n def __init__(self, message: str = \"Authentication failed\", **context: Any):\n with belief_scope(\"AuthenticationError.__init__\"):\n super().__init__(message, type=\"authentication\", **context)\n # [/DEF:__init__:Function]\n# [/DEF:AuthenticationError:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "PermissionDeniedError", "contract_type": "Class", "file_path": "backend/src/core/utils/network.py", - "start_line": 58, - "end_line": 69, + "start_line": 48, + "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52162,15 +41696,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PermissionDeniedError:Class]\n# @PURPOSE: Exception raised when access is denied.\nclass PermissionDeniedError(AuthenticationError):\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the permission denied error.\n # @PRE: message is a string, context is a dict.\n # @POST: PermissionDeniedError is initialized.\n def __init__(self, message: str = \"Permission denied\", **context: Any):\n with belief_scope(\"PermissionDeniedError.__init__\"):\n super().__init__(message, **context)\n # [/DEF:__init__:Function]\n# [/DEF:PermissionDeniedError:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DashboardNotFoundError", "contract_type": "Class", "file_path": "backend/src/core/utils/network.py", - "start_line": 71, - "end_line": 82, + "start_line": 60, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52188,15 +41722,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardNotFoundError:Class]\n# @PURPOSE: Exception raised when a dashboard cannot be found.\nclass DashboardNotFoundError(SupersetAPIError):\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the not found error with resource ID.\n # @PRE: resource_id is provided.\n # @POST: DashboardNotFoundError is initialized.\n def __init__(self, resource_id: Union[int, str], message: str = \"Dashboard not found\", **context: Any):\n with belief_scope(\"DashboardNotFoundError.__init__\"):\n super().__init__(f\"Dashboard '{resource_id}' {message}\", subtype=\"not_found\", resource_id=resource_id, **context)\n # [/DEF:__init__:Function]\n# [/DEF:DashboardNotFoundError:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "NetworkError", "contract_type": "Class", "file_path": "backend/src/core/utils/network.py", - "start_line": 84, - "end_line": 96, + "start_line": 72, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52214,15 +41748,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:NetworkError:Class]\n# @PURPOSE: Exception raised when a network level error occurs.\nclass NetworkError(Exception):\n # [DEF:NetworkError.__init__:Function]\n # @PURPOSE: Initializes the network error.\n # @PRE: message is a string.\n # @POST: NetworkError is initialized.\n def __init__(self, message: str = \"Network connection failed\", **context: Any):\n with belief_scope(\"NetworkError.__init__\"):\n self.context = context\n super().__init__(f\"[NETWORK_FAILURE] {message} | Context: {self.context}\")\n # [/DEF:NetworkError.__init__:Function]\n# [/DEF:NetworkError:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "NetworkError.__init__", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 87, - "end_line": 95, + "start_line": 75, + "end_line": 83, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52260,15 +41794,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:NetworkError.__init__:Function]\n # @PURPOSE: Initializes the network error.\n # @PRE: message is a string.\n # @POST: NetworkError is initialized.\n def __init__(self, message: str = \"Network connection failed\", **context: Any):\n with belief_scope(\"NetworkError.__init__\"):\n self.context = context\n super().__init__(f\"[NETWORK_FAILURE] {message} | Context: {self.context}\")\n # [/DEF:NetworkError.__init__:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "SupersetAuthCache", "contract_type": "Class", "file_path": "backend/src/core/utils/network.py", - "start_line": 99, - "end_line": 156, + "start_line": 85, + "end_line": 137, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52306,110 +41840,73 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetAuthCache:Class]\n# @PURPOSE: 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\n _lock = threading.Lock()\n _entries: Dict[Tuple[str, str, bool], Dict[str, Any]] = {}\n\n @classmethod\n def build_key(cls, base_url: str, auth: Optional[Dict[str, Any]], 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\n @classmethod\n # [DEF:SupersetAuthCache.get:Function]\n def get(cls, key: Tuple[str, str, bool]) -> Optional[Dict[str, str]]:\n now = time.time()\n with cls._lock:\n payload = cls._entries.get(key)\n if not payload:\n return None\n expires_at = float(payload.get(\"expires_at\") or 0)\n if expires_at <= now:\n cls._entries.pop(key, None)\n return None\n tokens = payload.get(\"tokens\")\n if not isinstance(tokens, dict):\n cls._entries.pop(key, None)\n return None\n return {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n }\n # [/DEF:SupersetAuthCache.get:Function]\n\n @classmethod\n # [DEF:SupersetAuthCache.set:Function]\n def set(cls, key: Tuple[str, str, bool], tokens: Dict[str, str], ttl_seconds: Optional[int] = None) -> None:\n normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1)\n with cls._lock:\n cls._entries[key] = {\n \"tokens\": {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n },\n \"expires_at\": time.time() + normalized_ttl,\n }\n # [/DEF:SupersetAuthCache.set:Function]\n\n @classmethod\n def invalidate(cls, key: Tuple[str, str, bool]) -> None:\n with cls._lock:\n cls._entries.pop(key, None)\n# [/DEF:SupersetAuthCache:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SupersetAuthCache.get", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 117, - "end_line": 136, + "start_line": 100, + "end_line": 119, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetAuthCache.get:Function]\n def get(cls, key: Tuple[str, str, bool]) -> Optional[Dict[str, str]]:\n now = time.time()\n with cls._lock:\n payload = cls._entries.get(key)\n if not payload:\n return None\n expires_at = float(payload.get(\"expires_at\") or 0)\n if expires_at <= now:\n cls._entries.pop(key, None)\n return None\n tokens = payload.get(\"tokens\")\n if not isinstance(tokens, dict):\n cls._entries.pop(key, None)\n return None\n return {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n }\n # [/DEF:SupersetAuthCache.get:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "SupersetAuthCache.set", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 139, - "end_line": 150, + "start_line": 121, + "end_line": 132, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetAuthCache.set:Function]\n def set(cls, key: Tuple[str, str, bool], tokens: Dict[str, str], ttl_seconds: Optional[int] = None) -> None:\n normalized_ttl = max(int(ttl_seconds or cls.TTL_SECONDS), 1)\n with cls._lock:\n cls._entries[key] = {\n \"tokens\": {\n \"access_token\": str(tokens.get(\"access_token\") or \"\"),\n \"csrf_token\": str(tokens.get(\"csrf_token\") or \"\"),\n },\n \"expires_at\": time.time() + normalized_ttl,\n }\n # [/DEF:SupersetAuthCache.set:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "APIClient", "contract_type": "Class", "file_path": "backend/src/core/utils/network.py", - "start_line": 158, - "end_line": 571, + "start_line": 138, + "end_line": 537, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Synchronous Superset API client with process-local auth token caching." }, "relations": [ { "source_id": "APIClient", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetAuthCache", "target_ref": "[SupersetAuthCache]" }, { "source_id": "APIClient", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LoggerModule", "target_ref": "[LoggerModule]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:APIClient:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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\n # [DEF:APIClient.__init__: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 log.reason(\"Initializing APIClient\")\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 log.reflect(\"APIClient initialized\")\n # [/DEF:APIClient.__init__:Function]\n\n # [DEF:_init_session: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 from urllib3.poolmanager import PoolManager\n import ssl\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 log.reflect(\"SSL verification disabled for session\", payload={\"base_url\": self.base_url})\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 # [/DEF:_init_session:Function]\n\n # [DEF:_normalize_base_url: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 # [/DEF:_normalize_base_url:Function]\n\n # [DEF:_build_api_url: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 # [/DEF:_build_api_url:Function]\n\n # [DEF:APIClient.authenticate: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 # @RATIONALE: Auth cache stores access_token and csrf_token but not the session cookie.\n # When a new APIClient instance reuses cached tokens, its fresh requests.Session\n # has no session cookie — causing superset CSRF check to fail with\n # \"CSRF session token is missing\". The fix is to always fetch a fresh CSRF token\n # (GET /security/csrf_token/) even on cache hit, which establishes a session cookie\n # in the new session while avoiding a full POST login.\n # @REJECTED: Skipping csrf_token GET on cache hit — session cookie would be missing, causing\n # all POST/PUT/DELETE requests to fail with CSRF error on new APIClient instances.\n def authenticate(self) -> Dict[str, str]:\n with belief_scope(\"authenticate\"):\n log.reason(\"Authenticating to Superset\", payload={\"base_url\": self.base_url})\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n # Always fetch a fresh CSRF token to establish session cookie in this session.\n # The cached access_token may still be valid; we avoid a full POST login.\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 {self._tokens['access_token']}\"},\n timeout=self.request_settings[\"timeout\"],\n )\n csrf_response.raise_for_status()\n # Update CSRF token (may have rotated)\n self._tokens[\"csrf_token\"] = csrf_response.json()[\"result\"]\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n log.reason(\"Reused cached tokens + refreshed CSRF token\", payload={\"base_url\": self.base_url})\n except Exception as csrf_err:\n # CSRF refresh failed — likely access_token expired, do full re-auth\n log.explore(\"CSRF refresh failed, performing full re-auth\", error=str(csrf_err))\n SupersetAuthCache.invalidate(self._auth_cache_key)\n # Fall through to full login below\n self._tokens = {}\n self._authenticated = False\n\n if not self._authenticated:\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 log.reason(\"Performing full Superset login\", payload={\"login_url\": login_url, \"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 log.explore(\"Superset login returned non-200 status\", payload={\"status_code\": response.status_code, \"response\": response.text}, error=f\"HTTP {response.status_code}: {response.text[:200]}\")\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 log.reflect(\"Authenticated successfully\")\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\n self._authenticated = True\n log.reflect(\"Authenticated successfully (from cached tokens)\")\n return self._tokens\n # [/DEF:APIClient.authenticate:Function]\n\n @property\n # [DEF:headers: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 # [/DEF:headers:Function]\n\n # [DEF:request: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: Optional[Dict] = None, raw_response: bool = False, **kwargs) -> Union[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 # [/DEF:request:Function]\n\n # [DEF:_handle_http_error: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 # [/DEF:_handle_http_error:Function]\n\n # [DEF:_is_dashboard_endpoint: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 # [/DEF:_is_dashboard_endpoint:Function]\n\n # [DEF:_handle_network_error: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 # [/DEF:_handle_network_error:Function]\n\n # [DEF:upload_file: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: Optional[Dict] = None, timeout: Optional[int] = 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 # [/DEF:upload_file:Function]\n\n # [DEF:_perform_upload: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: Optional[Dict], headers: Dict, timeout: Optional[int]) -> 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 log.explore(\"Upload response not valid JSON\", payload={\"preview\": response.text[:200]}, error=str(json_e))\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 # [/DEF:_perform_upload:Function]\n\n # [DEF:fetch_paginated_count: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 # [/DEF:fetch_paginated_count:Function]\n\n # [DEF:fetch_paginated_data: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 log.reason(\"Total count resolved from first page\", payload={\"total_count\": total_count})\n\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 # [/DEF:fetch_paginated_data:Function]\n\n# [/DEF:APIClient:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "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" }, { "contract_id": "APIClient.__init__", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 166, - "end_line": 189, + "start_line": 144, + "end_line": 167, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52454,15 +41951,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:APIClient.__init__: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 log.reason(\"Initializing APIClient\")\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 log.reflect(\"APIClient initialized\")\n # [/DEF:APIClient.__init__:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_init_session", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 191, - "end_line": 235, + "start_line": 168, + "end_line": 213, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52507,15 +42004,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:_init_session: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 from urllib3.poolmanager import PoolManager\n import ssl\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 log.reflect(\"SSL verification disabled for session\", payload={\"base_url\": self.base_url})\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 # [/DEF:_init_session:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_normalize_base_url", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 237, - "end_line": 247, + "start_line": 214, + "end_line": 224, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52560,15 +42057,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_base_url: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 # [/DEF:_normalize_base_url:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_build_api_url", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 249, - "end_line": 263, + "start_line": 225, + "end_line": 239, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52613,22 +42110,21 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:_build_api_url: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 # [/DEF:_build_api_url:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "APIClient.authenticate", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 265, - "end_line": 347, + "start_line": 240, + "end_line": 324, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "`self._tokens` заполнен, `self._authenticated` установлен в `True`.", "PRE": "self.auth and self.base_url must be valid.", "PURPOSE": "Выполняет аутентификацию в Superset API и получает access и CSRF токены.", - "RATIONALE": "Auth cache stores access_token and csrf_token but not the session cookie.", "RETURN": "Dict[str, str] - Словарь с токенами.", "THROW": "AuthenticationError, NetworkError - при ошибках." }, @@ -52674,15 +42170,6 @@ "contract_type": "Function" } }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RATIONALE", - "message": "@RATIONALE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, { "code": "unknown_tag", "tag": "RETURN", @@ -52741,15 +42228,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:APIClient.authenticate: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 # @RATIONALE: Auth cache stores access_token and csrf_token but not the session cookie.\n # When a new APIClient instance reuses cached tokens, its fresh requests.Session\n # has no session cookie — causing superset CSRF check to fail with\n # \"CSRF session token is missing\". The fix is to always fetch a fresh CSRF token\n # (GET /security/csrf_token/) even on cache hit, which establishes a session cookie\n # in the new session while avoiding a full POST login.\n # @REJECTED: Skipping csrf_token GET on cache hit — session cookie would be missing, causing\n # all POST/PUT/DELETE requests to fail with CSRF error on new APIClient instances.\n def authenticate(self) -> Dict[str, str]:\n with belief_scope(\"authenticate\"):\n log.reason(\"Authenticating to Superset\", payload={\"base_url\": self.base_url})\n cached_tokens = SupersetAuthCache.get(self._auth_cache_key)\n\n if cached_tokens and cached_tokens.get(\"access_token\") and cached_tokens.get(\"csrf_token\"):\n self._tokens = cached_tokens\n # Always fetch a fresh CSRF token to establish session cookie in this session.\n # The cached access_token may still be valid; we avoid a full POST login.\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 {self._tokens['access_token']}\"},\n timeout=self.request_settings[\"timeout\"],\n )\n csrf_response.raise_for_status()\n # Update CSRF token (may have rotated)\n self._tokens[\"csrf_token\"] = csrf_response.json()[\"result\"]\n SupersetAuthCache.set(self._auth_cache_key, self._tokens)\n log.reason(\"Reused cached tokens + refreshed CSRF token\", payload={\"base_url\": self.base_url})\n except Exception as csrf_err:\n # CSRF refresh failed — likely access_token expired, do full re-auth\n log.explore(\"CSRF refresh failed, performing full re-auth\", error=str(csrf_err))\n SupersetAuthCache.invalidate(self._auth_cache_key)\n # Fall through to full login below\n self._tokens = {}\n self._authenticated = False\n\n if not self._authenticated:\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 log.reason(\"Performing full Superset login\", payload={\"login_url\": login_url, \"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 log.explore(\"Superset login returned non-200 status\", payload={\"status_code\": response.status_code, \"response\": response.text}, error=f\"HTTP {response.status_code}: {response.text[:200]}\")\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 log.reflect(\"Authenticated successfully\")\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\n self._authenticated = True\n log.reflect(\"Authenticated successfully (from cached tokens)\")\n return self._tokens\n # [/DEF:APIClient.authenticate:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "headers", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 350, - "end_line": 363, + "start_line": 326, + "end_line": 339, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52787,15 +42274,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:headers: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 # [/DEF:headers:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "request", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 365, - "end_line": 393, + "start_line": 340, + "end_line": 368, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52854,15 +42341,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:request: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: Optional[Dict] = None, raw_response: bool = False, **kwargs) -> Union[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 # [/DEF:request:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_handle_http_error", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 395, - "end_line": 420, + "start_line": 369, + "end_line": 394, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52907,15 +42394,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_handle_http_error: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 # [/DEF:_handle_http_error:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_is_dashboard_endpoint", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 422, - "end_line": 438, + "start_line": 395, + "end_line": 411, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -52953,15 +42440,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_is_dashboard_endpoint: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 # [/DEF:_is_dashboard_endpoint:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_handle_network_error", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 440, - "end_line": 455, + "start_line": 412, + "end_line": 427, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53006,15 +42493,15 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_handle_network_error: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 # [/DEF:_handle_network_error:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "upload_file", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 457, - "end_line": 486, + "start_line": 428, + "end_line": 457, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53073,15 +42560,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:upload_file: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: Optional[Dict] = None, timeout: Optional[int] = 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 # [/DEF:upload_file:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "_perform_upload", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 488, - "end_line": 514, + "start_line": 458, + "end_line": 484, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53133,15 +42620,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:_perform_upload: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: Optional[Dict], headers: Dict, timeout: Optional[int]) -> 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 log.explore(\"Upload response not valid JSON\", payload={\"preview\": response.text[:200]}, error=str(json_e))\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 # [/DEF:_perform_upload:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "fetch_paginated_count", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 516, - "end_line": 528, + "start_line": 485, + "end_line": 497, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53193,15 +42680,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:fetch_paginated_count: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 # [/DEF:fetch_paginated_count:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "fetch_paginated_data", "contract_type": "Function", "file_path": "backend/src/core/utils/network.py", - "start_line": 530, - "end_line": 569, + "start_line": 498, + "end_line": 536, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -53253,43 +42740,36 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " # [DEF:fetch_paginated_data: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 log.reason(\"Total count resolved from first page\", payload={\"total_count\": total_count})\n\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 # [/DEF:fetch_paginated_data:Function]\n" + "anchor_syntax": "region", + "body": " # #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" }, { "contract_id": "SupersetCompilationAdapter", "contract_type": "Module", "file_path": "backend/src/core/utils/superset_compilation_adapter.py", "start_line": 1, - "end_line": 508, + "end_line": 448, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "INVARIANT": "The adapter never fabricates compiled SQL locally; preview truth is delegated to Superset only.", "LAYER": "Infra", "POST": "preview and launch calls return Superset-originated artifacts or explicit errors.", "PRE": "effective template params and dataset execution reference are available.", "PURPOSE": "Interact with Superset preview compilation and SQL Lab execution endpoints using the current approved execution context.", - "SEMANTICS": [ - "dataset_review", - "superset", - "compilation_preview", - "sql_lab_launch", - "execution_truth" - ], "SIDE_EFFECT": "performs upstream Superset preview and SQL Lab calls." }, "relations": [ { "source_id": "SupersetCompilationAdapter", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "SupersetClient", "target_ref": "[SupersetClient]" }, { "source_id": "SupersetCompilationAdapter", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CompiledPreview", "target_ref": "[CompiledPreview]" } @@ -53303,484 +42783,75 @@ "actual_complexity": 4, "contract_type": "Module" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetCompilationAdapter:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: dataset_review, superset, compilation_preview, sql_lab_launch, execution_truth\n# @PURPOSE: Interact with Superset preview compilation and SQL Lab execution endpoints using the current approved execution context.\n# @LAYER: Infra\n# @RELATION: [CALLS] ->[SupersetClient]\n# @RELATION: [DEPENDS_ON] ->[CompiledPreview]\n# @PRE: effective template params and dataset execution reference are available.\n# @POST: preview and launch calls return Superset-originated artifacts or explicit errors.\n# @SIDE_EFFECT: performs upstream Superset preview and SQL Lab calls.\n# @INVARIANT: The adapter never fabricates compiled SQL locally; preview truth is delegated to Superset only.\n\nfrom __future__ import annotations\n\n# [DEF:SupersetCompilationAdapter.imports:Block]\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nfrom src.core.config_models import Environment\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.models.dataset_review import CompiledPreview, PreviewStatus\n# [/DEF:SupersetCompilationAdapter.imports:Block]\n\n\n# [DEF:PreviewCompilationPayload:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed preview payload for Superset-side compilation.\n@dataclass(frozen=True)\nclass PreviewCompilationPayload:\n session_id: str\n dataset_id: int\n preview_fingerprint: str\n template_params: Dict[str, Any]\n effective_filters: List[Dict[str, Any]]\n\n\n# [/DEF:PreviewCompilationPayload:Class]\n\n\n# [DEF:SqlLabLaunchPayload:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed SQL Lab payload for audited launch handoff.\n@dataclass(frozen=True)\nclass SqlLabLaunchPayload:\n session_id: str\n dataset_id: int\n preview_id: str\n compiled_sql: str\n template_params: Dict[str, Any]\n\n\n# [/DEF:SqlLabLaunchPayload:Class]\n\n\n# [DEF:SupersetCompilationAdapter:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Delegate preview compilation and SQL Lab launch to Superset without local SQL fabrication.\n# @RELATION: [CALLS] ->[SupersetClient]\n# @PRE: environment is configured and Superset is reachable for the target session.\n# @POST: adapter can return explicit ready/failed preview artifacts and canonical SQL Lab references.\n# @SIDE_EFFECT: issues network requests to Superset API surfaces.\nclass SupersetCompilationAdapter:\n # [DEF:SupersetCompilationAdapter.__init__:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind adapter to one Superset environment and client instance.\n def __init__(\n self, environment: Environment, client: Optional[SupersetClient] = None\n ) -> None:\n self.environment = environment\n self.client = client or SupersetClient(environment)\n\n # [/DEF:SupersetCompilationAdapter.__init__:Function]\n\n # [DEF:SupersetCompilationAdapter._supports_client_method:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Detect explicitly implemented client capabilities without treating loose mocks as real methods.\n def _supports_client_method(self, method_name: str) -> bool:\n client_dict = getattr(self.client, \"__dict__\", {})\n if method_name in client_dict:\n return callable(client_dict[method_name])\n return callable(getattr(type(self.client), method_name, None))\n\n # [/DEF:SupersetCompilationAdapter._supports_client_method:Function]\n\n # [DEF:SupersetCompilationAdapter.compile_preview:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Request Superset-side compiled SQL preview for the current effective inputs.\n # @RELATION: [CALLS] ->[SupersetCompilationAdapter._request_superset_preview]\n # @PRE: dataset_id and effective inputs are available for the current session.\n # @POST: returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.\n # @SIDE_EFFECT: performs upstream preview requests.\n # @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[CompiledPreview]\n def compile_preview(self, payload: PreviewCompilationPayload) -> CompiledPreview:\n with belief_scope(\"SupersetCompilationAdapter.compile_preview\"):\n if payload.dataset_id <= 0:\n logger.explore(\n \"Preview compilation rejected because dataset identifier is invalid\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n raise ValueError(\"dataset_id must be a positive integer\")\n\n logger.reason(\n \"Requesting Superset-generated SQL preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"template_param_count\": len(payload.template_params),\n \"filter_count\": len(payload.effective_filters),\n },\n )\n\n try:\n preview_result = self._request_superset_preview(payload)\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed with explicit upstream error\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"error\": str(exc),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=str(exc),\n compiled_at=None,\n )\n\n compiled_sql = str(preview_result.get(\"compiled_sql\") or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"Superset preview response did not include compiled SQL\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"response_keys\": sorted(preview_result.keys()),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_empty\",\n error_details=\"Superset preview response did not include compiled SQL\",\n compiled_at=None,\n )\n\n preview = CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.READY,\n compiled_sql=compiled_sql,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.utcnow(),\n )\n logger.reflect(\n \"Superset-generated SQL preview captured successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"compiled_sql_length\": len(compiled_sql),\n },\n )\n return preview\n\n # [/DEF:SupersetCompilationAdapter.compile_preview:Function]\n\n # [DEF:SupersetCompilationAdapter.mark_preview_stale:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Invalidate previous preview after mapping or value changes.\n # @PRE: preview is a persisted preview artifact or current in-memory snapshot.\n # @POST: preview status becomes stale without fabricating a replacement artifact.\n def mark_preview_stale(self, preview: CompiledPreview) -> CompiledPreview:\n preview.preview_status = PreviewStatus.STALE\n return preview\n\n # [/DEF:SupersetCompilationAdapter.mark_preview_stale:Function]\n\n # [DEF:SupersetCompilationAdapter.create_sql_lab_session:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Create the canonical audited execution session after all launch gates pass.\n # @RELATION: [CALLS] ->[SupersetCompilationAdapter._request_sql_lab_session]\n # @PRE: compiled_sql is Superset-originated and launch gates are already satisfied.\n # @POST: returns one canonical SQL Lab session reference from Superset.\n # @SIDE_EFFECT: performs upstream SQL Lab execution/session creation.\n # @DATA_CONTRACT: Input[SqlLabLaunchPayload] -> Output[str]\n def create_sql_lab_session(self, payload: SqlLabLaunchPayload) -> str:\n with belief_scope(\"SupersetCompilationAdapter.create_sql_lab_session\"):\n compiled_sql = str(payload.compiled_sql or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"SQL Lab launch rejected because compiled SQL is empty\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise ValueError(\"compiled_sql must be non-empty\")\n\n logger.reason(\n \"Creating SQL Lab execution session from Superset-originated preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"preview_id\": payload.preview_id,\n },\n )\n result = self._request_sql_lab_session(payload)\n sql_lab_session_ref = str(\n result.get(\"sql_lab_session_ref\")\n or result.get(\"query_id\")\n or result.get(\"id\")\n or result.get(\"result\", {}).get(\"id\")\n or \"\"\n ).strip()\n if not sql_lab_session_ref:\n logger.explore(\n \"Superset SQL Lab launch response did not include a stable session reference\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise RuntimeError(\n \"Superset SQL Lab launch response did not include a session reference\"\n )\n\n logger.reflect(\n \"Canonical SQL Lab session created successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n \"sql_lab_session_ref\": sql_lab_session_ref,\n },\n )\n return sql_lab_session_ref\n\n # [/DEF:SupersetCompilationAdapter.create_sql_lab_session:Function]\n\n # [DEF:SupersetCompilationAdapter._request_superset_preview:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Request preview compilation through explicit client support backed by real Superset endpoints only.\n # @RELATION: [CALLS] ->[SupersetClient.compile_dataset_preview]\n # @PRE: payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt.\n # @POST: returns one normalized upstream compilation response including the chosen strategy metadata.\n # @SIDE_EFFECT: issues one or more Superset preview requests through the client fallback chain.\n # @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[Dict[str,Any]]\n def _request_superset_preview(\n self, payload: PreviewCompilationPayload\n ) -> Dict[str, Any]:\n direct_compile_preview = getattr(self.client, \"compile_preview\", None)\n if self._supports_client_method(\"compile_preview\") and callable(\n direct_compile_preview\n ):\n try:\n logger.reason(\n \"Attempting preview compilation via direct client capability\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n response = direct_compile_preview(payload)\n except TypeError:\n response = direct_compile_preview(\n payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Direct client preview capability failed; falling back to dataset preview strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n else:\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n\n direct_compile_dataset_preview = getattr(\n self.client, \"compile_dataset_preview\", None\n )\n if self._supports_client_method(\"compile_dataset_preview\") and callable(\n direct_compile_dataset_preview\n ):\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n response = direct_compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n if self._supports_client_method(\"compile_dataset_preview\"):\n response = self.client.compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n\n errors: List[str] = []\n for endpoint in (\n f\"/dataset/{payload.dataset_id}/preview\",\n f\"/dataset/{payload.dataset_id}/sql\",\n ):\n try:\n response = self.client.network.request(\n method=\"POST\",\n endpoint=endpoint,\n data=self._dump_json(\n {\n \"template_params\": payload.template_params,\n \"effective_filters\": payload.effective_filters,\n }\n ),\n headers={\"Content-Type\": \"application/json\"},\n )\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n errors.append(f\"{endpoint}:unrecognized_response\")\n except Exception as exc:\n errors.append(f\"{endpoint}:{exc}\")\n\n raise RuntimeError(\n \"; \".join(errors) or \"Superset preview compilation failed\"\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n\n # [/DEF:SupersetCompilationAdapter._request_superset_preview:Function]\n\n # [DEF:SupersetCompilationAdapter._request_sql_lab_session:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Probe supported SQL Lab execution surfaces and return the first successful response.\n # @RELATION: [CALLS] ->[SupersetClient.get_dataset]\n # @PRE: payload carries non-empty Superset-originated SQL and a preview identifier for the current launch.\n # @POST: returns the first successful SQL Lab execution response from Superset.\n # @SIDE_EFFECT: issues Superset dataset lookup and SQL Lab execution requests.\n # @DATA_CONTRACT: Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]]\n def _request_sql_lab_session(self, payload: SqlLabLaunchPayload) -> Dict[str, Any]:\n dataset_raw = self.client.get_dataset(payload.dataset_id)\n dataset_record = (\n dataset_raw.get(\"result\", dataset_raw)\n if isinstance(dataset_raw, dict)\n else {}\n )\n database_id = (\n dataset_record.get(\"database\", {}).get(\"id\")\n if isinstance(dataset_record.get(\"database\"), dict)\n else dataset_record.get(\"database_id\")\n )\n if database_id is None:\n raise RuntimeError(\n \"Superset dataset does not expose a database identifier for SQL Lab launch\"\n )\n\n request_payload = {\n \"database_id\": database_id,\n \"sql\": payload.compiled_sql,\n \"templateParams\": payload.template_params,\n \"schema\": dataset_record.get(\"schema\"),\n \"client_id\": payload.preview_id,\n }\n candidate_calls = [\n {\"kind\": \"network\", \"target\": \"/sqllab/execute/\", \"http_method\": \"POST\"},\n {\"kind\": \"network\", \"target\": \"/sql_lab/execute/\", \"http_method\": \"POST\"},\n ]\n errors: List[str] = []\n\n for candidate in candidate_calls:\n try:\n response = self.client.network.request(\n method=candidate[\"http_method\"],\n endpoint=candidate[\"target\"],\n data=self._dump_json(request_payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n if isinstance(response, dict) and response:\n return response\n except Exception as exc:\n errors.append(f\"{candidate['target']}:{exc}\")\n logger.explore(\n \"Superset SQL Lab candidate failed\",\n extra={\"target\": candidate[\"target\"], \"error\": str(exc)},\n )\n\n raise RuntimeError(\n \"; \".join(errors) or \"No Superset SQL Lab surface accepted the request\"\n )\n\n # [/DEF:SupersetCompilationAdapter._request_sql_lab_session:Function]\n\n # [DEF:SupersetCompilationAdapter._normalize_preview_response:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Normalize candidate Superset preview responses into one compiled-sql structure.\n # @RELATION: [DEPENDS_ON] ->[CompiledPreview]\n def _normalize_preview_response(self, response: Any) -> Optional[Dict[str, Any]]:\n if not isinstance(response, dict):\n return None\n\n compiled_sql_candidates = [\n response.get(\"compiled_sql\"),\n response.get(\"sql\"),\n response.get(\"query\"),\n ]\n result_payload = response.get(\"result\")\n if isinstance(result_payload, dict):\n compiled_sql_candidates.extend(\n [\n result_payload.get(\"compiled_sql\"),\n result_payload.get(\"sql\"),\n result_payload.get(\"query\"),\n ]\n )\n\n for candidate in compiled_sql_candidates:\n compiled_sql = str(candidate or \"\").strip()\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n }\n return None\n\n # [/DEF:SupersetCompilationAdapter._normalize_preview_response:Function]\n\n # [DEF:SupersetCompilationAdapter._dump_json:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Serialize Superset request payload deterministically for network transport.\n def _dump_json(self, payload: Dict[str, Any]) -> str:\n import json\n\n return json.dumps(payload, sort_keys=True, default=str)\n\n # [/DEF:SupersetCompilationAdapter._dump_json:Function]\n\n\n# [/DEF:SupersetCompilationAdapter:Class]\n\n# [/DEF:SupersetCompilationAdapter:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetCompilationAdapter [C:4] [TYPE Module] [SEMANTICS superset, preview, compile, execution, adapter]\n# @BRIEF Interact with Superset preview compilation and SQL Lab execution endpoints using the current approved execution context.\n# @LAYER: Infra\n# @RELATION CALLS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @PRE: effective template params and dataset execution reference are available.\n# @POST: preview and launch calls return Superset-originated artifacts or explicit errors.\n# @SIDE_EFFECT: performs upstream Superset preview and SQL Lab calls.\n# @INVARIANT: The adapter never fabricates compiled SQL locally; preview truth is delegated to Superset only.\nfrom __future__ import annotations\n\n# #region SupersetCompilationAdapter.imports [TYPE Block]\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any\n\nfrom src.core.config_models import Environment\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.models.dataset_review import CompiledPreview, PreviewStatus\n\n\n# #endregion SupersetCompilationAdapter.imports\n# #region PreviewCompilationPayload [C:2] [TYPE Class]\n# @BRIEF Typed preview payload for Superset-side compilation.\n@dataclass(frozen=True)\nclass PreviewCompilationPayload:\n session_id: str\n dataset_id: int\n preview_fingerprint: str\n template_params: dict[str, Any]\n effective_filters: list[dict[str, Any]]\n# #endregion PreviewCompilationPayload\n# #region SqlLabLaunchPayload [C:2] [TYPE Class]\n# @BRIEF Typed SQL Lab payload for audited launch handoff.\n@dataclass(frozen=True)\nclass SqlLabLaunchPayload:\n session_id: str\n dataset_id: int\n preview_id: str\n compiled_sql: str\n template_params: dict[str, Any]\n# #endregion SqlLabLaunchPayload\n# #region SupersetCompilationAdapter [C:4] [TYPE Class]\n# @BRIEF Delegate preview compilation and SQL Lab launch to Superset without local SQL fabrication.\n# @RELATION CALLS -> [SupersetClient]\n# @PRE: environment is configured and Superset is reachable for the target session.\n# @POST: adapter can return explicit ready/failed preview artifacts and canonical SQL Lab references.\n# @SIDE_EFFECT: issues network requests to Superset API surfaces.\nclass SupersetCompilationAdapter:\n # #region SupersetCompilationAdapter.__init__ [TYPE Function] [C:2]\n # @PURPOSE: Bind adapter to one Superset environment and client instance.\n def __init__(\n self, environment: Environment, client: SupersetClient | None = None\n ) -> None:\n self.environment = environment\n self.client = client or SupersetClient(environment)\n # #endregion SupersetCompilationAdapter.__init__\n # #region SupersetCompilationAdapter._supports_client_method [TYPE Function] [C:2]\n # @PURPOSE: Detect explicitly implemented client capabilities without treating loose mocks as real methods.\n def _supports_client_method(self, method_name: str) -> bool:\n client_dict = getattr(self.client, \"__dict__\", {})\n if method_name in client_dict:\n return callable(client_dict[method_name])\n return callable(getattr(type(self.client), method_name, None))\n # #endregion SupersetCompilationAdapter._supports_client_method\n # #region SupersetCompilationAdapter.compile_preview [TYPE Function] [C:4]\n # @PURPOSE: Request Superset-side compiled SQL preview for the current effective inputs.\n # @RELATION: [CALLS] ->[SupersetCompilationAdapter._request_superset_preview]\n # @PRE: dataset_id and effective inputs are available for the current session.\n # @POST: returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.\n # @SIDE_EFFECT: performs upstream preview requests.\n # @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[CompiledPreview]\n def compile_preview(self, payload: PreviewCompilationPayload) -> CompiledPreview:\n with belief_scope(\"SupersetCompilationAdapter.compile_preview\"):\n if payload.dataset_id <= 0:\n logger.explore(\n \"Preview compilation rejected because dataset identifier is invalid\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n raise ValueError(\"dataset_id must be a positive integer\")\n logger.reason(\n \"Requesting Superset-generated SQL preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"template_param_count\": len(payload.template_params),\n \"filter_count\": len(payload.effective_filters),\n },\n )\n try:\n preview_result = self._request_superset_preview(payload)\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed with explicit upstream error\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"error\": str(exc),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=str(exc),\n compiled_at=None,\n )\n compiled_sql = str(preview_result.get(\"compiled_sql\") or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"Superset preview response did not include compiled SQL\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"response_keys\": sorted(preview_result.keys()),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_empty\",\n error_details=\"Superset preview response did not include compiled SQL\",\n compiled_at=None,\n )\n preview = CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.READY,\n compiled_sql=compiled_sql,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.utcnow(),\n )\n logger.reflect(\n \"Superset-generated SQL preview captured successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"compiled_sql_length\": len(compiled_sql),\n },\n )\n return preview\n # #endregion SupersetCompilationAdapter.compile_preview\n # #region SupersetCompilationAdapter.mark_preview_stale [TYPE Function] [C:2]\n # @PURPOSE: Invalidate previous preview after mapping or value changes.\n # @PRE: preview is a persisted preview artifact or current in-memory snapshot.\n # @POST: preview status becomes stale without fabricating a replacement artifact.\n def mark_preview_stale(self, preview: CompiledPreview) -> CompiledPreview:\n preview.preview_status = PreviewStatus.STALE\n return preview\n # #endregion SupersetCompilationAdapter.mark_preview_stale\n # #region SupersetCompilationAdapter.create_sql_lab_session [TYPE Function] [C:4]\n # @PURPOSE: Create the canonical audited execution session after all launch gates pass.\n # @RELATION: [CALLS] ->[SupersetCompilationAdapter._request_sql_lab_session]\n # @PRE: compiled_sql is Superset-originated and launch gates are already satisfied.\n # @POST: returns one canonical SQL Lab session reference from Superset.\n # @SIDE_EFFECT: performs upstream SQL Lab execution/session creation.\n # @DATA_CONTRACT: Input[SqlLabLaunchPayload] -> Output[str]\n def create_sql_lab_session(self, payload: SqlLabLaunchPayload) -> str:\n with belief_scope(\"SupersetCompilationAdapter.create_sql_lab_session\"):\n compiled_sql = str(payload.compiled_sql or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"SQL Lab launch rejected because compiled SQL is empty\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise ValueError(\"compiled_sql must be non-empty\")\n logger.reason(\n \"Creating SQL Lab execution session from Superset-originated preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"preview_id\": payload.preview_id,\n },\n )\n result = self._request_sql_lab_session(payload)\n sql_lab_session_ref = str(\n result.get(\"sql_lab_session_ref\")\n or result.get(\"query_id\")\n or result.get(\"id\")\n or result.get(\"result\", {}).get(\"id\")\n or \"\"\n ).strip()\n if not sql_lab_session_ref:\n logger.explore(\n \"Superset SQL Lab launch response did not include a stable session reference\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise RuntimeError(\n \"Superset SQL Lab launch response did not include a session reference\"\n )\n logger.reflect(\n \"Canonical SQL Lab session created successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n \"sql_lab_session_ref\": sql_lab_session_ref,\n },\n )\n return sql_lab_session_ref\n # #endregion SupersetCompilationAdapter.create_sql_lab_session\n # #region SupersetCompilationAdapter._request_superset_preview [TYPE Function] [C:4]\n # @PURPOSE: Request preview compilation through explicit client support backed by real Superset endpoints only.\n # @RELATION: [CALLS] ->[SupersetClient.compile_dataset_preview]\n # @PRE: payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt.\n # @POST: returns one normalized upstream compilation response including the chosen strategy metadata.\n # @SIDE_EFFECT: issues one or more Superset preview requests through the client fallback chain.\n # @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[Dict[str,Any]]\n def _request_superset_preview(\n self, payload: PreviewCompilationPayload\n ) -> dict[str, Any]:\n direct_compile_preview = getattr(self.client, \"compile_preview\", None)\n if self._supports_client_method(\"compile_preview\") and callable(\n direct_compile_preview\n ):\n try:\n logger.reason(\n \"Attempting preview compilation via direct client capability\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n response = direct_compile_preview(payload)\n except TypeError:\n response = direct_compile_preview(\n payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Direct client preview capability failed; falling back to dataset preview strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n else:\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n direct_compile_dataset_preview = getattr(\n self.client, \"compile_dataset_preview\", None\n )\n if self._supports_client_method(\"compile_dataset_preview\") and callable(\n direct_compile_dataset_preview\n ):\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n response = direct_compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n if self._supports_client_method(\"compile_dataset_preview\"):\n response = self.client.compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n errors: list[str] = []\n for endpoint in (\n f\"/dataset/{payload.dataset_id}/preview\",\n f\"/dataset/{payload.dataset_id}/sql\",\n ):\n try:\n response = self.client.network.request(\n method=\"POST\",\n endpoint=endpoint,\n data=self._dump_json(\n {\n \"template_params\": payload.template_params,\n \"effective_filters\": payload.effective_filters,\n }\n ),\n headers={\"Content-Type\": \"application/json\"},\n )\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n errors.append(f\"{endpoint}:unrecognized_response\")\n except Exception as exc:\n errors.append(f\"{endpoint}:{exc}\")\n raise RuntimeError(\n \"; \".join(errors) or \"Superset preview compilation failed\"\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n # #endregion SupersetCompilationAdapter._request_superset_preview\n # #region SupersetCompilationAdapter._request_sql_lab_session [TYPE Function] [C:4]\n # @PURPOSE: Probe supported SQL Lab execution surfaces and return the first successful response.\n # @RELATION: [CALLS] ->[SupersetClient.get_dataset]\n # @PRE: payload carries non-empty Superset-originated SQL and a preview identifier for the current launch.\n # @POST: returns the first successful SQL Lab execution response from Superset.\n # @SIDE_EFFECT: issues Superset dataset lookup and SQL Lab execution requests.\n # @DATA_CONTRACT: Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]]\n def _request_sql_lab_session(self, payload: SqlLabLaunchPayload) -> dict[str, Any]:\n dataset_raw = self.client.get_dataset(payload.dataset_id)\n dataset_record = (\n dataset_raw.get(\"result\", dataset_raw)\n if isinstance(dataset_raw, dict)\n else {}\n )\n database_id = (\n dataset_record.get(\"database\", {}).get(\"id\")\n if isinstance(dataset_record.get(\"database\"), dict)\n else dataset_record.get(\"database_id\")\n )\n if database_id is None:\n raise RuntimeError(\n \"Superset dataset does not expose a database identifier for SQL Lab launch\"\n )\n request_payload = {\n \"database_id\": database_id,\n \"sql\": payload.compiled_sql,\n \"templateParams\": payload.template_params,\n \"schema\": dataset_record.get(\"schema\"),\n \"client_id\": payload.preview_id,\n }\n candidate_calls = [\n {\"kind\": \"network\", \"target\": \"/sqllab/execute/\", \"http_method\": \"POST\"},\n {\"kind\": \"network\", \"target\": \"/sql_lab/execute/\", \"http_method\": \"POST\"},\n ]\n errors: list[str] = []\n for candidate in candidate_calls:\n try:\n response = self.client.network.request(\n method=candidate[\"http_method\"],\n endpoint=candidate[\"target\"],\n data=self._dump_json(request_payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n if isinstance(response, dict) and response:\n return response\n except Exception as exc:\n errors.append(f\"{candidate['target']}:{exc}\")\n logger.explore(\n \"Superset SQL Lab candidate failed\",\n extra={\"target\": candidate[\"target\"], \"error\": str(exc)},\n )\n raise RuntimeError(\n \"; \".join(errors) or \"No Superset SQL Lab surface accepted the request\"\n )\n # #endregion SupersetCompilationAdapter._request_sql_lab_session\n # #region SupersetCompilationAdapter._normalize_preview_response [TYPE Function] [C:3]\n # @PURPOSE: Normalize candidate Superset preview responses into one compiled-sql structure.\n # @RELATION: [DEPENDS_ON] ->[CompiledPreview]\n def _normalize_preview_response(self, response: Any) -> dict[str, Any] | None:\n if not isinstance(response, dict):\n return None\n compiled_sql_candidates = [\n response.get(\"compiled_sql\"),\n response.get(\"sql\"),\n response.get(\"query\"),\n ]\n result_payload = response.get(\"result\")\n if isinstance(result_payload, dict):\n compiled_sql_candidates.extend(\n [\n result_payload.get(\"compiled_sql\"),\n result_payload.get(\"sql\"),\n result_payload.get(\"query\"),\n ]\n )\n for candidate in compiled_sql_candidates:\n compiled_sql = str(candidate or \"\").strip()\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n }\n return None\n # #endregion SupersetCompilationAdapter._normalize_preview_response\n # #region SupersetCompilationAdapter._dump_json [TYPE Function] [C:1]\n # @PURPOSE: Serialize Superset request payload deterministically for network transport.\n def _dump_json(self, payload: dict[str, Any]) -> str:\n import json\n return json.dumps(payload, sort_keys=True, default=str)\n # #endregion SupersetCompilationAdapter._dump_json\n# #endregion SupersetCompilationAdapter\n# #endregion SupersetCompilationAdapter\n" }, { "contract_id": "SupersetCompilationAdapter.imports", "contract_type": "Block", "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 15, - "end_line": 24, + "start_line": 12, + "end_line": 23, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetCompilationAdapter.imports:Block]\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nfrom src.core.config_models import Environment\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.models.dataset_review import CompiledPreview, PreviewStatus\n# [/DEF:SupersetCompilationAdapter.imports:Block]\n" + "anchor_syntax": "region", + "body": "# #region SupersetCompilationAdapter.imports [TYPE Block]\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any\n\nfrom src.core.config_models import Environment\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.models.dataset_review import CompiledPreview, PreviewStatus\n\n\n# #endregion SupersetCompilationAdapter.imports\n" }, { "contract_id": "PreviewCompilationPayload", "contract_type": "Class", "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 27, - "end_line": 39, + "start_line": 24, + "end_line": 33, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Typed preview payload for Superset-side compilation." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:PreviewCompilationPayload:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed preview payload for Superset-side compilation.\n@dataclass(frozen=True)\nclass PreviewCompilationPayload:\n session_id: str\n dataset_id: int\n preview_fingerprint: str\n template_params: Dict[str, Any]\n effective_filters: List[Dict[str, Any]]\n\n\n# [/DEF:PreviewCompilationPayload:Class]\n" + "anchor_syntax": "region", + "body": "# #region PreviewCompilationPayload [C:2] [TYPE Class]\n# @BRIEF Typed preview payload for Superset-side compilation.\n@dataclass(frozen=True)\nclass PreviewCompilationPayload:\n session_id: str\n dataset_id: int\n preview_fingerprint: str\n template_params: dict[str, Any]\n effective_filters: list[dict[str, Any]]\n# #endregion PreviewCompilationPayload\n" }, { "contract_id": "SqlLabLaunchPayload", "contract_type": "Class", "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 42, - "end_line": 54, + "start_line": 34, + "end_line": 43, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Typed SQL Lab payload for audited launch handoff." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SqlLabLaunchPayload:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed SQL Lab payload for audited launch handoff.\n@dataclass(frozen=True)\nclass SqlLabLaunchPayload:\n session_id: str\n dataset_id: int\n preview_id: str\n compiled_sql: str\n template_params: Dict[str, Any]\n\n\n# [/DEF:SqlLabLaunchPayload:Class]\n" - }, - { - "contract_id": "SupersetCompilationAdapter.__init__", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 65, - "end_line": 74, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Bind adapter to one Superset environment and client instance." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetCompilationAdapter.__init__:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind adapter to one Superset environment and client instance.\n def __init__(\n self, environment: Environment, client: Optional[SupersetClient] = None\n ) -> None:\n self.environment = environment\n self.client = client or SupersetClient(environment)\n\n # [/DEF:SupersetCompilationAdapter.__init__:Function]\n" - }, - { - "contract_id": "SupersetCompilationAdapter._supports_client_method", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 76, - "end_line": 85, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Detect explicitly implemented client capabilities without treating loose mocks as real methods." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetCompilationAdapter._supports_client_method:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Detect explicitly implemented client capabilities without treating loose mocks as real methods.\n def _supports_client_method(self, method_name: str) -> bool:\n client_dict = getattr(self.client, \"__dict__\", {})\n if method_name in client_dict:\n return callable(client_dict[method_name])\n return callable(getattr(type(self.client), method_name, None))\n\n # [/DEF:SupersetCompilationAdapter._supports_client_method:Function]\n" - }, - { - "contract_id": "SupersetCompilationAdapter.compile_preview", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 87, - "end_line": 180, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[PreviewCompilationPayload] -> Output[CompiledPreview]", - "POST": "returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.", - "PRE": "dataset_id and effective inputs are available for the current session.", - "PURPOSE": "Request Superset-side compiled SQL preview for the current effective inputs.", - "SIDE_EFFECT": "performs upstream preview requests." - }, - "relations": [ - { - "source_id": "SupersetCompilationAdapter.compile_preview", - "relation_type": "[CALLS]", - "target_id": "SupersetCompilationAdapter._request_superset_preview", - "target_ref": "[SupersetCompilationAdapter._request_superset_preview]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetCompilationAdapter.compile_preview:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Request Superset-side compiled SQL preview for the current effective inputs.\n # @RELATION: [CALLS] ->[SupersetCompilationAdapter._request_superset_preview]\n # @PRE: dataset_id and effective inputs are available for the current session.\n # @POST: returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.\n # @SIDE_EFFECT: performs upstream preview requests.\n # @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[CompiledPreview]\n def compile_preview(self, payload: PreviewCompilationPayload) -> CompiledPreview:\n with belief_scope(\"SupersetCompilationAdapter.compile_preview\"):\n if payload.dataset_id <= 0:\n logger.explore(\n \"Preview compilation rejected because dataset identifier is invalid\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n raise ValueError(\"dataset_id must be a positive integer\")\n\n logger.reason(\n \"Requesting Superset-generated SQL preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"template_param_count\": len(payload.template_params),\n \"filter_count\": len(payload.effective_filters),\n },\n )\n\n try:\n preview_result = self._request_superset_preview(payload)\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed with explicit upstream error\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"error\": str(exc),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=str(exc),\n compiled_at=None,\n )\n\n compiled_sql = str(preview_result.get(\"compiled_sql\") or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"Superset preview response did not include compiled SQL\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"response_keys\": sorted(preview_result.keys()),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_empty\",\n error_details=\"Superset preview response did not include compiled SQL\",\n compiled_at=None,\n )\n\n preview = CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.READY,\n compiled_sql=compiled_sql,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.utcnow(),\n )\n logger.reflect(\n \"Superset-generated SQL preview captured successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"compiled_sql_length\": len(compiled_sql),\n },\n )\n return preview\n\n # [/DEF:SupersetCompilationAdapter.compile_preview:Function]\n" - }, - { - "contract_id": "SupersetCompilationAdapter.mark_preview_stale", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 182, - "end_line": 191, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "POST": "preview status becomes stale without fabricating a replacement artifact.", - "PRE": "preview is a persisted preview artifact or current in-memory snapshot.", - "PURPOSE": "Invalidate previous preview after mapping or value changes." - }, - "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": "def", - "body": " # [DEF:SupersetCompilationAdapter.mark_preview_stale:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Invalidate previous preview after mapping or value changes.\n # @PRE: preview is a persisted preview artifact or current in-memory snapshot.\n # @POST: preview status becomes stale without fabricating a replacement artifact.\n def mark_preview_stale(self, preview: CompiledPreview) -> CompiledPreview:\n preview.preview_status = PreviewStatus.STALE\n return preview\n\n # [/DEF:SupersetCompilationAdapter.mark_preview_stale:Function]\n" - }, - { - "contract_id": "SupersetCompilationAdapter.create_sql_lab_session", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 193, - "end_line": 252, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[SqlLabLaunchPayload] -> Output[str]", - "POST": "returns one canonical SQL Lab session reference from Superset.", - "PRE": "compiled_sql is Superset-originated and launch gates are already satisfied.", - "PURPOSE": "Create the canonical audited execution session after all launch gates pass.", - "SIDE_EFFECT": "performs upstream SQL Lab execution/session creation." - }, - "relations": [ - { - "source_id": "SupersetCompilationAdapter.create_sql_lab_session", - "relation_type": "[CALLS]", - "target_id": "SupersetCompilationAdapter._request_sql_lab_session", - "target_ref": "[SupersetCompilationAdapter._request_sql_lab_session]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetCompilationAdapter.create_sql_lab_session:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Create the canonical audited execution session after all launch gates pass.\n # @RELATION: [CALLS] ->[SupersetCompilationAdapter._request_sql_lab_session]\n # @PRE: compiled_sql is Superset-originated and launch gates are already satisfied.\n # @POST: returns one canonical SQL Lab session reference from Superset.\n # @SIDE_EFFECT: performs upstream SQL Lab execution/session creation.\n # @DATA_CONTRACT: Input[SqlLabLaunchPayload] -> Output[str]\n def create_sql_lab_session(self, payload: SqlLabLaunchPayload) -> str:\n with belief_scope(\"SupersetCompilationAdapter.create_sql_lab_session\"):\n compiled_sql = str(payload.compiled_sql or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"SQL Lab launch rejected because compiled SQL is empty\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise ValueError(\"compiled_sql must be non-empty\")\n\n logger.reason(\n \"Creating SQL Lab execution session from Superset-originated preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"preview_id\": payload.preview_id,\n },\n )\n result = self._request_sql_lab_session(payload)\n sql_lab_session_ref = str(\n result.get(\"sql_lab_session_ref\")\n or result.get(\"query_id\")\n or result.get(\"id\")\n or result.get(\"result\", {}).get(\"id\")\n or \"\"\n ).strip()\n if not sql_lab_session_ref:\n logger.explore(\n \"Superset SQL Lab launch response did not include a stable session reference\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise RuntimeError(\n \"Superset SQL Lab launch response did not include a session reference\"\n )\n\n logger.reflect(\n \"Canonical SQL Lab session created successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n \"sql_lab_session_ref\": sql_lab_session_ref,\n },\n )\n return sql_lab_session_ref\n\n # [/DEF:SupersetCompilationAdapter.create_sql_lab_session:Function]\n" - }, - { - "contract_id": "SupersetCompilationAdapter._request_superset_preview", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 254, - "end_line": 398, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[PreviewCompilationPayload] -> Output[Dict[str,Any]]", - "POST": "returns one normalized upstream compilation response including the chosen strategy metadata.", - "PRE": "payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt.", - "PURPOSE": "Request preview compilation through explicit client support backed by real Superset endpoints only.", - "SIDE_EFFECT": "issues one or more Superset preview requests through the client fallback chain." - }, - "relations": [ - { - "source_id": "SupersetCompilationAdapter._request_superset_preview", - "relation_type": "[CALLS]", - "target_id": "SupersetClient.compile_dataset_preview", - "target_ref": "[SupersetClient.compile_dataset_preview]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetCompilationAdapter._request_superset_preview:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Request preview compilation through explicit client support backed by real Superset endpoints only.\n # @RELATION: [CALLS] ->[SupersetClient.compile_dataset_preview]\n # @PRE: payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt.\n # @POST: returns one normalized upstream compilation response including the chosen strategy metadata.\n # @SIDE_EFFECT: issues one or more Superset preview requests through the client fallback chain.\n # @DATA_CONTRACT: Input[PreviewCompilationPayload] -> Output[Dict[str,Any]]\n def _request_superset_preview(\n self, payload: PreviewCompilationPayload\n ) -> Dict[str, Any]:\n direct_compile_preview = getattr(self.client, \"compile_preview\", None)\n if self._supports_client_method(\"compile_preview\") and callable(\n direct_compile_preview\n ):\n try:\n logger.reason(\n \"Attempting preview compilation via direct client capability\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n response = direct_compile_preview(payload)\n except TypeError:\n response = direct_compile_preview(\n payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Direct client preview capability failed; falling back to dataset preview strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n else:\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n\n direct_compile_dataset_preview = getattr(\n self.client, \"compile_dataset_preview\", None\n )\n if self._supports_client_method(\"compile_dataset_preview\") and callable(\n direct_compile_dataset_preview\n ):\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n response = direct_compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n if self._supports_client_method(\"compile_dataset_preview\"):\n response = self.client.compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n\n errors: List[str] = []\n for endpoint in (\n f\"/dataset/{payload.dataset_id}/preview\",\n f\"/dataset/{payload.dataset_id}/sql\",\n ):\n try:\n response = self.client.network.request(\n method=\"POST\",\n endpoint=endpoint,\n data=self._dump_json(\n {\n \"template_params\": payload.template_params,\n \"effective_filters\": payload.effective_filters,\n }\n ),\n headers={\"Content-Type\": \"application/json\"},\n )\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n errors.append(f\"{endpoint}:unrecognized_response\")\n except Exception as exc:\n errors.append(f\"{endpoint}:{exc}\")\n\n raise RuntimeError(\n \"; \".join(errors) or \"Superset preview compilation failed\"\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n\n # [/DEF:SupersetCompilationAdapter._request_superset_preview:Function]\n" - }, - { - "contract_id": "SupersetCompilationAdapter._request_sql_lab_session", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 400, - "end_line": 459, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]]", - "POST": "returns the first successful SQL Lab execution response from Superset.", - "PRE": "payload carries non-empty Superset-originated SQL and a preview identifier for the current launch.", - "PURPOSE": "Probe supported SQL Lab execution surfaces and return the first successful response.", - "SIDE_EFFECT": "issues Superset dataset lookup and SQL Lab execution requests." - }, - "relations": [ - { - "source_id": "SupersetCompilationAdapter._request_sql_lab_session", - "relation_type": "[CALLS]", - "target_id": "SupersetClient.get_dataset", - "target_ref": "[SupersetClient.get_dataset]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetCompilationAdapter._request_sql_lab_session:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Probe supported SQL Lab execution surfaces and return the first successful response.\n # @RELATION: [CALLS] ->[SupersetClient.get_dataset]\n # @PRE: payload carries non-empty Superset-originated SQL and a preview identifier for the current launch.\n # @POST: returns the first successful SQL Lab execution response from Superset.\n # @SIDE_EFFECT: issues Superset dataset lookup and SQL Lab execution requests.\n # @DATA_CONTRACT: Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]]\n def _request_sql_lab_session(self, payload: SqlLabLaunchPayload) -> Dict[str, Any]:\n dataset_raw = self.client.get_dataset(payload.dataset_id)\n dataset_record = (\n dataset_raw.get(\"result\", dataset_raw)\n if isinstance(dataset_raw, dict)\n else {}\n )\n database_id = (\n dataset_record.get(\"database\", {}).get(\"id\")\n if isinstance(dataset_record.get(\"database\"), dict)\n else dataset_record.get(\"database_id\")\n )\n if database_id is None:\n raise RuntimeError(\n \"Superset dataset does not expose a database identifier for SQL Lab launch\"\n )\n\n request_payload = {\n \"database_id\": database_id,\n \"sql\": payload.compiled_sql,\n \"templateParams\": payload.template_params,\n \"schema\": dataset_record.get(\"schema\"),\n \"client_id\": payload.preview_id,\n }\n candidate_calls = [\n {\"kind\": \"network\", \"target\": \"/sqllab/execute/\", \"http_method\": \"POST\"},\n {\"kind\": \"network\", \"target\": \"/sql_lab/execute/\", \"http_method\": \"POST\"},\n ]\n errors: List[str] = []\n\n for candidate in candidate_calls:\n try:\n response = self.client.network.request(\n method=candidate[\"http_method\"],\n endpoint=candidate[\"target\"],\n data=self._dump_json(request_payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n if isinstance(response, dict) and response:\n return response\n except Exception as exc:\n errors.append(f\"{candidate['target']}:{exc}\")\n logger.explore(\n \"Superset SQL Lab candidate failed\",\n extra={\"target\": candidate[\"target\"], \"error\": str(exc)},\n )\n\n raise RuntimeError(\n \"; \".join(errors) or \"No Superset SQL Lab surface accepted the request\"\n )\n\n # [/DEF:SupersetCompilationAdapter._request_sql_lab_session:Function]\n" - }, - { - "contract_id": "SupersetCompilationAdapter._normalize_preview_response", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 461, - "end_line": 493, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Normalize candidate Superset preview responses into one compiled-sql structure." - }, - "relations": [ - { - "source_id": "SupersetCompilationAdapter._normalize_preview_response", - "relation_type": "[DEPENDS_ON]", - "target_id": "CompiledPreview", - "target_ref": "[CompiledPreview]" - } - ], - "schema_warnings": [ - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetCompilationAdapter._normalize_preview_response:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Normalize candidate Superset preview responses into one compiled-sql structure.\n # @RELATION: [DEPENDS_ON] ->[CompiledPreview]\n def _normalize_preview_response(self, response: Any) -> Optional[Dict[str, Any]]:\n if not isinstance(response, dict):\n return None\n\n compiled_sql_candidates = [\n response.get(\"compiled_sql\"),\n response.get(\"sql\"),\n response.get(\"query\"),\n ]\n result_payload = response.get(\"result\")\n if isinstance(result_payload, dict):\n compiled_sql_candidates.extend(\n [\n result_payload.get(\"compiled_sql\"),\n result_payload.get(\"sql\"),\n result_payload.get(\"query\"),\n ]\n )\n\n for candidate in compiled_sql_candidates:\n compiled_sql = str(candidate or \"\").strip()\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n }\n return None\n\n # [/DEF:SupersetCompilationAdapter._normalize_preview_response:Function]\n" - }, - { - "contract_id": "SupersetCompilationAdapter._dump_json", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_compilation_adapter.py", - "start_line": 495, - "end_line": 503, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Serialize Superset request payload deterministically for network transport." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetCompilationAdapter._dump_json:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Serialize Superset request payload deterministically for network transport.\n def _dump_json(self, payload: Dict[str, Any]) -> str:\n import json\n\n return json.dumps(payload, sort_keys=True, default=str)\n\n # [/DEF:SupersetCompilationAdapter._dump_json:Function]\n" + "anchor_syntax": "region", + "body": "# #region SqlLabLaunchPayload [C:2] [TYPE Class]\n# @BRIEF Typed SQL Lab payload for audited launch handoff.\n@dataclass(frozen=True)\nclass SqlLabLaunchPayload:\n session_id: str\n dataset_id: int\n preview_id: str\n compiled_sql: str\n template_params: dict[str, Any]\n# #endregion SqlLabLaunchPayload\n" }, { "contract_id": "SupersetContextExtractorPackage", "contract_type": "Module", "file_path": "backend/src/core/utils/superset_context_extractor/__init__.py", "start_line": 1, - "end_line": 19, + "end_line": 17, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "INVARIANT": "Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context.", "LAYER": "Infra", "POST": "Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary.", "PRE": "Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource.", "PURPOSE": "Recover dataset and dashboard context from Superset links while preserving explicit partial-recovery markers.", "RATIONALE": "Decomposed from monolithic superset_context_extractor.py (1397 lines) into", - "SEMANTICS": [ - "dataset_review", - "superset", - "link_parsing", - "context_recovery", - "partial_recovery" - ], "SIDE_EFFECT": "Performs upstream Superset API reads." }, "relations": [ @@ -53823,19 +42894,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetContextExtractorPackage:Module]\n# @COMPLEXITY: 4\n# @LAYER: Infra\n# @SEMANTICS: dataset_review, superset, link_parsing, context_recovery, partial_recovery\n# @PURPOSE: Recover dataset and dashboard context from Superset links while preserving explicit partial-recovery markers.\n# @RELATION: DEPENDS_ON -> [ImportedFilter]\n# @RELATION: DEPENDS_ON -> [TemplateVariable]\n# @RELATION: DEPENDS_ON -> [SupersetClient]\n# @PRE: Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource.\n# @POST: Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary.\n# @SIDE_EFFECT: Performs upstream Superset API reads.\n# @INVARIANT: Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context.\n#\n# @RATIONALE: Decomposed from monolithic superset_context_extractor.py (1397 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.utils.superset_context_extractor` without changes.\n# @REJECTED: Keeping a single 1397-line file — violates fractal limit INV_7.\n# [/DEF:SupersetContextExtractorPackage:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetContextExtractorPackage [C:4] [TYPE Module] [SEMANTICS superset, package, dashboard, dataset]\n# @LAYER: Infra\n# @BRIEF Recover dataset and dashboard context from Superset links while preserving explicit partial-recovery markers.\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @PRE: Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource.\n# @POST: Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary.\n# @SIDE_EFFECT: Performs upstream Superset API reads.\n# @INVARIANT: Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context.\n#\n# @RATIONALE: Decomposed from monolithic superset_context_extractor.py (1397 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.utils.superset_context_extractor` without changes.\n# @REJECTED: Keeping a single 1397-line file — violates fractal limit INV_7.\n# #endregion SupersetContextExtractorPackage\n" }, { "contract_id": "SupersetContextExtractor", "contract_type": "Class", "file_path": "backend/src/core/utils/superset_context_extractor/__init__.py", - "start_line": 29, - "end_line": 57, + "start_line": 27, + "end_line": 54, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "POST": "extractor instance is ready to parse links against one Superset environment.", "PRE": "constructor receives a configured environment with a usable Superset base URL.", "PURPOSE": "Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake.", @@ -53880,31 +42951,24 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetContextExtractor:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake.\n# @RELATION: DEPENDS_ON -> [Environment]\n# @RELATION: INHERITS -> [SupersetContextExtractorBase]\n# @RELATION: INHERITS -> [SupersetContextParsingMixin]\n# @RELATION: INHERITS -> [SupersetContextRecoveryMixin]\n# @RELATION: INHERITS -> [SupersetContextTemplatesMixin]\n# @RELATION: INHERITS -> [SupersetContextFiltersExtractMixin]\n# @PRE: constructor receives a configured environment with a usable Superset base URL.\n# @POST: extractor instance is ready to parse links against one Superset environment.\n# @SIDE_EFFECT: downstream parse operations may call Superset APIs through SupersetClient.\nclass SupersetContextExtractor(\n SupersetContextFiltersExtractMixin,\n SupersetContextRecoveryMixin,\n SupersetContextTemplatesMixin,\n SupersetContextParsingMixin,\n SupersetContextExtractorBase,\n):\n \"\"\"Composed Superset context extractor.\n\n MRO: FiltersExtractMixin -> RecoveryMixin -> TemplatesMixin -> ParsingMixin -> Base.\n All consumers continue to use:\n from src.core.utils.superset_context_extractor import SupersetContextExtractor\n \"\"\"\n pass\n\n\n# [/DEF:SupersetContextExtractor:Class]\n" + "anchor_syntax": "region", + "body": "# #region SupersetContextExtractor [C:4] [TYPE Class]\n# @BRIEF Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake.\n# @RELATION DEPENDS_ON -> [Environment]\n# @RELATION INHERITS -> [SupersetContextExtractorBase]\n# @RELATION INHERITS -> [SupersetContextParsingMixin]\n# @RELATION INHERITS -> [SupersetContextRecoveryMixin]\n# @RELATION INHERITS -> [SupersetContextTemplatesMixin]\n# @RELATION INHERITS -> [SupersetContextFiltersExtractMixin]\n# @PRE: constructor receives a configured environment with a usable Superset base URL.\n# @POST: extractor instance is ready to parse links against one Superset environment.\n# @SIDE_EFFECT: downstream parse operations may call Superset APIs through SupersetClient.\nclass SupersetContextExtractor(\n SupersetContextFiltersExtractMixin,\n SupersetContextRecoveryMixin,\n SupersetContextTemplatesMixin,\n SupersetContextParsingMixin,\n SupersetContextExtractorBase,\n):\n \"\"\"Composed Superset context extractor.\n\n MRO: FiltersExtractMixin -> RecoveryMixin -> TemplatesMixin -> ParsingMixin -> Base.\n All consumers continue to use:\n from src.core.utils.superset_context_extractor import SupersetContextExtractor\n \"\"\"\n pass\n\n\n# #endregion SupersetContextExtractor\n" }, { "contract_id": "SupersetContextExtractorBase", "contract_type": "Module", "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", "start_line": 1, - "end_line": 13, + "end_line": 11, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "INVARIANT": "Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context.", "LAYER": "Infra", "POST": "Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary.", "PRE": "Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource.", "PURPOSE": "Base class and helpers for recovering dataset/dashboard context from Superset links with explicit partial-recovery markers.", - "SEMANTICS": [ - "dataset_review", - "superset", - "link_parsing", - "context_recovery", - "partial_recovery" - ], "SIDE_EFFECT": "Performs upstream Superset API reads." }, "relations": [ @@ -53938,220 +43002,52 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetContextExtractorBase:Module]\n# @COMPLEXITY: 4\n# @LAYER: Infra\n# @SEMANTICS: dataset_review, superset, link_parsing, context_recovery, partial_recovery\n# @PURPOSE: Base class and helpers for recovering dataset/dashboard context from Superset links with explicit partial-recovery markers.\n# @RELATION: DEPENDS_ON -> [ImportedFilter]\n# @RELATION: DEPENDS_ON -> [TemplateVariable]\n# @RELATION: CALLS -> [SupersetClient]\n# @PRE: Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource.\n# @POST: Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary.\n# @SIDE_EFFECT: Performs upstream Superset API reads.\n# @INVARIANT: Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context.\n# [/DEF:SupersetContextExtractorBase:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetContextExtractorBase [C:4] [TYPE Module] [SEMANTICS superset, context, extract, base, recovery]\n# @LAYER: Infra\n# @BRIEF Base class and helpers for recovering dataset/dashboard context from Superset links with explicit partial-recovery markers.\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION CALLS -> [SupersetClient]\n# @PRE: Superset link or dataset reference must be parseable enough to resolve an environment-scoped target resource.\n# @POST: Returns the best available recovered context with explicit provenance and partial-recovery markers when necessary.\n# @SIDE_EFFECT: Performs upstream Superset API reads.\n# @INVARIANT: Partial recovery is surfaced explicitly and never misrepresented as fully confirmed context.\n# #endregion SupersetContextExtractorBase\n" }, { "contract_id": "_base_imports", "contract_type": "Block", "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 15, - "end_line": 28, + "start_line": 12, + "end_line": 24, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_base_imports:Block]\nfrom __future__ import annotations\n\nimport json\nimport re\nfrom copy import deepcopy\nfrom dataclasses import dataclass, field\nfrom typing import Any, Dict, List, Optional, Set, cast\nfrom urllib.parse import parse_qs, unquote, urlparse\n\nfrom ...config_models import Environment\nfrom ...logger import belief_scope, logger\nfrom ...superset_client import SupersetClient\n# [/DEF:_base_imports:Block]\n" + "anchor_syntax": "region", + "body": "# #region _base_imports [TYPE Block]\nfrom __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass, field\nfrom typing import Any, cast\nfrom urllib.parse import unquote\n\nfrom ...config_models import Environment\nfrom ...logger import logger\nfrom ...superset_client import SupersetClient\n\n# #endregion _base_imports\n" }, { "contract_id": "SupersetParsedContext", "contract_type": "Class", "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 33, - "end_line": 51, + "start_line": 26, + "end_line": 41, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Normalized output of Superset link parsing for session intake and recovery." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetParsedContext:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalized output of Superset link parsing for session intake and recovery.\n@dataclass\nclass SupersetParsedContext:\n source_url: str\n dataset_ref: str\n dataset_id: Optional[int] = None\n dashboard_id: Optional[int] = None\n chart_id: Optional[int] = None\n resource_type: str = \"unknown\"\n query_state: Dict[str, Any] = field(default_factory=dict)\n imported_filters: List[Dict[str, Any]] = field(default_factory=list)\n unresolved_references: List[str] = field(default_factory=list)\n partial_recovery: bool = False\n dataset_payload: Optional[Dict[str, Any]] = None\n\n\n# [/DEF:SupersetParsedContext:Class]\n" - }, - { - "contract_id": "SupersetContextExtractorBase.__init__", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 62, - "end_line": 71, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Bind extractor to one Superset environment and client instance." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase.__init__:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind extractor to one Superset environment and client instance.\n def __init__(\n self, environment: Environment, client: Optional[SupersetClient] = None\n ) -> None:\n self.environment = environment\n self.client = client or SupersetClient(environment)\n\n # [/DEF:SupersetContextExtractorBase.__init__:Function]\n" - }, - { - "contract_id": "SupersetContextExtractorBase.build_recovery_summary", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 73, - "end_line": 89, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Summarize recovered, partial, and unresolved context for session state and UX." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase.build_recovery_summary:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Summarize recovered, partial, and unresolved context for session state and UX.\n def build_recovery_summary(\n self, parsed_context: SupersetParsedContext\n ) -> Dict[str, Any]:\n return {\n \"dataset_ref\": parsed_context.dataset_ref,\n \"dataset_id\": parsed_context.dataset_id,\n \"dashboard_id\": parsed_context.dashboard_id,\n \"chart_id\": parsed_context.chart_id,\n \"partial_recovery\": parsed_context.partial_recovery,\n \"unresolved_references\": list(parsed_context.unresolved_references),\n \"imported_filter_count\": len(parsed_context.imported_filters),\n }\n\n # [/DEF:SupersetContextExtractorBase.build_recovery_summary:Function]\n" - }, - { - "contract_id": "SupersetContextExtractorBase._extract_numeric_identifier", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 91, - "end_line": 112, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Extract a numeric identifier from a REST-like Superset URL path." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase._extract_numeric_identifier:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Extract a numeric identifier from a REST-like Superset URL path.\n def _extract_numeric_identifier(\n self, path_parts: List[str], resource_name: str\n ) -> Optional[int]:\n if resource_name not in path_parts:\n return None\n try:\n resource_index = path_parts.index(resource_name)\n except ValueError:\n return None\n\n if resource_index + 1 >= len(path_parts):\n return None\n\n candidate = str(path_parts[resource_index + 1]).strip()\n if not candidate.isdigit():\n return None\n return int(candidate)\n\n # [/DEF:SupersetContextExtractorBase._extract_numeric_identifier:Function]\n" - }, - { - "contract_id": "SupersetContextExtractorBase._extract_dashboard_reference", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 114, - "end_line": 133, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Extract a dashboard id-or-slug reference from a Superset URL path." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase._extract_dashboard_reference:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Extract a dashboard id-or-slug reference from a Superset URL path.\n def _extract_dashboard_reference(self, path_parts: List[str]) -> Optional[str]:\n if \"dashboard\" not in path_parts:\n return None\n try:\n resource_index = path_parts.index(\"dashboard\")\n except ValueError:\n return None\n\n if resource_index + 1 >= len(path_parts):\n return None\n\n candidate = str(path_parts[resource_index + 1]).strip()\n if not candidate or candidate == \"p\":\n return None\n return candidate\n\n # [/DEF:SupersetContextExtractorBase._extract_dashboard_reference:Function]\n" - }, - { - "contract_id": "SupersetContextExtractorBase._extract_dashboard_permalink_key", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 135, - "end_line": 155, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Extract a dashboard permalink key from a Superset URL path." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase._extract_dashboard_permalink_key:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Extract a dashboard permalink key from a Superset URL path.\n def _extract_dashboard_permalink_key(self, path_parts: List[str]) -> Optional[str]:\n if \"dashboard\" not in path_parts:\n return None\n try:\n resource_index = path_parts.index(\"dashboard\")\n except ValueError:\n return None\n\n if resource_index + 2 >= len(path_parts):\n return None\n\n permalink_marker = str(path_parts[resource_index + 1]).strip()\n permalink_key = str(path_parts[resource_index + 2]).strip()\n if permalink_marker != \"p\" or not permalink_key:\n return None\n return permalink_key\n\n # [/DEF:SupersetContextExtractorBase._extract_dashboard_permalink_key:Function]\n" - }, - { - "contract_id": "SupersetContextExtractorBase._extract_dashboard_id_from_state", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 157, - "end_line": 166, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Extract a dashboard identifier from returned permalink state when present." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase._extract_dashboard_id_from_state:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Extract a dashboard identifier from returned permalink state when present.\n def _extract_dashboard_id_from_state(self, state: Dict[str, Any]) -> Optional[int]:\n return self._search_nested_numeric_key(\n payload=state,\n candidate_keys={\"dashboardId\", \"dashboard_id\", \"dashboard_id_value\"},\n )\n\n # [/DEF:SupersetContextExtractorBase._extract_dashboard_id_from_state:Function]\n" - }, - { - "contract_id": "SupersetContextExtractorBase._extract_chart_id_from_state", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 168, - "end_line": 177, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Extract a chart identifier from returned permalink state when dashboard id is absent." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase._extract_chart_id_from_state:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Extract a chart identifier from returned permalink state when dashboard id is absent.\n def _extract_chart_id_from_state(self, state: Dict[str, Any]) -> Optional[int]:\n return self._search_nested_numeric_key(\n payload=state,\n candidate_keys={\"slice_id\", \"sliceId\", \"chartId\", \"chart_id\"},\n )\n\n # [/DEF:SupersetContextExtractorBase._extract_chart_id_from_state:Function]\n" - }, - { - "contract_id": "SupersetContextExtractorBase._search_nested_numeric_key", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 179, - "end_line": 204, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Recursively search nested dict/list payloads for the first numeric value under a candidate key set." - }, - "relations": [ - { - "source_id": "SupersetContextExtractorBase._search_nested_numeric_key", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetContextExtractorBase", - "target_ref": "[SupersetContextExtractorBase]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase._search_nested_numeric_key:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Recursively search nested dict/list payloads for the first numeric value under a candidate key set.\n # @RELATION: DEPENDS_ON -> [SupersetContextExtractorBase]\n def _search_nested_numeric_key(\n self, payload: Any, candidate_keys: Set[str]\n ) -> Optional[int]:\n if isinstance(payload, dict):\n for key, value in payload.items():\n if key in candidate_keys:\n try:\n if value is not None:\n return int(value)\n except (TypeError, ValueError):\n pass\n found = self._search_nested_numeric_key(value, candidate_keys)\n if found is not None:\n return found\n elif isinstance(payload, list):\n for item in payload:\n found = self._search_nested_numeric_key(item, candidate_keys)\n if found is not None:\n return found\n return None\n\n # [/DEF:SupersetContextExtractorBase._search_nested_numeric_key:Function]\n" - }, - { - "contract_id": "SupersetContextExtractorBase._decode_query_state", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_base.py", - "start_line": 206, - "end_line": 228, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Decode query-string structures used by Superset URL state transport." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextExtractorBase._decode_query_state:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Decode query-string structures used by Superset URL state transport.\n def _decode_query_state(self, query_params: Dict[str, List[str]]) -> Dict[str, Any]:\n query_state: Dict[str, Any] = {}\n for key, values in query_params.items():\n if not values:\n continue\n raw_value = values[-1]\n decoded_value = unquote(raw_value)\n if key in {\"native_filters\", \"form_data\", \"q\"}:\n try:\n query_state[key] = json.loads(decoded_value)\n continue\n except Exception:\n logger.explore(\n \"Failed to decode structured Superset query state; preserving raw value\",\n extra={\"key\": key},\n )\n query_state[key] = decoded_value\n return query_state\n\n # [/DEF:SupersetContextExtractorBase._decode_query_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region SupersetParsedContext [C:2] [TYPE Class]\n# @BRIEF Normalized output of Superset link parsing for session intake and recovery.\n@dataclass\nclass SupersetParsedContext:\n source_url: str\n dataset_ref: str\n dataset_id: int | None = None\n dashboard_id: int | None = None\n chart_id: int | None = None\n resource_type: str = \"unknown\"\n query_state: dict[str, Any] = field(default_factory=dict)\n imported_filters: list[dict[str, Any]] = field(default_factory=list)\n unresolved_references: list[str] = field(default_factory=list)\n partial_recovery: bool = False\n dataset_payload: dict[str, Any] | None = None\n# #endregion SupersetParsedContext\n" }, { "contract_id": "SupersetContextFiltersExtractMixin", "contract_type": "Module", "file_path": "backend/src/core/utils/superset_context_extractor/_filters.py", "start_line": 1, - "end_line": 7, + "end_line": 5, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Extract normalized imported filters from decoded Superset query state (native_filters, dataMask, native_filter_state, form_data).", - "SEMANTICS": [ - "superset", - "filters", - "extraction", - "query_state", - "dataMask", - "native_filters" - ] + "PURPOSE": "Extract normalized imported filters from decoded Superset query state (native_filters, dataMask, native_filter_state, form_data)." }, "relations": [ { @@ -54162,59 +43058,35 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetContextFiltersExtractMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, filters, extraction, query_state, dataMask, native_filters\n# @PURPOSE: Extract normalized imported filters from decoded Superset query state (native_filters, dataMask, native_filter_state, form_data).\n# @RELATION: DEPENDS_ON -> [SupersetContextExtractorBase]\n# [/DEF:SupersetContextFiltersExtractMixin:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetContextFiltersExtractMixin [C:3] [TYPE Module] [SEMANTICS superset, filter, native, context, extract]\n# @LAYER: Infra\n# @BRIEF Extract normalized imported filters from decoded Superset query state (native_filters, dataMask, native_filter_state, form_data).\n# @RELATION DEPENDS_ON -> [SupersetContextExtractorBase]\n# #endregion SupersetContextFiltersExtractMixin\n" }, { "contract_id": "_filters_imports", "contract_type": "Block", "file_path": "backend/src/core/utils/superset_context_extractor/_filters.py", - "start_line": 9, - "end_line": 18, + "start_line": 6, + "end_line": 15, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_filters_imports:Block]\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nfrom typing import Any, Dict, List, cast\n\nfrom ...logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# [/DEF:_filters_imports:Block]\n" - }, - { - "contract_id": "SupersetContextFiltersExtractMixin._extract_imported_filters", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_filters.py", - "start_line": 25, - "end_line": 258, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Normalize imported filters from decoded query state without fabricating missing values." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextFiltersExtractMixin._extract_imported_filters:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize imported filters from decoded query state without fabricating missing values.\n def _extract_imported_filters(\n self, query_state: Dict[str, Any]\n ) -> List[Dict[str, Any]]:\n imported_filters: List[Dict[str, Any]] = []\n\n native_filters_payload = query_state.get(\"native_filters\")\n if isinstance(native_filters_payload, list):\n for index, item in enumerate(native_filters_payload):\n if not isinstance(item, dict):\n continue\n filter_name = (\n item.get(\"filter_name\")\n or item.get(\"column\")\n or item.get(\"name\")\n or f\"native_filter_{index}\"\n )\n direct_clause = None\n if item.get(\"column\") and (\"value\" in item or \"val\" in item):\n direct_clause = {\n \"col\": item.get(\"column\"),\n \"op\": item.get(\"op\")\n or (\n \"IN\"\n if isinstance(item.get(\"value\"), list)\n else \"==\"\n ),\n \"val\": item.get(\"val\", item.get(\"value\")),\n }\n imported_filters.append(\n {\n \"filter_name\": str(filter_name),\n \"raw_value\": item.get(\"value\"),\n \"display_name\": item.get(\"label\") or item.get(\"name\"),\n \"normalized_value\": {\n \"filter_clauses\": [direct_clause]\n if isinstance(direct_clause, dict)\n else [],\n \"extra_form_data\": {},\n \"value_origin\": \"native_filters\",\n },\n \"source\": \"superset_url\",\n \"recovery_status\": \"recovered\"\n if item.get(\"value\") is not None\n else \"partial\",\n \"requires_confirmation\": item.get(\"value\") is None,\n \"notes\": \"Recovered from Superset native filter URL state\",\n }\n )\n\n # Extract filters from permalink dataMask\n dashboard_data_mask = query_state.get(\"dataMask\")\n if isinstance(dashboard_data_mask, dict):\n for filter_key, item in dashboard_data_mask.items():\n if not isinstance(item, dict):\n continue\n filter_state = item.get(\"filterState\")\n extra_form_data = item.get(\"extraFormData\")\n display_name = None\n raw_value = None\n normalized_value = {\n \"filter_clauses\": [],\n \"extra_form_data\": deepcopy(extra_form_data)\n if isinstance(extra_form_data, dict)\n else {},\n \"value_origin\": \"unresolved\",\n }\n\n if isinstance(filter_state, dict):\n display_name = filter_state.get(\"label\")\n raw_value = filter_state.get(\"value\") or filter_state.get(\n \"values\"\n )\n if raw_value is not None:\n normalized_value[\"value_origin\"] = \"filter_state\"\n\n if isinstance(extra_form_data, dict):\n extra_filters = extra_form_data.get(\"filters\")\n if isinstance(extra_filters, list):\n normalized_value[\"filter_clauses\"] = [\n deepcopy(extra_filter)\n for extra_filter in extra_filters\n if isinstance(extra_filter, dict)\n ]\n\n if raw_value is None and normalized_value[\"filter_clauses\"]:\n first_filter = normalized_value[\"filter_clauses\"][0]\n raw_value = first_filter.get(\"val\")\n if raw_value is None:\n raw_value = first_filter.get(\"value\")\n if raw_value is not None:\n normalized_value[\"value_origin\"] = (\n \"extra_form_data.filters\"\n )\n\n if raw_value is None and isinstance(extra_form_data, dict):\n for field in [\n \"time_range\",\n \"time_grain_sqla\",\n \"time_column\",\n \"granularity\",\n ]:\n if field in extra_form_data:\n raw_value = extra_form_data[field]\n normalized_value[\"value_origin\"] = (\n f\"extra_form_data.{field}\"\n )\n break\n\n imported_filters.append(\n {\n \"filter_name\": str(item.get(\"id\") or filter_key),\n \"raw_value\": raw_value,\n \"display_name\": display_name,\n \"normalized_value\": normalized_value,\n \"source\": \"superset_permalink\",\n \"recovery_status\": \"recovered\"\n if raw_value is not None\n else \"partial\",\n \"requires_confirmation\": raw_value is None,\n \"notes\": \"Recovered from Superset dashboard permalink state\",\n }\n )\n\n # Extract filters from native_filter_state (fetched via native_filters_key)\n native_filter_state = query_state.get(\"native_filter_state\")\n if isinstance(native_filter_state, dict):\n for filter_key, item in native_filter_state.items():\n if not isinstance(item, dict):\n continue\n filter_id = item.get(\"id\") or filter_key\n filter_state = item.get(\"filterState\")\n extra_form_data = item.get(\"extraFormData\")\n display_name = None\n raw_value = None\n normalized_value = {\n \"filter_clauses\": [],\n \"extra_form_data\": deepcopy(extra_form_data)\n if isinstance(extra_form_data, dict)\n else {},\n \"value_origin\": \"unresolved\",\n }\n\n if isinstance(filter_state, dict):\n display_name = filter_state.get(\"label\")\n raw_value = filter_state.get(\"value\") or filter_state.get(\n \"values\"\n )\n if raw_value is not None:\n normalized_value[\"value_origin\"] = \"filter_state\"\n\n if isinstance(extra_form_data, dict):\n extra_filters = extra_form_data.get(\"filters\")\n if isinstance(extra_filters, list):\n normalized_value[\"filter_clauses\"] = [\n deepcopy(extra_filter)\n for extra_filter in extra_filters\n if isinstance(extra_filter, dict)\n ]\n\n if raw_value is None and normalized_value[\"filter_clauses\"]:\n first_filter = normalized_value[\"filter_clauses\"][0]\n raw_value = first_filter.get(\"val\")\n if raw_value is None:\n raw_value = first_filter.get(\"value\")\n if raw_value is not None:\n normalized_value[\"value_origin\"] = (\n \"extra_form_data.filters\"\n )\n\n if raw_value is None and isinstance(extra_form_data, dict):\n for field in [\n \"time_range\",\n \"time_grain_sqla\",\n \"time_column\",\n \"granularity\",\n ]:\n if field in extra_form_data:\n raw_value = extra_form_data[field]\n normalized_value[\"value_origin\"] = (\n f\"extra_form_data.{field}\"\n )\n break\n\n imported_filters.append(\n {\n \"filter_name\": str(filter_id),\n \"raw_value\": raw_value,\n \"display_name\": display_name,\n \"normalized_value\": normalized_value,\n \"source\": \"superset_native_filters_key\",\n \"recovery_status\": \"recovered\"\n if raw_value is not None\n else \"partial\",\n \"requires_confirmation\": raw_value is None,\n \"notes\": \"Recovered from Superset native_filters_key state\",\n }\n )\n\n form_data_payload = query_state.get(\"form_data\")\n if isinstance(form_data_payload, dict):\n extra_filters = form_data_payload.get(\"extra_filters\") or []\n for index, item in enumerate(extra_filters):\n if not isinstance(item, dict):\n continue\n filter_name = (\n item.get(\"col\")\n or item.get(\"column\")\n or f\"extra_filter_{index}\"\n )\n imported_filters.append(\n {\n \"filter_name\": str(filter_name),\n \"raw_value\": item.get(\"val\"),\n \"display_name\": item.get(\"label\"),\n \"normalized_value\": {\n \"filter_clauses\": [deepcopy(item)],\n \"extra_form_data\": {},\n \"value_origin\": \"form_data.extra_filters\",\n },\n \"source\": \"superset_url\",\n \"recovery_status\": \"recovered\"\n if item.get(\"val\") is not None\n else \"partial\",\n \"requires_confirmation\": item.get(\"val\") is None,\n \"notes\": \"Recovered from Superset form_data extra_filters\",\n }\n )\n\n return imported_filters\n\n # [/DEF:SupersetContextFiltersExtractMixin._extract_imported_filters:Function]\n" + "anchor_syntax": "region", + "body": "# #region _filters_imports [TYPE Block]\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nfrom typing import Any, cast\n\nfrom ...logger import logger as app_logger\n\napp_logger = cast(Any, app_logger)\n# #endregion _filters_imports\n" }, { "contract_id": "SupersetContextParsingMixin", "contract_type": "Module", "file_path": "backend/src/core/utils/superset_context_extractor/_parsing.py", "start_line": 1, - "end_line": 8, + "end_line": 6, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "LAYER": "Infra", - "PURPOSE": "Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake.", - "SEMANTICS": [ - "superset", - "link_parsing", - "url", - "permalink", - "recovery" - ] + "PURPOSE": "Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake." }, "relations": [ { @@ -54259,104 +43131,35 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetContextParsingMixin:Module]\n# @COMPLEXITY: 4\n# @LAYER: Infra\n# @SEMANTICS: superset, link_parsing, url, permalink, recovery\n# @PURPOSE: Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake.\n# @RELATION: CALLS -> [SupersetClient]\n# @RELATION: CALLS -> [SupersetContextExtractorBase]\n# [/DEF:SupersetContextParsingMixin:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetContextParsingMixin [C:4] [TYPE Module] [SEMANTICS superset, transform, dashboard, dataset, session, review]\n# @LAYER: Infra\n# @BRIEF Parse supported Superset URLs and recover canonical dataset/dashboard references for review-session intake.\n# @RELATION CALLS -> [SupersetClient]\n# @RELATION CALLS -> [SupersetContextExtractorBase]\n# #endregion SupersetContextParsingMixin\n" }, { "contract_id": "_parsing_imports", "contract_type": "Block", "file_path": "backend/src/core/utils/superset_context_extractor/_parsing.py", - "start_line": 10, - "end_line": 20, + "start_line": 7, + "end_line": 17, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_parsing_imports:Block]\nfrom __future__ import annotations\n\nfrom typing import Any, Dict, List, Optional, cast\nfrom urllib.parse import parse_qs, urlparse\n\nfrom ...logger import belief_scope, logger\nfrom ._base import SupersetParsedContext\n\nlogger = cast(Any, logger)\n# [/DEF:_parsing_imports:Block]\n" - }, - { - "contract_id": "SupersetContextParsingMixin.parse_superset_link", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_parsing.py", - "start_line": 27, - "end_line": 335, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[link:str] -> Output[SupersetParsedContext]", - "POST": "returns resolved dataset/dashboard context, preserving explicit partial-recovery state if some identifiers cannot be confirmed.", - "PRE": "link is a non-empty Superset URL compatible with the configured environment.", - "PURPOSE": "Extract candidate identifiers and query state from supported Superset URLs.", - "SIDE_EFFECT": "may issue Superset API reads to resolve dataset references from dashboard or chart URLs." - }, - "relations": [ - { - "source_id": "SupersetContextParsingMixin.parse_superset_link", - "relation_type": "CALLS", - "target_id": "SupersetClient.get_dashboard_detail", - "target_ref": "[SupersetClient.get_dashboard_detail]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextParsingMixin.parse_superset_link:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Extract candidate identifiers and query state from supported Superset URLs.\n # @RELATION: CALLS -> [SupersetClient.get_dashboard_detail]\n # @PRE: link is a non-empty Superset URL compatible with the configured environment.\n # @POST: returns resolved dataset/dashboard context, preserving explicit partial-recovery state if some identifiers cannot be confirmed.\n # @SIDE_EFFECT: may issue Superset API reads to resolve dataset references from dashboard or chart URLs.\n # @DATA_CONTRACT: Input[link:str] -> Output[SupersetParsedContext]\n def parse_superset_link(self, link: str) -> SupersetParsedContext:\n with belief_scope(\"SupersetContextExtractor.parse_superset_link\"):\n normalized_link = str(link or \"\").strip()\n if not normalized_link:\n logger.explore(\"Rejected empty Superset link during intake\")\n raise ValueError(\"Superset link must be non-empty\")\n\n parsed_url = urlparse(normalized_link)\n if parsed_url.scheme not in {\"http\", \"https\"} or not parsed_url.netloc:\n logger.explore(\n \"Superset link is not a parseable absolute URL\",\n extra={\"link\": normalized_link},\n )\n raise ValueError(\"Superset link must be an absolute http(s) URL\")\n\n logger.reason(\n \"Parsing Superset link for dataset review intake\",\n extra={\"path\": parsed_url.path, \"query\": parsed_url.query},\n )\n\n path_parts = [part for part in parsed_url.path.split(\"/\") if part]\n query_params = parse_qs(parsed_url.query, keep_blank_values=True)\n query_state = self._decode_query_state(query_params)\n\n dataset_id = self._extract_numeric_identifier(path_parts, \"dataset\")\n dashboard_id = self._extract_numeric_identifier(path_parts, \"dashboard\")\n dashboard_ref = self._extract_dashboard_reference(path_parts)\n dashboard_permalink_key = self._extract_dashboard_permalink_key(path_parts)\n chart_id = self._extract_numeric_identifier(path_parts, \"chart\")\n\n resource_type = \"unknown\"\n dataset_ref: Optional[str] = None\n partial_recovery = False\n unresolved_references: List[str] = []\n\n if dataset_id is not None:\n resource_type = \"dataset\"\n dataset_ref = f\"dataset:{dataset_id}\"\n logger.reason(\n \"Resolved direct dataset link\",\n extra={\"dataset_id\": dataset_id},\n )\n elif dashboard_permalink_key is not None:\n resource_type = \"dashboard\"\n partial_recovery = True\n dataset_ref = f\"dashboard_permalink:{dashboard_permalink_key}\"\n unresolved_references.append(\n \"dashboard_permalink_dataset_binding_unresolved\"\n )\n logger.reason(\n \"Resolving dashboard permalink state from Superset\",\n extra={\"permalink_key\": dashboard_permalink_key},\n )\n permalink_payload = self.client.get_dashboard_permalink_state(\n dashboard_permalink_key\n )\n permalink_state = (\n permalink_payload.get(\"state\", permalink_payload)\n if isinstance(permalink_payload, dict)\n else {}\n )\n if isinstance(permalink_state, dict):\n for key, value in permalink_state.items():\n query_state.setdefault(key, value)\n # Extract filters from permalink dataMask\n data_mask = permalink_state.get(\"dataMask\")\n if isinstance(data_mask, dict) and data_mask:\n query_state[\"dataMask\"] = data_mask\n logger.reason(\n \"Extracted native filters from permalink dataMask\",\n extra={\"filter_count\": len(data_mask)},\n )\n resolved_dashboard_id = self._extract_dashboard_id_from_state(\n permalink_state\n )\n resolved_chart_id = self._extract_chart_id_from_state(\n permalink_state\n )\n if resolved_dashboard_id is not None:\n dashboard_id = resolved_dashboard_id\n unresolved_references = [\n item\n for item in unresolved_references\n if item\n != \"dashboard_permalink_dataset_binding_unresolved\"\n ]\n dataset_id, unresolved_references = (\n self._recover_dataset_binding_from_dashboard(\n dashboard_id=dashboard_id,\n dataset_ref=dataset_ref,\n unresolved_references=unresolved_references,\n )\n )\n if dataset_id is not None:\n dataset_ref = f\"dataset:{dataset_id}\"\n elif resolved_chart_id is not None:\n chart_id = resolved_chart_id\n unresolved_references = [\n item\n for item in unresolved_references\n if item\n != \"dashboard_permalink_dataset_binding_unresolved\"\n ]\n try:\n chart_payload = self.client.get_chart(chart_id)\n chart_data = (\n chart_payload.get(\"result\", chart_payload)\n if isinstance(chart_payload, dict)\n else {}\n )\n datasource_id = chart_data.get(\"datasource_id\")\n if datasource_id is not None:\n dataset_id = int(datasource_id)\n dataset_ref = f\"dataset:{dataset_id}\"\n logger.reason(\n \"Recovered dataset reference from permalink chart context\",\n extra={\n \"chart_id\": chart_id,\n \"dataset_id\": dataset_id,\n },\n )\n else:\n unresolved_references.append(\n \"chart_dataset_binding_unresolved\"\n )\n except Exception as exc:\n unresolved_references.append(\n \"chart_dataset_binding_unresolved\"\n )\n logger.explore(\n \"Chart lookup failed during permalink recovery\",\n extra={\"chart_id\": chart_id, \"error\": str(exc)},\n )\n else:\n logger.explore(\n \"Dashboard permalink state was not a structured object\",\n extra={\"permalink_key\": dashboard_permalink_key},\n )\n elif dashboard_id is not None or dashboard_ref is not None:\n resource_type = \"dashboard\"\n resolved_dashboard_ref = (\n dashboard_id if dashboard_id is not None else dashboard_ref\n )\n if resolved_dashboard_ref is None:\n raise ValueError(\"Dashboard reference could not be resolved\")\n logger.reason(\n \"Resolving dashboard-bound dataset from Superset\",\n extra={\"dashboard_ref\": resolved_dashboard_ref},\n )\n\n # Resolve dashboard detail first — handles both numeric ID and slug,\n # ensuring dashboard_id is available for the native_filters_key fetch below.\n dashboard_detail = self.client.get_dashboard_detail(\n resolved_dashboard_ref\n )\n resolved_dashboard_id = dashboard_detail.get(\"id\")\n if resolved_dashboard_id is not None:\n dashboard_id = int(resolved_dashboard_id)\n\n # Check for native_filters_key in query params and fetch filter state.\n # This must run AFTER dashboard_id is resolved from slug above.\n native_filters_key = query_params.get(\"native_filters_key\", [None])[0]\n if native_filters_key and dashboard_id is not None:\n try:\n logger.reason(\n \"Fetching native filter state from Superset\",\n extra={\n \"dashboard_id\": dashboard_id,\n \"filter_key\": native_filters_key,\n },\n )\n extracted = self.client.extract_native_filters_from_key(\n dashboard_id, native_filters_key\n )\n data_mask = extracted.get(\"dataMask\")\n if isinstance(data_mask, dict) and data_mask:\n query_state[\"native_filter_state\"] = data_mask\n logger.reason(\n \"Extracted native filter state from Superset via native_filters_key\",\n extra={\"filter_count\": len(data_mask)},\n )\n else:\n logger.explore(\n \"Native filter state returned empty dataMask\",\n extra={\n \"dashboard_id\": dashboard_id,\n \"filter_key\": native_filters_key,\n },\n )\n except Exception as exc:\n logger.explore(\n \"Failed to fetch native filter state from Superset\",\n extra={\n \"dashboard_id\": dashboard_id,\n \"filter_key\": native_filters_key,\n \"error\": str(exc),\n },\n )\n\n datasets = dashboard_detail.get(\"datasets\") or []\n if datasets:\n first_dataset = datasets[0]\n resolved_dataset_id = first_dataset.get(\"id\")\n if resolved_dataset_id is not None:\n dataset_id = int(resolved_dataset_id)\n dataset_ref = f\"dataset:{dataset_id}\"\n logger.reason(\n \"Recovered dataset reference from dashboard context\",\n extra={\n \"dashboard_id\": dashboard_id,\n \"dataset_id\": dataset_id,\n \"dataset_count\": len(datasets),\n },\n )\n if len(datasets) > 1:\n partial_recovery = True\n unresolved_references.append(\n \"multiple_dashboard_datasets\"\n )\n else:\n partial_recovery = True\n unresolved_references.append(\"dashboard_dataset_id_missing\")\n else:\n partial_recovery = True\n unresolved_references.append(\"dashboard_dataset_binding_missing\")\n elif chart_id is not None:\n resource_type = \"chart\"\n partial_recovery = True\n unresolved_references.append(\"chart_dataset_binding_unresolved\")\n dataset_ref = f\"chart:{chart_id}\"\n logger.reason(\n \"Accepted chart link with explicit partial recovery\",\n extra={\"chart_id\": chart_id},\n )\n else:\n logger.explore(\n \"Unsupported Superset link shape encountered\",\n extra={\"path\": parsed_url.path},\n )\n raise ValueError(\"Unsupported Superset link shape\")\n\n dataset_payload: Optional[Dict[str, Any]] = None\n if dataset_id is not None:\n try:\n dataset_payload = self.client.get_dataset_detail(dataset_id)\n table_name = str(\n dataset_payload.get(\"table_name\") or \"\"\n ).strip()\n schema_name = str(\n dataset_payload.get(\"schema\") or \"\"\n ).strip()\n if table_name:\n dataset_ref = (\n f\"{schema_name}.{table_name}\"\n if schema_name\n else table_name\n )\n logger.reason(\n \"Canonicalized dataset reference from dataset detail\",\n extra={\n \"dataset_ref\": dataset_ref,\n \"dataset_id\": dataset_id,\n },\n )\n except Exception as exc:\n partial_recovery = True\n unresolved_references.append(\"dataset_detail_lookup_failed\")\n logger.explore(\n \"Dataset detail lookup failed during link parsing; keeping session usable\",\n extra={\"dataset_id\": dataset_id, \"error\": str(exc)},\n )\n\n imported_filters = self._extract_imported_filters(query_state)\n result = SupersetParsedContext(\n source_url=normalized_link,\n dataset_ref=dataset_ref or \"unresolved\",\n dataset_id=dataset_id,\n dashboard_id=dashboard_id,\n chart_id=chart_id,\n resource_type=resource_type,\n query_state=query_state,\n imported_filters=imported_filters,\n unresolved_references=unresolved_references,\n partial_recovery=partial_recovery,\n dataset_payload=dataset_payload,\n )\n logger.reflect(\n \"Superset link parsing completed\",\n extra={\n \"dataset_ref\": result.dataset_ref,\n \"dataset_id\": result.dataset_id,\n \"dashboard_id\": result.dashboard_id,\n \"chart_id\": result.chart_id,\n \"partial_recovery\": result.partial_recovery,\n \"unresolved_references\": result.unresolved_references,\n \"imported_filters\": len(result.imported_filters),\n },\n )\n return result\n\n # [/DEF:SupersetContextParsingMixin.parse_superset_link:Function]\n" - }, - { - "contract_id": "SupersetContextParsingMixin._recover_dataset_binding_from_dashboard", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_parsing.py", - "start_line": 337, - "end_line": 377, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Recover a dataset binding from resolved dashboard context while preserving explicit unresolved markers." - }, - "relations": [ - { - "source_id": "SupersetContextParsingMixin._recover_dataset_binding_from_dashboard", - "relation_type": "CALLS", - "target_id": "SupersetClient.get_dashboard_detail", - "target_ref": "[SupersetClient.get_dashboard_detail]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextParsingMixin._recover_dataset_binding_from_dashboard:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Recover a dataset binding from resolved dashboard context while preserving explicit unresolved markers.\n # @RELATION: CALLS -> [SupersetClient.get_dashboard_detail]\n def _recover_dataset_binding_from_dashboard(\n self,\n dashboard_id: int,\n dataset_ref: Optional[str],\n unresolved_references: List[str],\n ) -> tuple[Optional[int], List[str]]:\n dashboard_detail = self.client.get_dashboard_detail(dashboard_id)\n datasets = dashboard_detail.get(\"datasets\") or []\n if datasets:\n first_dataset = datasets[0]\n resolved_dataset_id = first_dataset.get(\"id\")\n if resolved_dataset_id is not None:\n resolved_dataset = int(resolved_dataset_id)\n logger.reason(\n \"Recovered dataset reference from dashboard permalink context\",\n extra={\n \"dashboard_id\": dashboard_id,\n \"dataset_id\": resolved_dataset,\n \"dataset_count\": len(datasets),\n \"dataset_ref\": dataset_ref,\n },\n )\n if (\n len(datasets) > 1\n and \"multiple_dashboard_datasets\" not in unresolved_references\n ):\n unresolved_references.append(\"multiple_dashboard_datasets\")\n return resolved_dataset, unresolved_references\n if \"dashboard_dataset_id_missing\" not in unresolved_references:\n unresolved_references.append(\"dashboard_dataset_id_missing\")\n return None, unresolved_references\n\n if \"dashboard_dataset_binding_missing\" not in unresolved_references:\n unresolved_references.append(\"dashboard_dataset_binding_missing\")\n return None, unresolved_references\n\n # [/DEF:SupersetContextParsingMixin._recover_dataset_binding_from_dashboard:Function]\n" + "anchor_syntax": "region", + "body": "# #region _parsing_imports [TYPE Block]\nfrom __future__ import annotations\n\nfrom typing import Any, cast\nfrom urllib.parse import parse_qs, urlparse\n\nfrom ...logger import belief_scope, logger\nfrom ._base import SupersetParsedContext\n\nlogger = cast(Any, logger)\n# #endregion _parsing_imports\n" }, { "contract_id": "SupersetContextExtractorPII", "contract_type": "Module", "file_path": "backend/src/core/utils/superset_context_extractor/_pii.py", "start_line": 1, - "end_line": 6, + "end_line": 4, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "PII redaction helpers for assistant-facing dataset-review context — mask emails, UUIDs, long digit strings, and mixed identifiers.", - "SEMANTICS": [ - "superset", - "pii", - "masking", - "sanitization", - "privacy" - ] + "PURPOSE": "PII redaction helpers for assistant-facing dataset-review context — mask emails, UUIDs, long digit strings, and mixed identifiers." }, "relations": [], "schema_warnings": [ @@ -54370,92 +43173,86 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetContextExtractorPII:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, pii, masking, sanitization, privacy\n# @PURPOSE: PII redaction helpers for assistant-facing dataset-review context — mask emails, UUIDs, long digit strings, and mixed identifiers.\n# [/DEF:SupersetContextExtractorPII:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetContextExtractorPII [C:3] [TYPE Module] [SEMANTICS superset, dataset, assistant, review]\n# @LAYER: Infra\n# @BRIEF PII redaction helpers for assistant-facing dataset-review context — mask emails, UUIDs, long digit strings, and mixed identifiers.\n# #endregion SupersetContextExtractorPII\n" }, { "contract_id": "_pii_imports", "contract_type": "Block", "file_path": "backend/src/core/utils/superset_context_extractor/_pii.py", - "start_line": 8, - "end_line": 12, + "start_line": 6, + "end_line": 11, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_pii_imports:Block]\nimport re\nfrom copy import deepcopy\nfrom typing import Any, Dict, List, Tuple\n# [/DEF:_pii_imports:Block]\n" + "anchor_syntax": "region", + "body": "# #region _pii_imports [TYPE Block]\nimport re\nfrom copy import deepcopy\nfrom typing import Any\n\n# #endregion _pii_imports\n" }, { "contract_id": "mask_pii_value", "contract_type": "Function", "file_path": "backend/src/core/utils/superset_context_extractor/_pii.py", - "start_line": 26, - "end_line": 51, + "start_line": 25, + "end_line": 49, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Redact likely sensitive identifiers while preserving structural hints for assistant-facing dataset-review context." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:mask_pii_value:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Redact likely sensitive identifiers while preserving structural hints for assistant-facing dataset-review context.\ndef mask_pii_value(value: Any) -> Tuple[Any, bool]:\n if isinstance(value, str):\n return _mask_sensitive_text(value)\n if isinstance(value, list):\n masked_any = False\n masked_list: List[Any] = []\n for item in value:\n masked_item, item_masked = mask_pii_value(item)\n masked_list.append(masked_item)\n masked_any = masked_any or item_masked\n return masked_list, masked_any\n if isinstance(value, dict):\n masked_any = False\n masked_dict: Dict[str, Any] = {}\n for key, item in value.items():\n masked_item, item_masked = mask_pii_value(item)\n masked_dict[key] = masked_item\n masked_any = masked_any or item_masked\n return masked_dict, masked_any\n return value, False\n\n\n# [/DEF:mask_pii_value:Function]\n" + "anchor_syntax": "region", + "body": "# #region mask_pii_value [C:2] [TYPE Function]\n# @BRIEF Redact likely sensitive identifiers while preserving structural hints for assistant-facing dataset-review context.\ndef mask_pii_value(value: Any) -> tuple[Any, bool]:\n if isinstance(value, str):\n return _mask_sensitive_text(value)\n if isinstance(value, list):\n masked_any = False\n masked_list: list[Any] = []\n for item in value:\n masked_item, item_masked = mask_pii_value(item)\n masked_list.append(masked_item)\n masked_any = masked_any or item_masked\n return masked_list, masked_any\n if isinstance(value, dict):\n masked_any = False\n masked_dict: dict[str, Any] = {}\n for key, item in value.items():\n masked_item, item_masked = mask_pii_value(item)\n masked_dict[key] = masked_item\n masked_any = masked_any or item_masked\n return masked_dict, masked_any\n return value, False\n\n\n# #endregion mask_pii_value\n" }, { "contract_id": "sanitize_imported_filter_for_assistant", "contract_type": "Function", "file_path": "backend/src/core/utils/superset_context_extractor/_pii.py", - "start_line": 54, - "end_line": 69, + "start_line": 52, + "end_line": 66, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Build assistant-safe imported-filter payload with raw-value redaction metadata when sensitive tokens are detected." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:sanitize_imported_filter_for_assistant:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build assistant-safe imported-filter payload with raw-value redaction metadata when sensitive tokens are detected.\ndef sanitize_imported_filter_for_assistant(\n filter_payload: Dict[str, Any],\n) -> Dict[str, Any]:\n sanitized = deepcopy(filter_payload)\n masked_raw_value, was_masked = mask_pii_value(filter_payload.get(\"raw_value\"))\n sanitized[\"raw_value\"] = masked_raw_value\n sanitized[\"raw_value_masked\"] = bool(\n filter_payload.get(\"raw_value_masked\", False) or was_masked\n )\n return sanitized\n\n\n# [/DEF:sanitize_imported_filter_for_assistant:Function]\n" + "anchor_syntax": "region", + "body": "# #region sanitize_imported_filter_for_assistant [C:2] [TYPE Function]\n# @BRIEF Build assistant-safe imported-filter payload with raw-value redaction metadata when sensitive tokens are detected.\ndef sanitize_imported_filter_for_assistant(\n filter_payload: dict[str, Any],\n) -> dict[str, Any]:\n sanitized = deepcopy(filter_payload)\n masked_raw_value, was_masked = mask_pii_value(filter_payload.get(\"raw_value\"))\n sanitized[\"raw_value\"] = masked_raw_value\n sanitized[\"raw_value_masked\"] = bool(\n filter_payload.get(\"raw_value_masked\", False) or was_masked\n )\n return sanitized\n\n\n# #endregion sanitize_imported_filter_for_assistant\n" }, { "contract_id": "_mask_sensitive_text", "contract_type": "Function", "file_path": "backend/src/core/utils/superset_context_extractor/_pii.py", - "start_line": 72, - "end_line": 112, + "start_line": 69, + "end_line": 108, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Redact likely PII substrings from one string while preserving non-sensitive delimiters and token shape." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_mask_sensitive_text:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Redact likely PII substrings from one string while preserving non-sensitive delimiters and token shape.\ndef _mask_sensitive_text(value: str) -> Tuple[str, bool]:\n masked = value\n masked_any = False\n\n def _replace_email(match: re.Match[str]) -> str:\n nonlocal masked_any\n masked_any = True\n domain = match.group(\"domain\")\n return f\"***@{domain}\"\n\n def _replace_uuid(match: re.Match[str]) -> str:\n nonlocal masked_any\n masked_any = True\n token = match.group(0)\n return \"********-****-****-****-\" + token[-12:]\n\n def _replace_long_digits(match: re.Match[str]) -> str:\n nonlocal masked_any\n masked_any = True\n token = match.group(0)\n return \"*\" * max(len(token) - 2, 1) + token[-2:]\n\n def _replace_mixed_identifier(match: re.Match[str]) -> str:\n nonlocal masked_any\n token = match.group(0)\n if token.isupper() and len(token) <= 5:\n return token\n masked_any = True\n return token[:2] + \"***\" + token[-2:]\n\n masked = _EMAIL_PATTERN.sub(_replace_email, masked)\n masked = _UUID_PATTERN.sub(_replace_uuid, masked)\n masked = _LONG_DIGIT_PATTERN.sub(_replace_long_digits, masked)\n masked = _MIXED_IDENTIFIER_PATTERN.sub(_replace_mixed_identifier, masked)\n return masked, masked_any\n\n\n# [/DEF:_mask_sensitive_text:Function]\n" + "anchor_syntax": "region", + "body": "# #region _mask_sensitive_text [C:2] [TYPE Function]\n# @BRIEF Redact likely PII substrings from one string while preserving non-sensitive delimiters and token shape.\ndef _mask_sensitive_text(value: str) -> tuple[str, bool]:\n masked = value\n masked_any = False\n\n def _replace_email(match: re.Match[str]) -> str:\n nonlocal masked_any\n masked_any = True\n domain = match.group(\"domain\")\n return f\"***@{domain}\"\n\n def _replace_uuid(match: re.Match[str]) -> str:\n nonlocal masked_any\n masked_any = True\n token = match.group(0)\n return \"********-****-****-****-\" + token[-12:]\n\n def _replace_long_digits(match: re.Match[str]) -> str:\n nonlocal masked_any\n masked_any = True\n token = match.group(0)\n return \"*\" * max(len(token) - 2, 1) + token[-2:]\n\n def _replace_mixed_identifier(match: re.Match[str]) -> str:\n nonlocal masked_any\n token = match.group(0)\n if token.isupper() and len(token) <= 5:\n return token\n masked_any = True\n return token[:2] + \"***\" + token[-2:]\n\n masked = _EMAIL_PATTERN.sub(_replace_email, masked)\n masked = _UUID_PATTERN.sub(_replace_uuid, masked)\n masked = _LONG_DIGIT_PATTERN.sub(_replace_long_digits, masked)\n masked = _MIXED_IDENTIFIER_PATTERN.sub(_replace_mixed_identifier, masked)\n return masked, masked_any\n\n\n# #endregion _mask_sensitive_text\n" }, { "contract_id": "SupersetContextRecoveryMixin", "contract_type": "Module", "file_path": "backend/src/core/utils/superset_context_extractor/_recovery.py", "start_line": 1, - "end_line": 8, + "end_line": 6, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "LAYER": "Infra", - "PURPOSE": "Recover imported filters from Superset parsed context and dashboard metadata.", - "SEMANTICS": [ - "superset", - "filters", - "recovery", - "imported_filters" - ] + "PURPOSE": "Recover imported filters from Superset parsed context and dashboard metadata." }, "relations": [ { @@ -54500,97 +43297,35 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SupersetContextRecoveryMixin:Module]\n# @COMPLEXITY: 4\n# @LAYER: Infra\n# @SEMANTICS: superset, filters, recovery, imported_filters\n# @PURPOSE: Recover imported filters from Superset parsed context and dashboard metadata.\n# @RELATION: CALLS -> [SupersetClient]\n# @RELATION: DEPENDS_ON -> [SupersetContextExtractorBase]\n# [/DEF:SupersetContextRecoveryMixin:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetContextRecoveryMixin [C:4] [TYPE Module] [SEMANTICS superset, context, recovery, dashboard, dataset]\n# @LAYER: Infra\n# @BRIEF Recover imported filters from Superset parsed context and dashboard metadata.\n# @RELATION CALLS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractorBase]\n# #endregion SupersetContextRecoveryMixin\n" }, { "contract_id": "_recovery_imports", "contract_type": "Block", "file_path": "backend/src/core/utils/superset_context_extractor/_recovery.py", - "start_line": 10, - "end_line": 21, + "start_line": 7, + "end_line": 18, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_recovery_imports:Block]\nfrom __future__ import annotations\n\nimport json\nfrom copy import deepcopy\nfrom typing import Any, Dict, List, Set, cast\n\nfrom ...logger import belief_scope, logger\nfrom ._base import SupersetParsedContext\n\nlogger = cast(Any, logger)\n# [/DEF:_recovery_imports:Block]\n" - }, - { - "contract_id": "SupersetContextRecoveryMixin.recover_imported_filters", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_recovery.py", - "start_line": 28, - "end_line": 266, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[SupersetParsedContext] -> Output[List[Dict[str,Any]]]", - "POST": "returns explicit recovered and partial filter entries with preserved provenance and confirmation requirements.", - "PRE": "parsed_context comes from a successful Superset link parse for one environment.", - "PURPOSE": "Build imported filter entries from URL state and Superset-side saved context.", - "SIDE_EFFECT": "may issue Superset reads for dashboard metadata enrichment." - }, - "relations": [ - { - "source_id": "SupersetContextRecoveryMixin.recover_imported_filters", - "relation_type": "CALLS", - "target_id": "SupersetClient.get_dashboard", - "target_ref": "[SupersetClient.get_dashboard]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextRecoveryMixin.recover_imported_filters:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Build imported filter entries from URL state and Superset-side saved context.\n # @RELATION: CALLS -> [SupersetClient.get_dashboard]\n # @PRE: parsed_context comes from a successful Superset link parse for one environment.\n # @POST: returns explicit recovered and partial filter entries with preserved provenance and confirmation requirements.\n # @SIDE_EFFECT: may issue Superset reads for dashboard metadata enrichment.\n # @DATA_CONTRACT: Input[SupersetParsedContext] -> Output[List[Dict[str,Any]]]\n def recover_imported_filters(\n self, parsed_context: SupersetParsedContext\n ) -> List[Dict[str, Any]]:\n with belief_scope(\"SupersetContextExtractor.recover_imported_filters\"):\n recovered_filters: List[Dict[str, Any]] = []\n seen_filter_keys: Set[str] = set()\n metadata_filters: List[Dict[str, Any]] = []\n metadata_filters_by_id: Dict[str, Dict[str, Any]] = {}\n\n def merge_recovered_filter(candidate: Dict[str, Any]) -> None:\n filter_key = candidate[\"filter_name\"].strip().lower()\n existing_index = next(\n (\n index\n for index, existing in enumerate(recovered_filters)\n if existing[\"filter_name\"].strip().lower() == filter_key\n ),\n None,\n )\n if existing_index is None:\n seen_filter_keys.add(filter_key)\n recovered_filters.append(candidate)\n return\n\n existing = recovered_filters[existing_index]\n if existing.get(\"display_name\") in {\n None,\n \"\",\n existing.get(\"filter_name\"),\n } and candidate.get(\"display_name\"):\n existing[\"display_name\"] = candidate[\"display_name\"]\n if (\n existing.get(\"raw_value\") is None\n and candidate.get(\"raw_value\") is not None\n ):\n existing[\"raw_value\"] = candidate[\"raw_value\"]\n existing[\"confidence_state\"] = candidate.get(\n \"confidence_state\", \"imported\"\n )\n existing[\"requires_confirmation\"] = candidate.get(\n \"requires_confirmation\", False\n )\n existing[\"recovery_status\"] = candidate.get(\n \"recovery_status\", \"recovered\"\n )\n existing[\"source\"] = candidate.get(\n \"source\", existing.get(\"source\")\n )\n if (\n existing.get(\"normalized_value\") is None\n and candidate.get(\"normalized_value\") is not None\n ):\n existing[\"normalized_value\"] = deepcopy(\n candidate[\"normalized_value\"]\n )\n if (\n existing.get(\"notes\")\n and candidate.get(\"notes\")\n and candidate[\"notes\"] not in existing[\"notes\"]\n ):\n existing[\"notes\"] = f\"{existing['notes']}; {candidate['notes']}\"\n\n if parsed_context.dashboard_id is not None:\n try:\n dashboard_payload = self.client.get_dashboard(\n parsed_context.dashboard_id\n )\n dashboard_record = (\n dashboard_payload.get(\"result\", dashboard_payload)\n if isinstance(dashboard_payload, dict)\n else {}\n )\n json_metadata = dashboard_record.get(\"json_metadata\")\n if isinstance(json_metadata, str) and json_metadata.strip():\n json_metadata = json.loads(json_metadata)\n if not isinstance(json_metadata, dict):\n json_metadata = {}\n\n native_filter_configuration = (\n json_metadata.get(\"native_filter_configuration\") or []\n )\n default_filters = json_metadata.get(\"default_filters\") or {}\n if isinstance(default_filters, str) and default_filters.strip():\n try:\n default_filters = json.loads(default_filters)\n except Exception:\n logger.explore(\n \"Superset default_filters payload was not valid JSON\",\n extra={\n \"dashboard_id\": parsed_context.dashboard_id\n },\n )\n default_filters = {}\n\n for item in native_filter_configuration:\n if not isinstance(item, dict):\n continue\n filter_name = str(\n item.get(\"name\")\n or item.get(\"filter_name\")\n or item.get(\"column\")\n or \"\"\n ).strip()\n if not filter_name:\n continue\n\n display_name = (\n item.get(\"label\") or item.get(\"name\") or filter_name\n )\n filter_id = str(item.get(\"id\") or \"\").strip()\n\n default_value = None\n if isinstance(default_filters, dict):\n default_value = default_filters.get(filter_name)\n\n metadata_filter = self._normalize_imported_filter_payload(\n {\n \"filter_name\": filter_name,\n \"display_name\": display_name,\n \"raw_value\": default_value,\n \"source\": \"superset_native\",\n \"recovery_status\": \"recovered\"\n if default_value is not None\n else \"partial\",\n \"requires_confirmation\": default_value is None,\n \"notes\": \"Recovered from Superset dashboard native filter configuration\",\n },\n default_source=\"superset_native\",\n default_note=\"Recovered from Superset dashboard native filter configuration\",\n )\n metadata_filters.append(metadata_filter)\n\n if filter_id:\n metadata_filters_by_id[filter_id.lower()] = {\n \"filter_name\": filter_name,\n \"display_name\": display_name,\n }\n except Exception as exc:\n logger.explore(\n \"Dashboard native filter enrichment failed; preserving partial imported filters\",\n extra={\n \"dashboard_id\": parsed_context.dashboard_id,\n \"error\": str(exc),\n \"filter_count\": len(recovered_filters),\n },\n )\n metadata_filters = []\n metadata_filters_by_id = {}\n\n for item in parsed_context.imported_filters:\n normalized = self._normalize_imported_filter_payload(\n item,\n default_source=\"superset_url\",\n default_note=\"Recovered from Superset URL state\",\n )\n metadata_match = metadata_filters_by_id.get(\n normalized[\"filter_name\"].strip().lower()\n )\n if metadata_match is not None:\n normalized[\"filter_name\"] = metadata_match[\"filter_name\"]\n normalized[\"display_name\"] = metadata_match[\"display_name\"]\n normalized[\"notes\"] = (\n \"Recovered from Superset URL state and reconciled against dashboard native filter metadata\"\n )\n\n merge_recovered_filter(normalized)\n logger.reflect(\n \"Recovered filter from URL state\",\n extra={\n \"filter_name\": normalized[\"filter_name\"],\n \"source\": normalized[\"source\"],\n \"has_value\": normalized[\"raw_value\"] is not None,\n \"canonicalized\": metadata_match is not None,\n },\n )\n\n if parsed_context.dashboard_id is None:\n logger.reflect(\n \"Imported filter recovery completed without dashboard enrichment\",\n extra={\n \"dashboard_id\": None,\n \"filter_count\": len(recovered_filters),\n \"partial_recovery\": parsed_context.partial_recovery,\n },\n )\n return recovered_filters\n\n for saved_filter in metadata_filters:\n merge_recovered_filter(saved_filter)\n logger.reflect(\n \"Recovered filter from dashboard metadata\",\n extra={\n \"filter_name\": saved_filter[\"filter_name\"],\n \"has_value\": saved_filter[\"raw_value\"] is not None,\n },\n )\n\n if not recovered_filters:\n recovered_filters.append(\n self._normalize_imported_filter_payload(\n {\n \"filter_name\": f\"dashboard_{parsed_context.dashboard_id}_filters\",\n \"display_name\": \"Dashboard native filters\",\n \"raw_value\": None,\n \"source\": \"superset_native\",\n \"recovery_status\": \"partial\",\n \"requires_confirmation\": True,\n \"notes\": \"Superset dashboard filter configuration could not be recovered fully\",\n },\n default_source=\"superset_native\",\n default_note=\"Superset dashboard filter configuration could not be recovered fully\",\n )\n )\n\n logger.reflect(\n \"Imported filter recovery completed with dashboard enrichment\",\n extra={\n \"dashboard_id\": parsed_context.dashboard_id,\n \"filter_count\": len(recovered_filters),\n \"partial_entries\": len(\n [\n item\n for item in recovered_filters\n if item[\"recovery_status\"] == \"partial\"\n ]\n ),\n },\n )\n return recovered_filters\n\n # [/DEF:SupersetContextRecoveryMixin.recover_imported_filters:Function]\n" - }, - { - "contract_id": "SupersetContextRecoveryMixin._normalize_imported_filter_payload", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_recovery.py", - "start_line": 268, - "end_line": 311, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Normalize one imported-filter payload with explicit provenance and confirmation state." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextRecoveryMixin._normalize_imported_filter_payload:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize one imported-filter payload with explicit provenance and confirmation state.\n def _normalize_imported_filter_payload(\n self,\n payload: Dict[str, Any],\n default_source: str,\n default_note: str,\n ) -> Dict[str, Any]:\n raw_value = payload.get(\"raw_value\")\n if \"raw_value\" not in payload and \"value\" in payload:\n raw_value = payload.get(\"value\")\n\n recovery_status = (\n str(\n payload.get(\"recovery_status\")\n or (\"recovered\" if raw_value is not None else \"partial\")\n )\n .strip()\n .lower()\n )\n requires_confirmation = bool(\n payload.get(\n \"requires_confirmation\",\n raw_value is None or recovery_status != \"recovered\",\n )\n )\n return {\n \"filter_name\": str(\n payload.get(\"filter_name\") or \"unresolved_filter\"\n ).strip(),\n \"display_name\": payload.get(\"display_name\"),\n \"raw_value\": raw_value,\n \"normalized_value\": payload.get(\"normalized_value\"),\n \"source\": str(payload.get(\"source\") or default_source),\n \"confidence_state\": \"imported\"\n if raw_value is not None\n else \"unresolved\",\n \"requires_confirmation\": requires_confirmation,\n \"recovery_status\": recovery_status,\n \"notes\": str(payload.get(\"notes\") or default_note),\n }\n\n # [/DEF:SupersetContextRecoveryMixin._normalize_imported_filter_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region _recovery_imports [TYPE Block]\nfrom __future__ import annotations\n\nimport json\nfrom copy import deepcopy\nfrom typing import Any, cast\n\nfrom ...logger import belief_scope, logger\nfrom ._base import SupersetParsedContext\n\nlogger = cast(Any, logger)\n# #endregion _recovery_imports\n" }, { "contract_id": "SupersetContextTemplatesMixin", "contract_type": "Module", "file_path": "backend/src/core/utils/superset_context_extractor/_templates.py", "start_line": 1, - "end_line": 8, + "end_line": 6, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Deterministically detect runtime variables and Jinja references from dataset query-bearing fields without execution.", - "SEMANTICS": [ - "superset", - "template_variables", - "jinja", - "discovery", - "deterministic" - ] + "PURPOSE": "Deterministically detect runtime variables and Jinja references from dataset query-bearing fields without execution." }, "relations": [ { @@ -54607,137 +43342,22 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetContextTemplatesMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: superset, template_variables, jinja, discovery, deterministic\n# @PURPOSE: Deterministically detect runtime variables and Jinja references from dataset query-bearing fields without execution.\n# @RELATION: DEPENDS_ON -> [TemplateVariable]\n# @RELATION: DEPENDS_ON -> [SupersetContextExtractorBase]\n# [/DEF:SupersetContextTemplatesMixin:Module]\n" + "anchor_syntax": "region", + "body": "# #region SupersetContextTemplatesMixin [C:3] [TYPE Module] [SEMANTICS superset, search, dataset, execution]\n# @LAYER: Infra\n# @BRIEF Deterministically detect runtime variables and Jinja references from dataset query-bearing fields without execution.\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractorBase]\n# #endregion SupersetContextTemplatesMixin\n" }, { "contract_id": "_templates_imports", "contract_type": "Block", "file_path": "backend/src/core/utils/superset_context_extractor/_templates.py", - "start_line": 10, - "end_line": 19, + "start_line": 7, + "end_line": 16, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:_templates_imports:Block]\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any, Dict, List, Optional, Set, cast\n\nfrom ...logger import belief_scope, logger\n\nlogger = cast(Any, logger)\n# [/DEF:_templates_imports:Block]\n" - }, - { - "contract_id": "SupersetContextTemplatesMixin.discover_template_variables", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_templates.py", - "start_line": 26, - "end_line": 124, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[dataset_payload:Dict[str,Any]] -> Output[List[Dict[str,Any]]]", - "POST": "returns deduplicated explicit variable records without executing Jinja or fabricating runtime values.", - "PRE": "dataset_payload is a Superset dataset-detail style payload with query-bearing fields when available.", - "PURPOSE": "Detect runtime variables and Jinja references from dataset query-bearing fields.", - "SIDE_EFFECT": "none." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "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": "def", - "body": " # [DEF:SupersetContextTemplatesMixin.discover_template_variables:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Detect runtime variables and Jinja references from dataset query-bearing fields.\n # @PRE: dataset_payload is a Superset dataset-detail style payload with query-bearing fields when available.\n # @POST: returns deduplicated explicit variable records without executing Jinja or fabricating runtime values.\n # @SIDE_EFFECT: none.\n # @DATA_CONTRACT: Input[dataset_payload:Dict[str,Any]] -> Output[List[Dict[str,Any]]]\n def discover_template_variables(\n self, dataset_payload: Dict[str, Any]\n ) -> List[Dict[str, Any]]:\n with belief_scope(\"SupersetContextExtractor.discover_template_variables\"):\n discovered: List[Dict[str, Any]] = []\n seen_variable_names: Set[str] = set()\n\n for expression_source in self._collect_query_bearing_expressions(\n dataset_payload\n ):\n for filter_match in re.finditer(\n r\"filter_values\\(\\s*['\\\"]([^'\\\"]+)['\\\"]\\s*\\)\",\n expression_source,\n flags=re.IGNORECASE,\n ):\n variable_name = str(filter_match.group(1) or \"\").strip()\n if not variable_name:\n continue\n self._append_template_variable(\n discovered=discovered,\n seen_variable_names=seen_variable_names,\n variable_name=variable_name,\n expression_source=expression_source,\n variable_kind=\"native_filter\",\n is_required=True,\n default_value=None,\n )\n\n for url_param_match in re.finditer(\n r\"url_param\\(\\s*['\\\"]([^'\\\"]+)['\\\"]\\s*(?:,\\s*([^)]+))?\\)\",\n expression_source,\n flags=re.IGNORECASE,\n ):\n variable_name = str(url_param_match.group(1) or \"\").strip()\n if not variable_name:\n continue\n default_literal = url_param_match.group(2)\n self._append_template_variable(\n discovered=discovered,\n seen_variable_names=seen_variable_names,\n variable_name=variable_name,\n expression_source=expression_source,\n variable_kind=\"parameter\",\n is_required=default_literal is None,\n default_value=self._normalize_default_literal(\n default_literal\n ),\n )\n\n for jinja_match in re.finditer(\n r\"\\{\\{\\s*(.*?)\\s*\\}\\}\", expression_source, flags=re.DOTALL\n ):\n expression = str(jinja_match.group(1) or \"\").strip()\n if not expression:\n continue\n if any(\n token in expression\n for token in (\n \"filter_values(\",\n \"url_param(\",\n \"get_filters(\",\n )\n ):\n continue\n variable_name = self._extract_primary_jinja_identifier(expression)\n if not variable_name:\n continue\n self._append_template_variable(\n discovered=discovered,\n seen_variable_names=seen_variable_names,\n variable_name=variable_name,\n expression_source=expression_source,\n variable_kind=\"derived\"\n if \".\" in expression or \"|\" in expression\n else \"parameter\",\n is_required=True,\n default_value=None,\n )\n\n logger.reflect(\n \"Template variable discovery completed deterministically\",\n extra={\n \"dataset_id\": dataset_payload.get(\"id\"),\n \"variable_count\": len(discovered),\n \"variable_names\": [\n item[\"variable_name\"] for item in discovered\n ],\n },\n )\n return discovered\n\n # [/DEF:SupersetContextTemplatesMixin.discover_template_variables:Function]\n" - }, - { - "contract_id": "SupersetContextTemplatesMixin._collect_query_bearing_expressions", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_templates.py", - "start_line": 126, - "end_line": 168, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Collect SQL and expression-bearing dataset fields for deterministic template-variable discovery." - }, - "relations": [ - { - "source_id": "SupersetContextTemplatesMixin._collect_query_bearing_expressions", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetContextTemplatesMixin.discover_template_variables", - "target_ref": "[SupersetContextTemplatesMixin.discover_template_variables]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextTemplatesMixin._collect_query_bearing_expressions:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Collect SQL and expression-bearing dataset fields for deterministic template-variable discovery.\n # @RELATION: DEPENDS_ON -> [SupersetContextTemplatesMixin.discover_template_variables]\n def _collect_query_bearing_expressions(\n self, dataset_payload: Dict[str, Any]\n ) -> List[str]:\n expressions: List[str] = []\n\n def append_expression(candidate: Any) -> None:\n if not isinstance(candidate, str):\n return\n normalized = candidate.strip()\n if normalized:\n expressions.append(normalized)\n\n append_expression(dataset_payload.get(\"sql\"))\n append_expression(dataset_payload.get(\"query\"))\n append_expression(dataset_payload.get(\"template_sql\"))\n\n metrics_payload = dataset_payload.get(\"metrics\") or []\n if isinstance(metrics_payload, list):\n for metric in metrics_payload:\n if isinstance(metric, str):\n append_expression(metric)\n continue\n if not isinstance(metric, dict):\n continue\n append_expression(metric.get(\"expression\"))\n append_expression(metric.get(\"sqlExpression\"))\n append_expression(metric.get(\"metric_name\"))\n\n columns_payload = dataset_payload.get(\"columns\") or []\n if isinstance(columns_payload, list):\n for column in columns_payload:\n if not isinstance(column, dict):\n continue\n append_expression(column.get(\"sqlExpression\"))\n append_expression(column.get(\"expression\"))\n\n return expressions\n\n # [/DEF:SupersetContextTemplatesMixin._collect_query_bearing_expressions:Function]\n" - }, - { - "contract_id": "SupersetContextTemplatesMixin._append_template_variable", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_templates.py", - "start_line": 170, - "end_line": 201, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Append one deduplicated template-variable descriptor." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextTemplatesMixin._append_template_variable:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Append one deduplicated template-variable descriptor.\n def _append_template_variable(\n self,\n discovered: List[Dict[str, Any]],\n seen_variable_names: Set[str],\n variable_name: str,\n expression_source: str,\n variable_kind: str,\n is_required: bool,\n default_value: Any,\n ) -> None:\n normalized_name = str(variable_name or \"\").strip()\n if not normalized_name:\n return\n seen_key = normalized_name.lower()\n if seen_key in seen_variable_names:\n return\n seen_variable_names.add(seen_key)\n discovered.append(\n {\n \"variable_name\": normalized_name,\n \"expression_source\": expression_source,\n \"variable_kind\": variable_kind,\n \"is_required\": is_required,\n \"default_value\": default_value,\n \"mapping_status\": \"unmapped\",\n }\n )\n\n # [/DEF:SupersetContextTemplatesMixin._append_template_variable:Function]\n" - }, - { - "contract_id": "SupersetContextTemplatesMixin._extract_primary_jinja_identifier", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_templates.py", - "start_line": 203, - "end_line": 224, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Extract a deterministic primary identifier from a Jinja expression without executing it." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextTemplatesMixin._extract_primary_jinja_identifier:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Extract a deterministic primary identifier from a Jinja expression without executing it.\n def _extract_primary_jinja_identifier(self, expression: str) -> Optional[str]:\n matched = re.match(r\"([A-Za-z_][A-Za-z0-9_]*)\", expression.strip())\n if matched is None:\n return None\n candidate = matched.group(1)\n if candidate in {\n \"if\",\n \"else\",\n \"for\",\n \"set\",\n \"True\",\n \"False\",\n \"none\",\n \"None\",\n }:\n return None\n return candidate\n\n # [/DEF:SupersetContextTemplatesMixin._extract_primary_jinja_identifier:Function]\n" - }, - { - "contract_id": "SupersetContextTemplatesMixin._normalize_default_literal", - "contract_type": "Function", - "file_path": "backend/src/core/utils/superset_context_extractor/_templates.py", - "start_line": 226, - "end_line": 252, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Normalize literal default fragments from template helper calls into JSON-safe values." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SupersetContextTemplatesMixin._normalize_default_literal:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize literal default fragments from template helper calls into JSON-safe values.\n def _normalize_default_literal(self, literal: Optional[str]) -> Any:\n normalized_literal = str(literal or \"\").strip()\n if not normalized_literal:\n return None\n if (\n normalized_literal.startswith(\"'\") and normalized_literal.endswith(\"'\")\n ) or (\n normalized_literal.startswith('\"') and normalized_literal.endswith('\"')\n ):\n return normalized_literal[1:-1]\n lowered = normalized_literal.lower()\n if lowered in {\"true\", \"false\"}:\n return lowered == \"true\"\n if lowered in {\"null\", \"none\"}:\n return None\n try:\n return int(normalized_literal)\n except ValueError:\n try:\n return float(normalized_literal)\n except ValueError:\n return normalized_literal\n\n # [/DEF:SupersetContextTemplatesMixin._normalize_default_literal:Function]\n" + "anchor_syntax": "region", + "body": "# #region _templates_imports [TYPE Block]\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any, cast\n\nfrom ...logger import belief_scope, logger\n\nlogger = cast(Any, logger)\n# #endregion _templates_imports\n" }, { "contract_id": "WsLogHandlerModule", @@ -54786,23 +43406,7 @@ } ], "anchor_syntax": "region", - "body": "# #region WsLogHandlerModule [C:3] [TYPE Module] [SEMANTICS logging,handler,websocket,buffer]\n# @BRIEF WebSocket log handler module providing LogEntry DTO and WebSocketLogHandler for buffered log streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION COMPOSES -> [CotJsonFormatter]\nimport logging\nfrom typing import Dict, Any, List, Optional\nfrom collections import deque\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n\n# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str\n message: str\n context: Optional[Dict[str, Any]] = None\n# #endregion LogEntry\n\n\n# #region WebSocketLogHandler [C:3] [TYPE Class] [SEMANTICS logging,handler,websocket,buffer]\n# @BRIEF Custom logging handler that captures log records into a fixed-capacity buffer for WebSocket streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION COMPOSES -> [CotJsonFormatter]\nclass WebSocketLogHandler(logging.Handler):\n \"\"\"\n A logging handler that stores log records and can be extended to send them\n over WebSockets.\n \"\"\"\n\n # #region WebSocketLogHandler.__init__ [C:1] [TYPE Function] [SEMANTICS init,handler,buffer]\n def __init__(self, capacity: int = 1000):\n super().__init__()\n self.log_buffer: deque[LogEntry] = deque(maxlen=capacity)\n # #endregion WebSocketLogHandler.__init__\n\n # #region WebSocketLogHandler.emit [C:2] [TYPE Function] [SEMANTICS log,capture,format,buffer]\n # @BRIEF Captures a log record, formats it, and stores it in the buffer as a LogEntry.\n def emit(self, record: logging.LogRecord):\n try:\n log_entry = LogEntry(\n level=record.levelname,\n message=self.format(record),\n context={\n \"name\": record.name,\n \"pathname\": record.pathname,\n \"lineno\": record.lineno,\n \"funcName\": record.funcName,\n \"process\": record.process,\n \"thread\": record.thread,\n }\n )\n self.log_buffer.append(log_entry)\n except Exception:\n self.handleError(record)\n # #endregion WebSocketLogHandler.emit\n\n # #region WebSocketLogHandler.get_recent_logs [C:2] [TYPE Function] [SEMANTICS log,retrieve,buffer]\n # @BRIEF Returns a list of recent log entries from the buffer.\n def get_recent_logs(self) -> List[LogEntry]:\n \"\"\"\n Returns a list of recent log entries from the buffer.\n \"\"\"\n return list(self.log_buffer)\n # #endregion WebSocketLogHandler.get_recent_logs\n\n\n# #endregion WebSocketLogHandler\n# #endregion WsLogHandlerModule\n" - }, - { - "contract_id": "LogEntry", - "contract_type": "Class", - "file_path": "backend/src/core/ws_log_handler.py", - "start_line": 13, - "end_line": 19, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str\n message: str\n context: Optional[Dict[str, Any]] = None\n# #endregion LogEntry\n" + "body": "# #region WsLogHandlerModule [C:3] [TYPE Module] [SEMANTICS pydantic, dto, log-entry]\n# @BRIEF WebSocket log handler module providing LogEntry DTO and WebSocketLogHandler for buffered log streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION COMPOSES -> [CotJsonFormatter]\nimport logging\nfrom collections import deque\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str\n message: str\n context: dict[str, Any] | None = None\n# #endregion LogEntry\n\n\n# #region WebSocketLogHandler [C:3] [TYPE Class] [SEMANTICS logging,handler,websocket,buffer]\n# @BRIEF Custom logging handler that captures log records into a fixed-capacity buffer for WebSocket streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION COMPOSES -> [CotJsonFormatter]\nclass WebSocketLogHandler(logging.Handler):\n \"\"\"\n A logging handler that stores log records and can be extended to send them\n over WebSockets.\n \"\"\"\n\n # #region WebSocketLogHandler.__init__ [C:1] [TYPE Function] [SEMANTICS init,handler,buffer]\n def __init__(self, capacity: int = 1000):\n super().__init__()\n self.log_buffer: deque[LogEntry] = deque(maxlen=capacity)\n # #endregion WebSocketLogHandler.__init__\n\n # #region WebSocketLogHandler.emit [C:2] [TYPE Function] [SEMANTICS log,capture,format,buffer]\n # @BRIEF Captures a log record, formats it, and stores it in the buffer as a LogEntry.\n def emit(self, record: logging.LogRecord):\n try:\n log_entry = LogEntry(\n level=record.levelname,\n message=self.format(record),\n context={\n \"name\": record.name,\n \"pathname\": record.pathname,\n \"lineno\": record.lineno,\n \"funcName\": record.funcName,\n \"process\": record.process,\n \"thread\": record.thread,\n }\n )\n self.log_buffer.append(log_entry)\n except Exception:\n self.handleError(record)\n # #endregion WebSocketLogHandler.emit\n\n # #region WebSocketLogHandler.get_recent_logs [C:2] [TYPE Function] [SEMANTICS log,retrieve,buffer]\n # @BRIEF Returns a list of recent log entries from the buffer.\n def get_recent_logs(self) -> list[LogEntry]:\n \"\"\"\n Returns a list of recent log entries from the buffer.\n \"\"\"\n return list(self.log_buffer)\n # #endregion WebSocketLogHandler.get_recent_logs\n\n\n# #endregion WebSocketLogHandler\n# #endregion WsLogHandlerModule\n" }, { "contract_id": "WebSocketLogHandler", @@ -54851,7 +43455,7 @@ } ], "anchor_syntax": "region", - "body": "# #region WebSocketLogHandler [C:3] [TYPE Class] [SEMANTICS logging,handler,websocket,buffer]\n# @BRIEF Custom logging handler that captures log records into a fixed-capacity buffer for WebSocket streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION COMPOSES -> [CotJsonFormatter]\nclass WebSocketLogHandler(logging.Handler):\n \"\"\"\n A logging handler that stores log records and can be extended to send them\n over WebSockets.\n \"\"\"\n\n # #region WebSocketLogHandler.__init__ [C:1] [TYPE Function] [SEMANTICS init,handler,buffer]\n def __init__(self, capacity: int = 1000):\n super().__init__()\n self.log_buffer: deque[LogEntry] = deque(maxlen=capacity)\n # #endregion WebSocketLogHandler.__init__\n\n # #region WebSocketLogHandler.emit [C:2] [TYPE Function] [SEMANTICS log,capture,format,buffer]\n # @BRIEF Captures a log record, formats it, and stores it in the buffer as a LogEntry.\n def emit(self, record: logging.LogRecord):\n try:\n log_entry = LogEntry(\n level=record.levelname,\n message=self.format(record),\n context={\n \"name\": record.name,\n \"pathname\": record.pathname,\n \"lineno\": record.lineno,\n \"funcName\": record.funcName,\n \"process\": record.process,\n \"thread\": record.thread,\n }\n )\n self.log_buffer.append(log_entry)\n except Exception:\n self.handleError(record)\n # #endregion WebSocketLogHandler.emit\n\n # #region WebSocketLogHandler.get_recent_logs [C:2] [TYPE Function] [SEMANTICS log,retrieve,buffer]\n # @BRIEF Returns a list of recent log entries from the buffer.\n def get_recent_logs(self) -> List[LogEntry]:\n \"\"\"\n Returns a list of recent log entries from the buffer.\n \"\"\"\n return list(self.log_buffer)\n # #endregion WebSocketLogHandler.get_recent_logs\n\n\n# #endregion WebSocketLogHandler\n" + "body": "# #region WebSocketLogHandler [C:3] [TYPE Class] [SEMANTICS logging,handler,websocket,buffer]\n# @BRIEF Custom logging handler that captures log records into a fixed-capacity buffer for WebSocket streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION COMPOSES -> [CotJsonFormatter]\nclass WebSocketLogHandler(logging.Handler):\n \"\"\"\n A logging handler that stores log records and can be extended to send them\n over WebSockets.\n \"\"\"\n\n # #region WebSocketLogHandler.__init__ [C:1] [TYPE Function] [SEMANTICS init,handler,buffer]\n def __init__(self, capacity: int = 1000):\n super().__init__()\n self.log_buffer: deque[LogEntry] = deque(maxlen=capacity)\n # #endregion WebSocketLogHandler.__init__\n\n # #region WebSocketLogHandler.emit [C:2] [TYPE Function] [SEMANTICS log,capture,format,buffer]\n # @BRIEF Captures a log record, formats it, and stores it in the buffer as a LogEntry.\n def emit(self, record: logging.LogRecord):\n try:\n log_entry = LogEntry(\n level=record.levelname,\n message=self.format(record),\n context={\n \"name\": record.name,\n \"pathname\": record.pathname,\n \"lineno\": record.lineno,\n \"funcName\": record.funcName,\n \"process\": record.process,\n \"thread\": record.thread,\n }\n )\n self.log_buffer.append(log_entry)\n except Exception:\n self.handleError(record)\n # #endregion WebSocketLogHandler.emit\n\n # #region WebSocketLogHandler.get_recent_logs [C:2] [TYPE Function] [SEMANTICS log,retrieve,buffer]\n # @BRIEF Returns a list of recent log entries from the buffer.\n def get_recent_logs(self) -> list[LogEntry]:\n \"\"\"\n Returns a list of recent log entries from the buffer.\n \"\"\"\n return list(self.log_buffer)\n # #endregion WebSocketLogHandler.get_recent_logs\n\n\n# #endregion WebSocketLogHandler\n" }, { "contract_id": "WebSocketLogHandler.__init__", @@ -54901,28 +43505,20 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": " # #region WebSocketLogHandler.get_recent_logs [C:2] [TYPE Function] [SEMANTICS log,retrieve,buffer]\n # @BRIEF Returns a list of recent log entries from the buffer.\n def get_recent_logs(self) -> List[LogEntry]:\n \"\"\"\n Returns a list of recent log entries from the buffer.\n \"\"\"\n return list(self.log_buffer)\n # #endregion WebSocketLogHandler.get_recent_logs\n" + "body": " # #region WebSocketLogHandler.get_recent_logs [C:2] [TYPE Function] [SEMANTICS log,retrieve,buffer]\n # @BRIEF Returns a list of recent log entries from the buffer.\n def get_recent_logs(self) -> list[LogEntry]:\n \"\"\"\n Returns a list of recent log entries from the buffer.\n \"\"\"\n return list(self.log_buffer)\n # #endregion WebSocketLogHandler.get_recent_logs\n" }, { "contract_id": "AppDependencies", "contract_type": "Module", "file_path": "backend/src/dependencies.py", "start_line": 1, - "end_line": 306, + "end_line": 276, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Core", - "PURPOSE": "Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports.", - "SEMANTICS": [ - "dependency", - "injection", - "singleton", - "factory", - "auth", - "jwt" - ] + "PURPOSE": "Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports." }, "relations": [ { @@ -54987,23 +43583,22 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:AppDependencies:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: dependency, injection, singleton, factory, auth, jwt\n# @PURPOSE: Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports.\n# @LAYER: Core\n# @RELATION: Used by main app and API routers to get access to shared instances.\n# @RELATION: CALLS ->[CleanReleaseRepository]\n# @RELATION: CALLS ->[ConfigManager]\n# @RELATION: CALLS ->[PluginLoader]\n# @RELATION: CALLS ->[SchedulerService]\n# @RELATION: CALLS ->[TaskManager]\n# @RELATION: CALLS ->[get_all_plugin_configs]\n# @RELATION: CALLS ->[get_db]\n# @RELATION: CALLS ->[info]\n# @RELATION: CALLS ->[init_db]\n\nfrom pathlib import Path\nfrom typing import Optional\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError\nfrom .core.plugin_loader import PluginLoader\nfrom .core.task_manager import TaskManager\nfrom .core.config_manager import ConfigManager\nfrom .core.scheduler import SchedulerService\nfrom .services.resource_service import ResourceService\nfrom .services.mapping_service import MappingService\nfrom .services.clean_release.repositories import (\n CandidateRepository,\n ArtifactRepository,\n ManifestRepository,\n PolicyRepository,\n ComplianceRepository,\n ReportRepository,\n ApprovalRepository,\n PublicationRepository,\n AuditRepository,\n CleanReleaseAuditLog,\n)\nfrom .services.clean_release.repository import CleanReleaseRepository\nfrom .services.clean_release.facade import CleanReleaseFacade\nfrom .services.reports.report_service import ReportsService\nfrom .core.database import init_db, get_auth_db, get_db\nfrom .core.cot_logger import MarkerLogger\nfrom .core.auth.jwt import decode_token\nfrom .core.auth.repository import AuthRepository\nfrom .models.auth import User\n\nlog = MarkerLogger(\"Dependencies\")\n\n# Initialize singletons lazily to avoid import-time DB side effects during test collection.\n# Use absolute path relative to this file to ensure plugins are found regardless of CWD\nproject_root = Path(__file__).parent.parent.parent\nconfig_path = project_root / \"config.json\"\n\nconfig_manager: Optional[ConfigManager] = None\nplugin_loader: Optional[PluginLoader] = None\ntask_manager: Optional[TaskManager] = None\nscheduler_service: Optional[SchedulerService] = None\nresource_service: Optional[ResourceService] = None\n\n\n# [DEF:get_config_manager:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for ConfigManager.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns shared ConfigManager instance.\n# @RETURN: ConfigManager - The shared config manager instance.\ndef get_config_manager() -> ConfigManager:\n \"\"\"Dependency injector for ConfigManager.\"\"\"\n global config_manager\n if config_manager is None:\n init_db()\n config_manager = ConfigManager(config_path=str(config_path))\n return config_manager\n\n\n# [/DEF:get_config_manager:Function]\n\nplugin_dir = Path(__file__).parent / \"plugins\"\n\n# Clean Release Redesign Singletons\n# Note: These use get_db() which is a generator, so we need a way to provide a session.\n# For singletons in dependencies.py, we might need a different approach or\n# initialize them inside the dependency functions.\n\n\n# [DEF:get_plugin_loader:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for PluginLoader.\n# @PRE: Global plugin_loader must be initialized.\n# @POST: Returns shared PluginLoader instance.\n# @RETURN: PluginLoader - The shared plugin loader instance.\ndef get_plugin_loader() -> PluginLoader:\n \"\"\"Dependency injector for PluginLoader.\"\"\"\n global plugin_loader\n if plugin_loader is None:\n plugin_loader = PluginLoader(plugin_dir=str(plugin_dir))\n log.reason(f\"PluginLoader initialized with directory: {plugin_dir}\")\n log.reason(\n f\"Available plugins: {[config.name for config in plugin_loader.get_all_plugin_configs()]}\"\n )\n return plugin_loader\n\n\n# [/DEF:get_plugin_loader:Function]\n\n\n# [DEF:get_task_manager:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for TaskManager.\n# @PRE: Global task_manager must be initialized.\n# @POST: Returns shared TaskManager instance.\n# @RETURN: TaskManager - The shared task manager instance.\ndef get_task_manager() -> TaskManager:\n \"\"\"Dependency injector for TaskManager.\"\"\"\n global task_manager\n if task_manager is None:\n task_manager = TaskManager(get_plugin_loader())\n log.reason(\"TaskManager initialized\")\n return task_manager\n\n\n# [/DEF:get_task_manager:Function]\n\n\n# [DEF:get_scheduler_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for SchedulerService.\n# @PRE: Global scheduler_service must be initialized.\n# @POST: Returns shared SchedulerService instance.\n# @RETURN: SchedulerService - The shared scheduler service instance.\ndef get_scheduler_service() -> SchedulerService:\n \"\"\"Dependency injector for SchedulerService.\"\"\"\n global scheduler_service\n if scheduler_service is None:\n scheduler_service = SchedulerService(get_task_manager(), get_config_manager())\n log.reason(\"SchedulerService initialized\")\n return scheduler_service\n\n\n# [/DEF:get_scheduler_service:Function]\n\n\n# [DEF:get_resource_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for ResourceService.\n# @PRE: Global resource_service must be initialized.\n# @POST: Returns shared ResourceService instance.\n# @RETURN: ResourceService - The shared resource service instance.\ndef get_resource_service() -> ResourceService:\n \"\"\"Dependency injector for ResourceService.\"\"\"\n global resource_service\n if resource_service is None:\n resource_service = ResourceService()\n log.reason(\"ResourceService initialized\")\n return resource_service\n\n\n# [/DEF:get_resource_service:Function]\n\n\n# [DEF:get_mapping_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for MappingService.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns new MappingService instance.\n# @RETURN: MappingService - A new mapping service instance.\ndef get_mapping_service() -> MappingService:\n \"\"\"Dependency injector for MappingService.\"\"\"\n return MappingService(get_config_manager())\n\n\n# [/DEF:get_mapping_service:Function]\n\n\n_clean_release_repository = CleanReleaseRepository()\n\n\n# [DEF:get_clean_release_repository:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Legacy compatibility shim for CleanReleaseRepository.\n# @POST: Returns a shared CleanReleaseRepository instance.\ndef get_clean_release_repository() -> CleanReleaseRepository:\n \"\"\"Legacy compatibility shim for CleanReleaseRepository.\"\"\"\n return _clean_release_repository\n\n\n# [/DEF:get_clean_release_repository:Function]\n\n\n# [DEF:get_clean_release_facade:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for CleanReleaseFacade.\n# @POST: Returns a facade instance with a fresh DB session.\ndef get_clean_release_facade(db=Depends(get_db)) -> CleanReleaseFacade:\n candidate_repo = CandidateRepository(db)\n artifact_repo = ArtifactRepository(db)\n manifest_repo = ManifestRepository(db)\n policy_repo = PolicyRepository(db)\n compliance_repo = ComplianceRepository(db)\n report_repo = ReportRepository(db)\n approval_repo = ApprovalRepository(db)\n publication_repo = PublicationRepository(db)\n audit_repo = AuditRepository(db)\n\n return CleanReleaseFacade(\n candidate_repo=candidate_repo,\n artifact_repo=artifact_repo,\n manifest_repo=manifest_repo,\n policy_repo=policy_repo,\n compliance_repo=compliance_repo,\n report_repo=report_repo,\n approval_repo=approval_repo,\n publication_repo=publication_repo,\n audit_repo=audit_repo,\n config_manager=get_config_manager(),\n )\n\n\n# [/DEF:get_clean_release_facade:Function]\n\n# [DEF:oauth2_scheme:Variable]\n# @RELATION: DEPENDS_ON -> OAuth2PasswordBearer\n# @COMPLEXITY: 1\n# @PURPOSE: OAuth2 password bearer scheme for token extraction.\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\")\n# [/DEF:oauth2_scheme:Variable]\n\n\n# [DEF:get_current_user:Function]\n# @RELATION: CALLS -> AuthRepository\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for retrieving currently authenticated user from a JWT.\n# @PRE: JWT token provided in Authorization header.\n# @POST: Returns User object if token is valid.\n# @THROW: HTTPException 401 if token is invalid or user not found.\n# @PARAM: token (str) - Extracted JWT token.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: User - The authenticated user.\ndef get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_auth_db)):\n credentials_exception = HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n try:\n payload = decode_token(token)\n username_value = payload.get(\"sub\")\n if not isinstance(username_value, str) or not username_value:\n raise credentials_exception\n username = username_value\n except JWTError:\n raise credentials_exception\n\n repo = AuthRepository(db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise credentials_exception\n return user\n\n\n# [/DEF:get_current_user:Function]\n\n\n# [DEF:has_permission:Function]\n# @RELATION: CALLS -> AuthRepository\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for checking if the current user has a specific permission.\n# @PRE: User is authenticated.\n# @POST: Returns True if user has permission.\n# @THROW: HTTPException 403 if permission is denied.\n# @PARAM: resource (str) - The resource identifier.\n# @PARAM: action (str) - The action identifier (READ, EXECUTE, WRITE).\n# @RETURN: User - The authenticated user if permission granted.\ndef has_permission(resource: str, action: str):\n def permission_checker(current_user: User = Depends(get_current_user)):\n # Union of all permissions across all roles\n for role in current_user.roles:\n for perm in role.permissions:\n if perm.resource == resource and perm.action == action:\n return current_user\n\n # Special case for Admin role (full access)\n if any(role.name == \"Admin\" for role in current_user.roles):\n return current_user\n\n from .core.auth.logger import log_security_event\n\n log_security_event(\n \"PERMISSION_DENIED\",\n str(getattr(current_user, \"username\", \"unknown\")),\n {\"resource\": resource, \"action\": action},\n )\n\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {resource}:{action}\",\n )\n\n return permission_checker\n\n\n# [/DEF:has_permission:Function]\n\n# [/DEF:AppDependencies:Module]\n" + "anchor_syntax": "region", + "body": "# #region AppDependencies [C:3] [TYPE Module] [SEMANTICS fastapi, schedule]\n# @BRIEF Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports.\n# @LAYER: Core\n# @RELATION Used by main app and API routers to get access to shared instances.\n# @RELATION CALLS -> [CleanReleaseRepository]\n# @RELATION CALLS -> [ConfigManager]\n# @RELATION CALLS -> [PluginLoader]\n# @RELATION CALLS -> [SchedulerService]\n# @RELATION CALLS -> [TaskManager]\n# @RELATION CALLS -> [get_all_plugin_configs]\n# @RELATION CALLS -> [get_db]\n# @RELATION CALLS -> [info]\n# @RELATION CALLS -> [init_db]\n\nfrom pathlib import Path\n\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError\n\nfrom .core.auth.jwt import decode_token\nfrom .core.auth.repository import AuthRepository\nfrom .core.config_manager import ConfigManager\nfrom .core.database import get_auth_db, get_db, init_db\nfrom .core.logger import logger\nfrom .core.plugin_loader import PluginLoader\nfrom .core.scheduler import SchedulerService\nfrom .core.task_manager import TaskManager\nfrom .models.auth import User\nfrom .services.clean_release.facade import CleanReleaseFacade\nfrom .services.clean_release.repositories import (\n ApprovalRepository,\n ArtifactRepository,\n AuditRepository,\n CandidateRepository,\n ComplianceRepository,\n ManifestRepository,\n PolicyRepository,\n PublicationRepository,\n ReportRepository,\n)\nfrom .services.clean_release.repository import CleanReleaseRepository\nfrom .services.mapping_service import MappingService\nfrom .services.resource_service import ResourceService\n\n# Initialize singletons lazily to avoid import-time DB side effects during test collection.\n# Use absolute path relative to this file to ensure plugins are found regardless of CWD\nproject_root = Path(__file__).parent.parent.parent\nconfig_path = project_root / \"config.json\"\n\nconfig_manager: ConfigManager | None = None\nplugin_loader: PluginLoader | None = None\ntask_manager: TaskManager | None = None\nscheduler_service: SchedulerService | None = None\nresource_service: ResourceService | None = None\n\n\n# #region get_config_manager [C:1] [TYPE Function]\n# @BRIEF Dependency injector for ConfigManager.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns shared ConfigManager instance.\ndef get_config_manager() -> ConfigManager:\n \"\"\"Dependency injector for ConfigManager.\"\"\"\n global config_manager\n if config_manager is None:\n init_db()\n config_manager = ConfigManager(config_path=str(config_path))\n return config_manager\n\n\n# #endregion get_config_manager\n\nplugin_dir = Path(__file__).parent / \"plugins\"\n\n# Clean Release Redesign Singletons\n# Note: These use get_db() which is a generator, so we need a way to provide a session.\n# For singletons in dependencies.py, we might need a different approach or\n# initialize them inside the dependency functions.\n\n\n# #region get_plugin_loader [C:1] [TYPE Function]\n# @BRIEF Dependency injector for PluginLoader.\n# @PRE: Global plugin_loader must be initialized.\n# @POST: Returns shared PluginLoader instance.\ndef get_plugin_loader() -> PluginLoader:\n \"\"\"Dependency injector for PluginLoader.\"\"\"\n global plugin_loader\n if plugin_loader is None:\n plugin_loader = PluginLoader(plugin_dir=str(plugin_dir))\n logger.info(f\"PluginLoader initialized with directory: {plugin_dir}\")\n logger.info(\n f\"Available plugins: {[config.name for config in plugin_loader.get_all_plugin_configs()]}\"\n )\n return plugin_loader\n\n\n# #endregion get_plugin_loader\n\n\n# #region get_task_manager [C:1] [TYPE Function]\n# @BRIEF Dependency injector for TaskManager.\n# @PRE: Global task_manager must be initialized.\n# @POST: Returns shared TaskManager instance.\ndef get_task_manager() -> TaskManager:\n \"\"\"Dependency injector for TaskManager.\"\"\"\n global task_manager\n if task_manager is None:\n task_manager = TaskManager(get_plugin_loader())\n logger.info(\"TaskManager initialized\")\n return task_manager\n\n\n# #endregion get_task_manager\n\n\n# #region get_scheduler_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for SchedulerService.\n# @PRE: Global scheduler_service must be initialized.\n# @POST: Returns shared SchedulerService instance.\ndef get_scheduler_service() -> SchedulerService:\n \"\"\"Dependency injector for SchedulerService.\"\"\"\n global scheduler_service\n if scheduler_service is None:\n scheduler_service = SchedulerService(get_task_manager(), get_config_manager())\n logger.info(\"SchedulerService initialized\")\n return scheduler_service\n\n\n# #endregion get_scheduler_service\n\n\n# #region get_resource_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for ResourceService.\n# @PRE: Global resource_service must be initialized.\n# @POST: Returns shared ResourceService instance.\ndef get_resource_service() -> ResourceService:\n \"\"\"Dependency injector for ResourceService.\"\"\"\n global resource_service\n if resource_service is None:\n resource_service = ResourceService()\n logger.info(\"ResourceService initialized\")\n return resource_service\n\n\n# #endregion get_resource_service\n\n\n# #region get_mapping_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for MappingService.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns new MappingService instance.\ndef get_mapping_service() -> MappingService:\n \"\"\"Dependency injector for MappingService.\"\"\"\n return MappingService(get_config_manager())\n\n\n# #endregion get_mapping_service\n\n\n_clean_release_repository = CleanReleaseRepository()\n\n\n# #region get_clean_release_repository [C:1] [TYPE Function]\n# @BRIEF Legacy compatibility shim for CleanReleaseRepository.\n# @POST: Returns a shared CleanReleaseRepository instance.\ndef get_clean_release_repository() -> CleanReleaseRepository:\n \"\"\"Legacy compatibility shim for CleanReleaseRepository.\"\"\"\n return _clean_release_repository\n\n\n# #endregion get_clean_release_repository\n\n\n# #region get_clean_release_facade [C:1] [TYPE Function]\n# @BRIEF Dependency injector for CleanReleaseFacade.\n# @POST: Returns a facade instance with a fresh DB session.\ndef get_clean_release_facade(db=Depends(get_db)) -> CleanReleaseFacade:\n candidate_repo = CandidateRepository(db)\n artifact_repo = ArtifactRepository(db)\n manifest_repo = ManifestRepository(db)\n policy_repo = PolicyRepository(db)\n compliance_repo = ComplianceRepository(db)\n report_repo = ReportRepository(db)\n approval_repo = ApprovalRepository(db)\n publication_repo = PublicationRepository(db)\n audit_repo = AuditRepository(db)\n\n return CleanReleaseFacade(\n candidate_repo=candidate_repo,\n artifact_repo=artifact_repo,\n manifest_repo=manifest_repo,\n policy_repo=policy_repo,\n compliance_repo=compliance_repo,\n report_repo=report_repo,\n approval_repo=approval_repo,\n publication_repo=publication_repo,\n audit_repo=audit_repo,\n config_manager=get_config_manager(),\n )\n\n\n# #endregion get_clean_release_facade\n\n# #region oauth2_scheme [C:1] [TYPE Variable]\n# @RELATION DEPENDS_ON -> OAuth2PasswordBearer\n# @BRIEF OAuth2 password bearer scheme for token extraction.\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\")\n# #endregion oauth2_scheme\n\n\n# #region get_current_user [C:3] [TYPE Function]\n# @RELATION CALLS -> AuthRepository\n# @BRIEF Dependency for retrieving currently authenticated user from a JWT.\n# @PRE: JWT token provided in Authorization header.\n# @POST: Returns User object if token is valid.\ndef get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_auth_db)):\n credentials_exception = HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n try:\n payload = decode_token(token)\n username_value = payload.get(\"sub\")\n if not isinstance(username_value, str) or not username_value:\n raise credentials_exception\n username = username_value\n except JWTError:\n raise credentials_exception\n\n repo = AuthRepository(db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise credentials_exception\n return user\n\n\n# #endregion get_current_user\n\n\n# #region has_permission [C:3] [TYPE Function]\n# @RELATION CALLS -> AuthRepository\n# @BRIEF Dependency for checking if the current user has a specific permission.\n# @PRE: User is authenticated.\n# @POST: Returns True if user has permission.\ndef has_permission(resource: str, action: str):\n def permission_checker(current_user: User = Depends(get_current_user)):\n # Union of all permissions across all roles\n for role in current_user.roles:\n for perm in role.permissions:\n if perm.resource == resource and perm.action == action:\n return current_user\n\n # Special case for Admin role (full access)\n if any(role.name == \"Admin\" for role in current_user.roles):\n return current_user\n\n from .core.auth.logger import log_security_event\n\n log_security_event(\n \"PERMISSION_DENIED\",\n str(getattr(current_user, \"username\", \"unknown\")),\n {\"resource\": resource, \"action\": action},\n )\n\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {resource}:{action}\",\n )\n\n return permission_checker\n\n\n# #endregion has_permission\n\n# #endregion AppDependencies\n" }, { "contract_id": "get_config_manager", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 63, - "end_line": 78, + "start_line": 58, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns shared ConfigManager instance.", "PRE": "Global config_manager must be initialized.", - "PURPOSE": "Dependency injector for ConfigManager.", - "RETURN": "ConfigManager - The shared config manager instance." + "PURPOSE": "Dependency injector for ConfigManager." }, "relations": [], "schema_warnings": [ @@ -55033,31 +43628,24 @@ "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": "def", - "body": "# [DEF:get_config_manager:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for ConfigManager.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns shared ConfigManager instance.\n# @RETURN: ConfigManager - The shared config manager instance.\ndef get_config_manager() -> ConfigManager:\n \"\"\"Dependency injector for ConfigManager.\"\"\"\n global config_manager\n if config_manager is None:\n init_db()\n config_manager = ConfigManager(config_path=str(config_path))\n return config_manager\n\n\n# [/DEF:get_config_manager:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_config_manager [C:1] [TYPE Function]\n# @BRIEF Dependency injector for ConfigManager.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns shared ConfigManager instance.\ndef get_config_manager() -> ConfigManager:\n \"\"\"Dependency injector for ConfigManager.\"\"\"\n global config_manager\n if config_manager is None:\n init_db()\n config_manager = ConfigManager(config_path=str(config_path))\n return config_manager\n\n\n# #endregion get_config_manager\n" }, { "contract_id": "get_plugin_loader", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 88, - "end_line": 106, + "start_line": 81, + "end_line": 97, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns shared PluginLoader instance.", "PRE": "Global plugin_loader must be initialized.", - "PURPOSE": "Dependency injector for PluginLoader.", - "RETURN": "PluginLoader - The shared plugin loader instance." + "PURPOSE": "Dependency injector for PluginLoader." }, "relations": [], "schema_warnings": [ @@ -55087,31 +43675,24 @@ "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": "def", - "body": "# [DEF:get_plugin_loader:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for PluginLoader.\n# @PRE: Global plugin_loader must be initialized.\n# @POST: Returns shared PluginLoader instance.\n# @RETURN: PluginLoader - The shared plugin loader instance.\ndef get_plugin_loader() -> PluginLoader:\n \"\"\"Dependency injector for PluginLoader.\"\"\"\n global plugin_loader\n if plugin_loader is None:\n plugin_loader = PluginLoader(plugin_dir=str(plugin_dir))\n log.reason(f\"PluginLoader initialized with directory: {plugin_dir}\")\n log.reason(\n f\"Available plugins: {[config.name for config in plugin_loader.get_all_plugin_configs()]}\"\n )\n return plugin_loader\n\n\n# [/DEF:get_plugin_loader:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_plugin_loader [C:1] [TYPE Function]\n# @BRIEF Dependency injector for PluginLoader.\n# @PRE: Global plugin_loader must be initialized.\n# @POST: Returns shared PluginLoader instance.\ndef get_plugin_loader() -> PluginLoader:\n \"\"\"Dependency injector for PluginLoader.\"\"\"\n global plugin_loader\n if plugin_loader is None:\n plugin_loader = PluginLoader(plugin_dir=str(plugin_dir))\n logger.info(f\"PluginLoader initialized with directory: {plugin_dir}\")\n logger.info(\n f\"Available plugins: {[config.name for config in plugin_loader.get_all_plugin_configs()]}\"\n )\n return plugin_loader\n\n\n# #endregion get_plugin_loader\n" }, { "contract_id": "get_task_manager", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 109, - "end_line": 124, + "start_line": 100, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns shared TaskManager instance.", "PRE": "Global task_manager must be initialized.", - "PURPOSE": "Dependency injector for TaskManager.", - "RETURN": "TaskManager - The shared task manager instance." + "PURPOSE": "Dependency injector for TaskManager." }, "relations": [], "schema_warnings": [ @@ -55141,31 +43722,24 @@ "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": "def", - "body": "# [DEF:get_task_manager:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for TaskManager.\n# @PRE: Global task_manager must be initialized.\n# @POST: Returns shared TaskManager instance.\n# @RETURN: TaskManager - The shared task manager instance.\ndef get_task_manager() -> TaskManager:\n \"\"\"Dependency injector for TaskManager.\"\"\"\n global task_manager\n if task_manager is None:\n task_manager = TaskManager(get_plugin_loader())\n log.reason(\"TaskManager initialized\")\n return task_manager\n\n\n# [/DEF:get_task_manager:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_task_manager [C:1] [TYPE Function]\n# @BRIEF Dependency injector for TaskManager.\n# @PRE: Global task_manager must be initialized.\n# @POST: Returns shared TaskManager instance.\ndef get_task_manager() -> TaskManager:\n \"\"\"Dependency injector for TaskManager.\"\"\"\n global task_manager\n if task_manager is None:\n task_manager = TaskManager(get_plugin_loader())\n logger.info(\"TaskManager initialized\")\n return task_manager\n\n\n# #endregion get_task_manager\n" }, { "contract_id": "get_scheduler_service", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 127, - "end_line": 142, + "start_line": 116, + "end_line": 129, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns shared SchedulerService instance.", "PRE": "Global scheduler_service must be initialized.", - "PURPOSE": "Dependency injector for SchedulerService.", - "RETURN": "SchedulerService - The shared scheduler service instance." + "PURPOSE": "Dependency injector for SchedulerService." }, "relations": [], "schema_warnings": [ @@ -55195,31 +43769,24 @@ "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": "def", - "body": "# [DEF:get_scheduler_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for SchedulerService.\n# @PRE: Global scheduler_service must be initialized.\n# @POST: Returns shared SchedulerService instance.\n# @RETURN: SchedulerService - The shared scheduler service instance.\ndef get_scheduler_service() -> SchedulerService:\n \"\"\"Dependency injector for SchedulerService.\"\"\"\n global scheduler_service\n if scheduler_service is None:\n scheduler_service = SchedulerService(get_task_manager(), get_config_manager())\n log.reason(\"SchedulerService initialized\")\n return scheduler_service\n\n\n# [/DEF:get_scheduler_service:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_scheduler_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for SchedulerService.\n# @PRE: Global scheduler_service must be initialized.\n# @POST: Returns shared SchedulerService instance.\ndef get_scheduler_service() -> SchedulerService:\n \"\"\"Dependency injector for SchedulerService.\"\"\"\n global scheduler_service\n if scheduler_service is None:\n scheduler_service = SchedulerService(get_task_manager(), get_config_manager())\n logger.info(\"SchedulerService initialized\")\n return scheduler_service\n\n\n# #endregion get_scheduler_service\n" }, { "contract_id": "get_resource_service", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 145, - "end_line": 160, + "start_line": 132, + "end_line": 145, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns shared ResourceService instance.", "PRE": "Global resource_service must be initialized.", - "PURPOSE": "Dependency injector for ResourceService.", - "RETURN": "ResourceService - The shared resource service instance." + "PURPOSE": "Dependency injector for ResourceService." }, "relations": [], "schema_warnings": [ @@ -55249,31 +43816,24 @@ "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": "def", - "body": "# [DEF:get_resource_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for ResourceService.\n# @PRE: Global resource_service must be initialized.\n# @POST: Returns shared ResourceService instance.\n# @RETURN: ResourceService - The shared resource service instance.\ndef get_resource_service() -> ResourceService:\n \"\"\"Dependency injector for ResourceService.\"\"\"\n global resource_service\n if resource_service is None:\n resource_service = ResourceService()\n log.reason(\"ResourceService initialized\")\n return resource_service\n\n\n# [/DEF:get_resource_service:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_resource_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for ResourceService.\n# @PRE: Global resource_service must be initialized.\n# @POST: Returns shared ResourceService instance.\ndef get_resource_service() -> ResourceService:\n \"\"\"Dependency injector for ResourceService.\"\"\"\n global resource_service\n if resource_service is None:\n resource_service = ResourceService()\n logger.info(\"ResourceService initialized\")\n return resource_service\n\n\n# #endregion get_resource_service\n" }, { "contract_id": "get_mapping_service", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 163, - "end_line": 174, + "start_line": 148, + "end_line": 157, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns new MappingService instance.", "PRE": "Global config_manager must be initialized.", - "PURPOSE": "Dependency injector for MappingService.", - "RETURN": "MappingService - A new mapping service instance." + "PURPOSE": "Dependency injector for MappingService." }, "relations": [], "schema_warnings": [ @@ -55303,27 +43863,21 @@ "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": "def", - "body": "# [DEF:get_mapping_service:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for MappingService.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns new MappingService instance.\n# @RETURN: MappingService - A new mapping service instance.\ndef get_mapping_service() -> MappingService:\n \"\"\"Dependency injector for MappingService.\"\"\"\n return MappingService(get_config_manager())\n\n\n# [/DEF:get_mapping_service:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_mapping_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for MappingService.\n# @PRE: Global config_manager must be initialized.\n# @POST: Returns new MappingService instance.\ndef get_mapping_service() -> MappingService:\n \"\"\"Dependency injector for MappingService.\"\"\"\n return MappingService(get_config_manager())\n\n\n# #endregion get_mapping_service\n" }, { "contract_id": "get_clean_release_repository", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 180, - "end_line": 189, + "start_line": 163, + "end_line": 171, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns a shared CleanReleaseRepository instance.", "PURPOSE": "Legacy compatibility shim for CleanReleaseRepository." }, @@ -55348,19 +43902,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_clean_release_repository:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Legacy compatibility shim for CleanReleaseRepository.\n# @POST: Returns a shared CleanReleaseRepository instance.\ndef get_clean_release_repository() -> CleanReleaseRepository:\n \"\"\"Legacy compatibility shim for CleanReleaseRepository.\"\"\"\n return _clean_release_repository\n\n\n# [/DEF:get_clean_release_repository:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_clean_release_repository [C:1] [TYPE Function]\n# @BRIEF Legacy compatibility shim for CleanReleaseRepository.\n# @POST: Returns a shared CleanReleaseRepository instance.\ndef get_clean_release_repository() -> CleanReleaseRepository:\n \"\"\"Legacy compatibility shim for CleanReleaseRepository.\"\"\"\n return _clean_release_repository\n\n\n# #endregion get_clean_release_repository\n" }, { "contract_id": "get_clean_release_facade", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 192, - "end_line": 221, + "start_line": 174, + "end_line": 202, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "POST": "Returns a facade instance with a fresh DB session.", "PURPOSE": "Dependency injector for CleanReleaseFacade." }, @@ -55385,19 +43939,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:get_clean_release_facade:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Dependency injector for CleanReleaseFacade.\n# @POST: Returns a facade instance with a fresh DB session.\ndef get_clean_release_facade(db=Depends(get_db)) -> CleanReleaseFacade:\n candidate_repo = CandidateRepository(db)\n artifact_repo = ArtifactRepository(db)\n manifest_repo = ManifestRepository(db)\n policy_repo = PolicyRepository(db)\n compliance_repo = ComplianceRepository(db)\n report_repo = ReportRepository(db)\n approval_repo = ApprovalRepository(db)\n publication_repo = PublicationRepository(db)\n audit_repo = AuditRepository(db)\n\n return CleanReleaseFacade(\n candidate_repo=candidate_repo,\n artifact_repo=artifact_repo,\n manifest_repo=manifest_repo,\n policy_repo=policy_repo,\n compliance_repo=compliance_repo,\n report_repo=report_repo,\n approval_repo=approval_repo,\n publication_repo=publication_repo,\n audit_repo=audit_repo,\n config_manager=get_config_manager(),\n )\n\n\n# [/DEF:get_clean_release_facade:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_clean_release_facade [C:1] [TYPE Function]\n# @BRIEF Dependency injector for CleanReleaseFacade.\n# @POST: Returns a facade instance with a fresh DB session.\ndef get_clean_release_facade(db=Depends(get_db)) -> CleanReleaseFacade:\n candidate_repo = CandidateRepository(db)\n artifact_repo = ArtifactRepository(db)\n manifest_repo = ManifestRepository(db)\n policy_repo = PolicyRepository(db)\n compliance_repo = ComplianceRepository(db)\n report_repo = ReportRepository(db)\n approval_repo = ApprovalRepository(db)\n publication_repo = PublicationRepository(db)\n audit_repo = AuditRepository(db)\n\n return CleanReleaseFacade(\n candidate_repo=candidate_repo,\n artifact_repo=artifact_repo,\n manifest_repo=manifest_repo,\n policy_repo=policy_repo,\n compliance_repo=compliance_repo,\n report_repo=report_repo,\n approval_repo=approval_repo,\n publication_repo=publication_repo,\n audit_repo=audit_repo,\n config_manager=get_config_manager(),\n )\n\n\n# #endregion get_clean_release_facade\n" }, { "contract_id": "oauth2_scheme", "contract_type": "Variable", "file_path": "backend/src/dependencies.py", - "start_line": 223, - "end_line": 228, + "start_line": 204, + "end_line": 208, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "OAuth2 password bearer scheme for token extraction." }, "relations": [ @@ -55472,25 +44026,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:oauth2_scheme:Variable]\n# @RELATION: DEPENDS_ON -> OAuth2PasswordBearer\n# @COMPLEXITY: 1\n# @PURPOSE: OAuth2 password bearer scheme for token extraction.\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\")\n# [/DEF:oauth2_scheme:Variable]\n" + "anchor_syntax": "region", + "body": "# #region oauth2_scheme [C:1] [TYPE Variable]\n# @RELATION DEPENDS_ON -> OAuth2PasswordBearer\n# @BRIEF OAuth2 password bearer scheme for token extraction.\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\")\n# #endregion oauth2_scheme\n" }, { "contract_id": "get_current_user", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 231, - "end_line": 263, + "start_line": 211, + "end_line": 238, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "db (Session) - Auth database session.", + "COMPLEXITY": 3, "POST": "Returns User object if token is valid.", "PRE": "JWT token provided in Authorization header.", - "PURPOSE": "Dependency for retrieving currently authenticated user from a JWT.", - "RETURN": "User - The authenticated user.", - "THROW": "HTTPException 401 if token is invalid or user not found." + "PURPOSE": "Dependency for retrieving currently authenticated user from a JWT." }, "relations": [ { @@ -55501,12 +44052,6 @@ } ], "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", @@ -55524,39 +44069,24 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:get_current_user:Function]\n# @RELATION: CALLS -> AuthRepository\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for retrieving currently authenticated user from a JWT.\n# @PRE: JWT token provided in Authorization header.\n# @POST: Returns User object if token is valid.\n# @THROW: HTTPException 401 if token is invalid or user not found.\n# @PARAM: token (str) - Extracted JWT token.\n# @PARAM: db (Session) - Auth database session.\n# @RETURN: User - The authenticated user.\ndef get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_auth_db)):\n credentials_exception = HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n try:\n payload = decode_token(token)\n username_value = payload.get(\"sub\")\n if not isinstance(username_value, str) or not username_value:\n raise credentials_exception\n username = username_value\n except JWTError:\n raise credentials_exception\n\n repo = AuthRepository(db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise credentials_exception\n return user\n\n\n# [/DEF:get_current_user:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_current_user [C:3] [TYPE Function]\n# @RELATION CALLS -> AuthRepository\n# @BRIEF Dependency for retrieving currently authenticated user from a JWT.\n# @PRE: JWT token provided in Authorization header.\n# @POST: Returns User object if token is valid.\ndef get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_auth_db)):\n credentials_exception = HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n try:\n payload = decode_token(token)\n username_value = payload.get(\"sub\")\n if not isinstance(username_value, str) or not username_value:\n raise credentials_exception\n username = username_value\n except JWTError:\n raise credentials_exception\n\n repo = AuthRepository(db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise credentials_exception\n return user\n\n\n# #endregion get_current_user\n" }, { "contract_id": "has_permission", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 266, - "end_line": 304, + "start_line": 241, + "end_line": 274, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "PARAM": "action (str) - The action identifier (READ, EXECUTE, WRITE).", + "COMPLEXITY": 3, "POST": "Returns True if user has permission.", "PRE": "User is authenticated.", - "PURPOSE": "Dependency for checking if the current user has a specific permission.", - "RETURN": "User - The authenticated user if permission granted.", - "THROW": "HTTPException 403 if permission is denied." + "PURPOSE": "Dependency for checking if the current user has a specific permission." }, "relations": [ { @@ -55567,12 +44097,6 @@ } ], "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", @@ -55590,40 +44114,28 @@ "actual_complexity": 3, "contract_type": "Function" } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "unknown_tag", - "tag": "THROW", - "message": "@THROW is not defined in axiom_config.yaml tags", - "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:has_permission:Function]\n# @RELATION: CALLS -> AuthRepository\n# @COMPLEXITY: 3\n# @PURPOSE: Dependency for checking if the current user has a specific permission.\n# @PRE: User is authenticated.\n# @POST: Returns True if user has permission.\n# @THROW: HTTPException 403 if permission is denied.\n# @PARAM: resource (str) - The resource identifier.\n# @PARAM: action (str) - The action identifier (READ, EXECUTE, WRITE).\n# @RETURN: User - The authenticated user if permission granted.\ndef has_permission(resource: str, action: str):\n def permission_checker(current_user: User = Depends(get_current_user)):\n # Union of all permissions across all roles\n for role in current_user.roles:\n for perm in role.permissions:\n if perm.resource == resource and perm.action == action:\n return current_user\n\n # Special case for Admin role (full access)\n if any(role.name == \"Admin\" for role in current_user.roles):\n return current_user\n\n from .core.auth.logger import log_security_event\n\n log_security_event(\n \"PERMISSION_DENIED\",\n str(getattr(current_user, \"username\", \"unknown\")),\n {\"resource\": resource, \"action\": action},\n )\n\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {resource}:{action}\",\n )\n\n return permission_checker\n\n\n# [/DEF:has_permission:Function]\n" + "anchor_syntax": "region", + "body": "# #region has_permission [C:3] [TYPE Function]\n# @RELATION CALLS -> AuthRepository\n# @BRIEF Dependency for checking if the current user has a specific permission.\n# @PRE: User is authenticated.\n# @POST: Returns True if user has permission.\ndef has_permission(resource: str, action: str):\n def permission_checker(current_user: User = Depends(get_current_user)):\n # Union of all permissions across all roles\n for role in current_user.roles:\n for perm in role.permissions:\n if perm.resource == resource and perm.action == action:\n return current_user\n\n # Special case for Admin role (full access)\n if any(role.name == \"Admin\" for role in current_user.roles):\n return current_user\n\n from .core.auth.logger import log_security_event\n\n log_security_event(\n \"PERMISSION_DENIED\",\n str(getattr(current_user, \"username\", \"unknown\")),\n {\"resource\": resource, \"action\": action},\n )\n\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {resource}:{action}\",\n )\n\n return permission_checker\n\n\n# #endregion has_permission\n" }, { "contract_id": "ModelsPackage", "contract_type": "Package", "file_path": "backend/src/models/__init__.py", "start_line": 1, - "end_line": 2, + "end_line": 3, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Domain model package root." }, "relations": [], "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Package'", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Package'", "detail": { "actual_type": "Package", "allowed_types": [ @@ -55632,6 +44144,7 @@ "Class", "Component", "Block", + "ADR", "Skill", "Agent" ] @@ -55639,8 +44152,8 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Package' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Package' at C1", "detail": { "actual_complexity": 1, "contract_type": "Package" @@ -55648,7 +44161,7 @@ } ], "anchor_syntax": "region", - "body": "# #region ModelsPackage [C:1] [TYPE Package]\n# #endregion ModelsPackage\n" + "body": "# #region ModelsPackage [TYPE Package] [SEMANTICS model, package, init]\n# @BRIEF Domain model package root.\n# #endregion ModelsPackage\n" }, { "contract_id": "TestCleanReleaseModels", @@ -55689,15 +44202,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TestCleanReleaseModels:Module]\n# @RELATION: VERIFIES -> [CleanReleaseModels]\n# @PURPOSE: Contract testing for Clean Release models\n# [/DEF:TestCleanReleaseModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region TestCleanReleaseModels [TYPE Module] [SEMANTICS test, clean-release, model, contract]\n# @RELATION VERIFIES -> [CleanReleaseModels]\n# @BRIEF Contract testing for Clean Release models\n# #endregion TestCleanReleaseModels\n" }, { "contract_id": "test_release_candidate_valid", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_clean_release.py", - "start_line": 41, - "end_line": 50, + "start_line": 39, + "end_line": 46, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -55722,15 +44235,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_release_candidate_valid:Function]\n# @RELATION: BINDS_TO -> [TestCleanReleaseModels]\n# @PURPOSE: Verify that a valid release candidate can be instantiated.\ndef test_release_candidate_valid(valid_candidate_data):\n rc = ReleaseCandidate(**valid_candidate_data)\n assert rc.candidate_id == \"RC-001\"\n assert rc.status == ReleaseCandidateStatus.DRAFT\n\n\n# [/DEF:test_release_candidate_valid:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_release_candidate_valid [TYPE Function]\n# @RELATION BINDS_TO -> [TestCleanReleaseModels]\n# @BRIEF Verify that a valid release candidate can be instantiated.\ndef test_release_candidate_valid(valid_candidate_data):\n rc = ReleaseCandidate(**valid_candidate_data)\n assert rc.candidate_id == \"RC-001\"\n assert rc.status == ReleaseCandidateStatus.DRAFT\n# #endregion test_release_candidate_valid\n" }, { "contract_id": "test_release_candidate_empty_id", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_clean_release.py", - "start_line": 53, - "end_line": 63, + "start_line": 47, + "end_line": 55, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -55776,15 +44289,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_release_candidate_empty_id:Function]\n# @RELATION: BINDS_TO -> [TestCleanReleaseModels]\n# @PURPOSE: Verify that a release candidate with an empty ID is rejected.\ndef test_release_candidate_empty_id(valid_candidate_data):\n valid_candidate_data[\"candidate_id\"] = \" \"\n with pytest.raises(ValueError, match=\"candidate_id must be non-empty\"):\n ReleaseCandidate(**valid_candidate_data)\n\n\n# @TEST_FIXTURE: valid_enterprise_policy\n# [/DEF:test_release_candidate_empty_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_release_candidate_empty_id [TYPE Function]\n# @RELATION BINDS_TO -> [TestCleanReleaseModels]\n# @BRIEF Verify that a release candidate with an empty ID is rejected.\ndef test_release_candidate_empty_id(valid_candidate_data):\n valid_candidate_data[\"candidate_id\"] = \" \"\n with pytest.raises(ValueError, match=\"candidate_id must be non-empty\"):\n ReleaseCandidate(**valid_candidate_data)\n# @TEST_FIXTURE: valid_enterprise_policy\n# #endregion test_release_candidate_empty_id\n" }, { "contract_id": "test_enterprise_policy_valid", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_clean_release.py", - "start_line": 81, - "end_line": 90, + "start_line": 69, + "end_line": 76, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -55810,15 +44323,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_enterprise_policy_valid:Function]\n# @RELATION: BINDS_TO -> [TestCleanReleaseModels]\n# @PURPOSE: Verify that a valid enterprise policy is accepted.\ndef test_enterprise_policy_valid(valid_policy_data):\n policy = CleanProfilePolicy(**valid_policy_data)\n assert policy.external_source_forbidden is True\n\n\n# @TEST_EDGE: enterprise_policy_missing_prohibited\n# [/DEF:test_enterprise_policy_valid:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_enterprise_policy_valid [TYPE Function]\n# @RELATION BINDS_TO -> [TestCleanReleaseModels]\n# @BRIEF Verify that a valid enterprise policy is accepted.\ndef test_enterprise_policy_valid(valid_policy_data):\n policy = CleanProfilePolicy(**valid_policy_data)\n assert policy.external_source_forbidden is True\n# @TEST_EDGE: enterprise_policy_missing_prohibited\n# #endregion test_enterprise_policy_valid\n" }, { "contract_id": "test_enterprise_policy_missing_prohibited", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_clean_release.py", - "start_line": 93, - "end_line": 106, + "start_line": 77, + "end_line": 88, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -55844,15 +44357,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_enterprise_policy_missing_prohibited:Function]\n# @RELATION: BINDS_TO -> [TestCleanReleaseModels]\n# @PURPOSE: Verify that an enterprise policy without prohibited categories is rejected.\ndef test_enterprise_policy_missing_prohibited(valid_policy_data):\n valid_policy_data[\"prohibited_artifact_categories\"] = []\n with pytest.raises(\n ValueError,\n match=\"enterprise-clean policy requires prohibited_artifact_categories\",\n ):\n CleanProfilePolicy(**valid_policy_data)\n\n\n# @TEST_EDGE: enterprise_policy_external_allowed\n# [/DEF:test_enterprise_policy_missing_prohibited:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_enterprise_policy_missing_prohibited [TYPE Function]\n# @RELATION BINDS_TO -> [TestCleanReleaseModels]\n# @BRIEF Verify that an enterprise policy without prohibited categories is rejected.\ndef test_enterprise_policy_missing_prohibited(valid_policy_data):\n valid_policy_data[\"prohibited_artifact_categories\"] = []\n with pytest.raises(\n ValueError,\n match=\"enterprise-clean policy requires prohibited_artifact_categories\",\n ):\n CleanProfilePolicy(**valid_policy_data)\n# @TEST_EDGE: enterprise_policy_external_allowed\n# #endregion test_enterprise_policy_missing_prohibited\n" }, { "contract_id": "test_enterprise_policy_external_allowed", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_clean_release.py", - "start_line": 109, - "end_line": 123, + "start_line": 89, + "end_line": 101, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -55879,15 +44392,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_enterprise_policy_external_allowed:Function]\n# @RELATION: BINDS_TO -> [TestCleanReleaseModels]\n# @PURPOSE: Verify that an enterprise policy allowing external sources is rejected.\ndef test_enterprise_policy_external_allowed(valid_policy_data):\n valid_policy_data[\"external_source_forbidden\"] = False\n with pytest.raises(\n ValueError,\n match=\"enterprise-clean policy requires external_source_forbidden=true\",\n ):\n CleanProfilePolicy(**valid_policy_data)\n\n\n# @TEST_INVARIANT: manifest_consistency\n# @TEST_EDGE: manifest_count_mismatch\n# [/DEF:test_enterprise_policy_external_allowed:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_enterprise_policy_external_allowed [TYPE Function]\n# @RELATION BINDS_TO -> [TestCleanReleaseModels]\n# @BRIEF Verify that an enterprise policy allowing external sources is rejected.\ndef test_enterprise_policy_external_allowed(valid_policy_data):\n valid_policy_data[\"external_source_forbidden\"] = False\n with pytest.raises(\n ValueError,\n match=\"enterprise-clean policy requires external_source_forbidden=true\",\n ):\n CleanProfilePolicy(**valid_policy_data)\n# @TEST_INVARIANT: manifest_consistency\n# @TEST_EDGE: manifest_count_mismatch\n# #endregion test_enterprise_policy_external_allowed\n" }, { "contract_id": "test_manifest_count_mismatch", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_clean_release.py", - "start_line": 126, - "end_line": 168, + "start_line": 102, + "end_line": 140, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -55912,15 +44425,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_manifest_count_mismatch:Function]\n# @RELATION: BINDS_TO -> [TestCleanReleaseModels]\n# @PURPOSE: Verify that a manifest with count mismatches is rejected.\ndef test_manifest_count_mismatch():\n summary = ManifestSummary(\n included_count=1, excluded_count=0, prohibited_detected_count=0\n )\n item = ManifestItem(\n path=\"p\", category=\"c\", classification=ClassificationType.ALLOWED, reason=\"r\"\n )\n\n # Valid\n DistributionManifest(\n manifest_id=\"m1\",\n candidate_id=\"rc1\",\n policy_id=\"p1\",\n generated_at=datetime.now(),\n generated_by=\"u\",\n items=[item],\n summary=summary,\n deterministic_hash=\"h\",\n )\n\n # Invalid count\n summary.included_count = 2\n with pytest.raises(\n ValueError, match=\"manifest summary counts must match items size\"\n ):\n DistributionManifest(\n manifest_id=\"m1\",\n candidate_id=\"rc1\",\n policy_id=\"p1\",\n generated_at=datetime.now(),\n generated_by=\"u\",\n items=[item],\n summary=summary,\n deterministic_hash=\"h\",\n )\n\n\n# @TEST_INVARIANT: run_integrity\n# @TEST_EDGE: compliant_run_stage_fail\n# [/DEF:test_manifest_count_mismatch:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_manifest_count_mismatch [TYPE Function]\n# @RELATION BINDS_TO -> [TestCleanReleaseModels]\n# @BRIEF Verify that a manifest with count mismatches is rejected.\ndef test_manifest_count_mismatch():\n summary = ManifestSummary(\n included_count=1, excluded_count=0, prohibited_detected_count=0\n )\n item = ManifestItem(\n path=\"p\", category=\"c\", classification=ClassificationType.ALLOWED, reason=\"r\"\n )\n # Valid\n DistributionManifest(\n manifest_id=\"m1\",\n candidate_id=\"rc1\",\n policy_id=\"p1\",\n generated_at=datetime.now(),\n generated_by=\"u\",\n items=[item],\n summary=summary,\n deterministic_hash=\"h\",\n )\n # Invalid count\n summary.included_count = 2\n with pytest.raises(\n ValueError, match=\"manifest summary counts must match items size\"\n ):\n DistributionManifest(\n manifest_id=\"m1\",\n candidate_id=\"rc1\",\n policy_id=\"p1\",\n generated_at=datetime.now(),\n generated_by=\"u\",\n items=[item],\n summary=summary,\n deterministic_hash=\"h\",\n )\n# @TEST_INVARIANT: run_integrity\n# @TEST_EDGE: compliant_run_stage_fail\n# #endregion test_manifest_count_mismatch\n" }, { "contract_id": "test_compliant_run_validation", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_clean_release.py", - "start_line": 171, - "end_line": 214, + "start_line": 141, + "end_line": 180, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -55945,15 +44458,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_compliant_run_validation:Function]\n# @RELATION: BINDS_TO -> [TestCleanReleaseModels]\n# @PURPOSE: Verify compliant run validation logic and mandatory stage checks.\ndef test_compliant_run_validation():\n base_run = {\n \"check_run_id\": \"run1\",\n \"candidate_id\": \"rc1\",\n \"policy_id\": \"p1\",\n \"started_at\": datetime.now(),\n \"triggered_by\": \"u\",\n \"execution_mode\": ExecutionMode.TUI,\n \"final_status\": CheckFinalStatus.COMPLIANT,\n \"checks\": [\n CheckStageResult(\n stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS\n ),\n CheckStageResult(\n stage=CheckStageName.INTERNAL_SOURCES_ONLY, status=CheckStageStatus.PASS\n ),\n CheckStageResult(\n stage=CheckStageName.NO_EXTERNAL_ENDPOINTS, status=CheckStageStatus.PASS\n ),\n CheckStageResult(\n stage=CheckStageName.MANIFEST_CONSISTENCY, status=CheckStageStatus.PASS\n ),\n ],\n }\n # Valid\n ComplianceCheckRun(**base_run)\n\n # One stage fails -> cannot be COMPLIANT\n base_run[\"checks\"][0].status = CheckStageStatus.FAIL\n with pytest.raises(\n ValueError, match=\"compliant run requires PASS on all mandatory stages\"\n ):\n ComplianceCheckRun(**base_run)\n\n # Missing stage -> cannot be COMPLIANT\n base_run[\"checks\"] = base_run[\"checks\"][1:]\n with pytest.raises(ValueError, match=\"compliant run requires all mandatory stages\"):\n ComplianceCheckRun(**base_run)\n\n\n# [/DEF:test_compliant_run_validation:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_compliant_run_validation [TYPE Function]\n# @RELATION BINDS_TO -> [TestCleanReleaseModels]\n# @BRIEF Verify compliant run validation logic and mandatory stage checks.\ndef test_compliant_run_validation():\n base_run = {\n \"check_run_id\": \"run1\",\n \"candidate_id\": \"rc1\",\n \"policy_id\": \"p1\",\n \"started_at\": datetime.now(),\n \"triggered_by\": \"u\",\n \"execution_mode\": ExecutionMode.TUI,\n \"final_status\": CheckFinalStatus.COMPLIANT,\n \"checks\": [\n CheckStageResult(\n stage=CheckStageName.DATA_PURITY, status=CheckStageStatus.PASS\n ),\n CheckStageResult(\n stage=CheckStageName.INTERNAL_SOURCES_ONLY, status=CheckStageStatus.PASS\n ),\n CheckStageResult(\n stage=CheckStageName.NO_EXTERNAL_ENDPOINTS, status=CheckStageStatus.PASS\n ),\n CheckStageResult(\n stage=CheckStageName.MANIFEST_CONSISTENCY, status=CheckStageStatus.PASS\n ),\n ],\n }\n # Valid\n ComplianceCheckRun(**base_run)\n # One stage fails -> cannot be COMPLIANT\n base_run[\"checks\"][0].status = CheckStageStatus.FAIL\n with pytest.raises(\n ValueError, match=\"compliant run requires PASS on all mandatory stages\"\n ):\n ComplianceCheckRun(**base_run)\n # Missing stage -> cannot be COMPLIANT\n base_run[\"checks\"] = base_run[\"checks\"][1:]\n with pytest.raises(ValueError, match=\"compliant run requires all mandatory stages\"):\n ComplianceCheckRun(**base_run)\n# #endregion test_compliant_run_validation\n" }, { "contract_id": "test_report_validation", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_clean_release.py", - "start_line": 217, - "end_line": 249, + "start_line": 181, + "end_line": 210, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -55978,59 +44491,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_report_validation:Function]\n# @RELATION: BINDS_TO -> [TestCleanReleaseModels]\n# @PURPOSE: Verify compliance report validation based on status and violation counts.\ndef test_report_validation():\n # Valid blocked report\n ComplianceReport(\n report_id=\"rep1\",\n check_run_id=\"run1\",\n candidate_id=\"rc1\",\n generated_at=datetime.now(),\n final_status=CheckFinalStatus.BLOCKED,\n operator_summary=\"Blocked\",\n structured_payload_ref=\"ref\",\n violations_count=2,\n blocking_violations_count=2,\n )\n\n # BLOCKED with 0 blocking violations\n with pytest.raises(ValueError, match=\"blocked report requires blocking violations\"):\n ComplianceReport(\n report_id=\"rep1\",\n check_run_id=\"run1\",\n candidate_id=\"rc1\",\n generated_at=datetime.now(),\n final_status=CheckFinalStatus.BLOCKED,\n operator_summary=\"Blocked\",\n structured_payload_ref=\"ref\",\n violations_count=2,\n blocking_violations_count=0,\n )\n\n\n# [/DEF:test_report_validation:Function]\n" - }, - { - "contract_id": "test_models", - "contract_type": "Module", - "file_path": "backend/src/models/__tests__/test_models.py", - "start_line": 1, - "end_line": 39, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "LAYER": "Domain", - "PURPOSE": "Unit tests for data models" - }, - "relations": [ - { - "source_id": "test_models", - "relation_type": "VERIFIES", - "target_id": "ModelsPackage", - "target_ref": "[ModelsPackage]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_models:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: Unit tests for data models\n# @LAYER: Domain\n# @RELATION: VERIFIES -> [ModelsPackage]\n\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent.parent / \"src\"))\n\nfrom src.core.config_models import Environment\nfrom src.core.logger import belief_scope\n\n\n# [DEF:test_environment_model:Function]\n# @RELATION: BINDS_TO -> test_models\n# @PURPOSE: Tests that Environment model correctly stores values.\n# @PRE: Environment class is available.\n# @POST: Values are verified.\ndef test_environment_model():\n with belief_scope(\"test_environment_model\"):\n env = Environment(\n id=\"test-id\",\n name=\"test-env\",\n url=\"http://localhost:8088/api/v1\",\n username=\"admin\",\n password=\"password\",\n )\n assert env.id == \"test-id\"\n assert env.name == \"test-env\"\n assert env.url == \"http://localhost:8088/api/v1\"\n\n\n# [/DEF:test_environment_model:Function]\n\n\n# [/DEF:test_models:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_report_validation [TYPE Function]\n# @RELATION BINDS_TO -> [TestCleanReleaseModels]\n# @BRIEF Verify compliance report validation based on status and violation counts.\ndef test_report_validation():\n # Valid blocked report\n ComplianceReport(\n report_id=\"rep1\",\n check_run_id=\"run1\",\n candidate_id=\"rc1\",\n generated_at=datetime.now(),\n final_status=CheckFinalStatus.BLOCKED,\n operator_summary=\"Blocked\",\n structured_payload_ref=\"ref\",\n violations_count=2,\n blocking_violations_count=2,\n )\n # BLOCKED with 0 blocking violations\n with pytest.raises(ValueError, match=\"blocked report requires blocking violations\"):\n ComplianceReport(\n report_id=\"rep1\",\n check_run_id=\"run1\",\n candidate_id=\"rc1\",\n generated_at=datetime.now(),\n final_status=CheckFinalStatus.BLOCKED,\n operator_summary=\"Blocked\",\n structured_payload_ref=\"ref\",\n violations_count=2,\n blocking_violations_count=0,\n )\n# #endregion test_report_validation\n" }, { "contract_id": "test_environment_model", "contract_type": "Function", "file_path": "backend/src/models/__tests__/test_models.py", - "start_line": 17, - "end_line": 36, + "start_line": 14, + "end_line": 31, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -56075,72 +44544,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_environment_model:Function]\n# @RELATION: BINDS_TO -> test_models\n# @PURPOSE: Tests that Environment model correctly stores values.\n# @PRE: Environment class is available.\n# @POST: Values are verified.\ndef test_environment_model():\n with belief_scope(\"test_environment_model\"):\n env = Environment(\n id=\"test-id\",\n name=\"test-env\",\n url=\"http://localhost:8088/api/v1\",\n username=\"admin\",\n password=\"password\",\n )\n assert env.id == \"test-id\"\n assert env.name == \"test-env\"\n assert env.url == \"http://localhost:8088/api/v1\"\n\n\n# [/DEF:test_environment_model:Function]\n" - }, - { - "contract_id": "test_report_models", - "contract_type": "Module", - "file_path": "backend/src/models/__tests__/test_report_models.py", - "start_line": 1, - "end_line": 235, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Domain", - "PURPOSE": "Unit tests for report Pydantic models and their validators" - }, - "relations": [ - { - "source_id": "test_report_models", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_report_models:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for report Pydantic models and their validators\n# @LAYER: Domain\n\nimport sys\nfrom pathlib import Path\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom datetime import datetime, timedelta\n\n\nclass TestTaskType:\n \"\"\"Tests for the TaskType enum.\"\"\"\n\n def test_enum_values(self):\n from src.models.report import TaskType\n assert TaskType.LLM_VERIFICATION == \"llm_verification\"\n assert TaskType.BACKUP == \"backup\"\n assert TaskType.MIGRATION == \"migration\"\n assert TaskType.DOCUMENTATION == \"documentation\"\n assert TaskType.UNKNOWN == \"unknown\"\n\n\nclass TestReportStatus:\n \"\"\"Tests for the ReportStatus enum.\"\"\"\n\n def test_enum_values(self):\n from src.models.report import ReportStatus\n assert ReportStatus.SUCCESS == \"success\"\n assert ReportStatus.FAILED == \"failed\"\n assert ReportStatus.IN_PROGRESS == \"in_progress\"\n assert ReportStatus.PARTIAL == \"partial\"\n\n\nclass TestErrorContext:\n \"\"\"Tests for ErrorContext model.\"\"\"\n\n def test_valid_creation(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Something failed\", code=\"ERR_001\", next_actions=[\"Retry\"])\n assert ctx.message == \"Something failed\"\n assert ctx.code == \"ERR_001\"\n assert ctx.next_actions == [\"Retry\"]\n\n def test_minimal_creation(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Error occurred\")\n assert ctx.code is None\n assert ctx.next_actions == []\n\n\nclass TestTaskReport:\n \"\"\"Tests for TaskReport model and its validators.\"\"\"\n\n def _make_report(self, **overrides):\n from src.models.report import TaskReport, TaskType, ReportStatus\n defaults = {\n \"report_id\": \"rpt-001\",\n \"task_id\": \"task-001\",\n \"task_type\": TaskType.BACKUP,\n \"status\": ReportStatus.SUCCESS,\n \"updated_at\": datetime(2024, 1, 15, 12, 0, 0),\n \"summary\": \"Backup completed\",\n }\n defaults.update(overrides)\n return TaskReport(**defaults)\n\n def test_valid_creation(self):\n report = self._make_report()\n assert report.report_id == \"rpt-001\"\n assert report.task_id == \"task-001\"\n assert report.summary == \"Backup completed\"\n\n def test_empty_report_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(report_id=\"\")\n\n def test_whitespace_report_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(report_id=\" \")\n\n def test_empty_task_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(task_id=\"\")\n\n def test_empty_summary_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(summary=\"\")\n\n def test_summary_whitespace_trimmed(self):\n report = self._make_report(summary=\" Trimmed \")\n assert report.summary == \"Trimmed\"\n\n def test_optional_fields(self):\n report = self._make_report()\n assert report.started_at is None\n assert report.details is None\n assert report.error_context is None\n assert report.source_ref is None\n\n def test_with_error_context(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Connection failed\")\n report = self._make_report(error_context=ctx)\n assert report.error_context.message == \"Connection failed\"\n\n\nclass TestReportQuery:\n \"\"\"Tests for ReportQuery model and its validators.\"\"\"\n\n def test_defaults(self):\n from src.models.report import ReportQuery\n q = ReportQuery()\n assert q.page == 1\n assert q.page_size == 20\n assert q.task_types == []\n assert q.statuses == []\n assert q.sort_by == \"updated_at\"\n assert q.sort_order == \"desc\"\n\n def test_invalid_sort_by_raises(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError, match=\"sort_by\"):\n ReportQuery(sort_by=\"invalid_field\")\n\n def test_valid_sort_by_values(self):\n from src.models.report import ReportQuery\n for field in [\"updated_at\", \"status\", \"task_type\"]:\n q = ReportQuery(sort_by=field)\n assert q.sort_by == field\n\n def test_invalid_sort_order_raises(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError, match=\"sort_order\"):\n ReportQuery(sort_order=\"invalid\")\n\n def test_valid_sort_order_values(self):\n from src.models.report import ReportQuery\n for order in [\"asc\", \"desc\"]:\n q = ReportQuery(sort_order=order)\n assert q.sort_order == order\n\n def test_time_range_validation_valid(self):\n from src.models.report import ReportQuery\n now = datetime.utcnow()\n q = ReportQuery(time_from=now - timedelta(days=1), time_to=now)\n assert q.time_from < q.time_to\n\n def test_time_range_validation_invalid(self):\n from src.models.report import ReportQuery\n now = datetime.utcnow()\n with pytest.raises(ValueError, match=\"time_from\"):\n ReportQuery(time_from=now, time_to=now - timedelta(days=1))\n\n def test_page_ge_1(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError):\n ReportQuery(page=0)\n\n def test_page_size_bounds(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError):\n ReportQuery(page_size=0)\n with pytest.raises(ValueError):\n ReportQuery(page_size=101)\n\n\nclass TestReportCollection:\n \"\"\"Tests for ReportCollection model.\"\"\"\n\n def test_valid_creation(self):\n from src.models.report import ReportCollection, ReportQuery\n col = ReportCollection(\n items=[],\n total=0,\n page=1,\n page_size=20,\n has_next=False,\n applied_filters=ReportQuery(),\n )\n assert col.total == 0\n assert col.has_next is False\n\n def test_with_items(self):\n from src.models.report import ReportCollection, ReportQuery, TaskReport, TaskType, ReportStatus\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.BACKUP,\n status=ReportStatus.SUCCESS, updated_at=datetime.utcnow(),\n summary=\"OK\"\n )\n col = ReportCollection(\n items=[report], total=1, page=1, page_size=20,\n has_next=False, applied_filters=ReportQuery()\n )\n assert len(col.items) == 1\n assert col.items[0].report_id == \"r1\"\n\n\nclass TestReportDetailView:\n \"\"\"Tests for ReportDetailView model.\"\"\"\n\n def test_valid_creation(self):\n from src.models.report import ReportDetailView, TaskReport, TaskType, ReportStatus\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.BACKUP,\n status=ReportStatus.SUCCESS, updated_at=datetime.utcnow(),\n summary=\"Backup OK\"\n )\n detail = ReportDetailView(report=report)\n assert detail.report.report_id == \"r1\"\n assert detail.timeline == []\n assert detail.diagnostics is None\n assert detail.next_actions == []\n\n def test_with_all_fields(self):\n from src.models.report import ReportDetailView, TaskReport, TaskType, ReportStatus\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.MIGRATION,\n status=ReportStatus.FAILED, updated_at=datetime.utcnow(),\n summary=\"Migration failed\"\n )\n detail = ReportDetailView(\n report=report,\n timeline=[{\"event\": \"started\", \"at\": \"2024-01-01T00:00:00\"}],\n diagnostics={\"cause\": \"timeout\"},\n next_actions=[\"Retry\", \"Check connection\"],\n )\n assert len(detail.timeline) == 1\n assert detail.diagnostics[\"cause\"] == \"timeout\"\n assert \"Retry\" in detail.next_actions\n\n# [/DEF:test_report_models:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_environment_model [TYPE Function]\n# @RELATION BINDS_TO -> test_models\n# @BRIEF Tests that Environment model correctly stores values.\n# @PRE: Environment class is available.\n# @POST: Values are verified.\ndef test_environment_model():\n with belief_scope(\"test_environment_model\"):\n env = Environment(\n id=\"test-id\",\n name=\"test-env\",\n url=\"http://localhost:8088/api/v1\",\n username=\"admin\",\n password=\"password\",\n )\n assert env.id == \"test-id\"\n assert env.name == \"test-env\"\n assert env.url == \"http://localhost:8088/api/v1\"\n# #endregion test_environment_model\n" }, { "contract_id": "AssistantModels", "contract_type": "Module", "file_path": "backend/src/models/assistant.py", "start_line": 1, - "end_line": 83, + "end_line": 78, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Assistant records preserve immutable ids and creation timestamps.", "LAYER": "Domain", - "PURPOSE": "SQLAlchemy models for assistant audit trail and confirmation tokens.", - "SEMANTICS": [ - "assistant", - "audit", - "confirmation", - "chat" - ] + "PURPOSE": "SQLAlchemy models for assistant audit trail and confirmation tokens." }, "relations": [ { @@ -56161,19 +44580,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: assistant, audit, confirmation, chat\n# @PURPOSE: 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 Column, String, DateTime, JSON, Text\n\nfrom .mapping import Base\n\n\n# [DEF:AssistantAuditRecord:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:AssistantAuditRecord:Class]\n\n\n# [DEF:AssistantMessageRecord:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:AssistantMessageRecord:Class]\n\n\n# [DEF:AssistantConfirmationRecord:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:AssistantConfirmationRecord:Class]\n# [/DEF:AssistantModels:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "AssistantAuditRecord", "contract_type": "Class", "file_path": "backend/src/models/assistant.py", - "start_line": 16, - "end_line": 35, + "start_line": 14, + "end_line": 32, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Audit payload remains available for compliance and debugging.", "PRE": "user_id must identify the actor for every record.", "PURPOSE": "Store audit decisions and outcomes produced by assistant command handling." @@ -56206,19 +44625,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantAuditRecord:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:AssistantAuditRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region AssistantAuditRecord [C:3] [TYPE Class]\n# @BRIEF Store audit decisions and outcomes produced by assistant command handling.\n# @RELATION INHERITS -> MappingModels\n# @PRE: user_id must identify the actor for every record.\n# @POST: Audit payload remains available for compliance and debugging.\nclass AssistantAuditRecord(Base):\n __tablename__ = \"assistant_audit\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=True)\n decision = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n message = Column(Text, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantAuditRecord\n" }, { "contract_id": "AssistantMessageRecord", "contract_type": "Class", "file_path": "backend/src/models/assistant.py", - "start_line": 38, - "end_line": 59, + "start_line": 35, + "end_line": 55, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Message row can be queried in chronological order.", "PRE": "user_id, conversation_id, role and text must be present.", "PURPOSE": "Persist chat history entries for assistant conversations." @@ -56251,19 +44670,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantMessageRecord:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:AssistantMessageRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region AssistantMessageRecord [C:3] [TYPE Class]\n# @BRIEF Persist chat history entries for assistant conversations.\n# @RELATION INHERITS -> MappingModels\n# @PRE: user_id, conversation_id, role and text must be present.\n# @POST: Message row can be queried in chronological order.\nclass AssistantMessageRecord(Base):\n __tablename__ = \"assistant_messages\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n role = Column(String, nullable=False) # user | assistant\n text = Column(Text, nullable=False)\n state = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n confirmation_id = Column(String, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantMessageRecord\n" }, { "contract_id": "AssistantConfirmationRecord", "contract_type": "Class", "file_path": "backend/src/models/assistant.py", - "start_line": 62, - "end_line": 82, + "start_line": 58, + "end_line": 77, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "State transitions can be tracked and audited.", "PRE": "intent/dispatch and expiry timestamp must be provided.", "PURPOSE": "Persist risky operation confirmation tokens with lifecycle state." @@ -56296,20 +44715,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AssistantConfirmationRecord:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:AssistantConfirmationRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region AssistantConfirmationRecord [C:3] [TYPE Class]\n# @BRIEF Persist risky operation confirmation tokens with lifecycle state.\n# @RELATION INHERITS -> MappingModels\n# @PRE: intent/dispatch and expiry timestamp must be provided.\n# @POST: State transitions can be tracked and audited.\nclass AssistantConfirmationRecord(Base):\n __tablename__ = \"assistant_confirmations\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n state = Column(String, index=True, nullable=False, default=\"pending\")\n intent = Column(JSON, nullable=False)\n dispatch = Column(JSON, nullable=False)\n expires_at = Column(DateTime, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n consumed_at = Column(DateTime, nullable=True)\n\n\n# #endregion AssistantConfirmationRecord\n" }, { "contract_id": "AuthModels", "contract_type": "Module", - "file_path": "backend/src/models/auth.py", + "file_path": "backend/src/models/auth.py.bak", "start_line": 1, - "end_line": 132, + "end_line": 134, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": "3", - "INVARIANT": "Usernames and emails must be unique.", "LAYER": "Domain", "PURPOSE": "SQLAlchemy models for multi-user authentication and authorization.", "SEMANTICS": [ @@ -56330,15 +44748,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", @@ -56359,17 +44768,34 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:AuthModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: auth, models, user, role, permission, sqlalchemy\n# @PURPOSE: SQLAlchemy models for multi-user authentication and authorization.\n# @LAYER: Domain\n# @RELATION: INHERITS_FROM -> [Base]\n#\n# @INVARIANT: Usernames and emails must be unique.\n\n# [SECTION: IMPORTS]\nimport uuid\nfrom datetime import datetime\nfrom sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, Table\nfrom sqlalchemy.orm import relationship\nfrom .mapping import Base\n# [/SECTION]\n\n\n# [DEF:generate_uuid:Function]\n# @PURPOSE: Generates a unique UUID string.\n# @POST: Returns a string representation of a new UUID.\n# @RELATION: DEPENDS_ON -> [uuid]\ndef generate_uuid():\n return str(uuid.uuid4())\n\n\n# [/DEF:generate_uuid:Function]\n\n# [DEF:user_roles:Table]\n# @PURPOSE: Association table for many-to-many relationship between Users and Roles.\n# @RELATION: DEPENDS_ON -> [Base]\n# @RELATION: DEPENDS_ON -> [User]\n# @RELATION: DEPENDS_ON -> [Role]\nuser_roles = Table(\n \"user_roles\",\n Base.metadata,\n Column(\"user_id\", String, ForeignKey(\"users.id\"), primary_key=True),\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n)\n# [/DEF:user_roles:Table]\n\n# [DEF:role_permissions:Table]\n# @PURPOSE: Association table for many-to-many relationship between Roles and Permissions.\n# @RELATION: DEPENDS_ON -> [Base]\n# @RELATION: DEPENDS_ON -> [Role]\n# @RELATION: DEPENDS_ON -> [Permission]\nrole_permissions = Table(\n \"role_permissions\",\n Base.metadata,\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n Column(\"permission_id\", String, ForeignKey(\"permissions.id\"), primary_key=True),\n)\n# [/DEF:role_permissions:Table]\n\n\n# [DEF:User:Class]\n# @PURPOSE: Represents an identity that can authenticate to the system.\n# @RELATION: HAS_MANY -> [Role]\nclass User(Base):\n __tablename__ = \"users\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n username = Column(String, unique=True, index=True, nullable=False)\n email = Column(String, unique=True, index=True, nullable=True)\n password_hash = Column(String, nullable=True)\n full_name = Column(String, nullable=True)\n auth_source = Column(String, default=\"LOCAL\") # LOCAL or ADFS\n is_active = Column(Boolean, default=True)\n is_ad_user = Column(Boolean, default=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n last_login = Column(DateTime, nullable=True)\n\n roles = relationship(\"Role\", secondary=user_roles, back_populates=\"users\")\n\n\n# [/DEF:User:Class]\n\n\n# [DEF:Role:Class]\n# @PURPOSE: Represents a collection of permissions.\n# @RELATION: HAS_MANY -> [User]\n# @RELATION: HAS_MANY -> [Permission]\nclass Role(Base):\n __tablename__ = \"roles\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, unique=True, index=True, nullable=False)\n description = Column(String, nullable=True)\n\n users = relationship(\"User\", secondary=user_roles, back_populates=\"roles\")\n permissions = relationship(\n \"Permission\", secondary=role_permissions, back_populates=\"roles\"\n )\n\n\n# [/DEF:Role:Class]\n\n\n# [DEF:Permission:Class]\n# @PURPOSE: Represents a specific capability within the system.\n# @RELATION: HAS_MANY -> [Role]\nclass Permission(Base):\n __tablename__ = \"permissions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n resource = Column(String, nullable=False) # e.g. \"plugin:backup\"\n action = Column(String, nullable=False) # e.g. \"READ\", \"EXECUTE\", \"WRITE\"\n\n roles = relationship(\n \"Role\", secondary=role_permissions, back_populates=\"permissions\"\n )\n\n\n# [/DEF:Permission:Class]\n\n\n# [DEF:ADGroupMapping:Class]\n# @PURPOSE: Maps an Active Directory group to a local System Role.\n# @RELATION: DEPENDS_ON -> [Role]\nclass ADGroupMapping(Base):\n __tablename__ = \"ad_group_mappings\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n ad_group = Column(String, unique=True, index=True, nullable=False)\n role_id = Column(String, ForeignKey(\"roles.id\"), nullable=False)\n\n role = relationship(\"Role\")\n\n\n# [/DEF:ADGroupMapping:Class]\n\n# [/DEF:AuthModels:Module]\n" + "body": "# [DEF:AuthModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: auth, models, user, role, permission, sqlalchemy\n# @PURPOSE: SQLAlchemy models for multi-user authentication and authorization.\n# @LAYER: Domain\n# @RELATION: INHERITS_FROM -> [Base]\n\n# [SECTION: IMPORTS]\nimport uuid\nfrom datetime import datetime\nfrom sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, Table\nfrom sqlalchemy.orm import relationship\nfrom .mapping import Base\n# [/SECTION]\n\n\n# [DEF:generate_uuid:Function]\n# @COMPLEXITY: 1\ndef generate_uuid():\n return str(uuid.uuid4())\n\n\n# [/DEF:generate_uuid:Function]\n\n# [DEF:user_roles:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Association table for many-to-many relationship between Users and Roles.\n# @RELATION: DEPENDS_ON -> [Base]\n# @RELATION: DEPENDS_ON -> [User]\n# @RELATION: DEPENDS_ON -> [Role]\nuser_roles = Table(\n \"user_roles\",\n Base.metadata,\n Column(\"user_id\", String, ForeignKey(\"users.id\"), primary_key=True),\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n)\n# [/DEF:user_roles:Class]\n\n# [DEF:role_permissions:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Association table for many-to-many relationship between Roles and Permissions.\n# @RELATION: DEPENDS_ON -> [Base]\n# @RELATION: DEPENDS_ON -> [Role]\n# @RELATION: DEPENDS_ON -> [Permission]\nrole_permissions = Table(\n \"role_permissions\",\n Base.metadata,\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n Column(\"permission_id\", String, ForeignKey(\"permissions.id\"), primary_key=True),\n)\n# [/DEF:role_permissions:Class]\n\n\n# [DEF:User:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Represents an identity that can authenticate to the system.\n# @RELATION: HAS_MANY -> [Role]\nclass User(Base):\n __tablename__ = \"users\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n username = Column(String, unique=True, index=True, nullable=False)\n email = Column(String, unique=True, index=True, nullable=True)\n password_hash = Column(String, nullable=True)\n full_name = Column(String, nullable=True)\n auth_source = Column(String, default=\"LOCAL\") # LOCAL or ADFS\n is_active = Column(Boolean, default=True)\n is_ad_user = Column(Boolean, default=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n last_login = Column(DateTime, nullable=True)\n\n roles = relationship(\"Role\", secondary=user_roles, back_populates=\"users\")\n\n\n# [/DEF:User:Class]\n\n\n# [DEF:Role:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Represents a collection of permissions.\n# @RELATION: HAS_MANY -> [User]\n# @RELATION: HAS_MANY -> [Permission]\nclass Role(Base):\n __tablename__ = \"roles\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, unique=True, index=True, nullable=False)\n description = Column(String, nullable=True)\n\n users = relationship(\"User\", secondary=user_roles, back_populates=\"roles\")\n permissions = relationship(\n \"Permission\", secondary=role_permissions, back_populates=\"roles\"\n )\n\n\n# [/DEF:Role:Class]\n\n\n# [DEF:Permission:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Represents a specific capability within the system.\n# @RELATION: HAS_MANY -> [Role]\nclass Permission(Base):\n __tablename__ = \"permissions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n resource = Column(String, nullable=False) # e.g. \"plugin:backup\"\n action = Column(String, nullable=False) # e.g. \"READ\", \"EXECUTE\", \"WRITE\"\n\n roles = relationship(\n \"Role\", secondary=role_permissions, back_populates=\"permissions\"\n )\n\n\n# [/DEF:Permission:Class]\n\n\n# [DEF:ADGroupMapping:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Maps an Active Directory group to a local System Role.\n# @RELATION: DEPENDS_ON -> [Role]\nclass ADGroupMapping(Base):\n __tablename__ = \"ad_group_mappings\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n ad_group = Column(String, unique=True, index=True, nullable=False)\n role_id = Column(String, ForeignKey(\"roles.id\"), nullable=False)\n\n role = relationship(\"Role\")\n\n\n# [/DEF:ADGroupMapping:Class]\n\n# [/DEF:AuthModels:Module]\n" }, { - "contract_id": "user_roles", - "contract_type": "Table", - "file_path": "backend/src/models/auth.py", - "start_line": 29, - "end_line": 40, + "contract_id": "generate_uuid", + "contract_type": "Function", + "file_path": "backend/src/models/auth.py.bak", + "start_line": 17, + "end_line": 23, "tier": "TIER_1", "complexity": 1, "metadata": { + "COMPLEXITY": "1" + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "def", + "body": "# [DEF:generate_uuid:Function]\n# @COMPLEXITY: 1\ndef generate_uuid():\n return str(uuid.uuid4())\n\n\n# [/DEF:generate_uuid:Function]\n" + }, + { + "contract_id": "user_roles", + "contract_type": "Class", + "file_path": "backend/src/models/auth.py.bak", + "start_line": 25, + "end_line": 37, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": "3", "PURPOSE": "Association table for many-to-many relationship between Users and Roles." }, "relations": [ @@ -56392,56 +44818,20 @@ "target_ref": "[Role]" } ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Table'", - "detail": { - "actual_type": "Table", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Table' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Table" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Table' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Table" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:user_roles:Table]\n# @PURPOSE: Association table for many-to-many relationship between Users and Roles.\n# @RELATION: DEPENDS_ON -> [Base]\n# @RELATION: DEPENDS_ON -> [User]\n# @RELATION: DEPENDS_ON -> [Role]\nuser_roles = Table(\n \"user_roles\",\n Base.metadata,\n Column(\"user_id\", String, ForeignKey(\"users.id\"), primary_key=True),\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n)\n# [/DEF:user_roles:Table]\n" + "body": "# [DEF:user_roles:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Association table for many-to-many relationship between Users and Roles.\n# @RELATION: DEPENDS_ON -> [Base]\n# @RELATION: DEPENDS_ON -> [User]\n# @RELATION: DEPENDS_ON -> [Role]\nuser_roles = Table(\n \"user_roles\",\n Base.metadata,\n Column(\"user_id\", String, ForeignKey(\"users.id\"), primary_key=True),\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n)\n# [/DEF:user_roles:Class]\n" }, { "contract_id": "role_permissions", - "contract_type": "Table", - "file_path": "backend/src/models/auth.py", - "start_line": 42, - "end_line": 53, - "tier": "TIER_1", - "complexity": 1, + "contract_type": "Class", + "file_path": "backend/src/models/auth.py.bak", + "start_line": 39, + "end_line": 51, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": "3", "PURPOSE": "Association table for many-to-many relationship between Roles and Permissions." }, "relations": [ @@ -56464,56 +44854,20 @@ "target_ref": "[Permission]" } ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Table'", - "detail": { - "actual_type": "Table", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Table' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Table" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Table' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Table" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:role_permissions:Table]\n# @PURPOSE: Association table for many-to-many relationship between Roles and Permissions.\n# @RELATION: DEPENDS_ON -> [Base]\n# @RELATION: DEPENDS_ON -> [Role]\n# @RELATION: DEPENDS_ON -> [Permission]\nrole_permissions = Table(\n \"role_permissions\",\n Base.metadata,\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n Column(\"permission_id\", String, ForeignKey(\"permissions.id\"), primary_key=True),\n)\n# [/DEF:role_permissions:Table]\n" + "body": "# [DEF:role_permissions:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Association table for many-to-many relationship between Roles and Permissions.\n# @RELATION: DEPENDS_ON -> [Base]\n# @RELATION: DEPENDS_ON -> [Role]\n# @RELATION: DEPENDS_ON -> [Permission]\nrole_permissions = Table(\n \"role_permissions\",\n Base.metadata,\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n Column(\"permission_id\", String, ForeignKey(\"permissions.id\"), primary_key=True),\n)\n# [/DEF:role_permissions:Class]\n" }, { "contract_id": "Role", "contract_type": "Class", - "file_path": "backend/src/models/auth.py", - "start_line": 79, + "file_path": "backend/src/models/auth.py.bak", + "start_line": 78, "end_line": 96, - "tier": "TIER_1", - "complexity": 1, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": "3", "PURPOSE": "Represents a collection of permissions." }, "relations": [ @@ -56531,24 +44885,6 @@ } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -56587,17 +44923,18 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:Role:Class]\n# @PURPOSE: Represents a collection of permissions.\n# @RELATION: HAS_MANY -> [User]\n# @RELATION: HAS_MANY -> [Permission]\nclass Role(Base):\n __tablename__ = \"roles\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, unique=True, index=True, nullable=False)\n description = Column(String, nullable=True)\n\n users = relationship(\"User\", secondary=user_roles, back_populates=\"roles\")\n permissions = relationship(\n \"Permission\", secondary=role_permissions, back_populates=\"roles\"\n )\n\n\n# [/DEF:Role:Class]\n" + "body": "# [DEF:Role:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Represents a collection of permissions.\n# @RELATION: HAS_MANY -> [User]\n# @RELATION: HAS_MANY -> [Permission]\nclass Role(Base):\n __tablename__ = \"roles\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, unique=True, index=True, nullable=False)\n description = Column(String, nullable=True)\n\n users = relationship(\"User\", secondary=user_roles, back_populates=\"roles\")\n permissions = relationship(\n \"Permission\", secondary=role_permissions, back_populates=\"roles\"\n )\n\n\n# [/DEF:Role:Class]\n" }, { "contract_id": "Permission", "contract_type": "Class", - "file_path": "backend/src/models/auth.py", + "file_path": "backend/src/models/auth.py.bak", "start_line": 99, - "end_line": 114, - "tier": "TIER_1", - "complexity": 1, + "end_line": 115, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": "3", "PURPOSE": "Represents a specific capability within the system." }, "relations": [ @@ -56609,24 +44946,6 @@ } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -56647,17 +44966,18 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:Permission:Class]\n# @PURPOSE: Represents a specific capability within the system.\n# @RELATION: HAS_MANY -> [Role]\nclass Permission(Base):\n __tablename__ = \"permissions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n resource = Column(String, nullable=False) # e.g. \"plugin:backup\"\n action = Column(String, nullable=False) # e.g. \"READ\", \"EXECUTE\", \"WRITE\"\n\n roles = relationship(\n \"Role\", secondary=role_permissions, back_populates=\"permissions\"\n )\n\n\n# [/DEF:Permission:Class]\n" + "body": "# [DEF:Permission:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Represents a specific capability within the system.\n# @RELATION: HAS_MANY -> [Role]\nclass Permission(Base):\n __tablename__ = \"permissions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n resource = Column(String, nullable=False) # e.g. \"plugin:backup\"\n action = Column(String, nullable=False) # e.g. \"READ\", \"EXECUTE\", \"WRITE\"\n\n roles = relationship(\n \"Role\", secondary=role_permissions, back_populates=\"permissions\"\n )\n\n\n# [/DEF:Permission:Class]\n" }, { "contract_id": "ADGroupMapping", "contract_type": "Class", - "file_path": "backend/src/models/auth.py", - "start_line": 117, - "end_line": 130, - "tier": "TIER_1", - "complexity": 1, + "file_path": "backend/src/models/auth.py.bak", + "start_line": 118, + "end_line": 132, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": "3", "PURPOSE": "Maps an Active Directory group to a local System Role." }, "relations": [ @@ -56668,53 +44988,26 @@ "target_ref": "[Role]" } ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], + "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:ADGroupMapping:Class]\n# @PURPOSE: Maps an Active Directory group to a local System Role.\n# @RELATION: DEPENDS_ON -> [Role]\nclass ADGroupMapping(Base):\n __tablename__ = \"ad_group_mappings\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n ad_group = Column(String, unique=True, index=True, nullable=False)\n role_id = Column(String, ForeignKey(\"roles.id\"), nullable=False)\n\n role = relationship(\"Role\")\n\n\n# [/DEF:ADGroupMapping:Class]\n" + "body": "# [DEF:ADGroupMapping:Class]\n# @COMPLEXITY: 3\n# @BRIEF: Maps an Active Directory group to a local System Role.\n# @RELATION: DEPENDS_ON -> [Role]\nclass ADGroupMapping(Base):\n __tablename__ = \"ad_group_mappings\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n ad_group = Column(String, unique=True, index=True, nullable=False)\n role_id = Column(String, ForeignKey(\"roles.id\"), nullable=False)\n\n role = relationship(\"Role\")\n\n\n# [/DEF:ADGroupMapping:Class]\n" }, { "contract_id": "CleanReleaseModels", "contract_type": "Module", "file_path": "backend/src/models/clean_release.py", "start_line": 1, - "end_line": 699, + "end_line": 695, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "DATA_CONTRACT": "Model[ReleaseCandidate, CandidateArtifact, DistributionManifest, ComplianceRun, ComplianceReport]", "INVARIANT": "Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected.", "LAYER": "Domain", "POST": "Provides SQLAlchemy and dataclass definitions for governance domain.", "PRE": "Base mapping model and release enums are available.", "PURPOSE": "Define canonical clean release domain entities and lifecycle guards.", - "SEMANTICS": [ - "clean-release", - "models", - "lifecycle", - "compliance", - "evidence", - "immutability" - ], "SIDE_EFFECT": "None (schema definition)." }, "relations": [ @@ -56772,15 +45065,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, models, lifecycle, compliance, evidence, immutability\n# @PURPOSE: Define canonical clean release domain entities and lifecycle guards.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> MappingModels\n# @PRE: Base mapping model and release enums are available.\n# @POST: Provides SQLAlchemy and dataclass definitions for governance domain.\n# @SIDE_EFFECT: None (schema definition).\n# @DATA_CONTRACT: Model[ReleaseCandidate, CandidateArtifact, DistributionManifest, ComplianceRun, ComplianceReport]\n# @INVARIANT: Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected.\n\nfrom datetime import datetime\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import List, Optional, Dict, Any\nfrom pydantic import ConfigDict, Field, model_validator\nfrom pydantic.dataclasses import dataclass as pydantic_dataclass\nfrom sqlalchemy import Column, String, DateTime, JSON, ForeignKey, Integer, Boolean\nfrom sqlalchemy.orm import relationship\nfrom .mapping import Base\nfrom ..services.clean_release.enums import (\n CandidateStatus, RunStatus, ComplianceDecision,\n ApprovalDecisionType, PublicationStatus, ClassificationType\n)\nfrom ..services.clean_release.exceptions import IllegalTransitionError\n\n# [DEF:ExecutionMode:Class]\n# @PURPOSE: Backward-compatible execution mode enum for legacy TUI/orchestrator tests.\nclass ExecutionMode(str, Enum):\n TUI = \"TUI\"\n API = \"API\"\n SCHEDULER = \"SCHEDULER\"\n# [/DEF:ExecutionMode:Class]\n\n# [DEF:CheckFinalStatus:Class]\n# @PURPOSE: Backward-compatible final status enum for legacy TUI/orchestrator tests.\nclass CheckFinalStatus(str, Enum):\n COMPLIANT = \"COMPLIANT\"\n BLOCKED = \"BLOCKED\"\n FAILED = \"FAILED\"\n RUNNING = \"RUNNING\"\n# [/DEF:CheckFinalStatus:Class]\n\n# [DEF:CheckStageName:Class]\n# @PURPOSE: Backward-compatible stage name enum for legacy TUI/orchestrator tests.\nclass CheckStageName(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n INTERNAL_SOURCES_ONLY = \"INTERNAL_SOURCES_ONLY\"\n NO_EXTERNAL_ENDPOINTS = \"NO_EXTERNAL_ENDPOINTS\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n# [/DEF:CheckStageName:Class]\n\n# [DEF:CheckStageStatus:Class]\n# @PURPOSE: Backward-compatible stage status enum for legacy TUI/orchestrator tests.\nclass CheckStageStatus(str, Enum):\n PASS = \"PASS\"\n FAIL = \"FAIL\"\n SKIPPED = \"SKIPPED\"\n RUNNING = \"RUNNING\"\n# [/DEF:CheckStageStatus:Class]\n\n# [DEF:CheckStageResult:Class]\n# @PURPOSE: Backward-compatible stage result container for legacy TUI/orchestrator tests.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CheckStageResult:\n stage: CheckStageName\n status: CheckStageStatus\n details: str = \"\"\n# [/DEF:CheckStageResult:Class]\n\n# [DEF:ProfileType:Class]\n# @PURPOSE: Backward-compatible profile enum for legacy TUI bootstrap logic.\nclass ProfileType(str, Enum):\n ENTERPRISE_CLEAN = \"enterprise-clean\"\n# [/DEF:ProfileType:Class]\n\n# [DEF:RegistryStatus:Class]\n# @PURPOSE: Backward-compatible registry status enum for legacy TUI bootstrap logic.\nclass RegistryStatus(str, Enum):\n ACTIVE = \"ACTIVE\"\n INACTIVE = \"INACTIVE\"\n# [/DEF:RegistryStatus:Class]\n\n# [DEF:ReleaseCandidateStatus:Class]\n# @PURPOSE: Backward-compatible release candidate status enum for legacy TUI.\nclass ReleaseCandidateStatus(str, Enum):\n DRAFT = CandidateStatus.DRAFT.value\n PREPARED = CandidateStatus.PREPARED.value\n MANIFEST_BUILT = CandidateStatus.MANIFEST_BUILT.value\n CHECK_PENDING = CandidateStatus.CHECK_PENDING.value\n CHECK_RUNNING = CandidateStatus.CHECK_RUNNING.value\n CHECK_PASSED = CandidateStatus.CHECK_PASSED.value\n CHECK_BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n CHECK_ERROR = CandidateStatus.CHECK_ERROR.value\n APPROVED = CandidateStatus.APPROVED.value\n PUBLISHED = CandidateStatus.PUBLISHED.value\n REVOKED = CandidateStatus.REVOKED.value\n# [/DEF:ReleaseCandidateStatus:Class]\n\n# [DEF:ResourceSourceEntry:Class]\n# @PURPOSE: Backward-compatible source entry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceEntry:\n source_id: str\n host: str\n protocol: str\n purpose: str\n enabled: bool = True\n# [/DEF:ResourceSourceEntry:Class]\n\n# [DEF:ResourceSourceRegistry:Class]\n# @PURPOSE: Backward-compatible source registry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceRegistry:\n registry_id: str\n name: str\n entries: List[ResourceSourceEntry]\n updated_at: datetime\n updated_by: str\n status: str = \"ACTIVE\"\n immutable: bool = True\n allowed_hosts: Optional[List[str]] = None\n allowed_schemes: Optional[List[str]] = None\n allowed_source_types: Optional[List[str]] = None\n\n @model_validator(mode=\"after\")\n def populate_legacy_allowlists(self):\n enabled_entries = [entry for entry in self.entries if getattr(entry, \"enabled\", True)]\n if self.allowed_hosts is None:\n self.allowed_hosts = [entry.host for entry in enabled_entries]\n if self.allowed_schemes is None:\n self.allowed_schemes = [entry.protocol for entry in enabled_entries]\n if self.allowed_source_types is None:\n self.allowed_source_types = [entry.purpose for entry in enabled_entries]\n return self\n\n @property\n def id(self) -> str:\n return self.registry_id\n# [/DEF:ResourceSourceRegistry:Class]\n\n# [DEF:CleanProfilePolicy:Class]\n# @PURPOSE: Backward-compatible policy model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CleanProfilePolicy:\n policy_id: str\n policy_version: str\n profile: ProfileType\n active: bool\n internal_source_registry_ref: str\n prohibited_artifact_categories: List[str]\n effective_from: datetime\n required_system_categories: Optional[List[str]] = None\n external_source_forbidden: bool = True\n immutable: bool = True\n content_json: Optional[Dict[str, Any]] = None\n\n @model_validator(mode=\"after\")\n def validate_enterprise_policy(self):\n if self.profile == ProfileType.ENTERPRISE_CLEAN:\n if not self.prohibited_artifact_categories:\n raise ValueError(\"enterprise-clean policy requires prohibited_artifact_categories\")\n if self.external_source_forbidden is not True:\n raise ValueError(\"enterprise-clean policy requires external_source_forbidden=true\")\n if self.content_json is None:\n self.content_json = {\n \"profile\": self.profile.value,\n \"prohibited_artifact_categories\": list(self.prohibited_artifact_categories or []),\n \"required_system_categories\": list(self.required_system_categories or []),\n \"external_source_forbidden\": self.external_source_forbidden,\n }\n return self\n\n @property\n def id(self) -> str:\n return self.policy_id\n\n @property\n def registry_snapshot_id(self) -> str:\n return self.internal_source_registry_ref\n# [/DEF:CleanProfilePolicy:Class]\n\n# [DEF:ComplianceCheckRun:Class]\n# @PURPOSE: Backward-compatible run model for legacy TUI typing/import compatibility.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ComplianceCheckRun:\n check_run_id: str\n candidate_id: str\n policy_id: str\n started_at: datetime\n triggered_by: str\n execution_mode: ExecutionMode\n final_status: CheckFinalStatus\n checks: List[CheckStageResult]\n finished_at: Optional[datetime] = None\n\n @model_validator(mode=\"after\")\n def validate_final_status_alignment(self):\n mandatory_stages = {\n CheckStageName.DATA_PURITY,\n CheckStageName.INTERNAL_SOURCES_ONLY,\n CheckStageName.NO_EXTERNAL_ENDPOINTS,\n CheckStageName.MANIFEST_CONSISTENCY,\n }\n if self.final_status == CheckFinalStatus.COMPLIANT:\n observed_stages = {check.stage for check in self.checks}\n if observed_stages != mandatory_stages:\n raise ValueError(\"compliant run requires all mandatory stages\")\n if any(check.status != CheckStageStatus.PASS for check in self.checks):\n raise ValueError(\"compliant run requires PASS on all mandatory stages\")\n return self\n\n @property\n def id(self) -> str:\n return self.check_run_id\n\n @property\n def run_id(self) -> str:\n return self.check_run_id\n\n @property\n def status(self) -> RunStatus:\n if self.final_status == CheckFinalStatus.RUNNING:\n return RunStatus.RUNNING\n if self.final_status == CheckFinalStatus.BLOCKED:\n return RunStatus.FAILED\n return RunStatus.SUCCEEDED\n# [/DEF:ComplianceCheckRun:Class]\n\n# [DEF:ReleaseCandidate:Class]\n# @PURPOSE: Represents the release unit being prepared and governed.\n# @PRE: id, version, source_snapshot_ref are non-empty.\n# @POST: status advances only through legal transitions.\nclass ReleaseCandidate(Base):\n __tablename__ = \"clean_release_candidates\"\n \n id = Column(String, primary_key=True)\n name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs\n version = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n build_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n status = Column(String, default=CandidateStatus.DRAFT)\n\n def __init__(self, **kwargs):\n if \"candidate_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"candidate_id\")\n if \"profile\" in kwargs:\n kwargs.pop(\"profile\")\n status = kwargs.get(\"status\")\n if status is None:\n kwargs[\"status\"] = CandidateStatus.DRAFT.value\n elif isinstance(status, ReleaseCandidateStatus):\n kwargs[\"status\"] = status.value\n elif isinstance(status, CandidateStatus):\n kwargs[\"status\"] = status.value\n if not str(kwargs.get(\"id\", \"\")).strip():\n raise ValueError(\"candidate_id must be non-empty\")\n super().__init__(**kwargs)\n\n @property\n def candidate_id(self) -> str:\n return self.id\n\n def transition_to(self, new_status: CandidateStatus):\n \"\"\"\n @PURPOSE: Enforce legal state transitions.\n @PRE: Transition must be allowed by lifecycle rules.\n \"\"\"\n allowed = {\n CandidateStatus.DRAFT: [CandidateStatus.PREPARED],\n CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],\n CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],\n CandidateStatus.CHECK_RUNNING: [\n CandidateStatus.CHECK_PASSED, \n CandidateStatus.CHECK_BLOCKED, \n CandidateStatus.CHECK_ERROR\n ],\n CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],\n CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],\n CandidateStatus.REVOKED: []\n }\n current_status = CandidateStatus(self.status)\n if new_status not in allowed.get(current_status, []):\n raise IllegalTransitionError(f\"Forbidden transition from {current_status} to {new_status}\")\n self.status = new_status.value\n# [/DEF:ReleaseCandidate:Class]\n\n# [DEF:CandidateArtifact:Class]\n# @PURPOSE: Represents one artifact associated with a release candidate.\nclass CandidateArtifact(Base):\n __tablename__ = \"clean_release_artifacts\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n path = Column(String, nullable=False)\n sha256 = Column(String, nullable=False)\n size = Column(Integer, nullable=False)\n detected_category = Column(String, nullable=True)\n declared_category = Column(String, nullable=True)\n source_uri = Column(String, nullable=True)\n source_host = Column(String, nullable=True)\n metadata_json = Column(JSON, default=dict)\n# [/DEF:CandidateArtifact:Class]\n\n# [DEF:ManifestItem:Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestItem:\n path: str\n category: str\n classification: ClassificationType\n reason: str\n checksum: Optional[str] = None\n# [/DEF:ManifestItem:Class]\n\n# [DEF:ManifestSummary:Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestSummary:\n included_count: int\n excluded_count: int\n prohibited_detected_count: int\n# [/DEF:ManifestSummary:Class]\n\n# [DEF:DistributionManifest:Class]\n# @PURPOSE: Immutable snapshot of the candidate payload.\n# @INVARIANT: Immutable after creation.\nclass DistributionManifest(Base):\n __tablename__ = \"clean_release_manifests\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_version = Column(Integer, nullable=False)\n manifest_digest = Column(String, nullable=False)\n artifacts_digest = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n content_json = Column(JSON, nullable=False)\n immutable = Column(Boolean, default=True)\n\n # Redesign compatibility fields (not persisted directly but used by builder/facade)\n def __init__(self, **kwargs):\n items = kwargs.pop(\"items\", None)\n summary = kwargs.pop(\"summary\", None)\n\n # Handle fields from manifest_builder.py\n if \"manifest_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"manifest_id\")\n if \"generated_at\" in kwargs:\n kwargs[\"created_at\"] = kwargs.pop(\"generated_at\")\n if \"generated_by\" in kwargs:\n kwargs[\"created_by\"] = kwargs.pop(\"generated_by\")\n if \"deterministic_hash\" in kwargs:\n kwargs[\"manifest_digest\"] = kwargs.pop(\"deterministic_hash\")\n if \"policy_id\" in kwargs:\n kwargs.pop(\"policy_id\")\n\n if items is not None and summary is not None:\n expected_count = int(summary.included_count) + int(summary.excluded_count)\n if expected_count != len(items):\n raise ValueError(\"manifest summary counts must match items size\")\n \n # Ensure required DB fields have defaults if missing\n if \"manifest_version\" not in kwargs:\n kwargs[\"manifest_version\"] = 1\n if \"artifacts_digest\" not in kwargs:\n kwargs[\"artifacts_digest\"] = kwargs.get(\"manifest_digest\", \"pending\")\n if \"source_snapshot_ref\" not in kwargs:\n kwargs[\"source_snapshot_ref\"] = \"pending\"\n \n # Pack items and summary into content_json if provided\n if items is not None or summary is not None:\n content = dict(kwargs.get(\"content_json\") or {})\n if items is not None:\n content[\"items\"] = [\n {\n \"path\": i.path,\n \"category\": i.category,\n \"classification\": i.classification.value,\n \"reason\": i.reason,\n \"checksum\": i.checksum\n } for i in items\n ]\n if summary is not None:\n content[\"summary\"] = {\n \"included_count\": summary.included_count,\n \"excluded_count\": summary.excluded_count,\n \"prohibited_detected_count\": summary.prohibited_detected_count\n }\n kwargs[\"content_json\"] = content\n\n super().__init__(**kwargs)\n\n @property\n def manifest_id(self) -> str:\n return self.id\n\n @property\n def deterministic_hash(self) -> str:\n return self.manifest_digest\n\n @property\n def summary(self) -> ManifestSummary:\n payload = (self.content_json or {}).get(\"summary\", {})\n return ManifestSummary(\n included_count=int(payload.get(\"included_count\", 0)),\n excluded_count=int(payload.get(\"excluded_count\", 0)),\n prohibited_detected_count=int(payload.get(\"prohibited_detected_count\", 0)),\n )\n# [/DEF:DistributionManifest:Class]\n\n# [DEF:SourceRegistrySnapshot:Class]\n# @PURPOSE: Immutable registry snapshot for allowed sources.\nclass SourceRegistrySnapshot(Base):\n __tablename__ = \"clean_release_registry_snapshots\"\n \n id = Column(String, primary_key=True)\n registry_id = Column(String, nullable=False)\n registry_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n allowed_hosts = Column(JSON, nullable=False) # List[str]\n allowed_schemes = Column(JSON, nullable=False) # List[str]\n allowed_source_types = Column(JSON, nullable=False) # List[str]\n immutable = Column(Boolean, default=True)\n# [/DEF:SourceRegistrySnapshot:Class]\n\n# [DEF:CleanPolicySnapshot:Class]\n# @PURPOSE: Immutable policy snapshot used to evaluate a run.\nclass CleanPolicySnapshot(Base):\n __tablename__ = \"clean_release_policy_snapshots\"\n \n id = Column(String, primary_key=True)\n policy_id = Column(String, nullable=False)\n policy_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n content_json = Column(JSON, nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n immutable = Column(Boolean, default=True)\n# [/DEF:CleanPolicySnapshot:Class]\n\n# [DEF:ComplianceRun:Class]\n# @PURPOSE: Operational record for one compliance execution.\nclass ComplianceRun(Base):\n __tablename__ = \"clean_release_compliance_runs\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_id = Column(String, ForeignKey(\"clean_release_manifests.id\"), nullable=False)\n manifest_digest = Column(String, nullable=False)\n policy_snapshot_id = Column(String, ForeignKey(\"clean_release_policy_snapshots.id\"), nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n requested_by = Column(String, nullable=False)\n requested_at = Column(DateTime, default=datetime.utcnow)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n status = Column(String, default=RunStatus.PENDING)\n final_status = Column(String, nullable=True) # ComplianceDecision\n failure_reason = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n\n @property\n def check_run_id(self) -> str:\n return self.id\n# [/DEF:ComplianceRun:Class]\n\n# [DEF:ComplianceStageRun:Class]\n# @PURPOSE: Stage-level execution record inside a run.\nclass ComplianceStageRun(Base):\n __tablename__ = \"clean_release_compliance_stage_runs\"\n \n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n status = Column(String, nullable=False)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n decision = Column(String, nullable=True) # ComplianceDecision\n details_json = Column(JSON, default=dict)\n# [/DEF:ComplianceStageRun:Class]\n\n# [DEF:ViolationSeverity:Class]\n# @PURPOSE: Backward-compatible violation severity enum for legacy clean-release tests.\nclass ViolationSeverity(str, Enum):\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n# [/DEF:ViolationSeverity:Class]\n\n# [DEF:ViolationCategory:Class]\n# @PURPOSE: Backward-compatible violation category enum for legacy clean-release tests.\nclass ViolationCategory(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n EXTERNAL_SOURCE = \"EXTERNAL_SOURCE\"\n SOURCE_ISOLATION = \"SOURCE_ISOLATION\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n EXTERNAL_ENDPOINT = \"EXTERNAL_ENDPOINT\"\n# [/DEF:ViolationCategory:Class]\n\n# [DEF:ComplianceViolation:Class]\n# @PURPOSE: Violation produced by a stage.\nclass ComplianceViolation(Base):\n __tablename__ = \"clean_release_compliance_violations\"\n \n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n code = Column(String, nullable=False)\n severity = Column(String, nullable=False)\n artifact_path = Column(String, nullable=True)\n artifact_sha256 = Column(String, nullable=True)\n message = Column(String, nullable=False)\n evidence_json = Column(JSON, default=dict)\n\n def __init__(self, **kwargs):\n if \"violation_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"violation_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n if \"category\" in kwargs:\n category = kwargs.pop(\"category\")\n kwargs[\"stage_name\"] = category.value if isinstance(category, ViolationCategory) else str(category)\n if \"location\" in kwargs:\n kwargs[\"artifact_path\"] = kwargs.pop(\"location\")\n if \"remediation\" in kwargs:\n remediation = kwargs.pop(\"remediation\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"remediation\"] = remediation\n kwargs[\"evidence_json\"] = evidence\n if \"blocked_release\" in kwargs:\n blocked_release = kwargs.pop(\"blocked_release\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"blocked_release\"] = blocked_release\n kwargs[\"evidence_json\"] = evidence\n if \"detected_at\" in kwargs:\n kwargs.pop(\"detected_at\")\n if \"code\" not in kwargs:\n kwargs[\"code\"] = \"LEGACY_VIOLATION\"\n if \"message\" not in kwargs:\n kwargs[\"message\"] = kwargs.get(\"stage_name\", \"LEGACY_VIOLATION\")\n super().__init__(**kwargs)\n\n @property\n def violation_id(self) -> str:\n return self.id\n\n @violation_id.setter\n def violation_id(self, value: str) -> None:\n self.id = value\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def category(self) -> ViolationCategory:\n return ViolationCategory(self.stage_name)\n\n @category.setter\n def category(self, value: ViolationCategory) -> None:\n self.stage_name = value.value if isinstance(value, ViolationCategory) else str(value)\n\n @property\n def location(self) -> Optional[str]:\n return self.artifact_path\n\n @property\n def remediation(self) -> Optional[str]:\n return (self.evidence_json or {}).get(\"remediation\")\n\n @property\n def blocked_release(self) -> bool:\n return bool((self.evidence_json or {}).get(\"blocked_release\", False))\n# [/DEF:ComplianceViolation:Class]\n\n# [DEF:ComplianceReport:Class]\n# @PURPOSE: Immutable result derived from a completed run.\n# @INVARIANT: Immutable after creation.\nclass ComplianceReport(Base):\n __tablename__ = \"clean_release_compliance_reports\"\n \n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n final_status = Column(String, nullable=False) # ComplianceDecision\n summary_json = Column(JSON, nullable=False)\n generated_at = Column(DateTime, default=datetime.utcnow)\n immutable = Column(Boolean, default=True)\n\n def __init__(self, **kwargs):\n if \"report_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"report_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n operator_summary = kwargs.pop(\"operator_summary\", None)\n structured_payload_ref = kwargs.pop(\"structured_payload_ref\", None)\n violations_count = kwargs.pop(\"violations_count\", None)\n blocking_violations_count = kwargs.pop(\"blocking_violations_count\", None)\n\n final_status = kwargs.get(\"final_status\")\n final_status_value = getattr(final_status, \"value\", final_status)\n\n if (\n final_status_value in {CheckFinalStatus.BLOCKED.value, ComplianceDecision.BLOCKED.value}\n and blocking_violations_count is not None\n and int(blocking_violations_count) <= 0\n ):\n raise ValueError(\"blocked report requires blocking violations\")\n\n if (\n operator_summary is not None\n or structured_payload_ref is not None\n or violations_count is not None\n or blocking_violations_count is not None\n ):\n kwargs[\"summary_json\"] = {\n \"operator_summary\": operator_summary or \"\",\n \"structured_payload_ref\": structured_payload_ref,\n \"violations_count\": int(violations_count or 0),\n \"blocking_violations_count\": int(blocking_violations_count or 0),\n }\n\n super().__init__(**kwargs)\n\n @property\n def report_id(self) -> str:\n return self.id\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def operator_summary(self) -> str:\n return (self.summary_json or {}).get(\"operator_summary\", \"\")\n\n @property\n def structured_payload_ref(self) -> Optional[str]:\n return (self.summary_json or {}).get(\"structured_payload_ref\")\n\n @property\n def violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"violations_count\", 0))\n\n @property\n def blocking_violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"blocking_violations_count\", 0))\n# [/DEF:ComplianceReport:Class]\n\n# [DEF:ApprovalDecision:Class]\n# @PURPOSE: Approval or rejection bound to a candidate and report.\nclass ApprovalDecision(Base):\n __tablename__ = \"clean_release_approval_decisions\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n decision = Column(String, nullable=False) # ApprovalDecisionType\n decided_by = Column(String, nullable=False)\n decided_at = Column(DateTime, default=datetime.utcnow)\n comment = Column(String, nullable=True)\n# [/DEF:ApprovalDecision:Class]\n\n# [DEF:PublicationRecord:Class]\n# @PURPOSE: Publication or revocation record.\nclass PublicationRecord(Base):\n __tablename__ = \"clean_release_publication_records\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n published_by = Column(String, nullable=False)\n published_at = Column(DateTime, default=datetime.utcnow)\n target_channel = Column(String, nullable=False)\n publication_ref = Column(String, nullable=True)\n status = Column(String, default=PublicationStatus.ACTIVE)\n# [/DEF:PublicationRecord:Class]\n\n# [DEF:CleanReleaseAuditLog:Class]\n# @PURPOSE: Represents a persistent audit log entry for clean release actions.\nimport uuid\nclass CleanReleaseAuditLog(Base):\n __tablename__ = \"clean_release_audit_logs\"\n \n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n candidate_id = Column(String, index=True, nullable=True)\n action = Column(String, nullable=False) # e.g. \"TRANSITION\", \"APPROVE\", \"PUBLISH\"\n actor = Column(String, nullable=False)\n timestamp = Column(DateTime, default=datetime.utcnow)\n details_json = Column(JSON, default=dict)\n# [/DEF:CleanReleaseAuditLog:Class]\n\n# [/DEF:CleanReleaseModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region CleanReleaseModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, clean-release, model, schema, schedule, release]\n# @BRIEF Define canonical clean release domain entities and lifecycle guards.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> MappingModels\n# @PRE: Base mapping model and release enums are available.\n# @POST: Provides SQLAlchemy and dataclass definitions for governance domain.\n# @SIDE_EFFECT: None (schema definition).\n# @DATA_CONTRACT: Model[ReleaseCandidate, CandidateArtifact, DistributionManifest, ComplianceRun, ComplianceReport]\n# @INVARIANT: Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected.\n\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any\n\nfrom pydantic import ConfigDict, model_validator\nfrom pydantic.dataclasses import dataclass as pydantic_dataclass\nfrom sqlalchemy import JSON, Boolean, Column, DateTime, ForeignKey, Integer, String\n\nfrom ..services.clean_release.enums import CandidateStatus, ClassificationType, ComplianceDecision, PublicationStatus, RunStatus\nfrom ..services.clean_release.exceptions import IllegalTransitionError\nfrom .mapping import Base\n\n\n# #region ExecutionMode [TYPE Class]\n# @BRIEF Backward-compatible execution mode enum for legacy TUI/orchestrator tests.\nclass ExecutionMode(str, Enum):\n TUI = \"TUI\"\n API = \"API\"\n SCHEDULER = \"SCHEDULER\"\n# #endregion ExecutionMode\n\n# #region CheckFinalStatus [TYPE Class]\n# @BRIEF Backward-compatible final status enum for legacy TUI/orchestrator tests.\nclass CheckFinalStatus(str, Enum):\n COMPLIANT = \"COMPLIANT\"\n BLOCKED = \"BLOCKED\"\n FAILED = \"FAILED\"\n RUNNING = \"RUNNING\"\n# #endregion CheckFinalStatus\n\n# #region CheckStageName [TYPE Class]\n# @BRIEF Backward-compatible stage name enum for legacy TUI/orchestrator tests.\nclass CheckStageName(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n INTERNAL_SOURCES_ONLY = \"INTERNAL_SOURCES_ONLY\"\n NO_EXTERNAL_ENDPOINTS = \"NO_EXTERNAL_ENDPOINTS\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n# #endregion CheckStageName\n\n# #region CheckStageStatus [TYPE Class]\n# @BRIEF Backward-compatible stage status enum for legacy TUI/orchestrator tests.\nclass CheckStageStatus(str, Enum):\n PASS = \"PASS\"\n FAIL = \"FAIL\"\n SKIPPED = \"SKIPPED\"\n RUNNING = \"RUNNING\"\n# #endregion CheckStageStatus\n\n# #region CheckStageResult [TYPE Class]\n# @BRIEF Backward-compatible stage result container for legacy TUI/orchestrator tests.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CheckStageResult:\n stage: CheckStageName\n status: CheckStageStatus\n details: str = \"\"\n# #endregion CheckStageResult\n\n# #region ProfileType [TYPE Class]\n# @BRIEF Backward-compatible profile enum for legacy TUI bootstrap logic.\nclass ProfileType(str, Enum):\n ENTERPRISE_CLEAN = \"enterprise-clean\"\n# #endregion ProfileType\n\n# #region RegistryStatus [TYPE Class]\n# @BRIEF Backward-compatible registry status enum for legacy TUI bootstrap logic.\nclass RegistryStatus(str, Enum):\n ACTIVE = \"ACTIVE\"\n INACTIVE = \"INACTIVE\"\n# #endregion RegistryStatus\n\n# #region ReleaseCandidateStatus [TYPE Class]\n# @BRIEF Backward-compatible release candidate status enum for legacy TUI.\nclass ReleaseCandidateStatus(str, Enum):\n DRAFT = CandidateStatus.DRAFT.value\n PREPARED = CandidateStatus.PREPARED.value\n MANIFEST_BUILT = CandidateStatus.MANIFEST_BUILT.value\n CHECK_PENDING = CandidateStatus.CHECK_PENDING.value\n CHECK_RUNNING = CandidateStatus.CHECK_RUNNING.value\n CHECK_PASSED = CandidateStatus.CHECK_PASSED.value\n CHECK_BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n CHECK_ERROR = CandidateStatus.CHECK_ERROR.value\n APPROVED = CandidateStatus.APPROVED.value\n PUBLISHED = CandidateStatus.PUBLISHED.value\n REVOKED = CandidateStatus.REVOKED.value\n# #endregion ReleaseCandidateStatus\n\n# #region ResourceSourceEntry [TYPE Class]\n# @BRIEF Backward-compatible source entry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceEntry:\n source_id: str\n host: str\n protocol: str\n purpose: str\n enabled: bool = True\n# #endregion ResourceSourceEntry\n\n# #region ResourceSourceRegistry [TYPE Class]\n# @BRIEF Backward-compatible source registry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceRegistry:\n registry_id: str\n name: str\n entries: list[ResourceSourceEntry]\n updated_at: datetime\n updated_by: str\n status: str = \"ACTIVE\"\n immutable: bool = True\n allowed_hosts: list[str] | None = None\n allowed_schemes: list[str] | None = None\n allowed_source_types: list[str] | None = None\n\n @model_validator(mode=\"after\")\n def populate_legacy_allowlists(self):\n enabled_entries = [entry for entry in self.entries if getattr(entry, \"enabled\", True)]\n if self.allowed_hosts is None:\n self.allowed_hosts = [entry.host for entry in enabled_entries]\n if self.allowed_schemes is None:\n self.allowed_schemes = [entry.protocol for entry in enabled_entries]\n if self.allowed_source_types is None:\n self.allowed_source_types = [entry.purpose for entry in enabled_entries]\n return self\n\n @property\n def id(self) -> str:\n return self.registry_id\n# #endregion ResourceSourceRegistry\n\n# #region CleanProfilePolicy [TYPE Class]\n# @BRIEF Backward-compatible policy model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CleanProfilePolicy:\n policy_id: str\n policy_version: str\n profile: ProfileType\n active: bool\n internal_source_registry_ref: str\n prohibited_artifact_categories: list[str]\n effective_from: datetime\n required_system_categories: list[str] | None = None\n external_source_forbidden: bool = True\n immutable: bool = True\n content_json: dict[str, Any] | None = None\n\n @model_validator(mode=\"after\")\n def validate_enterprise_policy(self):\n if self.profile == ProfileType.ENTERPRISE_CLEAN:\n if not self.prohibited_artifact_categories:\n raise ValueError(\"enterprise-clean policy requires prohibited_artifact_categories\")\n if self.external_source_forbidden is not True:\n raise ValueError(\"enterprise-clean policy requires external_source_forbidden=true\")\n if self.content_json is None:\n self.content_json = {\n \"profile\": self.profile.value,\n \"prohibited_artifact_categories\": list(self.prohibited_artifact_categories or []),\n \"required_system_categories\": list(self.required_system_categories or []),\n \"external_source_forbidden\": self.external_source_forbidden,\n }\n return self\n\n @property\n def id(self) -> str:\n return self.policy_id\n\n @property\n def registry_snapshot_id(self) -> str:\n return self.internal_source_registry_ref\n# #endregion CleanProfilePolicy\n\n# #region ComplianceCheckRun [TYPE Class]\n# @BRIEF Backward-compatible run model for legacy TUI typing/import compatibility.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ComplianceCheckRun:\n check_run_id: str\n candidate_id: str\n policy_id: str\n started_at: datetime\n triggered_by: str\n execution_mode: ExecutionMode\n final_status: CheckFinalStatus\n checks: list[CheckStageResult]\n finished_at: datetime | None = None\n\n @model_validator(mode=\"after\")\n def validate_final_status_alignment(self):\n mandatory_stages = {\n CheckStageName.DATA_PURITY,\n CheckStageName.INTERNAL_SOURCES_ONLY,\n CheckStageName.NO_EXTERNAL_ENDPOINTS,\n CheckStageName.MANIFEST_CONSISTENCY,\n }\n if self.final_status == CheckFinalStatus.COMPLIANT:\n observed_stages = {check.stage for check in self.checks}\n if observed_stages != mandatory_stages:\n raise ValueError(\"compliant run requires all mandatory stages\")\n if any(check.status != CheckStageStatus.PASS for check in self.checks):\n raise ValueError(\"compliant run requires PASS on all mandatory stages\")\n return self\n\n @property\n def id(self) -> str:\n return self.check_run_id\n\n @property\n def run_id(self) -> str:\n return self.check_run_id\n\n @property\n def status(self) -> RunStatus:\n if self.final_status == CheckFinalStatus.RUNNING:\n return RunStatus.RUNNING\n if self.final_status == CheckFinalStatus.BLOCKED:\n return RunStatus.FAILED\n return RunStatus.SUCCEEDED\n# #endregion ComplianceCheckRun\n\n# #region ReleaseCandidate [TYPE Class]\n# @BRIEF Represents the release unit being prepared and governed.\n# @PRE: id, version, source_snapshot_ref are non-empty.\n# @POST: status advances only through legal transitions.\nclass ReleaseCandidate(Base):\n __tablename__ = \"clean_release_candidates\"\n\n id = Column(String, primary_key=True)\n name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs\n version = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n build_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n status = Column(String, default=CandidateStatus.DRAFT)\n\n def __init__(self, **kwargs):\n if \"candidate_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"candidate_id\")\n if \"profile\" in kwargs:\n kwargs.pop(\"profile\")\n status = kwargs.get(\"status\")\n if status is None:\n kwargs[\"status\"] = CandidateStatus.DRAFT.value\n elif isinstance(status, ReleaseCandidateStatus) or isinstance(status, CandidateStatus):\n kwargs[\"status\"] = status.value\n if not str(kwargs.get(\"id\", \"\")).strip():\n raise ValueError(\"candidate_id must be non-empty\")\n super().__init__(**kwargs)\n\n @property\n def candidate_id(self) -> str:\n return self.id\n\n def transition_to(self, new_status: CandidateStatus):\n \"\"\"\n @PURPOSE: Enforce legal state transitions.\n @PRE: Transition must be allowed by lifecycle rules.\n \"\"\"\n allowed = {\n CandidateStatus.DRAFT: [CandidateStatus.PREPARED],\n CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],\n CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],\n CandidateStatus.CHECK_RUNNING: [\n CandidateStatus.CHECK_PASSED,\n CandidateStatus.CHECK_BLOCKED,\n CandidateStatus.CHECK_ERROR\n ],\n CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],\n CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],\n CandidateStatus.REVOKED: []\n }\n current_status = CandidateStatus(self.status)\n if new_status not in allowed.get(current_status, []):\n raise IllegalTransitionError(f\"Forbidden transition from {current_status} to {new_status}\")\n self.status = new_status.value\n# #endregion ReleaseCandidate\n\n# #region CandidateArtifact [TYPE Class]\n# @BRIEF Represents one artifact associated with a release candidate.\nclass CandidateArtifact(Base):\n __tablename__ = \"clean_release_artifacts\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n path = Column(String, nullable=False)\n sha256 = Column(String, nullable=False)\n size = Column(Integer, nullable=False)\n detected_category = Column(String, nullable=True)\n declared_category = Column(String, nullable=True)\n source_uri = Column(String, nullable=True)\n source_host = Column(String, nullable=True)\n metadata_json = Column(JSON, default=dict)\n# #endregion CandidateArtifact\n\n# #region ManifestItem [TYPE Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestItem:\n path: str\n category: str\n classification: ClassificationType\n reason: str\n checksum: str | None = None\n# #endregion ManifestItem\n\n# #region ManifestSummary [TYPE Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestSummary:\n included_count: int\n excluded_count: int\n prohibited_detected_count: int\n# #endregion ManifestSummary\n\n# #region DistributionManifest [TYPE Class]\n# @BRIEF Immutable snapshot of the candidate payload.\n# @INVARIANT: Immutable after creation.\nclass DistributionManifest(Base):\n __tablename__ = \"clean_release_manifests\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_version = Column(Integer, nullable=False)\n manifest_digest = Column(String, nullable=False)\n artifacts_digest = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n content_json = Column(JSON, nullable=False)\n immutable = Column(Boolean, default=True)\n\n # Redesign compatibility fields (not persisted directly but used by builder/facade)\n def __init__(self, **kwargs):\n items = kwargs.pop(\"items\", None)\n summary = kwargs.pop(\"summary\", None)\n\n # Handle fields from manifest_builder.py\n if \"manifest_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"manifest_id\")\n if \"generated_at\" in kwargs:\n kwargs[\"created_at\"] = kwargs.pop(\"generated_at\")\n if \"generated_by\" in kwargs:\n kwargs[\"created_by\"] = kwargs.pop(\"generated_by\")\n if \"deterministic_hash\" in kwargs:\n kwargs[\"manifest_digest\"] = kwargs.pop(\"deterministic_hash\")\n if \"policy_id\" in kwargs:\n kwargs.pop(\"policy_id\")\n\n if items is not None and summary is not None:\n expected_count = int(summary.included_count) + int(summary.excluded_count)\n if expected_count != len(items):\n raise ValueError(\"manifest summary counts must match items size\")\n\n # Ensure required DB fields have defaults if missing\n if \"manifest_version\" not in kwargs:\n kwargs[\"manifest_version\"] = 1\n if \"artifacts_digest\" not in kwargs:\n kwargs[\"artifacts_digest\"] = kwargs.get(\"manifest_digest\", \"pending\")\n if \"source_snapshot_ref\" not in kwargs:\n kwargs[\"source_snapshot_ref\"] = \"pending\"\n\n # Pack items and summary into content_json if provided\n if items is not None or summary is not None:\n content = dict(kwargs.get(\"content_json\") or {})\n if items is not None:\n content[\"items\"] = [\n {\n \"path\": i.path,\n \"category\": i.category,\n \"classification\": i.classification.value,\n \"reason\": i.reason,\n \"checksum\": i.checksum\n } for i in items\n ]\n if summary is not None:\n content[\"summary\"] = {\n \"included_count\": summary.included_count,\n \"excluded_count\": summary.excluded_count,\n \"prohibited_detected_count\": summary.prohibited_detected_count\n }\n kwargs[\"content_json\"] = content\n\n super().__init__(**kwargs)\n\n @property\n def manifest_id(self) -> str:\n return self.id\n\n @property\n def deterministic_hash(self) -> str:\n return self.manifest_digest\n\n @property\n def summary(self) -> ManifestSummary:\n payload = (self.content_json or {}).get(\"summary\", {})\n return ManifestSummary(\n included_count=int(payload.get(\"included_count\", 0)),\n excluded_count=int(payload.get(\"excluded_count\", 0)),\n prohibited_detected_count=int(payload.get(\"prohibited_detected_count\", 0)),\n )\n# #endregion DistributionManifest\n\n# #region SourceRegistrySnapshot [TYPE Class]\n# @BRIEF Immutable registry snapshot for allowed sources.\nclass SourceRegistrySnapshot(Base):\n __tablename__ = \"clean_release_registry_snapshots\"\n\n id = Column(String, primary_key=True)\n registry_id = Column(String, nullable=False)\n registry_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n allowed_hosts = Column(JSON, nullable=False) # List[str]\n allowed_schemes = Column(JSON, nullable=False) # List[str]\n allowed_source_types = Column(JSON, nullable=False) # List[str]\n immutable = Column(Boolean, default=True)\n# #endregion SourceRegistrySnapshot\n\n# #region CleanPolicySnapshot [TYPE Class]\n# @BRIEF Immutable policy snapshot used to evaluate a run.\nclass CleanPolicySnapshot(Base):\n __tablename__ = \"clean_release_policy_snapshots\"\n\n id = Column(String, primary_key=True)\n policy_id = Column(String, nullable=False)\n policy_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n content_json = Column(JSON, nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n immutable = Column(Boolean, default=True)\n# #endregion CleanPolicySnapshot\n\n# #region ComplianceRun [TYPE Class]\n# @BRIEF Operational record for one compliance execution.\nclass ComplianceRun(Base):\n __tablename__ = \"clean_release_compliance_runs\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_id = Column(String, ForeignKey(\"clean_release_manifests.id\"), nullable=False)\n manifest_digest = Column(String, nullable=False)\n policy_snapshot_id = Column(String, ForeignKey(\"clean_release_policy_snapshots.id\"), nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n requested_by = Column(String, nullable=False)\n requested_at = Column(DateTime, default=datetime.utcnow)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n status = Column(String, default=RunStatus.PENDING)\n final_status = Column(String, nullable=True) # ComplianceDecision\n failure_reason = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n\n @property\n def check_run_id(self) -> str:\n return self.id\n# #endregion ComplianceRun\n\n# #region ComplianceStageRun [TYPE Class]\n# @BRIEF Stage-level execution record inside a run.\nclass ComplianceStageRun(Base):\n __tablename__ = \"clean_release_compliance_stage_runs\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n status = Column(String, nullable=False)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n decision = Column(String, nullable=True) # ComplianceDecision\n details_json = Column(JSON, default=dict)\n# #endregion ComplianceStageRun\n\n# #region ViolationSeverity [TYPE Class]\n# @BRIEF Backward-compatible violation severity enum for legacy clean-release tests.\nclass ViolationSeverity(str, Enum):\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n# #endregion ViolationSeverity\n\n# #region ViolationCategory [TYPE Class]\n# @BRIEF Backward-compatible violation category enum for legacy clean-release tests.\nclass ViolationCategory(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n EXTERNAL_SOURCE = \"EXTERNAL_SOURCE\"\n SOURCE_ISOLATION = \"SOURCE_ISOLATION\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n EXTERNAL_ENDPOINT = \"EXTERNAL_ENDPOINT\"\n# #endregion ViolationCategory\n\n# #region ComplianceViolation [TYPE Class]\n# @BRIEF Violation produced by a stage.\nclass ComplianceViolation(Base):\n __tablename__ = \"clean_release_compliance_violations\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n code = Column(String, nullable=False)\n severity = Column(String, nullable=False)\n artifact_path = Column(String, nullable=True)\n artifact_sha256 = Column(String, nullable=True)\n message = Column(String, nullable=False)\n evidence_json = Column(JSON, default=dict)\n\n def __init__(self, **kwargs):\n if \"violation_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"violation_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n if \"category\" in kwargs:\n category = kwargs.pop(\"category\")\n kwargs[\"stage_name\"] = category.value if isinstance(category, ViolationCategory) else str(category)\n if \"location\" in kwargs:\n kwargs[\"artifact_path\"] = kwargs.pop(\"location\")\n if \"remediation\" in kwargs:\n remediation = kwargs.pop(\"remediation\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"remediation\"] = remediation\n kwargs[\"evidence_json\"] = evidence\n if \"blocked_release\" in kwargs:\n blocked_release = kwargs.pop(\"blocked_release\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"blocked_release\"] = blocked_release\n kwargs[\"evidence_json\"] = evidence\n if \"detected_at\" in kwargs:\n kwargs.pop(\"detected_at\")\n if \"code\" not in kwargs:\n kwargs[\"code\"] = \"LEGACY_VIOLATION\"\n if \"message\" not in kwargs:\n kwargs[\"message\"] = kwargs.get(\"stage_name\", \"LEGACY_VIOLATION\")\n super().__init__(**kwargs)\n\n @property\n def violation_id(self) -> str:\n return self.id\n\n @violation_id.setter\n def violation_id(self, value: str) -> None:\n self.id = value\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def category(self) -> ViolationCategory:\n return ViolationCategory(self.stage_name)\n\n @category.setter\n def category(self, value: ViolationCategory) -> None:\n self.stage_name = value.value if isinstance(value, ViolationCategory) else str(value)\n\n @property\n def location(self) -> str | None:\n return self.artifact_path\n\n @property\n def remediation(self) -> str | None:\n return (self.evidence_json or {}).get(\"remediation\")\n\n @property\n def blocked_release(self) -> bool:\n return bool((self.evidence_json or {}).get(\"blocked_release\", False))\n# #endregion ComplianceViolation\n\n# #region ComplianceReport [TYPE Class]\n# @BRIEF Immutable result derived from a completed run.\n# @INVARIANT: Immutable after creation.\nclass ComplianceReport(Base):\n __tablename__ = \"clean_release_compliance_reports\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n final_status = Column(String, nullable=False) # ComplianceDecision\n summary_json = Column(JSON, nullable=False)\n generated_at = Column(DateTime, default=datetime.utcnow)\n immutable = Column(Boolean, default=True)\n\n def __init__(self, **kwargs):\n if \"report_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"report_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n operator_summary = kwargs.pop(\"operator_summary\", None)\n structured_payload_ref = kwargs.pop(\"structured_payload_ref\", None)\n violations_count = kwargs.pop(\"violations_count\", None)\n blocking_violations_count = kwargs.pop(\"blocking_violations_count\", None)\n\n final_status = kwargs.get(\"final_status\")\n final_status_value = getattr(final_status, \"value\", final_status)\n\n if (\n final_status_value in {CheckFinalStatus.BLOCKED.value, ComplianceDecision.BLOCKED.value}\n and blocking_violations_count is not None\n and int(blocking_violations_count) <= 0\n ):\n raise ValueError(\"blocked report requires blocking violations\")\n\n if (\n operator_summary is not None\n or structured_payload_ref is not None\n or violations_count is not None\n or blocking_violations_count is not None\n ):\n kwargs[\"summary_json\"] = {\n \"operator_summary\": operator_summary or \"\",\n \"structured_payload_ref\": structured_payload_ref,\n \"violations_count\": int(violations_count or 0),\n \"blocking_violations_count\": int(blocking_violations_count or 0),\n }\n\n super().__init__(**kwargs)\n\n @property\n def report_id(self) -> str:\n return self.id\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def operator_summary(self) -> str:\n return (self.summary_json or {}).get(\"operator_summary\", \"\")\n\n @property\n def structured_payload_ref(self) -> str | None:\n return (self.summary_json or {}).get(\"structured_payload_ref\")\n\n @property\n def violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"violations_count\", 0))\n\n @property\n def blocking_violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"blocking_violations_count\", 0))\n# #endregion ComplianceReport\n\n# #region ApprovalDecision [TYPE Class]\n# @BRIEF Approval or rejection bound to a candidate and report.\nclass ApprovalDecision(Base):\n __tablename__ = \"clean_release_approval_decisions\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n decision = Column(String, nullable=False) # ApprovalDecisionType\n decided_by = Column(String, nullable=False)\n decided_at = Column(DateTime, default=datetime.utcnow)\n comment = Column(String, nullable=True)\n# #endregion ApprovalDecision\n\n# #region PublicationRecord [TYPE Class]\n# @BRIEF Publication or revocation record.\nclass PublicationRecord(Base):\n __tablename__ = \"clean_release_publication_records\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n published_by = Column(String, nullable=False)\n published_at = Column(DateTime, default=datetime.utcnow)\n target_channel = Column(String, nullable=False)\n publication_ref = Column(String, nullable=True)\n status = Column(String, default=PublicationStatus.ACTIVE)\n# #endregion PublicationRecord\n\n# #region CleanReleaseAuditLog [TYPE Class]\n# @BRIEF Represents a persistent audit log entry for clean release actions.\nimport uuid\n\n\nclass CleanReleaseAuditLog(Base):\n __tablename__ = \"clean_release_audit_logs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n candidate_id = Column(String, index=True, nullable=True)\n action = Column(String, nullable=False) # e.g. \"TRANSITION\", \"APPROVE\", \"PUBLISH\"\n actor = Column(String, nullable=False)\n timestamp = Column(DateTime, default=datetime.utcnow)\n details_json = Column(JSON, default=dict)\n# #endregion CleanReleaseAuditLog\n\n# #endregion CleanReleaseModels\n" }, { "contract_id": "ExecutionMode", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 28, - "end_line": 34, + "start_line": 24, + "end_line": 30, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56798,15 +45091,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ExecutionMode:Class]\n# @PURPOSE: Backward-compatible execution mode enum for legacy TUI/orchestrator tests.\nclass ExecutionMode(str, Enum):\n TUI = \"TUI\"\n API = \"API\"\n SCHEDULER = \"SCHEDULER\"\n# [/DEF:ExecutionMode:Class]\n" + "anchor_syntax": "region", + "body": "# #region ExecutionMode [TYPE Class]\n# @BRIEF Backward-compatible execution mode enum for legacy TUI/orchestrator tests.\nclass ExecutionMode(str, Enum):\n TUI = \"TUI\"\n API = \"API\"\n SCHEDULER = \"SCHEDULER\"\n# #endregion ExecutionMode\n" }, { "contract_id": "CheckFinalStatus", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 36, - "end_line": 43, + "start_line": 32, + "end_line": 39, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56824,15 +45117,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CheckFinalStatus:Class]\n# @PURPOSE: Backward-compatible final status enum for legacy TUI/orchestrator tests.\nclass CheckFinalStatus(str, Enum):\n COMPLIANT = \"COMPLIANT\"\n BLOCKED = \"BLOCKED\"\n FAILED = \"FAILED\"\n RUNNING = \"RUNNING\"\n# [/DEF:CheckFinalStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region CheckFinalStatus [TYPE Class]\n# @BRIEF Backward-compatible final status enum for legacy TUI/orchestrator tests.\nclass CheckFinalStatus(str, Enum):\n COMPLIANT = \"COMPLIANT\"\n BLOCKED = \"BLOCKED\"\n FAILED = \"FAILED\"\n RUNNING = \"RUNNING\"\n# #endregion CheckFinalStatus\n" }, { "contract_id": "CheckStageName", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 45, - "end_line": 52, + "start_line": 41, + "end_line": 48, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56850,15 +45143,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CheckStageName:Class]\n# @PURPOSE: Backward-compatible stage name enum for legacy TUI/orchestrator tests.\nclass CheckStageName(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n INTERNAL_SOURCES_ONLY = \"INTERNAL_SOURCES_ONLY\"\n NO_EXTERNAL_ENDPOINTS = \"NO_EXTERNAL_ENDPOINTS\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n# [/DEF:CheckStageName:Class]\n" + "anchor_syntax": "region", + "body": "# #region CheckStageName [TYPE Class]\n# @BRIEF Backward-compatible stage name enum for legacy TUI/orchestrator tests.\nclass CheckStageName(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n INTERNAL_SOURCES_ONLY = \"INTERNAL_SOURCES_ONLY\"\n NO_EXTERNAL_ENDPOINTS = \"NO_EXTERNAL_ENDPOINTS\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n# #endregion CheckStageName\n" }, { "contract_id": "CheckStageStatus", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 54, - "end_line": 61, + "start_line": 50, + "end_line": 57, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56876,15 +45169,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CheckStageStatus:Class]\n# @PURPOSE: Backward-compatible stage status enum for legacy TUI/orchestrator tests.\nclass CheckStageStatus(str, Enum):\n PASS = \"PASS\"\n FAIL = \"FAIL\"\n SKIPPED = \"SKIPPED\"\n RUNNING = \"RUNNING\"\n# [/DEF:CheckStageStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region CheckStageStatus [TYPE Class]\n# @BRIEF Backward-compatible stage status enum for legacy TUI/orchestrator tests.\nclass CheckStageStatus(str, Enum):\n PASS = \"PASS\"\n FAIL = \"FAIL\"\n SKIPPED = \"SKIPPED\"\n RUNNING = \"RUNNING\"\n# #endregion CheckStageStatus\n" }, { "contract_id": "CheckStageResult", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 63, - "end_line": 70, + "start_line": 59, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56902,15 +45195,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CheckStageResult:Class]\n# @PURPOSE: Backward-compatible stage result container for legacy TUI/orchestrator tests.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CheckStageResult:\n stage: CheckStageName\n status: CheckStageStatus\n details: str = \"\"\n# [/DEF:CheckStageResult:Class]\n" + "anchor_syntax": "region", + "body": "# #region CheckStageResult [TYPE Class]\n# @BRIEF Backward-compatible stage result container for legacy TUI/orchestrator tests.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CheckStageResult:\n stage: CheckStageName\n status: CheckStageStatus\n details: str = \"\"\n# #endregion CheckStageResult\n" }, { "contract_id": "ProfileType", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 72, - "end_line": 76, + "start_line": 68, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56928,15 +45221,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ProfileType:Class]\n# @PURPOSE: Backward-compatible profile enum for legacy TUI bootstrap logic.\nclass ProfileType(str, Enum):\n ENTERPRISE_CLEAN = \"enterprise-clean\"\n# [/DEF:ProfileType:Class]\n" + "anchor_syntax": "region", + "body": "# #region ProfileType [TYPE Class]\n# @BRIEF Backward-compatible profile enum for legacy TUI bootstrap logic.\nclass ProfileType(str, Enum):\n ENTERPRISE_CLEAN = \"enterprise-clean\"\n# #endregion ProfileType\n" }, { "contract_id": "RegistryStatus", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 78, - "end_line": 83, + "start_line": 74, + "end_line": 79, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56954,15 +45247,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RegistryStatus:Class]\n# @PURPOSE: Backward-compatible registry status enum for legacy TUI bootstrap logic.\nclass RegistryStatus(str, Enum):\n ACTIVE = \"ACTIVE\"\n INACTIVE = \"INACTIVE\"\n# [/DEF:RegistryStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region RegistryStatus [TYPE Class]\n# @BRIEF Backward-compatible registry status enum for legacy TUI bootstrap logic.\nclass RegistryStatus(str, Enum):\n ACTIVE = \"ACTIVE\"\n INACTIVE = \"INACTIVE\"\n# #endregion RegistryStatus\n" }, { "contract_id": "ReleaseCandidateStatus", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 85, - "end_line": 100, + "start_line": 81, + "end_line": 96, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -56980,15 +45273,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReleaseCandidateStatus:Class]\n# @PURPOSE: Backward-compatible release candidate status enum for legacy TUI.\nclass ReleaseCandidateStatus(str, Enum):\n DRAFT = CandidateStatus.DRAFT.value\n PREPARED = CandidateStatus.PREPARED.value\n MANIFEST_BUILT = CandidateStatus.MANIFEST_BUILT.value\n CHECK_PENDING = CandidateStatus.CHECK_PENDING.value\n CHECK_RUNNING = CandidateStatus.CHECK_RUNNING.value\n CHECK_PASSED = CandidateStatus.CHECK_PASSED.value\n CHECK_BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n CHECK_ERROR = CandidateStatus.CHECK_ERROR.value\n APPROVED = CandidateStatus.APPROVED.value\n PUBLISHED = CandidateStatus.PUBLISHED.value\n REVOKED = CandidateStatus.REVOKED.value\n# [/DEF:ReleaseCandidateStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region ReleaseCandidateStatus [TYPE Class]\n# @BRIEF Backward-compatible release candidate status enum for legacy TUI.\nclass ReleaseCandidateStatus(str, Enum):\n DRAFT = CandidateStatus.DRAFT.value\n PREPARED = CandidateStatus.PREPARED.value\n MANIFEST_BUILT = CandidateStatus.MANIFEST_BUILT.value\n CHECK_PENDING = CandidateStatus.CHECK_PENDING.value\n CHECK_RUNNING = CandidateStatus.CHECK_RUNNING.value\n CHECK_PASSED = CandidateStatus.CHECK_PASSED.value\n CHECK_BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n BLOCKED = CandidateStatus.CHECK_BLOCKED.value\n CHECK_ERROR = CandidateStatus.CHECK_ERROR.value\n APPROVED = CandidateStatus.APPROVED.value\n PUBLISHED = CandidateStatus.PUBLISHED.value\n REVOKED = CandidateStatus.REVOKED.value\n# #endregion ReleaseCandidateStatus\n" }, { "contract_id": "ResourceSourceEntry", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 102, - "end_line": 111, + "start_line": 98, + "end_line": 107, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57006,15 +45299,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ResourceSourceEntry:Class]\n# @PURPOSE: Backward-compatible source entry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceEntry:\n source_id: str\n host: str\n protocol: str\n purpose: str\n enabled: bool = True\n# [/DEF:ResourceSourceEntry:Class]\n" + "anchor_syntax": "region", + "body": "# #region ResourceSourceEntry [TYPE Class]\n# @BRIEF Backward-compatible source entry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceEntry:\n source_id: str\n host: str\n protocol: str\n purpose: str\n enabled: bool = True\n# #endregion ResourceSourceEntry\n" }, { "contract_id": "ResourceSourceRegistry", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 113, - "end_line": 142, + "start_line": 109, + "end_line": 138, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57032,15 +45325,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ResourceSourceRegistry:Class]\n# @PURPOSE: Backward-compatible source registry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceRegistry:\n registry_id: str\n name: str\n entries: List[ResourceSourceEntry]\n updated_at: datetime\n updated_by: str\n status: str = \"ACTIVE\"\n immutable: bool = True\n allowed_hosts: Optional[List[str]] = None\n allowed_schemes: Optional[List[str]] = None\n allowed_source_types: Optional[List[str]] = None\n\n @model_validator(mode=\"after\")\n def populate_legacy_allowlists(self):\n enabled_entries = [entry for entry in self.entries if getattr(entry, \"enabled\", True)]\n if self.allowed_hosts is None:\n self.allowed_hosts = [entry.host for entry in enabled_entries]\n if self.allowed_schemes is None:\n self.allowed_schemes = [entry.protocol for entry in enabled_entries]\n if self.allowed_source_types is None:\n self.allowed_source_types = [entry.purpose for entry in enabled_entries]\n return self\n\n @property\n def id(self) -> str:\n return self.registry_id\n# [/DEF:ResourceSourceRegistry:Class]\n" + "anchor_syntax": "region", + "body": "# #region ResourceSourceRegistry [TYPE Class]\n# @BRIEF Backward-compatible source registry model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ResourceSourceRegistry:\n registry_id: str\n name: str\n entries: list[ResourceSourceEntry]\n updated_at: datetime\n updated_by: str\n status: str = \"ACTIVE\"\n immutable: bool = True\n allowed_hosts: list[str] | None = None\n allowed_schemes: list[str] | None = None\n allowed_source_types: list[str] | None = None\n\n @model_validator(mode=\"after\")\n def populate_legacy_allowlists(self):\n enabled_entries = [entry for entry in self.entries if getattr(entry, \"enabled\", True)]\n if self.allowed_hosts is None:\n self.allowed_hosts = [entry.host for entry in enabled_entries]\n if self.allowed_schemes is None:\n self.allowed_schemes = [entry.protocol for entry in enabled_entries]\n if self.allowed_source_types is None:\n self.allowed_source_types = [entry.purpose for entry in enabled_entries]\n return self\n\n @property\n def id(self) -> str:\n return self.registry_id\n# #endregion ResourceSourceRegistry\n" }, { "contract_id": "CleanProfilePolicy", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 144, - "end_line": 183, + "start_line": 140, + "end_line": 179, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57058,15 +45351,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanProfilePolicy:Class]\n# @PURPOSE: Backward-compatible policy model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CleanProfilePolicy:\n policy_id: str\n policy_version: str\n profile: ProfileType\n active: bool\n internal_source_registry_ref: str\n prohibited_artifact_categories: List[str]\n effective_from: datetime\n required_system_categories: Optional[List[str]] = None\n external_source_forbidden: bool = True\n immutable: bool = True\n content_json: Optional[Dict[str, Any]] = None\n\n @model_validator(mode=\"after\")\n def validate_enterprise_policy(self):\n if self.profile == ProfileType.ENTERPRISE_CLEAN:\n if not self.prohibited_artifact_categories:\n raise ValueError(\"enterprise-clean policy requires prohibited_artifact_categories\")\n if self.external_source_forbidden is not True:\n raise ValueError(\"enterprise-clean policy requires external_source_forbidden=true\")\n if self.content_json is None:\n self.content_json = {\n \"profile\": self.profile.value,\n \"prohibited_artifact_categories\": list(self.prohibited_artifact_categories or []),\n \"required_system_categories\": list(self.required_system_categories or []),\n \"external_source_forbidden\": self.external_source_forbidden,\n }\n return self\n\n @property\n def id(self) -> str:\n return self.policy_id\n\n @property\n def registry_snapshot_id(self) -> str:\n return self.internal_source_registry_ref\n# [/DEF:CleanProfilePolicy:Class]\n" + "anchor_syntax": "region", + "body": "# #region CleanProfilePolicy [TYPE Class]\n# @BRIEF Backward-compatible policy model for legacy TUI bootstrap logic.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass CleanProfilePolicy:\n policy_id: str\n policy_version: str\n profile: ProfileType\n active: bool\n internal_source_registry_ref: str\n prohibited_artifact_categories: list[str]\n effective_from: datetime\n required_system_categories: list[str] | None = None\n external_source_forbidden: bool = True\n immutable: bool = True\n content_json: dict[str, Any] | None = None\n\n @model_validator(mode=\"after\")\n def validate_enterprise_policy(self):\n if self.profile == ProfileType.ENTERPRISE_CLEAN:\n if not self.prohibited_artifact_categories:\n raise ValueError(\"enterprise-clean policy requires prohibited_artifact_categories\")\n if self.external_source_forbidden is not True:\n raise ValueError(\"enterprise-clean policy requires external_source_forbidden=true\")\n if self.content_json is None:\n self.content_json = {\n \"profile\": self.profile.value,\n \"prohibited_artifact_categories\": list(self.prohibited_artifact_categories or []),\n \"required_system_categories\": list(self.required_system_categories or []),\n \"external_source_forbidden\": self.external_source_forbidden,\n }\n return self\n\n @property\n def id(self) -> str:\n return self.policy_id\n\n @property\n def registry_snapshot_id(self) -> str:\n return self.internal_source_registry_ref\n# #endregion CleanProfilePolicy\n" }, { "contract_id": "ComplianceCheckRun", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 185, - "end_line": 230, + "start_line": 181, + "end_line": 226, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57084,15 +45377,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceCheckRun:Class]\n# @PURPOSE: Backward-compatible run model for legacy TUI typing/import compatibility.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ComplianceCheckRun:\n check_run_id: str\n candidate_id: str\n policy_id: str\n started_at: datetime\n triggered_by: str\n execution_mode: ExecutionMode\n final_status: CheckFinalStatus\n checks: List[CheckStageResult]\n finished_at: Optional[datetime] = None\n\n @model_validator(mode=\"after\")\n def validate_final_status_alignment(self):\n mandatory_stages = {\n CheckStageName.DATA_PURITY,\n CheckStageName.INTERNAL_SOURCES_ONLY,\n CheckStageName.NO_EXTERNAL_ENDPOINTS,\n CheckStageName.MANIFEST_CONSISTENCY,\n }\n if self.final_status == CheckFinalStatus.COMPLIANT:\n observed_stages = {check.stage for check in self.checks}\n if observed_stages != mandatory_stages:\n raise ValueError(\"compliant run requires all mandatory stages\")\n if any(check.status != CheckStageStatus.PASS for check in self.checks):\n raise ValueError(\"compliant run requires PASS on all mandatory stages\")\n return self\n\n @property\n def id(self) -> str:\n return self.check_run_id\n\n @property\n def run_id(self) -> str:\n return self.check_run_id\n\n @property\n def status(self) -> RunStatus:\n if self.final_status == CheckFinalStatus.RUNNING:\n return RunStatus.RUNNING\n if self.final_status == CheckFinalStatus.BLOCKED:\n return RunStatus.FAILED\n return RunStatus.SUCCEEDED\n# [/DEF:ComplianceCheckRun:Class]\n" + "anchor_syntax": "region", + "body": "# #region ComplianceCheckRun [TYPE Class]\n# @BRIEF Backward-compatible run model for legacy TUI typing/import compatibility.\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ComplianceCheckRun:\n check_run_id: str\n candidate_id: str\n policy_id: str\n started_at: datetime\n triggered_by: str\n execution_mode: ExecutionMode\n final_status: CheckFinalStatus\n checks: list[CheckStageResult]\n finished_at: datetime | None = None\n\n @model_validator(mode=\"after\")\n def validate_final_status_alignment(self):\n mandatory_stages = {\n CheckStageName.DATA_PURITY,\n CheckStageName.INTERNAL_SOURCES_ONLY,\n CheckStageName.NO_EXTERNAL_ENDPOINTS,\n CheckStageName.MANIFEST_CONSISTENCY,\n }\n if self.final_status == CheckFinalStatus.COMPLIANT:\n observed_stages = {check.stage for check in self.checks}\n if observed_stages != mandatory_stages:\n raise ValueError(\"compliant run requires all mandatory stages\")\n if any(check.status != CheckStageStatus.PASS for check in self.checks):\n raise ValueError(\"compliant run requires PASS on all mandatory stages\")\n return self\n\n @property\n def id(self) -> str:\n return self.check_run_id\n\n @property\n def run_id(self) -> str:\n return self.check_run_id\n\n @property\n def status(self) -> RunStatus:\n if self.final_status == CheckFinalStatus.RUNNING:\n return RunStatus.RUNNING\n if self.final_status == CheckFinalStatus.BLOCKED:\n return RunStatus.FAILED\n return RunStatus.SUCCEEDED\n# #endregion ComplianceCheckRun\n" }, { "contract_id": "ReleaseCandidate", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 232, - "end_line": 294, + "start_line": 228, + "end_line": 288, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57130,15 +45423,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReleaseCandidate:Class]\n# @PURPOSE: Represents the release unit being prepared and governed.\n# @PRE: id, version, source_snapshot_ref are non-empty.\n# @POST: status advances only through legal transitions.\nclass ReleaseCandidate(Base):\n __tablename__ = \"clean_release_candidates\"\n \n id = Column(String, primary_key=True)\n name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs\n version = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n build_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n status = Column(String, default=CandidateStatus.DRAFT)\n\n def __init__(self, **kwargs):\n if \"candidate_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"candidate_id\")\n if \"profile\" in kwargs:\n kwargs.pop(\"profile\")\n status = kwargs.get(\"status\")\n if status is None:\n kwargs[\"status\"] = CandidateStatus.DRAFT.value\n elif isinstance(status, ReleaseCandidateStatus):\n kwargs[\"status\"] = status.value\n elif isinstance(status, CandidateStatus):\n kwargs[\"status\"] = status.value\n if not str(kwargs.get(\"id\", \"\")).strip():\n raise ValueError(\"candidate_id must be non-empty\")\n super().__init__(**kwargs)\n\n @property\n def candidate_id(self) -> str:\n return self.id\n\n def transition_to(self, new_status: CandidateStatus):\n \"\"\"\n @PURPOSE: Enforce legal state transitions.\n @PRE: Transition must be allowed by lifecycle rules.\n \"\"\"\n allowed = {\n CandidateStatus.DRAFT: [CandidateStatus.PREPARED],\n CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],\n CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],\n CandidateStatus.CHECK_RUNNING: [\n CandidateStatus.CHECK_PASSED, \n CandidateStatus.CHECK_BLOCKED, \n CandidateStatus.CHECK_ERROR\n ],\n CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],\n CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],\n CandidateStatus.REVOKED: []\n }\n current_status = CandidateStatus(self.status)\n if new_status not in allowed.get(current_status, []):\n raise IllegalTransitionError(f\"Forbidden transition from {current_status} to {new_status}\")\n self.status = new_status.value\n# [/DEF:ReleaseCandidate:Class]\n" + "anchor_syntax": "region", + "body": "# #region ReleaseCandidate [TYPE Class]\n# @BRIEF Represents the release unit being prepared and governed.\n# @PRE: id, version, source_snapshot_ref are non-empty.\n# @POST: status advances only through legal transitions.\nclass ReleaseCandidate(Base):\n __tablename__ = \"clean_release_candidates\"\n\n id = Column(String, primary_key=True)\n name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs\n version = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n build_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n status = Column(String, default=CandidateStatus.DRAFT)\n\n def __init__(self, **kwargs):\n if \"candidate_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"candidate_id\")\n if \"profile\" in kwargs:\n kwargs.pop(\"profile\")\n status = kwargs.get(\"status\")\n if status is None:\n kwargs[\"status\"] = CandidateStatus.DRAFT.value\n elif isinstance(status, ReleaseCandidateStatus) or isinstance(status, CandidateStatus):\n kwargs[\"status\"] = status.value\n if not str(kwargs.get(\"id\", \"\")).strip():\n raise ValueError(\"candidate_id must be non-empty\")\n super().__init__(**kwargs)\n\n @property\n def candidate_id(self) -> str:\n return self.id\n\n def transition_to(self, new_status: CandidateStatus):\n \"\"\"\n @PURPOSE: Enforce legal state transitions.\n @PRE: Transition must be allowed by lifecycle rules.\n \"\"\"\n allowed = {\n CandidateStatus.DRAFT: [CandidateStatus.PREPARED],\n CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],\n CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],\n CandidateStatus.CHECK_RUNNING: [\n CandidateStatus.CHECK_PASSED,\n CandidateStatus.CHECK_BLOCKED,\n CandidateStatus.CHECK_ERROR\n ],\n CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],\n CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],\n CandidateStatus.REVOKED: []\n }\n current_status = CandidateStatus(self.status)\n if new_status not in allowed.get(current_status, []):\n raise IllegalTransitionError(f\"Forbidden transition from {current_status} to {new_status}\")\n self.status = new_status.value\n# #endregion ReleaseCandidate\n" }, { "contract_id": "CandidateArtifact", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 296, - "end_line": 311, + "start_line": 290, + "end_line": 305, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57156,43 +45449,43 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CandidateArtifact:Class]\n# @PURPOSE: Represents one artifact associated with a release candidate.\nclass CandidateArtifact(Base):\n __tablename__ = \"clean_release_artifacts\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n path = Column(String, nullable=False)\n sha256 = Column(String, nullable=False)\n size = Column(Integer, nullable=False)\n detected_category = Column(String, nullable=True)\n declared_category = Column(String, nullable=True)\n source_uri = Column(String, nullable=True)\n source_host = Column(String, nullable=True)\n metadata_json = Column(JSON, default=dict)\n# [/DEF:CandidateArtifact:Class]\n" + "anchor_syntax": "region", + "body": "# #region CandidateArtifact [TYPE Class]\n# @BRIEF Represents one artifact associated with a release candidate.\nclass CandidateArtifact(Base):\n __tablename__ = \"clean_release_artifacts\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n path = Column(String, nullable=False)\n sha256 = Column(String, nullable=False)\n size = Column(Integer, nullable=False)\n detected_category = Column(String, nullable=True)\n declared_category = Column(String, nullable=True)\n source_uri = Column(String, nullable=True)\n source_host = Column(String, nullable=True)\n metadata_json = Column(JSON, default=dict)\n# #endregion CandidateArtifact\n" }, { "contract_id": "ManifestItem", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 313, - "end_line": 321, + "start_line": 307, + "end_line": 315, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ManifestItem:Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestItem:\n path: str\n category: str\n classification: ClassificationType\n reason: str\n checksum: Optional[str] = None\n# [/DEF:ManifestItem:Class]\n" + "anchor_syntax": "region", + "body": "# #region ManifestItem [TYPE Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestItem:\n path: str\n category: str\n classification: ClassificationType\n reason: str\n checksum: str | None = None\n# #endregion ManifestItem\n" }, { "contract_id": "ManifestSummary", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 323, - "end_line": 329, + "start_line": 317, + "end_line": 323, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ManifestSummary:Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestSummary:\n included_count: int\n excluded_count: int\n prohibited_detected_count: int\n# [/DEF:ManifestSummary:Class]\n" + "anchor_syntax": "region", + "body": "# #region ManifestSummary [TYPE Class]\n@pydantic_dataclass(config=ConfigDict(validate_assignment=True))\nclass ManifestSummary:\n included_count: int\n excluded_count: int\n prohibited_detected_count: int\n# #endregion ManifestSummary\n" }, { "contract_id": "DistributionManifest", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 331, - "end_line": 417, + "start_line": 325, + "end_line": 411, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57220,15 +45513,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DistributionManifest:Class]\n# @PURPOSE: Immutable snapshot of the candidate payload.\n# @INVARIANT: Immutable after creation.\nclass DistributionManifest(Base):\n __tablename__ = \"clean_release_manifests\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_version = Column(Integer, nullable=False)\n manifest_digest = Column(String, nullable=False)\n artifacts_digest = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n content_json = Column(JSON, nullable=False)\n immutable = Column(Boolean, default=True)\n\n # Redesign compatibility fields (not persisted directly but used by builder/facade)\n def __init__(self, **kwargs):\n items = kwargs.pop(\"items\", None)\n summary = kwargs.pop(\"summary\", None)\n\n # Handle fields from manifest_builder.py\n if \"manifest_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"manifest_id\")\n if \"generated_at\" in kwargs:\n kwargs[\"created_at\"] = kwargs.pop(\"generated_at\")\n if \"generated_by\" in kwargs:\n kwargs[\"created_by\"] = kwargs.pop(\"generated_by\")\n if \"deterministic_hash\" in kwargs:\n kwargs[\"manifest_digest\"] = kwargs.pop(\"deterministic_hash\")\n if \"policy_id\" in kwargs:\n kwargs.pop(\"policy_id\")\n\n if items is not None and summary is not None:\n expected_count = int(summary.included_count) + int(summary.excluded_count)\n if expected_count != len(items):\n raise ValueError(\"manifest summary counts must match items size\")\n \n # Ensure required DB fields have defaults if missing\n if \"manifest_version\" not in kwargs:\n kwargs[\"manifest_version\"] = 1\n if \"artifacts_digest\" not in kwargs:\n kwargs[\"artifacts_digest\"] = kwargs.get(\"manifest_digest\", \"pending\")\n if \"source_snapshot_ref\" not in kwargs:\n kwargs[\"source_snapshot_ref\"] = \"pending\"\n \n # Pack items and summary into content_json if provided\n if items is not None or summary is not None:\n content = dict(kwargs.get(\"content_json\") or {})\n if items is not None:\n content[\"items\"] = [\n {\n \"path\": i.path,\n \"category\": i.category,\n \"classification\": i.classification.value,\n \"reason\": i.reason,\n \"checksum\": i.checksum\n } for i in items\n ]\n if summary is not None:\n content[\"summary\"] = {\n \"included_count\": summary.included_count,\n \"excluded_count\": summary.excluded_count,\n \"prohibited_detected_count\": summary.prohibited_detected_count\n }\n kwargs[\"content_json\"] = content\n\n super().__init__(**kwargs)\n\n @property\n def manifest_id(self) -> str:\n return self.id\n\n @property\n def deterministic_hash(self) -> str:\n return self.manifest_digest\n\n @property\n def summary(self) -> ManifestSummary:\n payload = (self.content_json or {}).get(\"summary\", {})\n return ManifestSummary(\n included_count=int(payload.get(\"included_count\", 0)),\n excluded_count=int(payload.get(\"excluded_count\", 0)),\n prohibited_detected_count=int(payload.get(\"prohibited_detected_count\", 0)),\n )\n# [/DEF:DistributionManifest:Class]\n" + "anchor_syntax": "region", + "body": "# #region DistributionManifest [TYPE Class]\n# @BRIEF Immutable snapshot of the candidate payload.\n# @INVARIANT: Immutable after creation.\nclass DistributionManifest(Base):\n __tablename__ = \"clean_release_manifests\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_version = Column(Integer, nullable=False)\n manifest_digest = Column(String, nullable=False)\n artifacts_digest = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n content_json = Column(JSON, nullable=False)\n immutable = Column(Boolean, default=True)\n\n # Redesign compatibility fields (not persisted directly but used by builder/facade)\n def __init__(self, **kwargs):\n items = kwargs.pop(\"items\", None)\n summary = kwargs.pop(\"summary\", None)\n\n # Handle fields from manifest_builder.py\n if \"manifest_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"manifest_id\")\n if \"generated_at\" in kwargs:\n kwargs[\"created_at\"] = kwargs.pop(\"generated_at\")\n if \"generated_by\" in kwargs:\n kwargs[\"created_by\"] = kwargs.pop(\"generated_by\")\n if \"deterministic_hash\" in kwargs:\n kwargs[\"manifest_digest\"] = kwargs.pop(\"deterministic_hash\")\n if \"policy_id\" in kwargs:\n kwargs.pop(\"policy_id\")\n\n if items is not None and summary is not None:\n expected_count = int(summary.included_count) + int(summary.excluded_count)\n if expected_count != len(items):\n raise ValueError(\"manifest summary counts must match items size\")\n\n # Ensure required DB fields have defaults if missing\n if \"manifest_version\" not in kwargs:\n kwargs[\"manifest_version\"] = 1\n if \"artifacts_digest\" not in kwargs:\n kwargs[\"artifacts_digest\"] = kwargs.get(\"manifest_digest\", \"pending\")\n if \"source_snapshot_ref\" not in kwargs:\n kwargs[\"source_snapshot_ref\"] = \"pending\"\n\n # Pack items and summary into content_json if provided\n if items is not None or summary is not None:\n content = dict(kwargs.get(\"content_json\") or {})\n if items is not None:\n content[\"items\"] = [\n {\n \"path\": i.path,\n \"category\": i.category,\n \"classification\": i.classification.value,\n \"reason\": i.reason,\n \"checksum\": i.checksum\n } for i in items\n ]\n if summary is not None:\n content[\"summary\"] = {\n \"included_count\": summary.included_count,\n \"excluded_count\": summary.excluded_count,\n \"prohibited_detected_count\": summary.prohibited_detected_count\n }\n kwargs[\"content_json\"] = content\n\n super().__init__(**kwargs)\n\n @property\n def manifest_id(self) -> str:\n return self.id\n\n @property\n def deterministic_hash(self) -> str:\n return self.manifest_digest\n\n @property\n def summary(self) -> ManifestSummary:\n payload = (self.content_json or {}).get(\"summary\", {})\n return ManifestSummary(\n included_count=int(payload.get(\"included_count\", 0)),\n excluded_count=int(payload.get(\"excluded_count\", 0)),\n prohibited_detected_count=int(payload.get(\"prohibited_detected_count\", 0)),\n )\n# #endregion DistributionManifest\n" }, { "contract_id": "SourceRegistrySnapshot", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 419, - "end_line": 432, + "start_line": 413, + "end_line": 426, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57246,15 +45539,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SourceRegistrySnapshot:Class]\n# @PURPOSE: Immutable registry snapshot for allowed sources.\nclass SourceRegistrySnapshot(Base):\n __tablename__ = \"clean_release_registry_snapshots\"\n \n id = Column(String, primary_key=True)\n registry_id = Column(String, nullable=False)\n registry_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n allowed_hosts = Column(JSON, nullable=False) # List[str]\n allowed_schemes = Column(JSON, nullable=False) # List[str]\n allowed_source_types = Column(JSON, nullable=False) # List[str]\n immutable = Column(Boolean, default=True)\n# [/DEF:SourceRegistrySnapshot:Class]\n" + "anchor_syntax": "region", + "body": "# #region SourceRegistrySnapshot [TYPE Class]\n# @BRIEF Immutable registry snapshot for allowed sources.\nclass SourceRegistrySnapshot(Base):\n __tablename__ = \"clean_release_registry_snapshots\"\n\n id = Column(String, primary_key=True)\n registry_id = Column(String, nullable=False)\n registry_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n allowed_hosts = Column(JSON, nullable=False) # List[str]\n allowed_schemes = Column(JSON, nullable=False) # List[str]\n allowed_source_types = Column(JSON, nullable=False) # List[str]\n immutable = Column(Boolean, default=True)\n# #endregion SourceRegistrySnapshot\n" }, { "contract_id": "CleanPolicySnapshot", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 434, - "end_line": 446, + "start_line": 428, + "end_line": 440, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57272,15 +45565,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanPolicySnapshot:Class]\n# @PURPOSE: Immutable policy snapshot used to evaluate a run.\nclass CleanPolicySnapshot(Base):\n __tablename__ = \"clean_release_policy_snapshots\"\n \n id = Column(String, primary_key=True)\n policy_id = Column(String, nullable=False)\n policy_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n content_json = Column(JSON, nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n immutable = Column(Boolean, default=True)\n# [/DEF:CleanPolicySnapshot:Class]\n" + "anchor_syntax": "region", + "body": "# #region CleanPolicySnapshot [TYPE Class]\n# @BRIEF Immutable policy snapshot used to evaluate a run.\nclass CleanPolicySnapshot(Base):\n __tablename__ = \"clean_release_policy_snapshots\"\n\n id = Column(String, primary_key=True)\n policy_id = Column(String, nullable=False)\n policy_version = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n content_json = Column(JSON, nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n immutable = Column(Boolean, default=True)\n# #endregion CleanPolicySnapshot\n" }, { "contract_id": "ComplianceRun", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 448, - "end_line": 471, + "start_line": 442, + "end_line": 465, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57298,15 +45591,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceRun:Class]\n# @PURPOSE: Operational record for one compliance execution.\nclass ComplianceRun(Base):\n __tablename__ = \"clean_release_compliance_runs\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_id = Column(String, ForeignKey(\"clean_release_manifests.id\"), nullable=False)\n manifest_digest = Column(String, nullable=False)\n policy_snapshot_id = Column(String, ForeignKey(\"clean_release_policy_snapshots.id\"), nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n requested_by = Column(String, nullable=False)\n requested_at = Column(DateTime, default=datetime.utcnow)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n status = Column(String, default=RunStatus.PENDING)\n final_status = Column(String, nullable=True) # ComplianceDecision\n failure_reason = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n\n @property\n def check_run_id(self) -> str:\n return self.id\n# [/DEF:ComplianceRun:Class]\n" + "anchor_syntax": "region", + "body": "# #region ComplianceRun [TYPE Class]\n# @BRIEF Operational record for one compliance execution.\nclass ComplianceRun(Base):\n __tablename__ = \"clean_release_compliance_runs\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_id = Column(String, ForeignKey(\"clean_release_manifests.id\"), nullable=False)\n manifest_digest = Column(String, nullable=False)\n policy_snapshot_id = Column(String, ForeignKey(\"clean_release_policy_snapshots.id\"), nullable=False)\n registry_snapshot_id = Column(String, ForeignKey(\"clean_release_registry_snapshots.id\"), nullable=False)\n requested_by = Column(String, nullable=False)\n requested_at = Column(DateTime, default=datetime.utcnow)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n status = Column(String, default=RunStatus.PENDING)\n final_status = Column(String, nullable=True) # ComplianceDecision\n failure_reason = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n\n @property\n def check_run_id(self) -> str:\n return self.id\n# #endregion ComplianceRun\n" }, { "contract_id": "ComplianceStageRun", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 473, - "end_line": 486, + "start_line": 467, + "end_line": 480, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57324,15 +45617,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceStageRun:Class]\n# @PURPOSE: Stage-level execution record inside a run.\nclass ComplianceStageRun(Base):\n __tablename__ = \"clean_release_compliance_stage_runs\"\n \n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n status = Column(String, nullable=False)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n decision = Column(String, nullable=True) # ComplianceDecision\n details_json = Column(JSON, default=dict)\n# [/DEF:ComplianceStageRun:Class]\n" + "anchor_syntax": "region", + "body": "# #region ComplianceStageRun [TYPE Class]\n# @BRIEF Stage-level execution record inside a run.\nclass ComplianceStageRun(Base):\n __tablename__ = \"clean_release_compliance_stage_runs\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n status = Column(String, nullable=False)\n started_at = Column(DateTime, nullable=True)\n finished_at = Column(DateTime, nullable=True)\n decision = Column(String, nullable=True) # ComplianceDecision\n details_json = Column(JSON, default=dict)\n# #endregion ComplianceStageRun\n" }, { "contract_id": "ViolationSeverity", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 488, - "end_line": 494, + "start_line": 482, + "end_line": 488, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57350,15 +45643,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ViolationSeverity:Class]\n# @PURPOSE: Backward-compatible violation severity enum for legacy clean-release tests.\nclass ViolationSeverity(str, Enum):\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n# [/DEF:ViolationSeverity:Class]\n" + "anchor_syntax": "region", + "body": "# #region ViolationSeverity [TYPE Class]\n# @BRIEF Backward-compatible violation severity enum for legacy clean-release tests.\nclass ViolationSeverity(str, Enum):\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n# #endregion ViolationSeverity\n" }, { "contract_id": "ViolationCategory", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 496, - "end_line": 504, + "start_line": 490, + "end_line": 498, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57376,15 +45669,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ViolationCategory:Class]\n# @PURPOSE: Backward-compatible violation category enum for legacy clean-release tests.\nclass ViolationCategory(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n EXTERNAL_SOURCE = \"EXTERNAL_SOURCE\"\n SOURCE_ISOLATION = \"SOURCE_ISOLATION\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n EXTERNAL_ENDPOINT = \"EXTERNAL_ENDPOINT\"\n# [/DEF:ViolationCategory:Class]\n" + "anchor_syntax": "region", + "body": "# #region ViolationCategory [TYPE Class]\n# @BRIEF Backward-compatible violation category enum for legacy clean-release tests.\nclass ViolationCategory(str, Enum):\n DATA_PURITY = \"DATA_PURITY\"\n EXTERNAL_SOURCE = \"EXTERNAL_SOURCE\"\n SOURCE_ISOLATION = \"SOURCE_ISOLATION\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n EXTERNAL_ENDPOINT = \"EXTERNAL_ENDPOINT\"\n# #endregion ViolationCategory\n" }, { "contract_id": "ComplianceViolation", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 506, - "end_line": 580, + "start_line": 500, + "end_line": 574, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57402,15 +45695,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceViolation:Class]\n# @PURPOSE: Violation produced by a stage.\nclass ComplianceViolation(Base):\n __tablename__ = \"clean_release_compliance_violations\"\n \n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n code = Column(String, nullable=False)\n severity = Column(String, nullable=False)\n artifact_path = Column(String, nullable=True)\n artifact_sha256 = Column(String, nullable=True)\n message = Column(String, nullable=False)\n evidence_json = Column(JSON, default=dict)\n\n def __init__(self, **kwargs):\n if \"violation_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"violation_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n if \"category\" in kwargs:\n category = kwargs.pop(\"category\")\n kwargs[\"stage_name\"] = category.value if isinstance(category, ViolationCategory) else str(category)\n if \"location\" in kwargs:\n kwargs[\"artifact_path\"] = kwargs.pop(\"location\")\n if \"remediation\" in kwargs:\n remediation = kwargs.pop(\"remediation\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"remediation\"] = remediation\n kwargs[\"evidence_json\"] = evidence\n if \"blocked_release\" in kwargs:\n blocked_release = kwargs.pop(\"blocked_release\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"blocked_release\"] = blocked_release\n kwargs[\"evidence_json\"] = evidence\n if \"detected_at\" in kwargs:\n kwargs.pop(\"detected_at\")\n if \"code\" not in kwargs:\n kwargs[\"code\"] = \"LEGACY_VIOLATION\"\n if \"message\" not in kwargs:\n kwargs[\"message\"] = kwargs.get(\"stage_name\", \"LEGACY_VIOLATION\")\n super().__init__(**kwargs)\n\n @property\n def violation_id(self) -> str:\n return self.id\n\n @violation_id.setter\n def violation_id(self, value: str) -> None:\n self.id = value\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def category(self) -> ViolationCategory:\n return ViolationCategory(self.stage_name)\n\n @category.setter\n def category(self, value: ViolationCategory) -> None:\n self.stage_name = value.value if isinstance(value, ViolationCategory) else str(value)\n\n @property\n def location(self) -> Optional[str]:\n return self.artifact_path\n\n @property\n def remediation(self) -> Optional[str]:\n return (self.evidence_json or {}).get(\"remediation\")\n\n @property\n def blocked_release(self) -> bool:\n return bool((self.evidence_json or {}).get(\"blocked_release\", False))\n# [/DEF:ComplianceViolation:Class]\n" + "anchor_syntax": "region", + "body": "# #region ComplianceViolation [TYPE Class]\n# @BRIEF Violation produced by a stage.\nclass ComplianceViolation(Base):\n __tablename__ = \"clean_release_compliance_violations\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n stage_name = Column(String, nullable=False)\n code = Column(String, nullable=False)\n severity = Column(String, nullable=False)\n artifact_path = Column(String, nullable=True)\n artifact_sha256 = Column(String, nullable=True)\n message = Column(String, nullable=False)\n evidence_json = Column(JSON, default=dict)\n\n def __init__(self, **kwargs):\n if \"violation_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"violation_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n if \"category\" in kwargs:\n category = kwargs.pop(\"category\")\n kwargs[\"stage_name\"] = category.value if isinstance(category, ViolationCategory) else str(category)\n if \"location\" in kwargs:\n kwargs[\"artifact_path\"] = kwargs.pop(\"location\")\n if \"remediation\" in kwargs:\n remediation = kwargs.pop(\"remediation\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"remediation\"] = remediation\n kwargs[\"evidence_json\"] = evidence\n if \"blocked_release\" in kwargs:\n blocked_release = kwargs.pop(\"blocked_release\")\n evidence = dict(kwargs.get(\"evidence_json\") or {})\n evidence[\"blocked_release\"] = blocked_release\n kwargs[\"evidence_json\"] = evidence\n if \"detected_at\" in kwargs:\n kwargs.pop(\"detected_at\")\n if \"code\" not in kwargs:\n kwargs[\"code\"] = \"LEGACY_VIOLATION\"\n if \"message\" not in kwargs:\n kwargs[\"message\"] = kwargs.get(\"stage_name\", \"LEGACY_VIOLATION\")\n super().__init__(**kwargs)\n\n @property\n def violation_id(self) -> str:\n return self.id\n\n @violation_id.setter\n def violation_id(self, value: str) -> None:\n self.id = value\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def category(self) -> ViolationCategory:\n return ViolationCategory(self.stage_name)\n\n @category.setter\n def category(self, value: ViolationCategory) -> None:\n self.stage_name = value.value if isinstance(value, ViolationCategory) else str(value)\n\n @property\n def location(self) -> str | None:\n return self.artifact_path\n\n @property\n def remediation(self) -> str | None:\n return (self.evidence_json or {}).get(\"remediation\")\n\n @property\n def blocked_release(self) -> bool:\n return bool((self.evidence_json or {}).get(\"blocked_release\", False))\n# #endregion ComplianceViolation\n" }, { "contract_id": "ComplianceReport", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 582, - "end_line": 654, + "start_line": 576, + "end_line": 648, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57438,15 +45731,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceReport:Class]\n# @PURPOSE: Immutable result derived from a completed run.\n# @INVARIANT: Immutable after creation.\nclass ComplianceReport(Base):\n __tablename__ = \"clean_release_compliance_reports\"\n \n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n final_status = Column(String, nullable=False) # ComplianceDecision\n summary_json = Column(JSON, nullable=False)\n generated_at = Column(DateTime, default=datetime.utcnow)\n immutable = Column(Boolean, default=True)\n\n def __init__(self, **kwargs):\n if \"report_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"report_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n operator_summary = kwargs.pop(\"operator_summary\", None)\n structured_payload_ref = kwargs.pop(\"structured_payload_ref\", None)\n violations_count = kwargs.pop(\"violations_count\", None)\n blocking_violations_count = kwargs.pop(\"blocking_violations_count\", None)\n\n final_status = kwargs.get(\"final_status\")\n final_status_value = getattr(final_status, \"value\", final_status)\n\n if (\n final_status_value in {CheckFinalStatus.BLOCKED.value, ComplianceDecision.BLOCKED.value}\n and blocking_violations_count is not None\n and int(blocking_violations_count) <= 0\n ):\n raise ValueError(\"blocked report requires blocking violations\")\n\n if (\n operator_summary is not None\n or structured_payload_ref is not None\n or violations_count is not None\n or blocking_violations_count is not None\n ):\n kwargs[\"summary_json\"] = {\n \"operator_summary\": operator_summary or \"\",\n \"structured_payload_ref\": structured_payload_ref,\n \"violations_count\": int(violations_count or 0),\n \"blocking_violations_count\": int(blocking_violations_count or 0),\n }\n\n super().__init__(**kwargs)\n\n @property\n def report_id(self) -> str:\n return self.id\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def operator_summary(self) -> str:\n return (self.summary_json or {}).get(\"operator_summary\", \"\")\n\n @property\n def structured_payload_ref(self) -> Optional[str]:\n return (self.summary_json or {}).get(\"structured_payload_ref\")\n\n @property\n def violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"violations_count\", 0))\n\n @property\n def blocking_violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"blocking_violations_count\", 0))\n# [/DEF:ComplianceReport:Class]\n" + "anchor_syntax": "region", + "body": "# #region ComplianceReport [TYPE Class]\n# @BRIEF Immutable result derived from a completed run.\n# @INVARIANT: Immutable after creation.\nclass ComplianceReport(Base):\n __tablename__ = \"clean_release_compliance_reports\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n final_status = Column(String, nullable=False) # ComplianceDecision\n summary_json = Column(JSON, nullable=False)\n generated_at = Column(DateTime, default=datetime.utcnow)\n immutable = Column(Boolean, default=True)\n\n def __init__(self, **kwargs):\n if \"report_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"report_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n operator_summary = kwargs.pop(\"operator_summary\", None)\n structured_payload_ref = kwargs.pop(\"structured_payload_ref\", None)\n violations_count = kwargs.pop(\"violations_count\", None)\n blocking_violations_count = kwargs.pop(\"blocking_violations_count\", None)\n\n final_status = kwargs.get(\"final_status\")\n final_status_value = getattr(final_status, \"value\", final_status)\n\n if (\n final_status_value in {CheckFinalStatus.BLOCKED.value, ComplianceDecision.BLOCKED.value}\n and blocking_violations_count is not None\n and int(blocking_violations_count) <= 0\n ):\n raise ValueError(\"blocked report requires blocking violations\")\n\n if (\n operator_summary is not None\n or structured_payload_ref is not None\n or violations_count is not None\n or blocking_violations_count is not None\n ):\n kwargs[\"summary_json\"] = {\n \"operator_summary\": operator_summary or \"\",\n \"structured_payload_ref\": structured_payload_ref,\n \"violations_count\": int(violations_count or 0),\n \"blocking_violations_count\": int(blocking_violations_count or 0),\n }\n\n super().__init__(**kwargs)\n\n @property\n def report_id(self) -> str:\n return self.id\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def operator_summary(self) -> str:\n return (self.summary_json or {}).get(\"operator_summary\", \"\")\n\n @property\n def structured_payload_ref(self) -> str | None:\n return (self.summary_json or {}).get(\"structured_payload_ref\")\n\n @property\n def violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"violations_count\", 0))\n\n @property\n def blocking_violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"blocking_violations_count\", 0))\n# #endregion ComplianceReport\n" }, { "contract_id": "ApprovalDecision", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 656, - "end_line": 668, + "start_line": 650, + "end_line": 662, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57464,15 +45757,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ApprovalDecision:Class]\n# @PURPOSE: Approval or rejection bound to a candidate and report.\nclass ApprovalDecision(Base):\n __tablename__ = \"clean_release_approval_decisions\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n decision = Column(String, nullable=False) # ApprovalDecisionType\n decided_by = Column(String, nullable=False)\n decided_at = Column(DateTime, default=datetime.utcnow)\n comment = Column(String, nullable=True)\n# [/DEF:ApprovalDecision:Class]\n" + "anchor_syntax": "region", + "body": "# #region ApprovalDecision [TYPE Class]\n# @BRIEF Approval or rejection bound to a candidate and report.\nclass ApprovalDecision(Base):\n __tablename__ = \"clean_release_approval_decisions\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n decision = Column(String, nullable=False) # ApprovalDecisionType\n decided_by = Column(String, nullable=False)\n decided_at = Column(DateTime, default=datetime.utcnow)\n comment = Column(String, nullable=True)\n# #endregion ApprovalDecision\n" }, { "contract_id": "PublicationRecord", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 670, - "end_line": 683, + "start_line": 664, + "end_line": 677, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57490,15 +45783,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PublicationRecord:Class]\n# @PURPOSE: Publication or revocation record.\nclass PublicationRecord(Base):\n __tablename__ = \"clean_release_publication_records\"\n \n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n published_by = Column(String, nullable=False)\n published_at = Column(DateTime, default=datetime.utcnow)\n target_channel = Column(String, nullable=False)\n publication_ref = Column(String, nullable=True)\n status = Column(String, default=PublicationStatus.ACTIVE)\n# [/DEF:PublicationRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region PublicationRecord [TYPE Class]\n# @BRIEF Publication or revocation record.\nclass PublicationRecord(Base):\n __tablename__ = \"clean_release_publication_records\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n report_id = Column(String, ForeignKey(\"clean_release_compliance_reports.id\"), nullable=False)\n published_by = Column(String, nullable=False)\n published_at = Column(DateTime, default=datetime.utcnow)\n target_channel = Column(String, nullable=False)\n publication_ref = Column(String, nullable=True)\n status = Column(String, default=PublicationStatus.ACTIVE)\n# #endregion PublicationRecord\n" }, { "contract_id": "CleanReleaseAuditLog", "contract_type": "Class", "file_path": "backend/src/models/clean_release.py", - "start_line": 685, - "end_line": 697, + "start_line": 679, + "end_line": 693, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57516,15 +45809,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseAuditLog:Class]\n# @PURPOSE: Represents a persistent audit log entry for clean release actions.\nimport uuid\nclass CleanReleaseAuditLog(Base):\n __tablename__ = \"clean_release_audit_logs\"\n \n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n candidate_id = Column(String, index=True, nullable=True)\n action = Column(String, nullable=False) # e.g. \"TRANSITION\", \"APPROVE\", \"PUBLISH\"\n actor = Column(String, nullable=False)\n timestamp = Column(DateTime, default=datetime.utcnow)\n details_json = Column(JSON, default=dict)\n# [/DEF:CleanReleaseAuditLog:Class]\n" + "anchor_syntax": "region", + "body": "# #region CleanReleaseAuditLog [TYPE Class]\n# @BRIEF Represents a persistent audit log entry for clean release actions.\nimport uuid\n\n\nclass CleanReleaseAuditLog(Base):\n __tablename__ = \"clean_release_audit_logs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n candidate_id = Column(String, index=True, nullable=True)\n action = Column(String, nullable=False) # e.g. \"TRANSITION\", \"APPROVE\", \"PUBLISH\"\n actor = Column(String, nullable=False)\n timestamp = Column(DateTime, default=datetime.utcnow)\n details_json = Column(JSON, default=dict)\n# #endregion CleanReleaseAuditLog\n" }, { "contract_id": "AppConfigRecord", "contract_type": "Class", "file_path": "backend/src/models/config.py", - "start_line": 17, - "end_line": 31, + "start_line": 15, + "end_line": 29, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57582,15 +45875,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AppConfigRecord:Class]\n# @PURPOSE: Stores persisted application configuration as a single authoritative record model.\n# @PRE: SQLAlchemy declarative Base is initialized and table metadata registration is active.\n# @POST: ORM table 'app_configurations' exposes id, payload, and updated_at fields with declared nullability/default semantics.\n# @SIDE_EFFECT: Registers ORM mapping metadata during module import.\n# @DATA_CONTRACT: Input -> persistence row {id:str, payload:json, updated_at:datetime}; Output -> AppConfigRecord ORM entity.\nclass AppConfigRecord(Base):\n __tablename__ = \"app_configurations\"\n\n id = Column(String, primary_key=True)\n payload = Column(JSON, nullable=False)\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n\n\n# [/DEF:AppConfigRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region AppConfigRecord [TYPE Class]\n# @BRIEF Stores persisted application configuration as a single authoritative record model.\n# @PRE: SQLAlchemy declarative Base is initialized and table metadata registration is active.\n# @POST: ORM table 'app_configurations' exposes id, payload, and updated_at fields with declared nullability/default semantics.\n# @SIDE_EFFECT: Registers ORM mapping metadata during module import.\n# @DATA_CONTRACT: Input -> persistence row {id:str, payload:json, updated_at:datetime}; Output -> AppConfigRecord ORM entity.\nclass AppConfigRecord(Base):\n __tablename__ = \"app_configurations\"\n\n id = Column(String, primary_key=True)\n payload = Column(JSON, nullable=False)\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n\n\n# #endregion AppConfigRecord\n" }, { "contract_id": "NotificationConfig", "contract_type": "Class", "file_path": "backend/src/models/config.py", - "start_line": 33, - "end_line": 49, + "start_line": 31, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -57648,29 +45941,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:NotificationConfig:Class]\n# @PURPOSE: Stores persisted provider-level notification configuration and encrypted credentials metadata.\n# @PRE: SQLAlchemy declarative Base is initialized and uuid generation is available at instance creation time.\n# @POST: ORM table 'notification_configs' exposes id, type, name, credentials, is_active, created_at, updated_at fields with declared constraints/defaults.\n# @SIDE_EFFECT: Registers ORM mapping metadata during module import; may generate UUID values for new entity instances.\n# @DATA_CONTRACT: Input -> persistence row {id:str, type:str, name:str, credentials:json, is_active:bool, created_at:datetime, updated_at:datetime}; Output -> NotificationConfig ORM entity.\nclass NotificationConfig(Base):\n __tablename__ = \"notification_configs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n type = Column(String, nullable=False) # SMTP, SLACK, TELEGRAM\n name = Column(String, nullable=False)\n credentials = Column(JSON, nullable=False) # Encrypted connection details\n is_active = Column(Boolean, default=True)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# [/DEF:NotificationConfig:Class]\n" + "anchor_syntax": "region", + "body": "# #region NotificationConfig [TYPE Class]\n# @BRIEF Stores persisted provider-level notification configuration and encrypted credentials metadata.\n# @PRE: SQLAlchemy declarative Base is initialized and uuid generation is available at instance creation time.\n# @POST: ORM table 'notification_configs' exposes id, type, name, credentials, is_active, created_at, updated_at fields with declared constraints/defaults.\n# @SIDE_EFFECT: Registers ORM mapping metadata during module import; may generate UUID values for new entity instances.\n# @DATA_CONTRACT: Input -> persistence row {id:str, type:str, name:str, credentials:json, is_active:bool, created_at:datetime, updated_at:datetime}; Output -> NotificationConfig ORM entity.\nclass NotificationConfig(Base):\n __tablename__ = \"notification_configs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n type = Column(String, nullable=False) # SMTP, SLACK, TELEGRAM\n name = Column(String, nullable=False)\n credentials = Column(JSON, nullable=False) # Encrypted connection details\n is_active = Column(Boolean, default=True)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# #endregion NotificationConfig\n" }, { "contract_id": "ConnectionModels", "contract_type": "Module", "file_path": "backend/src/models/connection.py", "start_line": 1, - "end_line": 36, + "end_line": 34, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "INVARIANT": "All primary keys are UUID strings.", "LAYER": "Domain", - "PURPOSE": "Defines the database schema for external database connection configurations.", - "SEMANTICS": [ - "database", - "connection", - "configuration", - "sqlalchemy", - "sqlite" - ] + "PURPOSE": "Defines the database schema for external database connection configurations." }, "relations": [ { @@ -57709,19 +45995,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ConnectionModels:Module]\n#\n# @COMPLEXITY: 1\n# @SEMANTICS: database, connection, configuration, sqlalchemy, sqlite\n# @PURPOSE: Defines the database schema for external database connection configurations.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> sqlalchemy\n#\n# @INVARIANT: All primary keys are UUID strings.\n\n# [SECTION: IMPORTS]\nfrom sqlalchemy import Column, String, Integer, DateTime\nfrom sqlalchemy.sql import func\nfrom .mapping import Base\nimport uuid\n# [/SECTION]\n\n# [DEF:ConnectionConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Stores credentials for external databases used for column mapping.\nclass ConnectionConfig(Base):\n __tablename__ = \"connection_configs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String, nullable=False)\n type = Column(String, nullable=False) # e.g., \"postgres\"\n host = Column(String, nullable=True)\n port = Column(Integer, nullable=True)\n database = Column(String, nullable=True)\n username = Column(String, nullable=True)\n password = Column(String, nullable=True) # Encrypted/Obfuscated password\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# [/DEF:ConnectionConfig:Class]\n\n# [/DEF:ConnectionModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region ConnectionModels [C:1] [TYPE Module] [SEMANTICS sqlalchemy, connection, model, schema, connection-config]\n#\n# @BRIEF Defines the database schema for external database connection configurations.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> sqlalchemy\n#\n# @INVARIANT: All primary keys are UUID strings.\n\nimport uuid\n\nfrom sqlalchemy import Column, DateTime, Integer, String\nfrom sqlalchemy.sql import func\n\nfrom .mapping import Base\n\n\n# #region ConnectionConfig [C:1] [TYPE Class]\n# @BRIEF Stores credentials for external databases used for column mapping.\nclass ConnectionConfig(Base):\n __tablename__ = \"connection_configs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String, nullable=False)\n type = Column(String, nullable=False) # e.g., \"postgres\"\n host = Column(String, nullable=True)\n port = Column(Integer, nullable=True)\n database = Column(String, nullable=True)\n username = Column(String, nullable=True)\n password = Column(String, nullable=True) # Encrypted/Obfuscated password\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# #endregion ConnectionConfig\n\n# #endregion ConnectionModels\n" }, { "contract_id": "ConnectionConfig", "contract_type": "Class", "file_path": "backend/src/models/connection.py", - "start_line": 18, - "end_line": 34, + "start_line": 17, + "end_line": 32, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Stores credentials for external databases used for column mapping." }, "relations": [], @@ -57736,27 +46022,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ConnectionConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Stores credentials for external databases used for column mapping.\nclass ConnectionConfig(Base):\n __tablename__ = \"connection_configs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String, nullable=False)\n type = Column(String, nullable=False) # e.g., \"postgres\"\n host = Column(String, nullable=True)\n port = Column(Integer, nullable=True)\n database = Column(String, nullable=True)\n username = Column(String, nullable=True)\n password = Column(String, nullable=True) # Encrypted/Obfuscated password\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# [/DEF:ConnectionConfig:Class]\n" + "anchor_syntax": "region", + "body": "# #region ConnectionConfig [C:1] [TYPE Class]\n# @BRIEF Stores credentials for external databases used for column mapping.\nclass ConnectionConfig(Base):\n __tablename__ = \"connection_configs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String, nullable=False)\n type = Column(String, nullable=False) # e.g., \"postgres\"\n host = Column(String, nullable=True)\n port = Column(Integer, nullable=True)\n database = Column(String, nullable=True)\n username = Column(String, nullable=True)\n password = Column(String, nullable=True) # Encrypted/Obfuscated password\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# #endregion ConnectionConfig\n" }, { "contract_id": "DashboardModels", "contract_type": "Module", "file_path": "backend/src/models/dashboard.py", "start_line": 1, - "end_line": 32, + "end_line": 29, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Model", - "PURPOSE": "Defines data models for dashboard metadata and selection.", - "SEMANTICS": [ - "dashboard", - "model", - "metadata", - "migration" - ] + "PURPOSE": "Defines data models for dashboard metadata and selection." }, "relations": [ { @@ -57786,19 +46066,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: dashboard, model, metadata, migration\n# @PURPOSE: Defines data models for dashboard metadata and selection.\n# @LAYER: Model\n# @RELATION: USED_BY -> MigrationApi\n\nfrom pydantic import BaseModel\nfrom typing import List\n\n# [DEF:DashboardMetadata:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a dashboard available for migration.\nclass DashboardMetadata(BaseModel):\n id: int\n title: str\n last_modified: str\n status: str\n# [/DEF:DashboardMetadata:Class]\n\n# [DEF:DashboardSelection:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents the user's selection of dashboards to migrate.\nclass DashboardSelection(BaseModel):\n selected_ids: List[int]\n source_env_id: str\n target_env_id: str\n replace_db_config: bool = False\n fix_cross_filters: bool = True\n# [/DEF:DashboardSelection:Class]\n\n# [/DEF:DashboardModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region DashboardModels [C:3] [TYPE Module] [SEMANTICS pydantic, dashboard, model, schema, selection, dashboard-metadata]\n# @BRIEF Defines data models for dashboard metadata and selection.\n# @LAYER: Model\n# @RELATION USED_BY -> MigrationApi\n\n\nfrom pydantic import BaseModel\n\n\n# #region DashboardMetadata [C:1] [TYPE Class]\n# @BRIEF Represents a dashboard available for migration.\nclass DashboardMetadata(BaseModel):\n id: int\n title: str\n last_modified: str\n status: str\n# #endregion DashboardMetadata\n\n# #region DashboardSelection [C:1] [TYPE Class]\n# @BRIEF Represents the user's selection of dashboards to migrate.\nclass DashboardSelection(BaseModel):\n selected_ids: list[int]\n source_env_id: str\n target_env_id: str\n replace_db_config: bool = False\n fix_cross_filters: bool = True\n# #endregion DashboardSelection\n\n# #endregion DashboardModels\n" }, { "contract_id": "DashboardMetadata", "contract_type": "Class", "file_path": "backend/src/models/dashboard.py", - "start_line": 11, - "end_line": 19, + "start_line": 10, + "end_line": 17, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Represents a dashboard available for migration." }, "relations": [], @@ -57813,19 +46093,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardMetadata:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a dashboard available for migration.\nclass DashboardMetadata(BaseModel):\n id: int\n title: str\n last_modified: str\n status: str\n# [/DEF:DashboardMetadata:Class]\n" + "anchor_syntax": "region", + "body": "# #region DashboardMetadata [C:1] [TYPE Class]\n# @BRIEF Represents a dashboard available for migration.\nclass DashboardMetadata(BaseModel):\n id: int\n title: str\n last_modified: str\n status: str\n# #endregion DashboardMetadata\n" }, { "contract_id": "DashboardSelection", "contract_type": "Class", "file_path": "backend/src/models/dashboard.py", - "start_line": 21, - "end_line": 30, + "start_line": 19, + "end_line": 27, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Represents the user's selection of dashboards to migrate." }, "relations": [], @@ -57840,34 +46120,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardSelection:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents the user's selection of dashboards to migrate.\nclass DashboardSelection(BaseModel):\n selected_ids: List[int]\n source_env_id: str\n target_env_id: str\n replace_db_config: bool = False\n fix_cross_filters: bool = True\n# [/DEF:DashboardSelection:Class]\n" + "anchor_syntax": "region", + "body": "# #region DashboardSelection [C:1] [TYPE Class]\n# @BRIEF Represents the user's selection of dashboards to migrate.\nclass DashboardSelection(BaseModel):\n selected_ids: list[int]\n source_env_id: str\n target_env_id: str\n replace_db_config: bool = False\n fix_cross_filters: bool = True\n# #endregion DashboardSelection\n" }, { "contract_id": "DatasetReviewModels", "contract_type": "Module", "file_path": "backend/src/models/dataset_review.py", "start_line": 1, - "end_line": 81, + "end_line": 79, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "All public model classes and enums remain importable from `src.models.dataset_review` without changes.", "LAYER": "Domain", "PURPOSE": "Thin facade re-exporting all dataset review domain models from the decomposed sub-package.", "RATIONALE": "Original 984-line monolith violated INV_7 (400-line module limit). Decomposed into domain-focused sub-modules while preserving backward-compatible import paths.", - "REJECTED": "Keeping all models in a single file because it exceeded the fractal limit by 2.5x and accumulated structural erosion risk.", - "SEMANTICS": [ - "dataset_review", - "session", - "profile", - "findings", - "semantics", - "clarification", - "execution", - "sqlalchemy" - ] + "REJECTED": "Keeping all models in a single file because it exceeded the fractal limit by 2.5x and accumulated structural erosion risk." }, "relations": [ { @@ -58125,19 +46395,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewModels:Module]\n# @COMPLEXITY: 2\n# @SEMANTICS: dataset_review, session, profile, findings, semantics, clarification, execution, sqlalchemy\n# @PURPOSE: Thin facade re-exporting all dataset review domain models from the decomposed sub-package.\n# @LAYER: Domain\n# @RELATION: EXPORTS -> [DatasetReviewEnums:Module]\n# @RELATION: EXPORTS -> [DatasetReviewSessionModels:Module]\n# @RELATION: EXPORTS -> [DatasetReviewProfileModels:Module]\n# @RELATION: EXPORTS -> [DatasetReviewFindingModels:Module]\n# @RELATION: EXPORTS -> [DatasetReviewSemanticModels:Module]\n# @RELATION: EXPORTS -> [DatasetReviewFilterModels:Module]\n# @RELATION: EXPORTS -> [DatasetReviewMappingModels:Module]\n# @RELATION: EXPORTS -> [DatasetReviewClarificationModels:Module]\n# @RELATION: EXPORTS -> [DatasetReviewExecutionModels:Module]\n# @INVARIANT: All public model classes and enums remain importable from `src.models.dataset_review` without changes.\n# @RATIONALE: Original 984-line monolith violated INV_7 (400-line module limit). Decomposed into domain-focused sub-modules while preserving backward-compatible import paths.\n# @REJECTED: Keeping all models in a single file because it exceeded the fractal limit by 2.5x and accumulated structural erosion risk.\n\nfrom src.models.dataset_review_pkg._enums import ( # noqa: F401\n SessionStatus,\n SessionPhase,\n ReadinessState,\n RecommendedAction,\n SessionCollaboratorRole,\n BusinessSummarySource,\n ConfidenceState,\n FindingArea,\n FindingSeverity,\n ResolutionState,\n SemanticSourceType,\n TrustLevel,\n SemanticSourceStatus,\n FieldKind,\n FieldProvenance,\n CandidateMatchType,\n CandidateStatus,\n FilterSource,\n FilterConfidenceState,\n FilterRecoveryStatus,\n VariableKind,\n MappingStatus,\n MappingMethod,\n MappingWarningLevel,\n ApprovalState,\n ClarificationStatus,\n QuestionState,\n AnswerKind,\n PreviewStatus,\n LaunchStatus,\n ArtifactType,\n ArtifactFormat,\n)\nfrom src.models.dataset_review_pkg._session_models import ( # noqa: F401\n SessionCollaborator,\n DatasetReviewSession,\n)\nfrom src.models.dataset_review_pkg._profile_models import DatasetProfile # noqa: F401\nfrom src.models.dataset_review_pkg._finding_models import ValidationFinding # noqa: F401\nfrom src.models.dataset_review_pkg._semantic_models import ( # noqa: F401\n SemanticSource,\n SemanticFieldEntry,\n SemanticCandidate,\n)\nfrom src.models.dataset_review_pkg._filter_models import ( # noqa: F401\n ImportedFilter,\n TemplateVariable,\n)\nfrom src.models.dataset_review_pkg._mapping_models import ExecutionMapping # noqa: F401\nfrom src.models.dataset_review_pkg._clarification_models import ( # noqa: F401\n ClarificationSession,\n ClarificationQuestion,\n ClarificationOption,\n ClarificationAnswer,\n)\nfrom src.models.dataset_review_pkg._execution_models import ( # noqa: F401\n CompiledPreview,\n DatasetRunContext,\n SessionEvent,\n ExportArtifact,\n)\n# [/DEF:DatasetReviewModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, model, schema, facade]\n# @BRIEF Thin facade re-exporting all dataset review domain models from the decomposed sub-package.\n# @LAYER: Domain\n# @RELATION EXPORTS -> [DatasetReviewEnums:Module]\n# @RELATION EXPORTS -> [DatasetReviewSessionModels:Module]\n# @RELATION EXPORTS -> [DatasetReviewProfileModels:Module]\n# @RELATION EXPORTS -> [DatasetReviewFindingModels:Module]\n# @RELATION EXPORTS -> [DatasetReviewSemanticModels:Module]\n# @RELATION EXPORTS -> [DatasetReviewFilterModels:Module]\n# @RELATION EXPORTS -> [DatasetReviewMappingModels:Module]\n# @RELATION EXPORTS -> [DatasetReviewClarificationModels:Module]\n# @RELATION EXPORTS -> [DatasetReviewExecutionModels:Module]\n# @INVARIANT: All public model classes and enums remain importable from `src.models.dataset_review` without changes.\n# @RATIONALE: Original 984-line monolith violated INV_7 (400-line module limit). Decomposed into domain-focused sub-modules while preserving backward-compatible import paths.\n# @REJECTED: Keeping all models in a single file because it exceeded the fractal limit by 2.5x and accumulated structural erosion risk.\n\nfrom src.models.dataset_review_pkg._clarification_models import ( # noqa: F401\n ClarificationAnswer,\n ClarificationOption,\n ClarificationQuestion,\n ClarificationSession,\n)\nfrom src.models.dataset_review_pkg._enums import ( # noqa: F401\n AnswerKind,\n ApprovalState,\n ArtifactFormat,\n ArtifactType,\n BusinessSummarySource,\n CandidateMatchType,\n CandidateStatus,\n ClarificationStatus,\n ConfidenceState,\n FieldKind,\n FieldProvenance,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n LaunchStatus,\n MappingMethod,\n MappingStatus,\n MappingWarningLevel,\n PreviewStatus,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n SemanticSourceStatus,\n SemanticSourceType,\n SessionCollaboratorRole,\n SessionPhase,\n SessionStatus,\n TrustLevel,\n VariableKind,\n)\nfrom src.models.dataset_review_pkg._execution_models import ( # noqa: F401\n CompiledPreview,\n DatasetRunContext,\n ExportArtifact,\n SessionEvent,\n)\nfrom src.models.dataset_review_pkg._filter_models import ( # noqa: F401\n ImportedFilter,\n TemplateVariable,\n)\nfrom src.models.dataset_review_pkg._finding_models import ValidationFinding # noqa: F401\nfrom src.models.dataset_review_pkg._mapping_models import ExecutionMapping # noqa: F401\nfrom src.models.dataset_review_pkg._profile_models import DatasetProfile # noqa: F401\nfrom src.models.dataset_review_pkg._semantic_models import ( # noqa: F401\n SemanticCandidate,\n SemanticFieldEntry,\n SemanticSource,\n)\nfrom src.models.dataset_review_pkg._session_models import ( # noqa: F401\n DatasetReviewSession,\n SessionCollaborator,\n)\n# #endregion DatasetReviewModels\n" }, { "contract_id": "DatasetReviewClarificationModels", "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", "start_line": 1, - "end_line": 125, + "end_line": 122, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Only one active clarification question may exist at a time per session.", "LAYER": "Domain", "PURPOSE": "Clarification session, question, option, and answer models for guided review flow." @@ -58167,19 +46437,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewClarificationModels:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 String,\n Integer,\n Text,\n DateTime,\n ForeignKey,\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.mapping import Base\nfrom src.models.dataset_review_pkg._enums import (\n ClarificationStatus,\n QuestionState,\n AnswerKind,\n)\n\n\n# [DEF:ClarificationSession:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:ClarificationSession:Class]\n\n\n# [DEF:ClarificationQuestion:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:ClarificationQuestion:Class]\n\n\n# [DEF:ClarificationOption:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:ClarificationOption:Class]\n\n\n# [DEF:ClarificationAnswer:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:ClarificationAnswer:Class]\n\n\n# [/DEF:DatasetReviewClarificationModels:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ClarificationSession", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", - "start_line": 32, + "start_line": 33, "end_line": 54, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "One clarification session aggregate owning questions and tracking resolution progress." }, "relations": [ @@ -58201,19 +46471,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationSession:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:ClarificationSession:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationSession [C:2] [TYPE Class]\n# @BRIEF One clarification session aggregate owning questions and tracking resolution progress.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ClarificationSession(Base):\n __tablename__ = \"clarification_sessions\"\n\n clarification_session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n status = Column(SQLEnum(ClarificationStatus), nullable=False, default=ClarificationStatus.PENDING)\n current_question_id = Column(String, nullable=True)\n resolved_count = Column(Integer, nullable=False, default=0)\n remaining_count = Column(Integer, nullable=False, default=0)\n summary_delta = Column(Text, nullable=True)\n started_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n completed_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"clarification_sessions\")\n questions = relationship(\"ClarificationQuestion\", back_populates=\"clarification_session\", cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationSession\n" }, { "contract_id": "ClarificationQuestion", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", "start_line": 57, - "end_line": 80, + "end_line": 79, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "One clarification question with priority ordering, options, and state machine." }, "relations": [ @@ -58235,19 +46505,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationQuestion:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:ClarificationQuestion:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationQuestion [C:2] [TYPE Class]\n# @BRIEF One clarification question with priority ordering, options, and state machine.\n# @RELATION DEPENDS_ON -> [ClarificationSession]\nclass ClarificationQuestion(Base):\n __tablename__ = \"clarification_questions\"\n\n question_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n clarification_session_id = Column(String, ForeignKey(\"clarification_sessions.clarification_session_id\"), nullable=False)\n topic_ref = Column(String, nullable=False)\n question_text = Column(Text, nullable=False)\n why_it_matters = Column(Text, nullable=False)\n current_guess = Column(Text, nullable=True)\n priority = Column(Integer, nullable=False, default=0)\n state = Column(SQLEnum(QuestionState), nullable=False, default=QuestionState.OPEN)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n clarification_session = relationship(\"ClarificationSession\", back_populates=\"questions\")\n options = relationship(\"ClarificationOption\", back_populates=\"question\", cascade=\"all, delete-orphan\")\n answer = relationship(\"ClarificationAnswer\", back_populates=\"question\", uselist=False, cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationQuestion\n" }, { "contract_id": "ClarificationOption", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", - "start_line": 83, - "end_line": 100, + "start_line": 82, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "One selectable option for a clarification question with recommendation flag." }, "relations": [ @@ -58278,19 +46548,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationOption:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: 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# [/DEF:ClarificationOption:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ClarificationAnswer", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_clarification_models.py", - "start_line": 103, - "end_line": 122, + "start_line": 101, + "end_line": 119, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "One persisted clarification answer with impact summary and feedback tracking." }, "relations": [ @@ -58312,19 +46582,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationAnswer:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:ClarificationAnswer:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationAnswer [C:2] [TYPE Class]\n# @BRIEF One persisted clarification answer with impact summary and feedback tracking.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationAnswer(Base):\n __tablename__ = \"clarification_answers\"\n\n answer_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False, unique=True)\n answer_kind = Column(SQLEnum(AnswerKind), nullable=False)\n answer_value = Column(Text, nullable=True)\n answered_by_user_id = Column(String, nullable=False)\n impact_summary = Column(Text, nullable=True)\n user_feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"answer\")\n\n\n# #endregion ClarificationAnswer\n" }, { "contract_id": "DatasetReviewEnums", "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", "start_line": 1, - "end_line": 463, + "end_line": 430, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "Enum values are string-based for JSON serialization compatibility.", "LAYER": "Domain", "PURPOSE": "All enumeration types for the dataset review domain, grouped for stable cross-module reuse." @@ -58341,19 +46611,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewEnums:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: All enumeration types for the dataset review domain, grouped for stable cross-module reuse.\n# @LAYER: Domain\n# @INVARIANT: Enum values are string-based for JSON serialization compatibility.\n\nimport enum\n\n\n# [DEF:SessionStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status of a dataset review session.\nclass SessionStatus(str, enum.Enum):\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n ARCHIVED = \"archived\"\n CANCELLED = \"cancelled\"\n\n\n# [/DEF:SessionStatus:Class]\n\n\n# [DEF:SessionPhase:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Ordered phase progression for dataset review orchestration.\nclass SessionPhase(str, enum.Enum):\n INTAKE = \"intake\"\n RECOVERY = \"recovery\"\n REVIEW = \"review\"\n SEMANTIC_REVIEW = \"semantic_review\"\n CLARIFICATION = \"clarification\"\n MAPPING_REVIEW = \"mapping_review\"\n PREVIEW = \"preview\"\n LAUNCH = \"launch\"\n POST_RUN = \"post_run\"\n\n\n# [/DEF:SessionPhase:Class]\n\n\n# [DEF:ReadinessState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Granular readiness indicator driving the recommended-action UX flow.\nclass ReadinessState(str, enum.Enum):\n EMPTY = \"empty\"\n IMPORTING = \"importing\"\n REVIEW_READY = \"review_ready\"\n SEMANTIC_SOURCE_REVIEW_NEEDED = \"semantic_source_review_needed\"\n CLARIFICATION_NEEDED = \"clarification_needed\"\n CLARIFICATION_ACTIVE = \"clarification_active\"\n MAPPING_REVIEW_NEEDED = \"mapping_review_needed\"\n COMPILED_PREVIEW_READY = \"compiled_preview_ready\"\n PARTIALLY_READY = \"partially_ready\"\n RUN_READY = \"run_ready\"\n RUN_IN_PROGRESS = \"run_in_progress\"\n COMPLETED = \"completed\"\n RECOVERY_REQUIRED = \"recovery_required\"\n\n\n# [/DEF:ReadinessState:Class]\n\n\n# [DEF:RecommendedAction:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Next-action guidance derived from the current readiness state.\nclass RecommendedAction(str, enum.Enum):\n IMPORT_FROM_SUPERSET = \"import_from_superset\"\n REVIEW_DOCUMENTATION = \"review_documentation\"\n APPLY_SEMANTIC_SOURCE = \"apply_semantic_source\"\n START_CLARIFICATION = \"start_clarification\"\n ANSWER_NEXT_QUESTION = \"answer_next_question\"\n APPROVE_MAPPING = \"approve_mapping\"\n GENERATE_SQL_PREVIEW = \"generate_sql_preview\"\n COMPLETE_REQUIRED_VALUES = \"complete_required_values\"\n LAUNCH_DATASET = \"launch_dataset\"\n RESUME_SESSION = \"resume_session\"\n EXPORT_OUTPUTS = \"export_outputs\"\n\n\n# [/DEF:RecommendedAction:Class]\n\n\n# [DEF:SessionCollaboratorRole:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: RBAC role for session collaborators.\nclass SessionCollaboratorRole(str, enum.Enum):\n VIEWER = \"viewer\"\n REVIEWER = \"reviewer\"\n APPROVER = \"approver\"\n\n\n# [/DEF:SessionCollaboratorRole:Class]\n\n\n# [DEF:BusinessSummarySource:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Provenance of the dataset business summary text.\nclass BusinessSummarySource(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# [/DEF:BusinessSummarySource:Class]\n\n\n# [DEF:ConfidenceState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Confidence level for dataset profile completeness.\nclass ConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n MOSTLY_CONFIRMED = \"mostly_confirmed\"\n MIXED = \"mixed\"\n LOW_CONFIDENCE = \"low_confidence\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:ConfidenceState:Class]\n\n\n# [DEF:FindingArea:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Domain area classification for validation findings.\nclass FindingArea(str, enum.Enum):\n SOURCE_INTAKE = \"source_intake\"\n DATASET_PROFILE = \"dataset_profile\"\n SEMANTIC_ENRICHMENT = \"semantic_enrichment\"\n CLARIFICATION = \"clarification\"\n FILTER_RECOVERY = \"filter_recovery\"\n TEMPLATE_MAPPING = \"template_mapping\"\n COMPILED_PREVIEW = \"compiled_preview\"\n LAUNCH = \"launch\"\n AUDIT = \"audit\"\n\n\n# [/DEF:FindingArea:Class]\n\n\n# [DEF:FindingSeverity:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Severity classification for validation findings.\nclass FindingSeverity(str, enum.Enum):\n BLOCKING = \"blocking\"\n WARNING = \"warning\"\n INFORMATIONAL = \"informational\"\n\n\n# [/DEF:FindingSeverity:Class]\n\n\n# [DEF:ResolutionState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolution status for validation findings and clarification items.\nclass ResolutionState(str, enum.Enum):\n OPEN = \"open\"\n RESOLVED = \"resolved\"\n APPROVED = \"approved\"\n SKIPPED = \"skipped\"\n DEFERRED = \"deferred\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# [/DEF:ResolutionState:Class]\n\n\n# [DEF:SemanticSourceType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Classification of semantic enrichment source origins.\nclass SemanticSourceType(str, enum.Enum):\n UPLOADED_FILE = \"uploaded_file\"\n CONNECTED_DICTIONARY = \"connected_dictionary\"\n REFERENCE_DATASET = \"reference_dataset\"\n NEIGHBOR_DATASET = \"neighbor_dataset\"\n AI_GENERATED = \"ai_generated\"\n\n\n# [/DEF:SemanticSourceType:Class]\n\n\n# [DEF:TrustLevel:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Trust classification for semantic source reliability.\nclass TrustLevel(str, enum.Enum):\n TRUSTED = \"trusted\"\n RECOMMENDED = \"recommended\"\n CANDIDATE = \"candidate\"\n GENERATED = \"generated\"\n\n\n# [/DEF:TrustLevel:Class]\n\n\n# [DEF:SemanticSourceStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for semantic source application.\nclass SemanticSourceStatus(str, enum.Enum):\n AVAILABLE = \"available\"\n SELECTED = \"selected\"\n APPLIED = \"applied\"\n REJECTED = \"rejected\"\n PARTIAL = \"partial\"\n FAILED = \"failed\"\n\n\n# [/DEF:SemanticSourceStatus:Class]\n\n\n# [DEF:FieldKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Kind classification for semantic field entries.\nclass FieldKind(str, enum.Enum):\n COLUMN = \"column\"\n METRIC = \"metric\"\n FILTER_DIMENSION = \"filter_dimension\"\n PARAMETER = \"parameter\"\n\n\n# [/DEF:FieldKind:Class]\n\n\n# [DEF:FieldProvenance:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Provenance tracking for semantic field value origin.\nclass FieldProvenance(str, enum.Enum):\n DICTIONARY_EXACT = \"dictionary_exact\"\n REFERENCE_IMPORTED = \"reference_imported\"\n FUZZY_INFERRED = \"fuzzy_inferred\"\n AI_GENERATED = \"ai_generated\"\n MANUAL_OVERRIDE = \"manual_override\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:FieldProvenance:Class]\n\n\n# [DEF:CandidateMatchType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Match type classification for semantic candidates.\nclass CandidateMatchType(str, enum.Enum):\n EXACT = \"exact\"\n REFERENCE = \"reference\"\n FUZZY = \"fuzzy\"\n GENERATED = \"generated\"\n\n\n# [/DEF:CandidateMatchType:Class]\n\n\n# [DEF:CandidateStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for semantic candidate proposals.\nclass CandidateStatus(str, enum.Enum):\n PROPOSED = \"proposed\"\n ACCEPTED = \"accepted\"\n REJECTED = \"rejected\"\n SUPERSEDED = \"superseded\"\n\n\n# [/DEF:CandidateStatus:Class]\n\n\n# [DEF:FilterSource:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Origin classification for imported filters.\nclass FilterSource(str, enum.Enum):\n SUPERSET_NATIVE = \"superset_native\"\n SUPERSET_URL = \"superset_url\"\n SUPERSET_PERMALINK = \"superset_permalink\"\n SUPERSET_NATIVE_FILTERS_KEY = \"superset_native_filters_key\"\n MANUAL = \"manual\"\n INFERRED = \"inferred\"\n\n\n# [/DEF:FilterSource:Class]\n\n\n# [DEF:FilterConfidenceState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Confidence classification for imported filter values.\nclass FilterConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:FilterConfidenceState:Class]\n\n\n# [DEF:FilterRecoveryStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Recovery quality status for imported filters.\nclass FilterRecoveryStatus(str, enum.Enum):\n RECOVERED = \"recovered\"\n PARTIAL = \"partial\"\n MISSING = \"missing\"\n CONFLICTED = \"conflicted\"\n\n\n# [/DEF:FilterRecoveryStatus:Class]\n\n\n# [DEF:VariableKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Kind classification for template variables.\nclass VariableKind(str, enum.Enum):\n NATIVE_FILTER = \"native_filter\"\n PARAMETER = \"parameter\"\n DERIVED = \"derived\"\n UNKNOWN = \"unknown\"\n\n\n# [/DEF:VariableKind:Class]\n\n\n# [DEF:MappingStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for template variable mapping.\nclass MappingStatus(str, enum.Enum):\n UNMAPPED = \"unmapped\"\n PROPOSED = \"proposed\"\n APPROVED = \"approved\"\n OVERRIDDEN = \"overridden\"\n INVALID = \"invalid\"\n\n\n# [/DEF:MappingStatus:Class]\n\n\n# [DEF:MappingMethod:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Method classification for execution mapping creation.\nclass MappingMethod(str, enum.Enum):\n DIRECT_MATCH = \"direct_match\"\n HEURISTIC_MATCH = \"heuristic_match\"\n SEMANTIC_MATCH = \"semantic_match\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# [/DEF:MappingMethod:Class]\n\n\n# [DEF:MappingWarningLevel:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Warning severity for execution mapping quality indicators.\nclass MappingWarningLevel(str, enum.Enum):\n LOW = \"low\"\n MEDIUM = \"medium\"\n HIGH = \"high\"\n\n\n# [/DEF:MappingWarningLevel:Class]\n\n\n# [DEF:ApprovalState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Approval lifecycle for execution mapping gate checks.\nclass ApprovalState(str, enum.Enum):\n PENDING = \"pending\"\n APPROVED = \"approved\"\n REJECTED = \"rejected\"\n NOT_REQUIRED = \"not_required\"\n\n\n# [/DEF:ApprovalState:Class]\n\n\n# [DEF:ClarificationStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for clarification sessions.\nclass ClarificationStatus(str, enum.Enum):\n PENDING = \"pending\"\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n CANCELLED = \"cancelled\"\n\n\n# [/DEF:ClarificationStatus:Class]\n\n\n# [DEF:QuestionState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: State machine for individual clarification questions.\nclass QuestionState(str, enum.Enum):\n OPEN = \"open\"\n ANSWERED = \"answered\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n SUPERSEDED = \"superseded\"\n\n\n# [/DEF:QuestionState:Class]\n\n\n# [DEF:AnswerKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Classification of clarification answer types.\nclass AnswerKind(str, enum.Enum):\n SELECTED = \"selected\"\n CUSTOM = \"custom\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# [/DEF:AnswerKind:Class]\n\n\n# [DEF:PreviewStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for compiled SQL previews.\nclass PreviewStatus(str, enum.Enum):\n PENDING = \"pending\"\n READY = \"ready\"\n FAILED = \"failed\"\n STALE = \"stale\"\n\n\n# [/DEF:PreviewStatus:Class]\n\n\n# [DEF:LaunchStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Outcome status for dataset launch handoff.\nclass LaunchStatus(str, enum.Enum):\n STARTED = \"started\"\n SUCCESS = \"success\"\n FAILED = \"failed\"\n\n\n# [/DEF:LaunchStatus:Class]\n\n\n# [DEF:ArtifactType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Type classification for export artifacts.\nclass ArtifactType(str, enum.Enum):\n DOCUMENTATION = \"documentation\"\n VALIDATION_REPORT = \"validation_report\"\n RUN_SUMMARY = \"run_summary\"\n\n\n# [/DEF:ArtifactType:Class]\n\n\n# [DEF:ArtifactFormat:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Format classification for export artifact output.\nclass ArtifactFormat(str, enum.Enum):\n JSON = \"json\"\n MARKDOWN = \"markdown\"\n CSV = \"csv\"\n PDF = \"pdf\"\n\n\n# [/DEF:ArtifactFormat:Class]\n\n\n# [/DEF:DatasetReviewEnums:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewEnums [C:2] [TYPE Module] [SEMANTICS enum, dataset, review, model, domain]\n# @BRIEF All enumeration types for the dataset review domain, grouped for stable cross-module reuse.\n# @LAYER: Domain\n# @INVARIANT: Enum values are string-based for JSON serialization compatibility.\n\nimport enum\n\n\n# #region SessionStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status of a dataset review session.\nclass SessionStatus(str, enum.Enum):\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n ARCHIVED = \"archived\"\n CANCELLED = \"cancelled\"\n\n\n# #endregion SessionStatus\n\n\n# #region SessionPhase [C:1] [TYPE Class]\n# @BRIEF Ordered phase progression for dataset review orchestration.\nclass SessionPhase(str, enum.Enum):\n INTAKE = \"intake\"\n RECOVERY = \"recovery\"\n REVIEW = \"review\"\n SEMANTIC_REVIEW = \"semantic_review\"\n CLARIFICATION = \"clarification\"\n MAPPING_REVIEW = \"mapping_review\"\n PREVIEW = \"preview\"\n LAUNCH = \"launch\"\n POST_RUN = \"post_run\"\n\n\n# #endregion SessionPhase\n\n\n# #region ReadinessState [C:1] [TYPE Class]\n# @BRIEF Granular readiness indicator driving the recommended-action UX flow.\nclass ReadinessState(str, enum.Enum):\n EMPTY = \"empty\"\n IMPORTING = \"importing\"\n REVIEW_READY = \"review_ready\"\n SEMANTIC_SOURCE_REVIEW_NEEDED = \"semantic_source_review_needed\"\n CLARIFICATION_NEEDED = \"clarification_needed\"\n CLARIFICATION_ACTIVE = \"clarification_active\"\n MAPPING_REVIEW_NEEDED = \"mapping_review_needed\"\n COMPILED_PREVIEW_READY = \"compiled_preview_ready\"\n PARTIALLY_READY = \"partially_ready\"\n RUN_READY = \"run_ready\"\n RUN_IN_PROGRESS = \"run_in_progress\"\n COMPLETED = \"completed\"\n RECOVERY_REQUIRED = \"recovery_required\"\n\n\n# #endregion ReadinessState\n\n\n# #region RecommendedAction [C:1] [TYPE Class]\n# @BRIEF Next-action guidance derived from the current readiness state.\nclass RecommendedAction(str, enum.Enum):\n IMPORT_FROM_SUPERSET = \"import_from_superset\"\n REVIEW_DOCUMENTATION = \"review_documentation\"\n APPLY_SEMANTIC_SOURCE = \"apply_semantic_source\"\n START_CLARIFICATION = \"start_clarification\"\n ANSWER_NEXT_QUESTION = \"answer_next_question\"\n APPROVE_MAPPING = \"approve_mapping\"\n GENERATE_SQL_PREVIEW = \"generate_sql_preview\"\n COMPLETE_REQUIRED_VALUES = \"complete_required_values\"\n LAUNCH_DATASET = \"launch_dataset\"\n RESUME_SESSION = \"resume_session\"\n EXPORT_OUTPUTS = \"export_outputs\"\n\n\n# #endregion RecommendedAction\n\n\n# #region SessionCollaboratorRole [C:1] [TYPE Class]\n# @BRIEF RBAC role for session collaborators.\nclass SessionCollaboratorRole(str, enum.Enum):\n VIEWER = \"viewer\"\n REVIEWER = \"reviewer\"\n APPROVER = \"approver\"\n\n\n# #endregion SessionCollaboratorRole\n\n\n# #region BusinessSummarySource [C:1] [TYPE Class]\n# @BRIEF Provenance of the dataset business summary text.\nclass BusinessSummarySource(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# #endregion BusinessSummarySource\n\n\n# #region ConfidenceState [C:1] [TYPE Class]\n# @BRIEF Confidence level for dataset profile completeness.\nclass ConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n MOSTLY_CONFIRMED = \"mostly_confirmed\"\n MIXED = \"mixed\"\n LOW_CONFIDENCE = \"low_confidence\"\n UNRESOLVED = \"unresolved\"\n\n\n# #endregion ConfidenceState\n\n\n# #region FindingArea [C:1] [TYPE Class]\n# @BRIEF Domain area classification for validation findings.\nclass FindingArea(str, enum.Enum):\n SOURCE_INTAKE = \"source_intake\"\n DATASET_PROFILE = \"dataset_profile\"\n SEMANTIC_ENRICHMENT = \"semantic_enrichment\"\n CLARIFICATION = \"clarification\"\n FILTER_RECOVERY = \"filter_recovery\"\n TEMPLATE_MAPPING = \"template_mapping\"\n COMPILED_PREVIEW = \"compiled_preview\"\n LAUNCH = \"launch\"\n AUDIT = \"audit\"\n\n\n# #endregion FindingArea\n\n\n# #region FindingSeverity [C:1] [TYPE Class]\n# @BRIEF Severity classification for validation findings.\nclass FindingSeverity(str, enum.Enum):\n BLOCKING = \"blocking\"\n WARNING = \"warning\"\n INFORMATIONAL = \"informational\"\n\n\n# #endregion FindingSeverity\n\n\n# #region ResolutionState [C:1] [TYPE Class]\n# @BRIEF Resolution status for validation findings and clarification items.\nclass ResolutionState(str, enum.Enum):\n OPEN = \"open\"\n RESOLVED = \"resolved\"\n APPROVED = \"approved\"\n SKIPPED = \"skipped\"\n DEFERRED = \"deferred\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# #endregion ResolutionState\n\n\n# #region SemanticSourceType [C:1] [TYPE Class]\n# @BRIEF Classification of semantic enrichment source origins.\nclass SemanticSourceType(str, enum.Enum):\n UPLOADED_FILE = \"uploaded_file\"\n CONNECTED_DICTIONARY = \"connected_dictionary\"\n REFERENCE_DATASET = \"reference_dataset\"\n NEIGHBOR_DATASET = \"neighbor_dataset\"\n AI_GENERATED = \"ai_generated\"\n\n\n# #endregion SemanticSourceType\n\n\n# #region TrustLevel [C:1] [TYPE Class]\n# @BRIEF Trust classification for semantic source reliability.\nclass TrustLevel(str, enum.Enum):\n TRUSTED = \"trusted\"\n RECOMMENDED = \"recommended\"\n CANDIDATE = \"candidate\"\n GENERATED = \"generated\"\n\n\n# #endregion TrustLevel\n\n\n# #region SemanticSourceStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for semantic source application.\nclass SemanticSourceStatus(str, enum.Enum):\n AVAILABLE = \"available\"\n SELECTED = \"selected\"\n APPLIED = \"applied\"\n REJECTED = \"rejected\"\n PARTIAL = \"partial\"\n FAILED = \"failed\"\n\n\n# #endregion SemanticSourceStatus\n\n\n# #region FieldKind [C:1] [TYPE Class]\n# @BRIEF Kind classification for semantic field entries.\nclass FieldKind(str, enum.Enum):\n COLUMN = \"column\"\n METRIC = \"metric\"\n FILTER_DIMENSION = \"filter_dimension\"\n PARAMETER = \"parameter\"\n\n\n# #endregion FieldKind\n\n\n# #region FieldProvenance [C:1] [TYPE Class]\n# @BRIEF Provenance tracking for semantic field value origin.\nclass FieldProvenance(str, enum.Enum):\n DICTIONARY_EXACT = \"dictionary_exact\"\n REFERENCE_IMPORTED = \"reference_imported\"\n FUZZY_INFERRED = \"fuzzy_inferred\"\n AI_GENERATED = \"ai_generated\"\n MANUAL_OVERRIDE = \"manual_override\"\n UNRESOLVED = \"unresolved\"\n\n\n# #endregion FieldProvenance\n\n\n# #region CandidateMatchType [C:1] [TYPE Class]\n# @BRIEF Match type classification for semantic candidates.\nclass CandidateMatchType(str, enum.Enum):\n EXACT = \"exact\"\n REFERENCE = \"reference\"\n FUZZY = \"fuzzy\"\n GENERATED = \"generated\"\n\n\n# #endregion CandidateMatchType\n\n\n# #region CandidateStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for semantic candidate proposals.\nclass CandidateStatus(str, enum.Enum):\n PROPOSED = \"proposed\"\n ACCEPTED = \"accepted\"\n REJECTED = \"rejected\"\n SUPERSEDED = \"superseded\"\n\n\n# #endregion CandidateStatus\n\n\n# #region FilterSource [C:1] [TYPE Class]\n# @BRIEF Origin classification for imported filters.\nclass FilterSource(str, enum.Enum):\n SUPERSET_NATIVE = \"superset_native\"\n SUPERSET_URL = \"superset_url\"\n SUPERSET_PERMALINK = \"superset_permalink\"\n SUPERSET_NATIVE_FILTERS_KEY = \"superset_native_filters_key\"\n MANUAL = \"manual\"\n INFERRED = \"inferred\"\n\n\n# #endregion FilterSource\n\n\n# #region FilterConfidenceState [C:1] [TYPE Class]\n# @BRIEF Confidence classification for imported filter values.\nclass FilterConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n UNRESOLVED = \"unresolved\"\n\n\n# #endregion FilterConfidenceState\n\n\n# #region FilterRecoveryStatus [C:1] [TYPE Class]\n# @BRIEF Recovery quality status for imported filters.\nclass FilterRecoveryStatus(str, enum.Enum):\n RECOVERED = \"recovered\"\n PARTIAL = \"partial\"\n MISSING = \"missing\"\n CONFLICTED = \"conflicted\"\n\n\n# #endregion FilterRecoveryStatus\n\n\n# #region VariableKind [C:1] [TYPE Class]\n# @BRIEF Kind classification for template variables.\nclass VariableKind(str, enum.Enum):\n NATIVE_FILTER = \"native_filter\"\n PARAMETER = \"parameter\"\n DERIVED = \"derived\"\n UNKNOWN = \"unknown\"\n\n\n# #endregion VariableKind\n\n\n# #region MappingStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for template variable mapping.\nclass MappingStatus(str, enum.Enum):\n UNMAPPED = \"unmapped\"\n PROPOSED = \"proposed\"\n APPROVED = \"approved\"\n OVERRIDDEN = \"overridden\"\n INVALID = \"invalid\"\n\n\n# #endregion MappingStatus\n\n\n# #region MappingMethod [C:1] [TYPE Class]\n# @BRIEF Method classification for execution mapping creation.\nclass MappingMethod(str, enum.Enum):\n DIRECT_MATCH = \"direct_match\"\n HEURISTIC_MATCH = \"heuristic_match\"\n SEMANTIC_MATCH = \"semantic_match\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# #endregion MappingMethod\n\n\n# #region MappingWarningLevel [C:1] [TYPE Class]\n# @BRIEF Warning severity for execution mapping quality indicators.\nclass MappingWarningLevel(str, enum.Enum):\n LOW = \"low\"\n MEDIUM = \"medium\"\n HIGH = \"high\"\n\n\n# #endregion MappingWarningLevel\n\n\n# #region ApprovalState [C:1] [TYPE Class]\n# @BRIEF Approval lifecycle for execution mapping gate checks.\nclass ApprovalState(str, enum.Enum):\n PENDING = \"pending\"\n APPROVED = \"approved\"\n REJECTED = \"rejected\"\n NOT_REQUIRED = \"not_required\"\n\n\n# #endregion ApprovalState\n\n\n# #region ClarificationStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for clarification sessions.\nclass ClarificationStatus(str, enum.Enum):\n PENDING = \"pending\"\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n CANCELLED = \"cancelled\"\n\n\n# #endregion ClarificationStatus\n\n\n# #region QuestionState [C:1] [TYPE Class]\n# @BRIEF State machine for individual clarification questions.\nclass QuestionState(str, enum.Enum):\n OPEN = \"open\"\n ANSWERED = \"answered\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n SUPERSEDED = \"superseded\"\n\n\n# #endregion QuestionState\n\n\n# #region AnswerKind [C:1] [TYPE Class]\n# @BRIEF Classification of clarification answer types.\nclass AnswerKind(str, enum.Enum):\n SELECTED = \"selected\"\n CUSTOM = \"custom\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# #endregion AnswerKind\n\n\n# #region PreviewStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for compiled SQL previews.\nclass PreviewStatus(str, enum.Enum):\n PENDING = \"pending\"\n READY = \"ready\"\n FAILED = \"failed\"\n STALE = \"stale\"\n\n\n# #endregion PreviewStatus\n\n\n# #region LaunchStatus [C:1] [TYPE Class]\n# @BRIEF Outcome status for dataset launch handoff.\nclass LaunchStatus(str, enum.Enum):\n STARTED = \"started\"\n SUCCESS = \"success\"\n FAILED = \"failed\"\n\n\n# #endregion LaunchStatus\n\n\n# #region ArtifactType [C:1] [TYPE Class]\n# @BRIEF Type classification for export artifacts.\nclass ArtifactType(str, enum.Enum):\n DOCUMENTATION = \"documentation\"\n VALIDATION_REPORT = \"validation_report\"\n RUN_SUMMARY = \"run_summary\"\n\n\n# #endregion ArtifactType\n\n\n# #region ArtifactFormat [C:1] [TYPE Class]\n# @BRIEF Format classification for export artifact output.\nclass ArtifactFormat(str, enum.Enum):\n JSON = \"json\"\n MARKDOWN = \"markdown\"\n CSV = \"csv\"\n PDF = \"pdf\"\n\n\n# #endregion ArtifactFormat\n\n\n# #endregion DatasetReviewEnums\n" }, { "contract_id": "SessionStatus", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 10, - "end_line": 21, + "start_line": 9, + "end_line": 19, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Lifecycle status of a dataset review session." }, "relations": [], @@ -58368,19 +46638,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status of a dataset review session.\nclass SessionStatus(str, enum.Enum):\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n ARCHIVED = \"archived\"\n CANCELLED = \"cancelled\"\n\n\n# [/DEF:SessionStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status of a dataset review session.\nclass SessionStatus(str, enum.Enum):\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n ARCHIVED = \"archived\"\n CANCELLED = \"cancelled\"\n\n\n# #endregion SessionStatus\n" }, { "contract_id": "SessionPhase", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 24, - "end_line": 39, + "start_line": 22, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Ordered phase progression for dataset review orchestration." }, "relations": [], @@ -58395,19 +46665,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionPhase:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Ordered phase progression for dataset review orchestration.\nclass SessionPhase(str, enum.Enum):\n INTAKE = \"intake\"\n RECOVERY = \"recovery\"\n REVIEW = \"review\"\n SEMANTIC_REVIEW = \"semantic_review\"\n CLARIFICATION = \"clarification\"\n MAPPING_REVIEW = \"mapping_review\"\n PREVIEW = \"preview\"\n LAUNCH = \"launch\"\n POST_RUN = \"post_run\"\n\n\n# [/DEF:SessionPhase:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionPhase [C:1] [TYPE Class]\n# @BRIEF Ordered phase progression for dataset review orchestration.\nclass SessionPhase(str, enum.Enum):\n INTAKE = \"intake\"\n RECOVERY = \"recovery\"\n REVIEW = \"review\"\n SEMANTIC_REVIEW = \"semantic_review\"\n CLARIFICATION = \"clarification\"\n MAPPING_REVIEW = \"mapping_review\"\n PREVIEW = \"preview\"\n LAUNCH = \"launch\"\n POST_RUN = \"post_run\"\n\n\n# #endregion SessionPhase\n" }, { "contract_id": "ReadinessState", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 42, - "end_line": 61, + "start_line": 39, + "end_line": 57, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Granular readiness indicator driving the recommended-action UX flow." }, "relations": [], @@ -58422,19 +46692,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReadinessState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Granular readiness indicator driving the recommended-action UX flow.\nclass ReadinessState(str, enum.Enum):\n EMPTY = \"empty\"\n IMPORTING = \"importing\"\n REVIEW_READY = \"review_ready\"\n SEMANTIC_SOURCE_REVIEW_NEEDED = \"semantic_source_review_needed\"\n CLARIFICATION_NEEDED = \"clarification_needed\"\n CLARIFICATION_ACTIVE = \"clarification_active\"\n MAPPING_REVIEW_NEEDED = \"mapping_review_needed\"\n COMPILED_PREVIEW_READY = \"compiled_preview_ready\"\n PARTIALLY_READY = \"partially_ready\"\n RUN_READY = \"run_ready\"\n RUN_IN_PROGRESS = \"run_in_progress\"\n COMPLETED = \"completed\"\n RECOVERY_REQUIRED = \"recovery_required\"\n\n\n# [/DEF:ReadinessState:Class]\n" + "anchor_syntax": "region", + "body": "# #region ReadinessState [C:1] [TYPE Class]\n# @BRIEF Granular readiness indicator driving the recommended-action UX flow.\nclass ReadinessState(str, enum.Enum):\n EMPTY = \"empty\"\n IMPORTING = \"importing\"\n REVIEW_READY = \"review_ready\"\n SEMANTIC_SOURCE_REVIEW_NEEDED = \"semantic_source_review_needed\"\n CLARIFICATION_NEEDED = \"clarification_needed\"\n CLARIFICATION_ACTIVE = \"clarification_active\"\n MAPPING_REVIEW_NEEDED = \"mapping_review_needed\"\n COMPILED_PREVIEW_READY = \"compiled_preview_ready\"\n PARTIALLY_READY = \"partially_ready\"\n RUN_READY = \"run_ready\"\n RUN_IN_PROGRESS = \"run_in_progress\"\n COMPLETED = \"completed\"\n RECOVERY_REQUIRED = \"recovery_required\"\n\n\n# #endregion ReadinessState\n" }, { "contract_id": "RecommendedAction", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 64, - "end_line": 81, + "start_line": 60, + "end_line": 76, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Next-action guidance derived from the current readiness state." }, "relations": [], @@ -58449,19 +46719,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RecommendedAction:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Next-action guidance derived from the current readiness state.\nclass RecommendedAction(str, enum.Enum):\n IMPORT_FROM_SUPERSET = \"import_from_superset\"\n REVIEW_DOCUMENTATION = \"review_documentation\"\n APPLY_SEMANTIC_SOURCE = \"apply_semantic_source\"\n START_CLARIFICATION = \"start_clarification\"\n ANSWER_NEXT_QUESTION = \"answer_next_question\"\n APPROVE_MAPPING = \"approve_mapping\"\n GENERATE_SQL_PREVIEW = \"generate_sql_preview\"\n COMPLETE_REQUIRED_VALUES = \"complete_required_values\"\n LAUNCH_DATASET = \"launch_dataset\"\n RESUME_SESSION = \"resume_session\"\n EXPORT_OUTPUTS = \"export_outputs\"\n\n\n# [/DEF:RecommendedAction:Class]\n" + "anchor_syntax": "region", + "body": "# #region RecommendedAction [C:1] [TYPE Class]\n# @BRIEF Next-action guidance derived from the current readiness state.\nclass RecommendedAction(str, enum.Enum):\n IMPORT_FROM_SUPERSET = \"import_from_superset\"\n REVIEW_DOCUMENTATION = \"review_documentation\"\n APPLY_SEMANTIC_SOURCE = \"apply_semantic_source\"\n START_CLARIFICATION = \"start_clarification\"\n ANSWER_NEXT_QUESTION = \"answer_next_question\"\n APPROVE_MAPPING = \"approve_mapping\"\n GENERATE_SQL_PREVIEW = \"generate_sql_preview\"\n COMPLETE_REQUIRED_VALUES = \"complete_required_values\"\n LAUNCH_DATASET = \"launch_dataset\"\n RESUME_SESSION = \"resume_session\"\n EXPORT_OUTPUTS = \"export_outputs\"\n\n\n# #endregion RecommendedAction\n" }, { "contract_id": "SessionCollaboratorRole", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 84, - "end_line": 93, + "start_line": 79, + "end_line": 87, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "RBAC role for session collaborators." }, "relations": [], @@ -58476,19 +46746,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionCollaboratorRole:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: RBAC role for session collaborators.\nclass SessionCollaboratorRole(str, enum.Enum):\n VIEWER = \"viewer\"\n REVIEWER = \"reviewer\"\n APPROVER = \"approver\"\n\n\n# [/DEF:SessionCollaboratorRole:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionCollaboratorRole [C:1] [TYPE Class]\n# @BRIEF RBAC role for session collaborators.\nclass SessionCollaboratorRole(str, enum.Enum):\n VIEWER = \"viewer\"\n REVIEWER = \"reviewer\"\n APPROVER = \"approver\"\n\n\n# #endregion SessionCollaboratorRole\n" }, { "contract_id": "BusinessSummarySource", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 96, - "end_line": 107, + "start_line": 90, + "end_line": 100, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Provenance of the dataset business summary text." }, "relations": [], @@ -58503,19 +46773,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BusinessSummarySource:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Provenance of the dataset business summary text.\nclass BusinessSummarySource(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# [/DEF:BusinessSummarySource:Class]\n" + "anchor_syntax": "region", + "body": "# #region BusinessSummarySource [C:1] [TYPE Class]\n# @BRIEF Provenance of the dataset business summary text.\nclass BusinessSummarySource(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# #endregion BusinessSummarySource\n" }, { "contract_id": "ConfidenceState", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 110, - "end_line": 121, + "start_line": 103, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Confidence level for dataset profile completeness." }, "relations": [], @@ -58530,19 +46800,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ConfidenceState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Confidence level for dataset profile completeness.\nclass ConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n MOSTLY_CONFIRMED = \"mostly_confirmed\"\n MIXED = \"mixed\"\n LOW_CONFIDENCE = \"low_confidence\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:ConfidenceState:Class]\n" + "anchor_syntax": "region", + "body": "# #region ConfidenceState [C:1] [TYPE Class]\n# @BRIEF Confidence level for dataset profile completeness.\nclass ConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n MOSTLY_CONFIRMED = \"mostly_confirmed\"\n MIXED = \"mixed\"\n LOW_CONFIDENCE = \"low_confidence\"\n UNRESOLVED = \"unresolved\"\n\n\n# #endregion ConfidenceState\n" }, { "contract_id": "FindingArea", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 124, - "end_line": 139, + "start_line": 116, + "end_line": 130, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Domain area classification for validation findings." }, "relations": [], @@ -58557,19 +46827,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FindingArea:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Domain area classification for validation findings.\nclass FindingArea(str, enum.Enum):\n SOURCE_INTAKE = \"source_intake\"\n DATASET_PROFILE = \"dataset_profile\"\n SEMANTIC_ENRICHMENT = \"semantic_enrichment\"\n CLARIFICATION = \"clarification\"\n FILTER_RECOVERY = \"filter_recovery\"\n TEMPLATE_MAPPING = \"template_mapping\"\n COMPILED_PREVIEW = \"compiled_preview\"\n LAUNCH = \"launch\"\n AUDIT = \"audit\"\n\n\n# [/DEF:FindingArea:Class]\n" + "anchor_syntax": "region", + "body": "# #region FindingArea [C:1] [TYPE Class]\n# @BRIEF Domain area classification for validation findings.\nclass FindingArea(str, enum.Enum):\n SOURCE_INTAKE = \"source_intake\"\n DATASET_PROFILE = \"dataset_profile\"\n SEMANTIC_ENRICHMENT = \"semantic_enrichment\"\n CLARIFICATION = \"clarification\"\n FILTER_RECOVERY = \"filter_recovery\"\n TEMPLATE_MAPPING = \"template_mapping\"\n COMPILED_PREVIEW = \"compiled_preview\"\n LAUNCH = \"launch\"\n AUDIT = \"audit\"\n\n\n# #endregion FindingArea\n" }, { "contract_id": "FindingSeverity", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 142, - "end_line": 151, + "start_line": 133, + "end_line": 141, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Severity classification for validation findings." }, "relations": [], @@ -58584,19 +46854,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FindingSeverity:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Severity classification for validation findings.\nclass FindingSeverity(str, enum.Enum):\n BLOCKING = \"blocking\"\n WARNING = \"warning\"\n INFORMATIONAL = \"informational\"\n\n\n# [/DEF:FindingSeverity:Class]\n" + "anchor_syntax": "region", + "body": "# #region FindingSeverity [C:1] [TYPE Class]\n# @BRIEF Severity classification for validation findings.\nclass FindingSeverity(str, enum.Enum):\n BLOCKING = \"blocking\"\n WARNING = \"warning\"\n INFORMATIONAL = \"informational\"\n\n\n# #endregion FindingSeverity\n" }, { "contract_id": "ResolutionState", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 154, - "end_line": 166, + "start_line": 144, + "end_line": 155, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Resolution status for validation findings and clarification items." }, "relations": [], @@ -58611,19 +46881,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ResolutionState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Resolution status for validation findings and clarification items.\nclass ResolutionState(str, enum.Enum):\n OPEN = \"open\"\n RESOLVED = \"resolved\"\n APPROVED = \"approved\"\n SKIPPED = \"skipped\"\n DEFERRED = \"deferred\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# [/DEF:ResolutionState:Class]\n" + "anchor_syntax": "region", + "body": "# #region ResolutionState [C:1] [TYPE Class]\n# @BRIEF Resolution status for validation findings and clarification items.\nclass ResolutionState(str, enum.Enum):\n OPEN = \"open\"\n RESOLVED = \"resolved\"\n APPROVED = \"approved\"\n SKIPPED = \"skipped\"\n DEFERRED = \"deferred\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# #endregion ResolutionState\n" }, { "contract_id": "SemanticSourceType", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 169, - "end_line": 180, + "start_line": 158, + "end_line": 168, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Classification of semantic enrichment source origins." }, "relations": [], @@ -58638,19 +46908,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SemanticSourceType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Classification of semantic enrichment source origins.\nclass SemanticSourceType(str, enum.Enum):\n UPLOADED_FILE = \"uploaded_file\"\n CONNECTED_DICTIONARY = \"connected_dictionary\"\n REFERENCE_DATASET = \"reference_dataset\"\n NEIGHBOR_DATASET = \"neighbor_dataset\"\n AI_GENERATED = \"ai_generated\"\n\n\n# [/DEF:SemanticSourceType:Class]\n" + "anchor_syntax": "region", + "body": "# #region SemanticSourceType [C:1] [TYPE Class]\n# @BRIEF Classification of semantic enrichment source origins.\nclass SemanticSourceType(str, enum.Enum):\n UPLOADED_FILE = \"uploaded_file\"\n CONNECTED_DICTIONARY = \"connected_dictionary\"\n REFERENCE_DATASET = \"reference_dataset\"\n NEIGHBOR_DATASET = \"neighbor_dataset\"\n AI_GENERATED = \"ai_generated\"\n\n\n# #endregion SemanticSourceType\n" }, { "contract_id": "TrustLevel", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 183, - "end_line": 193, + "start_line": 171, + "end_line": 180, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Trust classification for semantic source reliability." }, "relations": [], @@ -58665,19 +46935,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TrustLevel:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Trust classification for semantic source reliability.\nclass TrustLevel(str, enum.Enum):\n TRUSTED = \"trusted\"\n RECOMMENDED = \"recommended\"\n CANDIDATE = \"candidate\"\n GENERATED = \"generated\"\n\n\n# [/DEF:TrustLevel:Class]\n" + "anchor_syntax": "region", + "body": "# #region TrustLevel [C:1] [TYPE Class]\n# @BRIEF Trust classification for semantic source reliability.\nclass TrustLevel(str, enum.Enum):\n TRUSTED = \"trusted\"\n RECOMMENDED = \"recommended\"\n CANDIDATE = \"candidate\"\n GENERATED = \"generated\"\n\n\n# #endregion TrustLevel\n" }, { "contract_id": "SemanticSourceStatus", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 196, - "end_line": 208, + "start_line": 183, + "end_line": 194, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Lifecycle status for semantic source application." }, "relations": [], @@ -58692,19 +46962,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SemanticSourceStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for semantic source application.\nclass SemanticSourceStatus(str, enum.Enum):\n AVAILABLE = \"available\"\n SELECTED = \"selected\"\n APPLIED = \"applied\"\n REJECTED = \"rejected\"\n PARTIAL = \"partial\"\n FAILED = \"failed\"\n\n\n# [/DEF:SemanticSourceStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region SemanticSourceStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for semantic source application.\nclass SemanticSourceStatus(str, enum.Enum):\n AVAILABLE = \"available\"\n SELECTED = \"selected\"\n APPLIED = \"applied\"\n REJECTED = \"rejected\"\n PARTIAL = \"partial\"\n FAILED = \"failed\"\n\n\n# #endregion SemanticSourceStatus\n" }, { "contract_id": "FieldKind", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 211, - "end_line": 221, + "start_line": 197, + "end_line": 206, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Kind classification for semantic field entries." }, "relations": [], @@ -58719,19 +46989,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FieldKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Kind classification for semantic field entries.\nclass FieldKind(str, enum.Enum):\n COLUMN = \"column\"\n METRIC = \"metric\"\n FILTER_DIMENSION = \"filter_dimension\"\n PARAMETER = \"parameter\"\n\n\n# [/DEF:FieldKind:Class]\n" + "anchor_syntax": "region", + "body": "# #region FieldKind [C:1] [TYPE Class]\n# @BRIEF Kind classification for semantic field entries.\nclass FieldKind(str, enum.Enum):\n COLUMN = \"column\"\n METRIC = \"metric\"\n FILTER_DIMENSION = \"filter_dimension\"\n PARAMETER = \"parameter\"\n\n\n# #endregion FieldKind\n" }, { "contract_id": "FieldProvenance", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 224, - "end_line": 236, + "start_line": 209, + "end_line": 220, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Provenance tracking for semantic field value origin." }, "relations": [], @@ -58746,19 +47016,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FieldProvenance:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Provenance tracking for semantic field value origin.\nclass FieldProvenance(str, enum.Enum):\n DICTIONARY_EXACT = \"dictionary_exact\"\n REFERENCE_IMPORTED = \"reference_imported\"\n FUZZY_INFERRED = \"fuzzy_inferred\"\n AI_GENERATED = \"ai_generated\"\n MANUAL_OVERRIDE = \"manual_override\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:FieldProvenance:Class]\n" + "anchor_syntax": "region", + "body": "# #region FieldProvenance [C:1] [TYPE Class]\n# @BRIEF Provenance tracking for semantic field value origin.\nclass FieldProvenance(str, enum.Enum):\n DICTIONARY_EXACT = \"dictionary_exact\"\n REFERENCE_IMPORTED = \"reference_imported\"\n FUZZY_INFERRED = \"fuzzy_inferred\"\n AI_GENERATED = \"ai_generated\"\n MANUAL_OVERRIDE = \"manual_override\"\n UNRESOLVED = \"unresolved\"\n\n\n# #endregion FieldProvenance\n" }, { "contract_id": "CandidateMatchType", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 239, - "end_line": 249, + "start_line": 223, + "end_line": 232, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Match type classification for semantic candidates." }, "relations": [], @@ -58773,19 +47043,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CandidateMatchType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Match type classification for semantic candidates.\nclass CandidateMatchType(str, enum.Enum):\n EXACT = \"exact\"\n REFERENCE = \"reference\"\n FUZZY = \"fuzzy\"\n GENERATED = \"generated\"\n\n\n# [/DEF:CandidateMatchType:Class]\n" + "anchor_syntax": "region", + "body": "# #region CandidateMatchType [C:1] [TYPE Class]\n# @BRIEF Match type classification for semantic candidates.\nclass CandidateMatchType(str, enum.Enum):\n EXACT = \"exact\"\n REFERENCE = \"reference\"\n FUZZY = \"fuzzy\"\n GENERATED = \"generated\"\n\n\n# #endregion CandidateMatchType\n" }, { "contract_id": "CandidateStatus", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 252, - "end_line": 262, + "start_line": 235, + "end_line": 244, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Lifecycle status for semantic candidate proposals." }, "relations": [], @@ -58800,19 +47070,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CandidateStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for semantic candidate proposals.\nclass CandidateStatus(str, enum.Enum):\n PROPOSED = \"proposed\"\n ACCEPTED = \"accepted\"\n REJECTED = \"rejected\"\n SUPERSEDED = \"superseded\"\n\n\n# [/DEF:CandidateStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region CandidateStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for semantic candidate proposals.\nclass CandidateStatus(str, enum.Enum):\n PROPOSED = \"proposed\"\n ACCEPTED = \"accepted\"\n REJECTED = \"rejected\"\n SUPERSEDED = \"superseded\"\n\n\n# #endregion CandidateStatus\n" }, { "contract_id": "FilterSource", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 265, - "end_line": 277, + "start_line": 247, + "end_line": 258, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Origin classification for imported filters." }, "relations": [], @@ -58827,19 +47097,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FilterSource:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Origin classification for imported filters.\nclass FilterSource(str, enum.Enum):\n SUPERSET_NATIVE = \"superset_native\"\n SUPERSET_URL = \"superset_url\"\n SUPERSET_PERMALINK = \"superset_permalink\"\n SUPERSET_NATIVE_FILTERS_KEY = \"superset_native_filters_key\"\n MANUAL = \"manual\"\n INFERRED = \"inferred\"\n\n\n# [/DEF:FilterSource:Class]\n" + "anchor_syntax": "region", + "body": "# #region FilterSource [C:1] [TYPE Class]\n# @BRIEF Origin classification for imported filters.\nclass FilterSource(str, enum.Enum):\n SUPERSET_NATIVE = \"superset_native\"\n SUPERSET_URL = \"superset_url\"\n SUPERSET_PERMALINK = \"superset_permalink\"\n SUPERSET_NATIVE_FILTERS_KEY = \"superset_native_filters_key\"\n MANUAL = \"manual\"\n INFERRED = \"inferred\"\n\n\n# #endregion FilterSource\n" }, { "contract_id": "FilterConfidenceState", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 280, - "end_line": 291, + "start_line": 261, + "end_line": 271, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Confidence classification for imported filter values." }, "relations": [], @@ -58854,19 +47124,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FilterConfidenceState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Confidence classification for imported filter values.\nclass FilterConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n UNRESOLVED = \"unresolved\"\n\n\n# [/DEF:FilterConfidenceState:Class]\n" + "anchor_syntax": "region", + "body": "# #region FilterConfidenceState [C:1] [TYPE Class]\n# @BRIEF Confidence classification for imported filter values.\nclass FilterConfidenceState(str, enum.Enum):\n CONFIRMED = \"confirmed\"\n IMPORTED = \"imported\"\n INFERRED = \"inferred\"\n AI_DRAFT = \"ai_draft\"\n UNRESOLVED = \"unresolved\"\n\n\n# #endregion FilterConfidenceState\n" }, { "contract_id": "FilterRecoveryStatus", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 294, - "end_line": 304, + "start_line": 274, + "end_line": 283, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Recovery quality status for imported filters." }, "relations": [], @@ -58881,19 +47151,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FilterRecoveryStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Recovery quality status for imported filters.\nclass FilterRecoveryStatus(str, enum.Enum):\n RECOVERED = \"recovered\"\n PARTIAL = \"partial\"\n MISSING = \"missing\"\n CONFLICTED = \"conflicted\"\n\n\n# [/DEF:FilterRecoveryStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region FilterRecoveryStatus [C:1] [TYPE Class]\n# @BRIEF Recovery quality status for imported filters.\nclass FilterRecoveryStatus(str, enum.Enum):\n RECOVERED = \"recovered\"\n PARTIAL = \"partial\"\n MISSING = \"missing\"\n CONFLICTED = \"conflicted\"\n\n\n# #endregion FilterRecoveryStatus\n" }, { "contract_id": "VariableKind", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 307, - "end_line": 317, + "start_line": 286, + "end_line": 295, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Kind classification for template variables." }, "relations": [], @@ -58908,19 +47178,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:VariableKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Kind classification for template variables.\nclass VariableKind(str, enum.Enum):\n NATIVE_FILTER = \"native_filter\"\n PARAMETER = \"parameter\"\n DERIVED = \"derived\"\n UNKNOWN = \"unknown\"\n\n\n# [/DEF:VariableKind:Class]\n" + "anchor_syntax": "region", + "body": "# #region VariableKind [C:1] [TYPE Class]\n# @BRIEF Kind classification for template variables.\nclass VariableKind(str, enum.Enum):\n NATIVE_FILTER = \"native_filter\"\n PARAMETER = \"parameter\"\n DERIVED = \"derived\"\n UNKNOWN = \"unknown\"\n\n\n# #endregion VariableKind\n" }, { "contract_id": "MappingStatus", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 320, - "end_line": 331, + "start_line": 298, + "end_line": 308, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Lifecycle status for template variable mapping." }, "relations": [], @@ -58935,19 +47205,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MappingStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for template variable mapping.\nclass MappingStatus(str, enum.Enum):\n UNMAPPED = \"unmapped\"\n PROPOSED = \"proposed\"\n APPROVED = \"approved\"\n OVERRIDDEN = \"overridden\"\n INVALID = \"invalid\"\n\n\n# [/DEF:MappingStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region MappingStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for template variable mapping.\nclass MappingStatus(str, enum.Enum):\n UNMAPPED = \"unmapped\"\n PROPOSED = \"proposed\"\n APPROVED = \"approved\"\n OVERRIDDEN = \"overridden\"\n INVALID = \"invalid\"\n\n\n# #endregion MappingStatus\n" }, { "contract_id": "MappingMethod", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 334, - "end_line": 344, + "start_line": 311, + "end_line": 320, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Method classification for execution mapping creation." }, "relations": [], @@ -58962,19 +47232,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MappingMethod:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Method classification for execution mapping creation.\nclass MappingMethod(str, enum.Enum):\n DIRECT_MATCH = \"direct_match\"\n HEURISTIC_MATCH = \"heuristic_match\"\n SEMANTIC_MATCH = \"semantic_match\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# [/DEF:MappingMethod:Class]\n" + "anchor_syntax": "region", + "body": "# #region MappingMethod [C:1] [TYPE Class]\n# @BRIEF Method classification for execution mapping creation.\nclass MappingMethod(str, enum.Enum):\n DIRECT_MATCH = \"direct_match\"\n HEURISTIC_MATCH = \"heuristic_match\"\n SEMANTIC_MATCH = \"semantic_match\"\n MANUAL_OVERRIDE = \"manual_override\"\n\n\n# #endregion MappingMethod\n" }, { "contract_id": "MappingWarningLevel", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 347, - "end_line": 356, + "start_line": 323, + "end_line": 331, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Warning severity for execution mapping quality indicators." }, "relations": [], @@ -58989,19 +47259,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MappingWarningLevel:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Warning severity for execution mapping quality indicators.\nclass MappingWarningLevel(str, enum.Enum):\n LOW = \"low\"\n MEDIUM = \"medium\"\n HIGH = \"high\"\n\n\n# [/DEF:MappingWarningLevel:Class]\n" + "anchor_syntax": "region", + "body": "# #region MappingWarningLevel [C:1] [TYPE Class]\n# @BRIEF Warning severity for execution mapping quality indicators.\nclass MappingWarningLevel(str, enum.Enum):\n LOW = \"low\"\n MEDIUM = \"medium\"\n HIGH = \"high\"\n\n\n# #endregion MappingWarningLevel\n" }, { "contract_id": "ApprovalState", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 359, - "end_line": 369, + "start_line": 334, + "end_line": 343, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Approval lifecycle for execution mapping gate checks." }, "relations": [], @@ -59016,19 +47286,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ApprovalState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Approval lifecycle for execution mapping gate checks.\nclass ApprovalState(str, enum.Enum):\n PENDING = \"pending\"\n APPROVED = \"approved\"\n REJECTED = \"rejected\"\n NOT_REQUIRED = \"not_required\"\n\n\n# [/DEF:ApprovalState:Class]\n" + "anchor_syntax": "region", + "body": "# #region ApprovalState [C:1] [TYPE Class]\n# @BRIEF Approval lifecycle for execution mapping gate checks.\nclass ApprovalState(str, enum.Enum):\n PENDING = \"pending\"\n APPROVED = \"approved\"\n REJECTED = \"rejected\"\n NOT_REQUIRED = \"not_required\"\n\n\n# #endregion ApprovalState\n" }, { "contract_id": "ClarificationStatus", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 372, - "end_line": 383, + "start_line": 346, + "end_line": 356, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Lifecycle status for clarification sessions." }, "relations": [], @@ -59043,19 +47313,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for clarification sessions.\nclass ClarificationStatus(str, enum.Enum):\n PENDING = \"pending\"\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n CANCELLED = \"cancelled\"\n\n\n# [/DEF:ClarificationStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for clarification sessions.\nclass ClarificationStatus(str, enum.Enum):\n PENDING = \"pending\"\n ACTIVE = \"active\"\n PAUSED = \"paused\"\n COMPLETED = \"completed\"\n CANCELLED = \"cancelled\"\n\n\n# #endregion ClarificationStatus\n" }, { "contract_id": "QuestionState", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 386, - "end_line": 397, + "start_line": 359, + "end_line": 369, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "State machine for individual clarification questions." }, "relations": [], @@ -59070,19 +47340,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:QuestionState:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: State machine for individual clarification questions.\nclass QuestionState(str, enum.Enum):\n OPEN = \"open\"\n ANSWERED = \"answered\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n SUPERSEDED = \"superseded\"\n\n\n# [/DEF:QuestionState:Class]\n" + "anchor_syntax": "region", + "body": "# #region QuestionState [C:1] [TYPE Class]\n# @BRIEF State machine for individual clarification questions.\nclass QuestionState(str, enum.Enum):\n OPEN = \"open\"\n ANSWERED = \"answered\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n SUPERSEDED = \"superseded\"\n\n\n# #endregion QuestionState\n" }, { "contract_id": "AnswerKind", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 400, - "end_line": 410, + "start_line": 372, + "end_line": 381, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Classification of clarification answer types." }, "relations": [], @@ -59097,19 +47367,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AnswerKind:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Classification of clarification answer types.\nclass AnswerKind(str, enum.Enum):\n SELECTED = \"selected\"\n CUSTOM = \"custom\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# [/DEF:AnswerKind:Class]\n" + "anchor_syntax": "region", + "body": "# #region AnswerKind [C:1] [TYPE Class]\n# @BRIEF Classification of clarification answer types.\nclass AnswerKind(str, enum.Enum):\n SELECTED = \"selected\"\n CUSTOM = \"custom\"\n SKIPPED = \"skipped\"\n EXPERT_REVIEW = \"expert_review\"\n\n\n# #endregion AnswerKind\n" }, { "contract_id": "PreviewStatus", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 413, - "end_line": 423, + "start_line": 384, + "end_line": 393, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Lifecycle status for compiled SQL previews." }, "relations": [], @@ -59124,19 +47394,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PreviewStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Lifecycle status for compiled SQL previews.\nclass PreviewStatus(str, enum.Enum):\n PENDING = \"pending\"\n READY = \"ready\"\n FAILED = \"failed\"\n STALE = \"stale\"\n\n\n# [/DEF:PreviewStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region PreviewStatus [C:1] [TYPE Class]\n# @BRIEF Lifecycle status for compiled SQL previews.\nclass PreviewStatus(str, enum.Enum):\n PENDING = \"pending\"\n READY = \"ready\"\n FAILED = \"failed\"\n STALE = \"stale\"\n\n\n# #endregion PreviewStatus\n" }, { "contract_id": "LaunchStatus", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 426, - "end_line": 435, + "start_line": 396, + "end_line": 404, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Outcome status for dataset launch handoff." }, "relations": [], @@ -59151,19 +47421,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LaunchStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Outcome status for dataset launch handoff.\nclass LaunchStatus(str, enum.Enum):\n STARTED = \"started\"\n SUCCESS = \"success\"\n FAILED = \"failed\"\n\n\n# [/DEF:LaunchStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region LaunchStatus [C:1] [TYPE Class]\n# @BRIEF Outcome status for dataset launch handoff.\nclass LaunchStatus(str, enum.Enum):\n STARTED = \"started\"\n SUCCESS = \"success\"\n FAILED = \"failed\"\n\n\n# #endregion LaunchStatus\n" }, { "contract_id": "ArtifactType", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 438, - "end_line": 447, + "start_line": 407, + "end_line": 415, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Type classification for export artifacts." }, "relations": [], @@ -59178,19 +47448,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ArtifactType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Type classification for export artifacts.\nclass ArtifactType(str, enum.Enum):\n DOCUMENTATION = \"documentation\"\n VALIDATION_REPORT = \"validation_report\"\n RUN_SUMMARY = \"run_summary\"\n\n\n# [/DEF:ArtifactType:Class]\n" + "anchor_syntax": "region", + "body": "# #region ArtifactType [C:1] [TYPE Class]\n# @BRIEF Type classification for export artifacts.\nclass ArtifactType(str, enum.Enum):\n DOCUMENTATION = \"documentation\"\n VALIDATION_REPORT = \"validation_report\"\n RUN_SUMMARY = \"run_summary\"\n\n\n# #endregion ArtifactType\n" }, { "contract_id": "ArtifactFormat", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_enums.py", - "start_line": 450, - "end_line": 460, + "start_line": 418, + "end_line": 427, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Format classification for export artifact output." }, "relations": [], @@ -59205,19 +47475,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ArtifactFormat:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Format classification for export artifact output.\nclass ArtifactFormat(str, enum.Enum):\n JSON = \"json\"\n MARKDOWN = \"markdown\"\n CSV = \"csv\"\n PDF = \"pdf\"\n\n\n# [/DEF:ArtifactFormat:Class]\n" + "anchor_syntax": "region", + "body": "# #region ArtifactFormat [C:1] [TYPE Class]\n# @BRIEF Format classification for export artifact output.\nclass ArtifactFormat(str, enum.Enum):\n JSON = \"json\"\n MARKDOWN = \"markdown\"\n CSV = \"csv\"\n PDF = \"pdf\"\n\n\n# #endregion ArtifactFormat\n" }, { "contract_id": "DatasetReviewExecutionModels", "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_execution_models.py", "start_line": 1, - "end_line": 140, + "end_line": 137, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", "PURPOSE": "Compiled preview, run context, session event, and export artifact models for execution and audit." }, @@ -59236,19 +47506,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewExecutionModels:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Compiled preview, run context, session event, and export artifact models for execution and audit.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION: DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Column,\n String,\n Text,\n DateTime,\n ForeignKey,\n JSON,\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.mapping import Base\nfrom src.models.dataset_review_pkg._enums import (\n PreviewStatus,\n LaunchStatus,\n ArtifactType,\n ArtifactFormat,\n)\n\n\n# [DEF:CompiledPreview:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One compiled SQL preview snapshot with fingerprint for staleness detection.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass CompiledPreview(Base):\n __tablename__ = \"compiled_previews\"\n\n preview_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n preview_status = Column(\n SQLEnum(PreviewStatus), nullable=False, default=PreviewStatus.PENDING\n )\n compiled_sql = Column(Text, nullable=True)\n preview_fingerprint = Column(String, nullable=False)\n compiled_by = Column(String, nullable=False, default=\"superset\")\n error_code = Column(String, nullable=True)\n error_details = Column(Text, nullable=True)\n compiled_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"previews\")\n\n\n# [/DEF:CompiledPreview:Class]\n\n\n# [DEF:DatasetRunContext:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetRunContext(Base):\n __tablename__ = \"dataset_run_contexts\"\n\n run_context_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n dataset_ref = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n preview_id = Column(String, nullable=False)\n sql_lab_session_ref = Column(String, nullable=False)\n effective_filters = Column(JSON, nullable=False)\n template_params = Column(JSON, nullable=False)\n approved_mapping_ids = Column(JSON, nullable=False)\n semantic_decision_refs = Column(JSON, nullable=False)\n open_warning_refs = Column(JSON, nullable=False)\n launch_status = Column(SQLEnum(LaunchStatus), nullable=False)\n launch_error = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"run_contexts\")\n\n\n# [/DEF:DatasetRunContext:Class]\n\n\n# [DEF:SessionEvent:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One persisted audit event for dataset review session mutations.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass SessionEvent(Base):\n __tablename__ = \"session_events\"\n\n session_event_id = Column(\n String, primary_key=True, default=lambda: str(uuid.uuid4())\n )\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n actor_user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n event_type = Column(String, nullable=False)\n event_summary = Column(Text, nullable=False)\n current_phase = Column(String, nullable=True)\n readiness_state = Column(String, nullable=True)\n event_details = Column(JSON, nullable=False, default=dict)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"events\")\n actor = relationship(\"User\")\n\n\n# [/DEF:SessionEvent:Class]\n\n\n# [DEF:ExportArtifact:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One persisted export artifact reference for documentation and validation outputs.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass ExportArtifact(Base):\n __tablename__ = \"export_artifacts\"\n\n artifact_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n artifact_type = Column(SQLEnum(ArtifactType), nullable=False)\n format = Column(SQLEnum(ArtifactFormat), nullable=False)\n storage_ref = Column(String, nullable=False)\n created_by_user_id = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"export_artifacts\")\n\n\n# [/DEF:ExportArtifact:Class]\n\n\n# [/DEF:DatasetReviewExecutionModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewExecutionModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, execution, model]\n# @BRIEF Compiled preview, run context, session event, and export artifact models for execution and audit.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n JSON,\n Column,\n DateTime,\n ForeignKey,\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 ArtifactFormat,\n ArtifactType,\n LaunchStatus,\n PreviewStatus,\n)\nfrom src.models.mapping import Base\n\n\n# #region CompiledPreview [C:2] [TYPE Class]\n# @BRIEF One compiled SQL preview snapshot with fingerprint for staleness detection.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass CompiledPreview(Base):\n __tablename__ = \"compiled_previews\"\n\n preview_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n preview_status = Column(\n SQLEnum(PreviewStatus), nullable=False, default=PreviewStatus.PENDING\n )\n compiled_sql = Column(Text, nullable=True)\n preview_fingerprint = Column(String, nullable=False)\n compiled_by = Column(String, nullable=False, default=\"superset\")\n error_code = Column(String, nullable=True)\n error_details = Column(Text, nullable=True)\n compiled_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"previews\")\n\n\n# #endregion CompiledPreview\n\n\n# #region DatasetRunContext [C:2] [TYPE Class]\n# @BRIEF Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetRunContext(Base):\n __tablename__ = \"dataset_run_contexts\"\n\n run_context_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n dataset_ref = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n preview_id = Column(String, nullable=False)\n sql_lab_session_ref = Column(String, nullable=False)\n effective_filters = Column(JSON, nullable=False)\n template_params = Column(JSON, nullable=False)\n approved_mapping_ids = Column(JSON, nullable=False)\n semantic_decision_refs = Column(JSON, nullable=False)\n open_warning_refs = Column(JSON, nullable=False)\n launch_status = Column(SQLEnum(LaunchStatus), nullable=False)\n launch_error = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"run_contexts\")\n\n\n# #endregion DatasetRunContext\n\n\n# #region SessionEvent [C:2] [TYPE Class]\n# @BRIEF One persisted audit event for dataset review session mutations.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionEvent(Base):\n __tablename__ = \"session_events\"\n\n session_event_id = Column(\n String, primary_key=True, default=lambda: str(uuid.uuid4())\n )\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n actor_user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n event_type = Column(String, nullable=False)\n event_summary = Column(Text, nullable=False)\n current_phase = Column(String, nullable=True)\n readiness_state = Column(String, nullable=True)\n event_details = Column(JSON, nullable=False, default=dict)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"events\")\n actor = relationship(\"User\")\n\n\n# #endregion SessionEvent\n\n\n# #region ExportArtifact [C:2] [TYPE Class]\n# @BRIEF One persisted export artifact reference for documentation and validation outputs.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ExportArtifact(Base):\n __tablename__ = \"export_artifacts\"\n\n artifact_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n artifact_type = Column(SQLEnum(ArtifactType), nullable=False)\n format = Column(SQLEnum(ArtifactFormat), nullable=False)\n storage_ref = Column(String, nullable=False)\n created_by_user_id = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"export_artifacts\")\n\n\n# #endregion ExportArtifact\n\n\n# #endregion DatasetReviewExecutionModels\n" }, { "contract_id": "CompiledPreview", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_execution_models.py", - "start_line": 31, + "start_line": 32, "end_line": 56, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "One compiled SQL preview snapshot with fingerprint for staleness detection." }, "relations": [ @@ -59270,19 +47540,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CompiledPreview:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One compiled SQL preview snapshot with fingerprint for staleness detection.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass CompiledPreview(Base):\n __tablename__ = \"compiled_previews\"\n\n preview_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n preview_status = Column(\n SQLEnum(PreviewStatus), nullable=False, default=PreviewStatus.PENDING\n )\n compiled_sql = Column(Text, nullable=True)\n preview_fingerprint = Column(String, nullable=False)\n compiled_by = Column(String, nullable=False, default=\"superset\")\n error_code = Column(String, nullable=True)\n error_details = Column(Text, nullable=True)\n compiled_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"previews\")\n\n\n# [/DEF:CompiledPreview:Class]\n" + "anchor_syntax": "region", + "body": "# #region CompiledPreview [C:2] [TYPE Class]\n# @BRIEF One compiled SQL preview snapshot with fingerprint for staleness detection.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass CompiledPreview(Base):\n __tablename__ = \"compiled_previews\"\n\n preview_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n preview_status = Column(\n SQLEnum(PreviewStatus), nullable=False, default=PreviewStatus.PENDING\n )\n compiled_sql = Column(Text, nullable=True)\n preview_fingerprint = Column(String, nullable=False)\n compiled_by = Column(String, nullable=False, default=\"superset\")\n error_code = Column(String, nullable=True)\n error_details = Column(Text, nullable=True)\n compiled_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"previews\")\n\n\n# #endregion CompiledPreview\n" }, { "contract_id": "DatasetRunContext", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_execution_models.py", "start_line": 59, - "end_line": 86, + "end_line": 85, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time." }, "relations": [ @@ -59304,19 +47574,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetRunContext:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetRunContext(Base):\n __tablename__ = \"dataset_run_contexts\"\n\n run_context_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n dataset_ref = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n preview_id = Column(String, nullable=False)\n sql_lab_session_ref = Column(String, nullable=False)\n effective_filters = Column(JSON, nullable=False)\n template_params = Column(JSON, nullable=False)\n approved_mapping_ids = Column(JSON, nullable=False)\n semantic_decision_refs = Column(JSON, nullable=False)\n open_warning_refs = Column(JSON, nullable=False)\n launch_status = Column(SQLEnum(LaunchStatus), nullable=False)\n launch_error = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"run_contexts\")\n\n\n# [/DEF:DatasetRunContext:Class]\n" + "anchor_syntax": "region", + "body": "# #region DatasetRunContext [C:2] [TYPE Class]\n# @BRIEF Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetRunContext(Base):\n __tablename__ = \"dataset_run_contexts\"\n\n run_context_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n dataset_ref = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n preview_id = Column(String, nullable=False)\n sql_lab_session_ref = Column(String, nullable=False)\n effective_filters = Column(JSON, nullable=False)\n template_params = Column(JSON, nullable=False)\n approved_mapping_ids = Column(JSON, nullable=False)\n semantic_decision_refs = Column(JSON, nullable=False)\n open_warning_refs = Column(JSON, nullable=False)\n launch_status = Column(SQLEnum(LaunchStatus), nullable=False)\n launch_error = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"run_contexts\")\n\n\n# #endregion DatasetRunContext\n" }, { "contract_id": "SessionEvent", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_execution_models.py", - "start_line": 89, - "end_line": 114, + "start_line": 88, + "end_line": 112, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "One persisted audit event for dataset review session mutations." }, "relations": [ @@ -59338,19 +47608,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionEvent:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One persisted audit event for dataset review session mutations.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass SessionEvent(Base):\n __tablename__ = \"session_events\"\n\n session_event_id = Column(\n String, primary_key=True, default=lambda: str(uuid.uuid4())\n )\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n actor_user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n event_type = Column(String, nullable=False)\n event_summary = Column(Text, nullable=False)\n current_phase = Column(String, nullable=True)\n readiness_state = Column(String, nullable=True)\n event_details = Column(JSON, nullable=False, default=dict)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"events\")\n actor = relationship(\"User\")\n\n\n# [/DEF:SessionEvent:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionEvent [C:2] [TYPE Class]\n# @BRIEF One persisted audit event for dataset review session mutations.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionEvent(Base):\n __tablename__ = \"session_events\"\n\n session_event_id = Column(\n String, primary_key=True, default=lambda: str(uuid.uuid4())\n )\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n actor_user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n event_type = Column(String, nullable=False)\n event_summary = Column(Text, nullable=False)\n current_phase = Column(String, nullable=True)\n readiness_state = Column(String, nullable=True)\n event_details = Column(JSON, nullable=False, default=dict)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"events\")\n actor = relationship(\"User\")\n\n\n# #endregion SessionEvent\n" }, { "contract_id": "ExportArtifact", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_execution_models.py", - "start_line": 117, - "end_line": 137, + "start_line": 115, + "end_line": 134, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "One persisted export artifact reference for documentation and validation outputs." }, "relations": [ @@ -59372,19 +47642,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ExportArtifact:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One persisted export artifact reference for documentation and validation outputs.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass ExportArtifact(Base):\n __tablename__ = \"export_artifacts\"\n\n artifact_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n artifact_type = Column(SQLEnum(ArtifactType), nullable=False)\n format = Column(SQLEnum(ArtifactFormat), nullable=False)\n storage_ref = Column(String, nullable=False)\n created_by_user_id = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"export_artifacts\")\n\n\n# [/DEF:ExportArtifact:Class]\n" + "anchor_syntax": "region", + "body": "# #region ExportArtifact [C:2] [TYPE Class]\n# @BRIEF One persisted export artifact reference for documentation and validation outputs.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ExportArtifact(Base):\n __tablename__ = \"export_artifacts\"\n\n artifact_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n artifact_type = Column(SQLEnum(ArtifactType), nullable=False)\n format = Column(SQLEnum(ArtifactFormat), nullable=False)\n storage_ref = Column(String, nullable=False)\n created_by_user_id = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"export_artifacts\")\n\n\n# #endregion ExportArtifact\n" }, { "contract_id": "DatasetReviewFilterModels", "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_filter_models.py", "start_line": 1, - "end_line": 95, + "end_line": 94, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", "PURPOSE": "Imported filter and template variable models for Superset context recovery and execution mapping." }, @@ -59403,19 +47673,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewFilterModels:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Imported filter and template variable models for Superset context recovery and execution mapping.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION: DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Column,\n String,\n Text,\n Boolean,\n DateTime,\n ForeignKey,\n JSON,\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.mapping import Base\nfrom src.models.dataset_review_pkg._enums import (\n FilterSource,\n FilterConfidenceState,\n FilterRecoveryStatus,\n VariableKind,\n MappingStatus,\n)\n\n\n# [DEF:ImportedFilter:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Recovered Superset filter with confidence and recovery status tracking.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass ImportedFilter(Base):\n __tablename__ = \"imported_filters\"\n\n filter_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n filter_name = Column(String, nullable=False)\n display_name = Column(String, nullable=True)\n raw_value = Column(JSON, nullable=False)\n raw_value_masked = Column(Boolean, nullable=False, default=False)\n normalized_value = Column(JSON, nullable=True)\n source = Column(SQLEnum(FilterSource), nullable=False)\n confidence_state = Column(SQLEnum(FilterConfidenceState), nullable=False)\n requires_confirmation = Column(Boolean, nullable=False, default=False)\n recovery_status = Column(SQLEnum(FilterRecoveryStatus), nullable=False)\n notes = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"imported_filters\")\n\n\n# [/DEF:ImportedFilter:Class]\n\n\n# [DEF:TemplateVariable:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Discovered template variable from dataset SQL with mapping status tracking.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass TemplateVariable(Base):\n __tablename__ = \"template_variables\"\n\n variable_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n variable_name = Column(String, nullable=False)\n expression_source = Column(Text, nullable=False)\n variable_kind = Column(SQLEnum(VariableKind), nullable=False)\n is_required = Column(Boolean, nullable=False, default=True)\n default_value = Column(JSON, nullable=True)\n mapping_status = Column(\n SQLEnum(MappingStatus), nullable=False, default=MappingStatus.UNMAPPED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"template_variables\")\n\n\n# [/DEF:TemplateVariable:Class]\n\n\n# [/DEF:DatasetReviewFilterModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewFilterModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, filter, template]\n# @BRIEF Imported filter and template variable models for Superset context recovery and execution mapping.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n JSON,\n Boolean,\n Column,\n DateTime,\n ForeignKey,\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 FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n MappingStatus,\n VariableKind,\n)\nfrom src.models.mapping import Base\n\n\n# #region ImportedFilter [C:2] [TYPE Class]\n# @BRIEF Recovered Superset filter with confidence and recovery status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ImportedFilter(Base):\n __tablename__ = \"imported_filters\"\n\n filter_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n filter_name = Column(String, nullable=False)\n display_name = Column(String, nullable=True)\n raw_value = Column(JSON, nullable=False)\n raw_value_masked = Column(Boolean, nullable=False, default=False)\n normalized_value = Column(JSON, nullable=True)\n source = Column(SQLEnum(FilterSource), nullable=False)\n confidence_state = Column(SQLEnum(FilterConfidenceState), nullable=False)\n requires_confirmation = Column(Boolean, nullable=False, default=False)\n recovery_status = Column(SQLEnum(FilterRecoveryStatus), nullable=False)\n notes = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"imported_filters\")\n\n\n# #endregion ImportedFilter\n\n\n# #region TemplateVariable [C:2] [TYPE Class]\n# @BRIEF Discovered template variable from dataset SQL with mapping status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass TemplateVariable(Base):\n __tablename__ = \"template_variables\"\n\n variable_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n variable_name = Column(String, nullable=False)\n expression_source = Column(Text, nullable=False)\n variable_kind = Column(SQLEnum(VariableKind), nullable=False)\n is_required = Column(Boolean, nullable=False, default=True)\n default_value = Column(JSON, nullable=True)\n mapping_status = Column(\n SQLEnum(MappingStatus), nullable=False, default=MappingStatus.UNMAPPED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"template_variables\")\n\n\n# #endregion TemplateVariable\n\n\n# #endregion DatasetReviewFilterModels\n" }, { "contract_id": "ImportedFilter", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_filter_models.py", - "start_line": 33, + "start_line": 34, "end_line": 62, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Recovered Superset filter with confidence and recovery status tracking." }, "relations": [ @@ -59437,19 +47707,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ImportedFilter:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Recovered Superset filter with confidence and recovery status tracking.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass ImportedFilter(Base):\n __tablename__ = \"imported_filters\"\n\n filter_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n filter_name = Column(String, nullable=False)\n display_name = Column(String, nullable=True)\n raw_value = Column(JSON, nullable=False)\n raw_value_masked = Column(Boolean, nullable=False, default=False)\n normalized_value = Column(JSON, nullable=True)\n source = Column(SQLEnum(FilterSource), nullable=False)\n confidence_state = Column(SQLEnum(FilterConfidenceState), nullable=False)\n requires_confirmation = Column(Boolean, nullable=False, default=False)\n recovery_status = Column(SQLEnum(FilterRecoveryStatus), nullable=False)\n notes = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"imported_filters\")\n\n\n# [/DEF:ImportedFilter:Class]\n" + "anchor_syntax": "region", + "body": "# #region ImportedFilter [C:2] [TYPE Class]\n# @BRIEF Recovered Superset filter with confidence and recovery status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ImportedFilter(Base):\n __tablename__ = \"imported_filters\"\n\n filter_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n filter_name = Column(String, nullable=False)\n display_name = Column(String, nullable=True)\n raw_value = Column(JSON, nullable=False)\n raw_value_masked = Column(Boolean, nullable=False, default=False)\n normalized_value = Column(JSON, nullable=True)\n source = Column(SQLEnum(FilterSource), nullable=False)\n confidence_state = Column(SQLEnum(FilterConfidenceState), nullable=False)\n requires_confirmation = Column(Boolean, nullable=False, default=False)\n recovery_status = Column(SQLEnum(FilterRecoveryStatus), nullable=False)\n notes = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"imported_filters\")\n\n\n# #endregion ImportedFilter\n" }, { "contract_id": "TemplateVariable", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_filter_models.py", "start_line": 65, - "end_line": 92, + "end_line": 91, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Discovered template variable from dataset SQL with mapping status tracking." }, "relations": [ @@ -59471,8 +47741,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TemplateVariable:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Discovered template variable from dataset SQL with mapping status tracking.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass TemplateVariable(Base):\n __tablename__ = \"template_variables\"\n\n variable_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n variable_name = Column(String, nullable=False)\n expression_source = Column(Text, nullable=False)\n variable_kind = Column(SQLEnum(VariableKind), nullable=False)\n is_required = Column(Boolean, nullable=False, default=True)\n default_value = Column(JSON, nullable=True)\n mapping_status = Column(\n SQLEnum(MappingStatus), nullable=False, default=MappingStatus.UNMAPPED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"template_variables\")\n\n\n# [/DEF:TemplateVariable:Class]\n" + "anchor_syntax": "region", + "body": "# #region TemplateVariable [C:2] [TYPE Class]\n# @BRIEF Discovered template variable from dataset SQL with mapping status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass TemplateVariable(Base):\n __tablename__ = \"template_variables\"\n\n variable_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n variable_name = Column(String, nullable=False)\n expression_source = Column(Text, nullable=False)\n variable_kind = Column(SQLEnum(VariableKind), nullable=False)\n is_required = Column(Boolean, nullable=False, default=True)\n default_value = Column(JSON, nullable=True)\n mapping_status = Column(\n SQLEnum(MappingStatus), nullable=False, default=MappingStatus.UNMAPPED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"template_variables\")\n\n\n# #endregion TemplateVariable\n" }, { "contract_id": "DatasetReviewFindingModels", @@ -59483,7 +47753,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "Domain", "PURPOSE": "Validation finding model for tracking blocking, warning, and informational issues during review." }, @@ -59512,19 +47782,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewFindingModels:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Validation finding model for tracking blocking, warning, and informational issues during review.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION: DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Column,\n String,\n Text,\n DateTime,\n ForeignKey,\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.mapping import Base\nfrom src.models.dataset_review_pkg._enums import (\n FindingArea,\n FindingSeverity,\n ResolutionState,\n)\n\n\n# [DEF:ValidationFinding:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Structured finding record for dataset review validation issues with resolution tracking.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass ValidationFinding(Base):\n __tablename__ = \"validation_findings\"\n\n finding_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n area = Column(SQLEnum(FindingArea), nullable=False)\n severity = Column(SQLEnum(FindingSeverity), nullable=False)\n code = Column(String, nullable=False)\n title = Column(String, nullable=False)\n message = Column(Text, nullable=False)\n resolution_state = Column(\n SQLEnum(ResolutionState), nullable=False, default=ResolutionState.OPEN\n )\n resolution_note = Column(Text, nullable=True)\n caused_by_ref = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n resolved_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"findings\")\n\n\n# [/DEF:ValidationFinding:Class]\n\n\n# [/DEF:DatasetReviewFindingModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewFindingModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, finding, validation]\n# @BRIEF Validation finding model for tracking blocking, warning, and informational issues during review.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Column,\n DateTime,\n ForeignKey,\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 FindingArea,\n FindingSeverity,\n ResolutionState,\n)\nfrom src.models.mapping import Base\n\n\n# #region ValidationFinding [C:2] [TYPE Class]\n# @BRIEF Structured finding record for dataset review validation issues with resolution tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ValidationFinding(Base):\n __tablename__ = \"validation_findings\"\n\n finding_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n area = Column(SQLEnum(FindingArea), nullable=False)\n severity = Column(SQLEnum(FindingSeverity), nullable=False)\n code = Column(String, nullable=False)\n title = Column(String, nullable=False)\n message = Column(Text, nullable=False)\n resolution_state = Column(\n SQLEnum(ResolutionState), nullable=False, default=ResolutionState.OPEN\n )\n resolution_note = Column(Text, nullable=True)\n caused_by_ref = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n resolved_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"findings\")\n\n\n# #endregion ValidationFinding\n\n\n# #endregion DatasetReviewFindingModels\n" }, { "contract_id": "ValidationFinding", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_finding_models.py", - "start_line": 29, + "start_line": 30, "end_line": 56, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Structured finding record for dataset review validation issues with resolution tracking." }, "relations": [ @@ -59546,8 +47816,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationFinding:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Structured finding record for dataset review validation issues with resolution tracking.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass ValidationFinding(Base):\n __tablename__ = \"validation_findings\"\n\n finding_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n area = Column(SQLEnum(FindingArea), nullable=False)\n severity = Column(SQLEnum(FindingSeverity), nullable=False)\n code = Column(String, nullable=False)\n title = Column(String, nullable=False)\n message = Column(Text, nullable=False)\n resolution_state = Column(\n SQLEnum(ResolutionState), nullable=False, default=ResolutionState.OPEN\n )\n resolution_note = Column(Text, nullable=True)\n caused_by_ref = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n resolved_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"findings\")\n\n\n# [/DEF:ValidationFinding:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationFinding [C:2] [TYPE Class]\n# @BRIEF Structured finding record for dataset review validation issues with resolution tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ValidationFinding(Base):\n __tablename__ = \"validation_findings\"\n\n finding_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n area = Column(SQLEnum(FindingArea), nullable=False)\n severity = Column(SQLEnum(FindingSeverity), nullable=False)\n code = Column(String, nullable=False)\n title = Column(String, nullable=False)\n message = Column(Text, nullable=False)\n resolution_state = Column(\n SQLEnum(ResolutionState), nullable=False, default=ResolutionState.OPEN\n )\n resolution_note = Column(Text, nullable=True)\n caused_by_ref = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n resolved_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"findings\")\n\n\n# #endregion ValidationFinding\n" }, { "contract_id": "DatasetReviewMappingModels", @@ -59558,7 +47828,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "Domain", "PURPOSE": "Execution mapping model linking imported filters to template variables with approval gates." }, @@ -59587,19 +47857,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewMappingModels:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Execution mapping model linking imported filters to template variables with approval gates.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION: DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Column,\n String,\n Text,\n Boolean,\n DateTime,\n ForeignKey,\n JSON,\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.mapping import Base\nfrom src.models.dataset_review_pkg._enums import (\n MappingMethod,\n MappingWarningLevel,\n ApprovalState,\n)\n\n\n# [DEF:ExecutionMapping:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One filter-to-variable mapping with approval gate, effective value, and transformation metadata.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\n# @INVARIANT: Explicit approval is required before launch when requires_explicit_approval is true.\nclass ExecutionMapping(Base):\n __tablename__ = \"execution_mappings\"\n\n mapping_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n filter_id = Column(String, nullable=False)\n variable_id = Column(String, nullable=False)\n mapping_method = Column(SQLEnum(MappingMethod), nullable=False)\n raw_input_value = Column(JSON, nullable=False)\n effective_value = Column(JSON, nullable=True)\n transformation_note = Column(Text, nullable=True)\n warning_level = Column(SQLEnum(MappingWarningLevel), nullable=True)\n requires_explicit_approval = Column(Boolean, nullable=False, default=False)\n approval_state = Column(SQLEnum(ApprovalState), nullable=False, default=ApprovalState.NOT_REQUIRED)\n approved_by_user_id = Column(String, nullable=True)\n approved_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"execution_mappings\")\n\n\n# [/DEF:ExecutionMapping:Class]\n\n\n# [/DEF:DatasetReviewMappingModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewMappingModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, mapping, model]\n# @BRIEF Execution mapping model linking imported filters to template variables with approval gates.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n JSON,\n Boolean,\n Column,\n DateTime,\n ForeignKey,\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 ApprovalState,\n MappingMethod,\n MappingWarningLevel,\n)\nfrom src.models.mapping import Base\n\n\n# #region ExecutionMapping [C:2] [TYPE Class]\n# @BRIEF One filter-to-variable mapping with approval gate, effective value, and transformation metadata.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @INVARIANT: Explicit approval is required before launch when requires_explicit_approval is true.\nclass ExecutionMapping(Base):\n __tablename__ = \"execution_mappings\"\n\n mapping_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n filter_id = Column(String, nullable=False)\n variable_id = Column(String, nullable=False)\n mapping_method = Column(SQLEnum(MappingMethod), nullable=False)\n raw_input_value = Column(JSON, nullable=False)\n effective_value = Column(JSON, nullable=True)\n transformation_note = Column(Text, nullable=True)\n warning_level = Column(SQLEnum(MappingWarningLevel), nullable=True)\n requires_explicit_approval = Column(Boolean, nullable=False, default=False)\n approval_state = Column(SQLEnum(ApprovalState), nullable=False, default=ApprovalState.NOT_REQUIRED)\n approved_by_user_id = Column(String, nullable=True)\n approved_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"execution_mappings\")\n\n\n# #endregion ExecutionMapping\n\n\n# #endregion DatasetReviewMappingModels\n" }, { "contract_id": "ExecutionMapping", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_mapping_models.py", - "start_line": 31, + "start_line": 32, "end_line": 58, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "Explicit approval is required before launch when requires_explicit_approval is true.", "PURPOSE": "One filter-to-variable mapping with approval gate, effective value, and transformation metadata." }, @@ -59631,8 +47901,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ExecutionMapping:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One filter-to-variable mapping with approval gate, effective value, and transformation metadata.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\n# @INVARIANT: Explicit approval is required before launch when requires_explicit_approval is true.\nclass ExecutionMapping(Base):\n __tablename__ = \"execution_mappings\"\n\n mapping_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n filter_id = Column(String, nullable=False)\n variable_id = Column(String, nullable=False)\n mapping_method = Column(SQLEnum(MappingMethod), nullable=False)\n raw_input_value = Column(JSON, nullable=False)\n effective_value = Column(JSON, nullable=True)\n transformation_note = Column(Text, nullable=True)\n warning_level = Column(SQLEnum(MappingWarningLevel), nullable=True)\n requires_explicit_approval = Column(Boolean, nullable=False, default=False)\n approval_state = Column(SQLEnum(ApprovalState), nullable=False, default=ApprovalState.NOT_REQUIRED)\n approved_by_user_id = Column(String, nullable=True)\n approved_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"execution_mappings\")\n\n\n# [/DEF:ExecutionMapping:Class]\n" + "anchor_syntax": "region", + "body": "# #region ExecutionMapping [C:2] [TYPE Class]\n# @BRIEF One filter-to-variable mapping with approval gate, effective value, and transformation metadata.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @INVARIANT: Explicit approval is required before launch when requires_explicit_approval is true.\nclass ExecutionMapping(Base):\n __tablename__ = \"execution_mappings\"\n\n mapping_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n filter_id = Column(String, nullable=False)\n variable_id = Column(String, nullable=False)\n mapping_method = Column(SQLEnum(MappingMethod), nullable=False)\n raw_input_value = Column(JSON, nullable=False)\n effective_value = Column(JSON, nullable=True)\n transformation_note = Column(Text, nullable=True)\n warning_level = Column(SQLEnum(MappingWarningLevel), nullable=True)\n requires_explicit_approval = Column(Boolean, nullable=False, default=False)\n approval_state = Column(SQLEnum(ApprovalState), nullable=False, default=ApprovalState.NOT_REQUIRED)\n approved_by_user_id = Column(String, nullable=True)\n approved_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"execution_mappings\")\n\n\n# #endregion ExecutionMapping\n" }, { "contract_id": "DatasetReviewProfileModels", @@ -59643,7 +47913,7 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "Domain", "PURPOSE": "Dataset profile model capturing business summary, confidence, and completeness metadata." }, @@ -59672,19 +47942,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewProfileModels:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Dataset profile model capturing business summary, confidence, and completeness metadata.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION: DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Column,\n String,\n Text,\n Float,\n Boolean,\n DateTime,\n ForeignKey,\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.mapping import Base\nfrom src.models.dataset_review_pkg._enums import (\n BusinessSummarySource,\n ConfidenceState,\n)\n\n\n# [DEF:DatasetProfile:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetProfile(Base):\n __tablename__ = \"dataset_profiles\"\n\n profile_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String,\n ForeignKey(\"dataset_review_sessions.session_id\"),\n nullable=False,\n unique=True,\n )\n dataset_name = Column(String, nullable=False)\n schema_name = Column(String, nullable=True)\n database_name = Column(String, nullable=True)\n business_summary = Column(Text, nullable=False)\n business_summary_source = Column(SQLEnum(BusinessSummarySource), nullable=False)\n description = Column(Text, nullable=True)\n dataset_type = Column(String, nullable=True)\n is_sqllab_view = Column(Boolean, nullable=False, default=False)\n completeness_score = Column(Float, nullable=True)\n confidence_state = Column(SQLEnum(ConfidenceState), nullable=False)\n has_blocking_findings = Column(Boolean, nullable=False, default=False)\n has_warning_findings = Column(Boolean, nullable=False, default=False)\n manual_summary_locked = Column(Boolean, nullable=False, default=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"profile\")\n\n\n# [/DEF:DatasetProfile:Class]\n\n\n# [/DEF:DatasetReviewProfileModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewProfileModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, profile, summary]\n# @BRIEF Dataset profile model capturing business summary, confidence, and completeness metadata.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums:Module]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Float,\n ForeignKey,\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 BusinessSummarySource,\n ConfidenceState,\n)\nfrom src.models.mapping import Base\n\n\n# #region DatasetProfile [C:2] [TYPE Class]\n# @BRIEF One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetProfile(Base):\n __tablename__ = \"dataset_profiles\"\n\n profile_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String,\n ForeignKey(\"dataset_review_sessions.session_id\"),\n nullable=False,\n unique=True,\n )\n dataset_name = Column(String, nullable=False)\n schema_name = Column(String, nullable=True)\n database_name = Column(String, nullable=True)\n business_summary = Column(Text, nullable=False)\n business_summary_source = Column(SQLEnum(BusinessSummarySource), nullable=False)\n description = Column(Text, nullable=True)\n dataset_type = Column(String, nullable=True)\n is_sqllab_view = Column(Boolean, nullable=False, default=False)\n completeness_score = Column(Float, nullable=True)\n confidence_state = Column(SQLEnum(ConfidenceState), nullable=False)\n has_blocking_findings = Column(Boolean, nullable=False, default=False)\n has_warning_findings = Column(Boolean, nullable=False, default=False)\n manual_summary_locked = Column(Boolean, nullable=False, default=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"profile\")\n\n\n# #endregion DatasetProfile\n\n\n# #endregion DatasetReviewProfileModels\n" }, { "contract_id": "DatasetProfile", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_profile_models.py", - "start_line": 30, + "start_line": 31, "end_line": 65, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness." }, "relations": [ @@ -59706,19 +47976,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetProfile:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetProfile(Base):\n __tablename__ = \"dataset_profiles\"\n\n profile_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String,\n ForeignKey(\"dataset_review_sessions.session_id\"),\n nullable=False,\n unique=True,\n )\n dataset_name = Column(String, nullable=False)\n schema_name = Column(String, nullable=True)\n database_name = Column(String, nullable=True)\n business_summary = Column(Text, nullable=False)\n business_summary_source = Column(SQLEnum(BusinessSummarySource), nullable=False)\n description = Column(Text, nullable=True)\n dataset_type = Column(String, nullable=True)\n is_sqllab_view = Column(Boolean, nullable=False, default=False)\n completeness_score = Column(Float, nullable=True)\n confidence_state = Column(SQLEnum(ConfidenceState), nullable=False)\n has_blocking_findings = Column(Boolean, nullable=False, default=False)\n has_warning_findings = Column(Boolean, nullable=False, default=False)\n manual_summary_locked = Column(Boolean, nullable=False, default=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"profile\")\n\n\n# [/DEF:DatasetProfile:Class]\n" + "anchor_syntax": "region", + "body": "# #region DatasetProfile [C:2] [TYPE Class]\n# @BRIEF One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetProfile(Base):\n __tablename__ = \"dataset_profiles\"\n\n profile_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String,\n ForeignKey(\"dataset_review_sessions.session_id\"),\n nullable=False,\n unique=True,\n )\n dataset_name = Column(String, nullable=False)\n schema_name = Column(String, nullable=True)\n database_name = Column(String, nullable=True)\n business_summary = Column(Text, nullable=False)\n business_summary_source = Column(SQLEnum(BusinessSummarySource), nullable=False)\n description = Column(Text, nullable=True)\n dataset_type = Column(String, nullable=True)\n is_sqllab_view = Column(Boolean, nullable=False, default=False)\n completeness_score = Column(Float, nullable=True)\n confidence_state = Column(SQLEnum(ConfidenceState), nullable=False)\n has_blocking_findings = Column(Boolean, nullable=False, default=False)\n has_warning_findings = Column(Boolean, nullable=False, default=False)\n manual_summary_locked = Column(Boolean, nullable=False, default=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"profile\")\n\n\n# #endregion DatasetProfile\n" }, { "contract_id": "DatasetReviewSemanticModels", "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_semantic_models.py", "start_line": 1, - "end_line": 139, + "end_line": 137, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Manual overrides are never silently replaced by imported, inferred, or AI-generated values.", "LAYER": "Domain", "PURPOSE": "Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment." @@ -59748,19 +48018,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewSemanticModels:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 Column,\n String,\n Integer,\n Text,\n Float,\n Boolean,\n DateTime,\n ForeignKey,\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.mapping import Base\nfrom src.models.dataset_review_pkg._enums import (\n SemanticSourceType,\n TrustLevel,\n SemanticSourceStatus,\n FieldKind,\n FieldProvenance,\n CandidateMatchType,\n CandidateStatus,\n)\n\n\n# [DEF:SemanticSource:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:SemanticSource:Class]\n\n\n# [DEF:SemanticFieldEntry:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:SemanticFieldEntry:Class]\n\n\n# [DEF:SemanticCandidate:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:SemanticCandidate:Class]\n\n\n# [/DEF:DatasetReviewSemanticModels:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "SemanticSource", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_semantic_models.py", - "start_line": 37, + "start_line": 38, "end_line": 64, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Registered semantic enrichment source with trust level and application status." }, "relations": [ @@ -59782,19 +48052,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SemanticSource:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:SemanticSource:Class]\n" + "anchor_syntax": "region", + "body": "# #region SemanticSource [C:2] [TYPE Class]\n# @BRIEF Registered semantic enrichment source with trust level and application status.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SemanticSource(Base):\n __tablename__ = \"semantic_sources\"\n\n source_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n source_type = Column(SQLEnum(SemanticSourceType), nullable=False)\n source_ref = Column(String, nullable=False)\n source_version = Column(String, nullable=False)\n display_name = Column(String, nullable=False)\n trust_level = Column(SQLEnum(TrustLevel), nullable=False)\n schema_overlap_score = Column(Float, nullable=True)\n status = Column(\n SQLEnum(SemanticSourceStatus),\n nullable=False,\n default=SemanticSourceStatus.AVAILABLE,\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_sources\")\n\n\n# #endregion SemanticSource\n" }, { "contract_id": "SemanticFieldEntry", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_semantic_models.py", "start_line": 67, - "end_line": 107, + "end_line": 106, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Locked fields preserve their active value regardless of later candidate proposals.", "PURPOSE": "Per-field semantic metadata entry with provenance tracking, lock state, and candidate set." }, @@ -59823,19 +48093,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SemanticFieldEntry:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:SemanticFieldEntry:Class]\n" + "anchor_syntax": "region", + "body": "# #region SemanticFieldEntry [C:3] [TYPE Class]\n# @BRIEF Per-field semantic metadata entry with provenance tracking, lock state, and candidate set.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @INVARIANT: Locked fields preserve their active value regardless of later candidate proposals.\nclass SemanticFieldEntry(Base):\n __tablename__ = \"semantic_field_entries\"\n\n field_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n field_name = Column(String, nullable=False)\n field_kind = Column(SQLEnum(FieldKind), nullable=False)\n verbose_name = Column(String, nullable=True)\n description = Column(Text, nullable=True)\n display_format = Column(String, nullable=True)\n provenance = Column(\n SQLEnum(FieldProvenance), nullable=False, default=FieldProvenance.UNRESOLVED\n )\n source_id = Column(String, nullable=True)\n source_version = Column(String, nullable=True)\n confidence_rank = Column(Integer, nullable=True)\n is_locked = Column(Boolean, nullable=False, default=False)\n has_conflict = Column(Boolean, nullable=False, default=False)\n needs_review = Column(Boolean, nullable=False, default=True)\n last_changed_by = Column(String, nullable=False)\n user_feedback = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_fields\")\n candidates = relationship(\n \"SemanticCandidate\", back_populates=\"field\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion SemanticFieldEntry\n" }, { "contract_id": "SemanticCandidate", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_semantic_models.py", - "start_line": 110, - "end_line": 136, + "start_line": 109, + "end_line": 134, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "One proposed semantic value for a field entry, ranked by match type and confidence." }, "relations": [ @@ -59857,19 +48127,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SemanticCandidate:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:SemanticCandidate:Class]\n" + "anchor_syntax": "region", + "body": "# #region SemanticCandidate [C:2] [TYPE Class]\n# @BRIEF One proposed semantic value for a field entry, ranked by match type and confidence.\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\nclass SemanticCandidate(Base):\n __tablename__ = \"semantic_candidates\"\n\n candidate_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n field_id = Column(\n String, ForeignKey(\"semantic_field_entries.field_id\"), nullable=False\n )\n source_id = Column(String, nullable=True)\n candidate_rank = Column(Integer, nullable=False)\n match_type = Column(SQLEnum(CandidateMatchType), nullable=False)\n confidence_score = Column(Float, nullable=False)\n proposed_verbose_name = Column(String, nullable=True)\n proposed_description = Column(Text, nullable=True)\n proposed_display_format = Column(String, nullable=True)\n status = Column(\n SQLEnum(CandidateStatus), nullable=False, default=CandidateStatus.PROPOSED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n field = relationship(\"SemanticFieldEntry\", back_populates=\"candidates\")\n\n\n# #endregion SemanticCandidate\n" }, { "contract_id": "DatasetReviewSessionModels", "contract_type": "Module", "file_path": "backend/src/models/dataset_review_pkg/_session_models.py", "start_line": 1, - "end_line": 156, + "end_line": 155, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Session and profile entities are strictly scoped to an authenticated user.", "LAYER": "Domain", "PURPOSE": "Session aggregate root and collaborator models for dataset review orchestration." @@ -59899,19 +48169,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewSessionModels:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: 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 String,\n Integer,\n DateTime,\n ForeignKey,\n Enum as SQLEnum,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.mapping import Base\nfrom src.models.dataset_review_pkg._enums import (\n SessionStatus,\n SessionPhase,\n ReadinessState,\n RecommendedAction,\n SessionCollaboratorRole,\n)\n\n\n# [DEF:SessionCollaborator:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:SessionCollaborator:Class]\n\n\n# [DEF:DatasetReviewSession:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:DatasetReviewSession:Class]\n\n\n# [/DEF:DatasetReviewSessionModels:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "SessionCollaborator", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_session_models.py", - "start_line": 32, + "start_line": 33, "end_line": 51, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "RBAC collaborator record linking a user to a dataset review session with a specific role." }, "relations": [ @@ -59933,19 +48203,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionCollaborator:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:SessionCollaborator:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionCollaborator [C:2] [TYPE Class]\n# @BRIEF RBAC collaborator record linking a user to a dataset review session with a specific role.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionCollaborator(Base):\n __tablename__ = \"session_collaborators\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n role = Column(SQLEnum(SessionCollaboratorRole), nullable=False)\n added_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"collaborators\")\n user = relationship(\"User\")\n\n\n# #endregion SessionCollaborator\n" }, { "contract_id": "DatasetReviewSession", "contract_type": "Class", "file_path": "backend/src/models/dataset_review_pkg/_session_models.py", "start_line": 54, - "end_line": 153, + "end_line": 152, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Optimistic-lock version column prevents lost-update races on concurrent mutations.", "PURPOSE": "Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions." }, @@ -60040,34 +48310,26 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewSession:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:DatasetReviewSession:Class]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewSession [C:3] [TYPE Class]\n# @BRIEF Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions.\n# @RELATION DEPENDS_ON -> [SessionCollaborator]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [SemanticSource]\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [ExecutionMapping]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DEPENDS_ON -> [DatasetRunContext]\n# @RELATION DEPENDS_ON -> [ExportArtifact]\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @INVARIANT: Optimistic-lock version column prevents lost-update races on concurrent mutations.\nclass DatasetReviewSession(Base):\n __tablename__ = \"dataset_review_sessions\"\n\n session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n environment_id = Column(String, ForeignKey(\"environments.id\"), 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" }, { "contract_id": "FilterStateModels", "contract_type": "Module", "file_path": "backend/src/models/filter_state.py", "start_line": 1, - "end_line": 151, + "end_line": 143, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "Models", - "PURPOSE": "Pydantic models for Superset native filter state extraction and restoration.", - "SEMANTICS": [ - "superset", - "native", - "filters", - "pydantic", - "models", - "dataclasses" - ] + "PURPOSE": "Pydantic models for Superset native filter state extraction and restoration." }, "relations": [ { "source_id": "FilterStateModels", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "pydantic", "target_ref": "[pydantic]" } @@ -60081,39 +48343,21 @@ "actual_complexity": 2, "contract_type": "Module" } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:FilterStateModels:Module]\n#\n# @COMPLEXITY: 2\n# @SEMANTICS: superset, native, filters, pydantic, models, dataclasses\n# @PURPOSE: Pydantic models for Superset native filter state extraction and restoration.\n# @LAYER: Models\n# @RELATION: [DEPENDS_ON] ->[pydantic]\n\n# [SECTION: IMPORTS]\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import BaseModel, ConfigDict, Field\n# [/SECTION]\n\n\n# [DEF:FilterState:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Represents the state of a single native filter.\n# @DATA_CONTRACT: Input[extraFormData: Dict, filterState: Dict, ownState: Optional[Dict]] -> Model[FilterState]\nclass FilterState(BaseModel):\n \"\"\"Single native filter state with extraFormData, filterState, and ownState.\"\"\"\n \n model_config = ConfigDict(extra=\"allow\")\n \n extraFormData: Dict[str, Any] = Field(default_factory=dict, description=\"Extra form data for the filter\")\n filterState: Dict[str, Any] = Field(default_factory=dict, description=\"Current filter state\")\n ownState: Dict[str, Any] = Field(default_factory=dict, description=\"Own state of the filter\")\n# [/DEF:FilterState:Model]\n\n\n# [DEF:NativeFilterDataMask:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Represents the dataMask containing all native filter states.\n# @DATA_CONTRACT: Input[Dict[filter_id, FilterState]] -> Model[NativeFilterDataMask]\nclass NativeFilterDataMask(BaseModel):\n \"\"\"Container for all native filter states in a dashboard.\"\"\"\n \n model_config = ConfigDict(extra=\"allow\")\n \n filters: Dict[str, Any] = Field(default_factory=dict, description=\"Map of filter ID to filter state data\")\n \n def get_filter_ids(self) -> List[str]:\n \"\"\"Return list of all filter IDs.\"\"\"\n return list(self.filters.keys())\n \n def get_extra_form_data(self, filter_id: str) -> Dict[str, Any]:\n \"\"\"Get extraFormData for a specific filter.\"\"\"\n filter_state = self.filters.get(filter_id)\n if filter_state:\n return filter_state.extraFormData\n return {}\n# [/DEF:NativeFilterDataMask:Model]\n\n\n# [DEF:ParsedNativeFilters:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Result of parsing native filters from permalink or native_filters_key.\n# @DATA_CONTRACT: Input[dataMask: Dict, metadata: Dict] -> Model[ParsedNativeFilters]\nclass ParsedNativeFilters(BaseModel):\n \"\"\"Result of extracting native filters from a Superset URL.\"\"\"\n \n model_config = ConfigDict(extra=\"allow\")\n \n dataMask: Dict[str, Any] = Field(default_factory=dict, description=\"Extracted dataMask from filters\")\n filter_type: Optional[str] = Field(default=None, description=\"Type of filter: permalink, native_filters_key, or native_filters\")\n dashboard_id: Optional[str] = Field(default=None, description=\"Dashboard ID if available\")\n permalink_key: Optional[str] = Field(default=None, description=\"Permalink key if used\")\n filter_state_key: Optional[str] = Field(default=None, description=\"Filter state key if used\")\n active_tabs: List[str] = Field(default_factory=list, description=\"Active tabs in dashboard\")\n anchor: Optional[str] = Field(default=None, description=\"Anchor position in dashboard\")\n chart_states: Dict[str, Any] = Field(default_factory=dict, description=\"Chart states in dashboard\")\n \n def has_filters(self) -> bool:\n \"\"\"Check if any filters were extracted.\"\"\"\n return bool(self.dataMask)\n \n def get_filter_count(self) -> int:\n \"\"\"Get the number of filters extracted.\"\"\"\n return len(self.dataMask)\n# [/DEF:ParsedNativeFilters:Model]\n\n\n# [DEF:DashboardURLFilterExtraction:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Result of parsing a complete dashboard URL for filter information.\n# @DATA_CONTRACT: Input[url: str, dashboard_id: Optional, filter_type: Optional, filters: Dict] -> Model[DashboardURLFilterExtraction]\nclass DashboardURLFilterExtraction(BaseModel):\n \"\"\"Result of parsing a Superset dashboard URL to extract filter state.\"\"\"\n \n model_config = ConfigDict(extra=\"allow\")\n \n url: str = Field(..., description=\"Original dashboard URL\")\n dashboard_id: Optional[str] = Field(default=None, description=\"Extracted dashboard ID\")\n filter_type: Optional[str] = Field(default=None, description=\"Type of filter found\")\n filters: ParsedNativeFilters = Field(default_factory=ParsedNativeFilters, description=\"Extracted filter data\")\n success: bool = Field(default=True, description=\"Whether extraction was successful\")\n error: Optional[str] = Field(default=None, description=\"Error message if extraction failed\")\n# [/DEF:DashboardURLFilterExtraction:Model]\n\n\n# [DEF:ExtraFormDataMerge:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Configuration for merging extraFormData from different sources.\n# @DATA_CONTRACT: Input[append_keys: List[str], override_keys: List[str]] -> Model[ExtraFormDataMerge]\nclass ExtraFormDataMerge(BaseModel):\n \"\"\"Configuration for merging extraFormData between original and new filter values.\"\"\"\n \n # Keys that should be appended (arrays, filters)\n append_keys: List[str] = Field(\n default_factory=lambda: [\"filters\", \"extras\", \"columns\", \"metrics\"],\n description=\"Keys that should be merged by appending\"\n )\n # Keys that should be overridden (single values)\n override_keys: List[str] = Field(\n default_factory=lambda: [\"time_range\", \"time_grain_sqla\", \"time_column\", \"granularity\"],\n description=\"Keys that should be overridden by new values\"\n )\n \n def merge(self, original: Dict[str, Any], new: Dict[str, Any]) -> Dict[str, Any]:\n \"\"\"\n Merge two extraFormData dictionaries.\n \n @param original: Original extraFormData from dashboard metadata\n @param new: New extraFormData from URL/permalink\n @return: Merged extraFormData dictionary\n \"\"\"\n result = {}\n \n # Start with original\n for key, value in original.items():\n result[key] = value\n \n # Apply overrides and appends from new\n for key, new_value in new.items():\n if key in self.override_keys:\n # Override the value\n result[key] = new_value\n elif key in self.append_keys:\n # Append to the existing value\n existing = result.get(key)\n if isinstance(existing, list) and isinstance(new_value, list):\n result[key] = existing + new_value\n else:\n result[key] = new_value\n else:\n result[key] = new_value\n \n return result\n# [/DEF:ExtraFormDataMerge:Model]\n\n\n# [/DEF:FilterStateModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region FilterStateModels [C:2] [TYPE Module] [SEMANTICS pydantic, filter, model, schema, superset, filter-state]\n#\n# @BRIEF Pydantic models for Superset native filter state extraction and restoration.\n# @LAYER: Models\n# @RELATION DEPENDS_ON -> [pydantic]\n\nfrom typing import Any\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\n# #region FilterState [C:2] [TYPE Model]\n# @BRIEF Represents the state of a single native filter.\n# @DATA_CONTRACT: Input[extraFormData: Dict, filterState: Dict, ownState: Optional[Dict]] -> Model[FilterState]\nclass FilterState(BaseModel):\n \"\"\"Single native filter state with extraFormData, filterState, and ownState.\"\"\"\n\n model_config = ConfigDict(extra=\"allow\")\n\n extraFormData: dict[str, Any] = Field(default_factory=dict, description=\"Extra form data for the filter\")\n filterState: dict[str, Any] = Field(default_factory=dict, description=\"Current filter state\")\n ownState: dict[str, Any] = Field(default_factory=dict, description=\"Own state of the filter\")\n# #endregion FilterState\n\n\n# #region NativeFilterDataMask [C:2] [TYPE Model]\n# @BRIEF Represents the dataMask containing all native filter states.\n# @DATA_CONTRACT: Input[Dict[filter_id, FilterState]] -> Model[NativeFilterDataMask]\nclass NativeFilterDataMask(BaseModel):\n \"\"\"Container for all native filter states in a dashboard.\"\"\"\n\n model_config = ConfigDict(extra=\"allow\")\n\n filters: dict[str, Any] = Field(default_factory=dict, description=\"Map of filter ID to filter state data\")\n\n def get_filter_ids(self) -> list[str]:\n \"\"\"Return list of all filter IDs.\"\"\"\n return list(self.filters.keys())\n\n def get_extra_form_data(self, filter_id: str) -> dict[str, Any]:\n \"\"\"Get extraFormData for a specific filter.\"\"\"\n filter_state = self.filters.get(filter_id)\n if filter_state:\n return filter_state.extraFormData\n return {}\n# #endregion NativeFilterDataMask\n\n\n# #region ParsedNativeFilters [C:2] [TYPE Model]\n# @BRIEF Result of parsing native filters from permalink or native_filters_key.\n# @DATA_CONTRACT: Input[dataMask: Dict, metadata: Dict] -> Model[ParsedNativeFilters]\nclass ParsedNativeFilters(BaseModel):\n \"\"\"Result of extracting native filters from a Superset URL.\"\"\"\n\n model_config = ConfigDict(extra=\"allow\")\n\n dataMask: dict[str, Any] = Field(default_factory=dict, description=\"Extracted dataMask from filters\")\n filter_type: str | None = Field(default=None, description=\"Type of filter: permalink, native_filters_key, or native_filters\")\n dashboard_id: str | None = Field(default=None, description=\"Dashboard ID if available\")\n permalink_key: str | None = Field(default=None, description=\"Permalink key if used\")\n filter_state_key: str | None = Field(default=None, description=\"Filter state key if used\")\n active_tabs: list[str] = Field(default_factory=list, description=\"Active tabs in dashboard\")\n anchor: str | None = Field(default=None, description=\"Anchor position in dashboard\")\n chart_states: dict[str, Any] = Field(default_factory=dict, description=\"Chart states in dashboard\")\n\n def has_filters(self) -> bool:\n \"\"\"Check if any filters were extracted.\"\"\"\n return bool(self.dataMask)\n\n def get_filter_count(self) -> int:\n \"\"\"Get the number of filters extracted.\"\"\"\n return len(self.dataMask)\n# #endregion ParsedNativeFilters\n\n\n# #region DashboardURLFilterExtraction [C:2] [TYPE Model]\n# @BRIEF Result of parsing a complete dashboard URL for filter information.\n# @DATA_CONTRACT: Input[url: str, dashboard_id: Optional, filter_type: Optional, filters: Dict] -> Model[DashboardURLFilterExtraction]\nclass DashboardURLFilterExtraction(BaseModel):\n \"\"\"Result of parsing a Superset dashboard URL to extract filter state.\"\"\"\n\n model_config = ConfigDict(extra=\"allow\")\n\n url: str = Field(..., description=\"Original dashboard URL\")\n dashboard_id: str | None = Field(default=None, description=\"Extracted dashboard ID\")\n filter_type: str | None = Field(default=None, description=\"Type of filter found\")\n filters: ParsedNativeFilters = Field(default_factory=ParsedNativeFilters, description=\"Extracted filter data\")\n success: bool = Field(default=True, description=\"Whether extraction was successful\")\n error: str | None = Field(default=None, description=\"Error message if extraction failed\")\n# #endregion DashboardURLFilterExtraction\n\n\n# #region ExtraFormDataMerge [C:2] [TYPE Model]\n# @BRIEF Configuration for merging extraFormData from different sources.\n# @DATA_CONTRACT: Input[append_keys: List[str], override_keys: List[str]] -> Model[ExtraFormDataMerge]\nclass ExtraFormDataMerge(BaseModel):\n \"\"\"Configuration for merging extraFormData between original and new filter values.\"\"\"\n\n # Keys that should be appended (arrays, filters)\n append_keys: list[str] = Field(\n default_factory=lambda: [\"filters\", \"extras\", \"columns\", \"metrics\"],\n description=\"Keys that should be merged by appending\"\n )\n # Keys that should be overridden (single values)\n override_keys: list[str] = Field(\n default_factory=lambda: [\"time_range\", \"time_grain_sqla\", \"time_column\", \"granularity\"],\n description=\"Keys that should be overridden by new values\"\n )\n\n def merge(self, original: dict[str, Any], new: dict[str, Any]) -> dict[str, Any]:\n \"\"\"\n Merge two extraFormData dictionaries.\n \n @param original: Original extraFormData from dashboard metadata\n @param new: New extraFormData from URL/permalink\n @return: Merged extraFormData dictionary\n \"\"\"\n result = {}\n\n # Start with original\n for key, value in original.items():\n result[key] = value\n\n # Apply overrides and appends from new\n for key, new_value in new.items():\n if key in self.override_keys:\n # Override the value\n result[key] = new_value\n elif key in self.append_keys:\n # Append to the existing value\n existing = result.get(key)\n if isinstance(existing, list) and isinstance(new_value, list):\n result[key] = existing + new_value\n else:\n result[key] = new_value\n else:\n result[key] = new_value\n\n return result\n# #endregion ExtraFormDataMerge\n\n\n# #endregion FilterStateModels\n" }, { "contract_id": "FilterState", "contract_type": "Model", "file_path": "backend/src/models/filter_state.py", - "start_line": 15, - "end_line": 27, + "start_line": 12, + "end_line": 23, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "DATA_CONTRACT": "Input[extraFormData: Dict, filterState: Dict, ownState: Optional[Dict]] -> Model[FilterState]", "PURPOSE": "Represents the state of a single native filter." }, @@ -60197,19 +48441,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FilterState:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Represents the state of a single native filter.\n# @DATA_CONTRACT: Input[extraFormData: Dict, filterState: Dict, ownState: Optional[Dict]] -> Model[FilterState]\nclass FilterState(BaseModel):\n \"\"\"Single native filter state with extraFormData, filterState, and ownState.\"\"\"\n \n model_config = ConfigDict(extra=\"allow\")\n \n extraFormData: Dict[str, Any] = Field(default_factory=dict, description=\"Extra form data for the filter\")\n filterState: Dict[str, Any] = Field(default_factory=dict, description=\"Current filter state\")\n ownState: Dict[str, Any] = Field(default_factory=dict, description=\"Own state of the filter\")\n# [/DEF:FilterState:Model]\n" + "anchor_syntax": "region", + "body": "# #region FilterState [C:2] [TYPE Model]\n# @BRIEF Represents the state of a single native filter.\n# @DATA_CONTRACT: Input[extraFormData: Dict, filterState: Dict, ownState: Optional[Dict]] -> Model[FilterState]\nclass FilterState(BaseModel):\n \"\"\"Single native filter state with extraFormData, filterState, and ownState.\"\"\"\n\n model_config = ConfigDict(extra=\"allow\")\n\n extraFormData: dict[str, Any] = Field(default_factory=dict, description=\"Extra form data for the filter\")\n filterState: dict[str, Any] = Field(default_factory=dict, description=\"Current filter state\")\n ownState: dict[str, Any] = Field(default_factory=dict, description=\"Own state of the filter\")\n# #endregion FilterState\n" }, { "contract_id": "NativeFilterDataMask", "contract_type": "Model", "file_path": "backend/src/models/filter_state.py", - "start_line": 30, - "end_line": 51, + "start_line": 26, + "end_line": 46, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "DATA_CONTRACT": "Input[Dict[filter_id, FilterState]] -> Model[NativeFilterDataMask]", "PURPOSE": "Represents the dataMask containing all native filter states." }, @@ -60293,19 +48537,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:NativeFilterDataMask:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Represents the dataMask containing all native filter states.\n# @DATA_CONTRACT: Input[Dict[filter_id, FilterState]] -> Model[NativeFilterDataMask]\nclass NativeFilterDataMask(BaseModel):\n \"\"\"Container for all native filter states in a dashboard.\"\"\"\n \n model_config = ConfigDict(extra=\"allow\")\n \n filters: Dict[str, Any] = Field(default_factory=dict, description=\"Map of filter ID to filter state data\")\n \n def get_filter_ids(self) -> List[str]:\n \"\"\"Return list of all filter IDs.\"\"\"\n return list(self.filters.keys())\n \n def get_extra_form_data(self, filter_id: str) -> Dict[str, Any]:\n \"\"\"Get extraFormData for a specific filter.\"\"\"\n filter_state = self.filters.get(filter_id)\n if filter_state:\n return filter_state.extraFormData\n return {}\n# [/DEF:NativeFilterDataMask:Model]\n" + "anchor_syntax": "region", + "body": "# #region NativeFilterDataMask [C:2] [TYPE Model]\n# @BRIEF Represents the dataMask containing all native filter states.\n# @DATA_CONTRACT: Input[Dict[filter_id, FilterState]] -> Model[NativeFilterDataMask]\nclass NativeFilterDataMask(BaseModel):\n \"\"\"Container for all native filter states in a dashboard.\"\"\"\n\n model_config = ConfigDict(extra=\"allow\")\n\n filters: dict[str, Any] = Field(default_factory=dict, description=\"Map of filter ID to filter state data\")\n\n def get_filter_ids(self) -> list[str]:\n \"\"\"Return list of all filter IDs.\"\"\"\n return list(self.filters.keys())\n\n def get_extra_form_data(self, filter_id: str) -> dict[str, Any]:\n \"\"\"Get extraFormData for a specific filter.\"\"\"\n filter_state = self.filters.get(filter_id)\n if filter_state:\n return filter_state.extraFormData\n return {}\n# #endregion NativeFilterDataMask\n" }, { "contract_id": "ParsedNativeFilters", "contract_type": "Model", "file_path": "backend/src/models/filter_state.py", - "start_line": 54, - "end_line": 79, + "start_line": 49, + "end_line": 73, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "DATA_CONTRACT": "Input[dataMask: Dict, metadata: Dict] -> Model[ParsedNativeFilters]", "PURPOSE": "Result of parsing native filters from permalink or native_filters_key." }, @@ -60389,19 +48633,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ParsedNativeFilters:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Result of parsing native filters from permalink or native_filters_key.\n# @DATA_CONTRACT: Input[dataMask: Dict, metadata: Dict] -> Model[ParsedNativeFilters]\nclass ParsedNativeFilters(BaseModel):\n \"\"\"Result of extracting native filters from a Superset URL.\"\"\"\n \n model_config = ConfigDict(extra=\"allow\")\n \n dataMask: Dict[str, Any] = Field(default_factory=dict, description=\"Extracted dataMask from filters\")\n filter_type: Optional[str] = Field(default=None, description=\"Type of filter: permalink, native_filters_key, or native_filters\")\n dashboard_id: Optional[str] = Field(default=None, description=\"Dashboard ID if available\")\n permalink_key: Optional[str] = Field(default=None, description=\"Permalink key if used\")\n filter_state_key: Optional[str] = Field(default=None, description=\"Filter state key if used\")\n active_tabs: List[str] = Field(default_factory=list, description=\"Active tabs in dashboard\")\n anchor: Optional[str] = Field(default=None, description=\"Anchor position in dashboard\")\n chart_states: Dict[str, Any] = Field(default_factory=dict, description=\"Chart states in dashboard\")\n \n def has_filters(self) -> bool:\n \"\"\"Check if any filters were extracted.\"\"\"\n return bool(self.dataMask)\n \n def get_filter_count(self) -> int:\n \"\"\"Get the number of filters extracted.\"\"\"\n return len(self.dataMask)\n# [/DEF:ParsedNativeFilters:Model]\n" + "anchor_syntax": "region", + "body": "# #region ParsedNativeFilters [C:2] [TYPE Model]\n# @BRIEF Result of parsing native filters from permalink or native_filters_key.\n# @DATA_CONTRACT: Input[dataMask: Dict, metadata: Dict] -> Model[ParsedNativeFilters]\nclass ParsedNativeFilters(BaseModel):\n \"\"\"Result of extracting native filters from a Superset URL.\"\"\"\n\n model_config = ConfigDict(extra=\"allow\")\n\n dataMask: dict[str, Any] = Field(default_factory=dict, description=\"Extracted dataMask from filters\")\n filter_type: str | None = Field(default=None, description=\"Type of filter: permalink, native_filters_key, or native_filters\")\n dashboard_id: str | None = Field(default=None, description=\"Dashboard ID if available\")\n permalink_key: str | None = Field(default=None, description=\"Permalink key if used\")\n filter_state_key: str | None = Field(default=None, description=\"Filter state key if used\")\n active_tabs: list[str] = Field(default_factory=list, description=\"Active tabs in dashboard\")\n anchor: str | None = Field(default=None, description=\"Anchor position in dashboard\")\n chart_states: dict[str, Any] = Field(default_factory=dict, description=\"Chart states in dashboard\")\n\n def has_filters(self) -> bool:\n \"\"\"Check if any filters were extracted.\"\"\"\n return bool(self.dataMask)\n\n def get_filter_count(self) -> int:\n \"\"\"Get the number of filters extracted.\"\"\"\n return len(self.dataMask)\n# #endregion ParsedNativeFilters\n" }, { "contract_id": "DashboardURLFilterExtraction", "contract_type": "Model", "file_path": "backend/src/models/filter_state.py", - "start_line": 82, - "end_line": 97, + "start_line": 76, + "end_line": 90, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "DATA_CONTRACT": "Input[url: str, dashboard_id: Optional, filter_type: Optional, filters: Dict] -> Model[DashboardURLFilterExtraction]", "PURPOSE": "Result of parsing a complete dashboard URL for filter information." }, @@ -60485,19 +48729,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardURLFilterExtraction:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Result of parsing a complete dashboard URL for filter information.\n# @DATA_CONTRACT: Input[url: str, dashboard_id: Optional, filter_type: Optional, filters: Dict] -> Model[DashboardURLFilterExtraction]\nclass DashboardURLFilterExtraction(BaseModel):\n \"\"\"Result of parsing a Superset dashboard URL to extract filter state.\"\"\"\n \n model_config = ConfigDict(extra=\"allow\")\n \n url: str = Field(..., description=\"Original dashboard URL\")\n dashboard_id: Optional[str] = Field(default=None, description=\"Extracted dashboard ID\")\n filter_type: Optional[str] = Field(default=None, description=\"Type of filter found\")\n filters: ParsedNativeFilters = Field(default_factory=ParsedNativeFilters, description=\"Extracted filter data\")\n success: bool = Field(default=True, description=\"Whether extraction was successful\")\n error: Optional[str] = Field(default=None, description=\"Error message if extraction failed\")\n# [/DEF:DashboardURLFilterExtraction:Model]\n" + "anchor_syntax": "region", + "body": "# #region DashboardURLFilterExtraction [C:2] [TYPE Model]\n# @BRIEF Result of parsing a complete dashboard URL for filter information.\n# @DATA_CONTRACT: Input[url: str, dashboard_id: Optional, filter_type: Optional, filters: Dict] -> Model[DashboardURLFilterExtraction]\nclass DashboardURLFilterExtraction(BaseModel):\n \"\"\"Result of parsing a Superset dashboard URL to extract filter state.\"\"\"\n\n model_config = ConfigDict(extra=\"allow\")\n\n url: str = Field(..., description=\"Original dashboard URL\")\n dashboard_id: str | None = Field(default=None, description=\"Extracted dashboard ID\")\n filter_type: str | None = Field(default=None, description=\"Type of filter found\")\n filters: ParsedNativeFilters = Field(default_factory=ParsedNativeFilters, description=\"Extracted filter data\")\n success: bool = Field(default=True, description=\"Whether extraction was successful\")\n error: str | None = Field(default=None, description=\"Error message if extraction failed\")\n# #endregion DashboardURLFilterExtraction\n" }, { "contract_id": "ExtraFormDataMerge", "contract_type": "Model", "file_path": "backend/src/models/filter_state.py", - "start_line": 100, - "end_line": 148, + "start_line": 93, + "end_line": 140, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "DATA_CONTRACT": "Input[append_keys: List[str], override_keys: List[str]] -> Model[ExtraFormDataMerge]", "PURPOSE": "Configuration for merging extraFormData from different sources." }, @@ -60581,8 +48825,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ExtraFormDataMerge:Model]\n# @COMPLEXITY: 2\n# @PURPOSE: Configuration for merging extraFormData from different sources.\n# @DATA_CONTRACT: Input[append_keys: List[str], override_keys: List[str]] -> Model[ExtraFormDataMerge]\nclass ExtraFormDataMerge(BaseModel):\n \"\"\"Configuration for merging extraFormData between original and new filter values.\"\"\"\n \n # Keys that should be appended (arrays, filters)\n append_keys: List[str] = Field(\n default_factory=lambda: [\"filters\", \"extras\", \"columns\", \"metrics\"],\n description=\"Keys that should be merged by appending\"\n )\n # Keys that should be overridden (single values)\n override_keys: List[str] = Field(\n default_factory=lambda: [\"time_range\", \"time_grain_sqla\", \"time_column\", \"granularity\"],\n description=\"Keys that should be overridden by new values\"\n )\n \n def merge(self, original: Dict[str, Any], new: Dict[str, Any]) -> Dict[str, Any]:\n \"\"\"\n Merge two extraFormData dictionaries.\n \n @param original: Original extraFormData from dashboard metadata\n @param new: New extraFormData from URL/permalink\n @return: Merged extraFormData dictionary\n \"\"\"\n result = {}\n \n # Start with original\n for key, value in original.items():\n result[key] = value\n \n # Apply overrides and appends from new\n for key, new_value in new.items():\n if key in self.override_keys:\n # Override the value\n result[key] = new_value\n elif key in self.append_keys:\n # Append to the existing value\n existing = result.get(key)\n if isinstance(existing, list) and isinstance(new_value, list):\n result[key] = existing + new_value\n else:\n result[key] = new_value\n else:\n result[key] = new_value\n \n return result\n# [/DEF:ExtraFormDataMerge:Model]\n" + "anchor_syntax": "region", + "body": "# #region ExtraFormDataMerge [C:2] [TYPE Model]\n# @BRIEF Configuration for merging extraFormData from different sources.\n# @DATA_CONTRACT: Input[append_keys: List[str], override_keys: List[str]] -> Model[ExtraFormDataMerge]\nclass ExtraFormDataMerge(BaseModel):\n \"\"\"Configuration for merging extraFormData between original and new filter values.\"\"\"\n\n # Keys that should be appended (arrays, filters)\n append_keys: list[str] = Field(\n default_factory=lambda: [\"filters\", \"extras\", \"columns\", \"metrics\"],\n description=\"Keys that should be merged by appending\"\n )\n # Keys that should be overridden (single values)\n override_keys: list[str] = Field(\n default_factory=lambda: [\"time_range\", \"time_grain_sqla\", \"time_column\", \"granularity\"],\n description=\"Keys that should be overridden by new values\"\n )\n\n def merge(self, original: dict[str, Any], new: dict[str, Any]) -> dict[str, Any]:\n \"\"\"\n Merge two extraFormData dictionaries.\n \n @param original: Original extraFormData from dashboard metadata\n @param new: New extraFormData from URL/permalink\n @return: Merged extraFormData dictionary\n \"\"\"\n result = {}\n\n # Start with original\n for key, value in original.items():\n result[key] = value\n\n # Apply overrides and appends from new\n for key, new_value in new.items():\n if key in self.override_keys:\n # Override the value\n result[key] = new_value\n elif key in self.append_keys:\n # Append to the existing value\n existing = result.get(key)\n if isinstance(existing, list) and isinstance(new_value, list):\n result[key] = existing + new_value\n else:\n result[key] = new_value\n else:\n result[key] = new_value\n\n return result\n# #endregion ExtraFormDataMerge\n" }, { "contract_id": "GitModels", @@ -60595,19 +48839,19 @@ "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:GitModels:Module]\n\nimport enum\nfrom datetime import datetime\nfrom sqlalchemy import Column, String, Integer, DateTime, Enum, ForeignKey, Boolean\nimport uuid\nfrom src.core.database import Base\n\nclass GitProvider(str, enum.Enum):\n GITHUB = \"GITHUB\"\n GITLAB = \"GITLAB\"\n GITEA = \"GITEA\"\n\nclass GitStatus(str, enum.Enum):\n CONNECTED = \"CONNECTED\"\n FAILED = \"FAILED\"\n UNKNOWN = \"UNKNOWN\"\n\nclass SyncStatus(str, enum.Enum):\n CLEAN = \"CLEAN\"\n DIRTY = \"DIRTY\"\n CONFLICT = \"CONFLICT\"\n\n# [DEF:GitServerConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Configuration for a Git server connection.\nclass GitServerConfig(Base):\n __tablename__ = \"git_server_configs\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n provider = Column(Enum(GitProvider), nullable=False)\n url = Column(String(255), nullable=False)\n pat = Column(String(255), nullable=False) # PERSONAL ACCESS TOKEN\n default_repository = Column(String(255), nullable=True)\n default_branch = Column(String(255), default=\"main\")\n status = Column(Enum(GitStatus), default=GitStatus.UNKNOWN)\n last_validated = Column(DateTime, default=datetime.utcnow)\n# [/DEF:GitServerConfig:Class]\n\n# [DEF:GitRepository:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Tracking for a local Git repository linked to a dashboard.\nclass GitRepository(Base):\n __tablename__ = \"git_repositories\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n dashboard_id = Column(Integer, nullable=False, unique=True)\n config_id = Column(String(36), ForeignKey(\"git_server_configs.id\"), nullable=False)\n remote_url = Column(String(255), nullable=False)\n local_path = Column(String(255), nullable=False)\n current_branch = Column(String(255), default=\"dev\")\n sync_status = Column(Enum(SyncStatus), default=SyncStatus.CLEAN)\n# [/DEF:GitRepository:Class]\n\n# [DEF:DeploymentEnvironment:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Target Superset environments for dashboard deployment.\nclass DeploymentEnvironment(Base):\n __tablename__ = \"deployment_environments\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n superset_url = Column(String(255), nullable=False)\n superset_token = Column(String(255), nullable=False)\n is_active = Column(Boolean, default=True)\n# [/DEF:DeploymentEnvironment:Class]\n\n# [/DEF:GitModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitModels [TYPE Module] [SEMANTICS sqlalchemy, git, model, config, sync]\n\nimport enum\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Integer, String\n\nfrom src.core.database import Base\n\n\nclass GitProvider(str, enum.Enum):\n GITHUB = \"GITHUB\"\n GITLAB = \"GITLAB\"\n GITEA = \"GITEA\"\n\nclass GitStatus(str, enum.Enum):\n CONNECTED = \"CONNECTED\"\n FAILED = \"FAILED\"\n UNKNOWN = \"UNKNOWN\"\n\nclass SyncStatus(str, enum.Enum):\n CLEAN = \"CLEAN\"\n DIRTY = \"DIRTY\"\n CONFLICT = \"CONFLICT\"\n\n# #region GitServerConfig [C:1] [TYPE Class]\n# @BRIEF Configuration for a Git server connection.\nclass GitServerConfig(Base):\n __tablename__ = \"git_server_configs\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n provider = Column(Enum(GitProvider), nullable=False)\n url = Column(String(255), nullable=False)\n pat = Column(String(255), nullable=False) # PERSONAL ACCESS TOKEN\n default_repository = Column(String(255), nullable=True)\n default_branch = Column(String(255), default=\"main\")\n status = Column(Enum(GitStatus), default=GitStatus.UNKNOWN)\n last_validated = Column(DateTime, default=datetime.utcnow)\n# #endregion GitServerConfig\n\n# #region GitRepository [C:1] [TYPE Class]\n# @BRIEF Tracking for a local Git repository linked to a dashboard.\nclass GitRepository(Base):\n __tablename__ = \"git_repositories\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n dashboard_id = Column(Integer, nullable=False, unique=True)\n config_id = Column(String(36), ForeignKey(\"git_server_configs.id\"), nullable=False)\n remote_url = Column(String(255), nullable=False)\n local_path = Column(String(255), nullable=False)\n current_branch = Column(String(255), default=\"dev\")\n sync_status = Column(Enum(SyncStatus), default=SyncStatus.CLEAN)\n# #endregion GitRepository\n\n# #region DeploymentEnvironment [C:1] [TYPE Class]\n# @BRIEF Target Superset environments for dashboard deployment.\nclass DeploymentEnvironment(Base):\n __tablename__ = \"deployment_environments\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n superset_url = Column(String(255), nullable=False)\n superset_token = Column(String(255), nullable=False)\n is_active = Column(Boolean, default=True)\n# #endregion DeploymentEnvironment\n\n# #endregion GitModels\n" }, { "contract_id": "GitServerConfig", "contract_type": "Class", "file_path": "backend/src/models/git.py", - "start_line": 24, - "end_line": 39, + "start_line": 27, + "end_line": 41, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Configuration for a Git server connection." }, "relations": [], @@ -60622,19 +48866,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServerConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Configuration for a Git server connection.\nclass GitServerConfig(Base):\n __tablename__ = \"git_server_configs\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n provider = Column(Enum(GitProvider), nullable=False)\n url = Column(String(255), nullable=False)\n pat = Column(String(255), nullable=False) # PERSONAL ACCESS TOKEN\n default_repository = Column(String(255), nullable=True)\n default_branch = Column(String(255), default=\"main\")\n status = Column(Enum(GitStatus), default=GitStatus.UNKNOWN)\n last_validated = Column(DateTime, default=datetime.utcnow)\n# [/DEF:GitServerConfig:Class]\n" + "anchor_syntax": "region", + "body": "# #region GitServerConfig [C:1] [TYPE Class]\n# @BRIEF Configuration for a Git server connection.\nclass GitServerConfig(Base):\n __tablename__ = \"git_server_configs\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n provider = Column(Enum(GitProvider), nullable=False)\n url = Column(String(255), nullable=False)\n pat = Column(String(255), nullable=False) # PERSONAL ACCESS TOKEN\n default_repository = Column(String(255), nullable=True)\n default_branch = Column(String(255), default=\"main\")\n status = Column(Enum(GitStatus), default=GitStatus.UNKNOWN)\n last_validated = Column(DateTime, default=datetime.utcnow)\n# #endregion GitServerConfig\n" }, { "contract_id": "GitRepository", "contract_type": "Class", "file_path": "backend/src/models/git.py", - "start_line": 41, - "end_line": 54, + "start_line": 43, + "end_line": 55, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Tracking for a local Git repository linked to a dashboard." }, "relations": [], @@ -60649,19 +48893,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitRepository:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Tracking for a local Git repository linked to a dashboard.\nclass GitRepository(Base):\n __tablename__ = \"git_repositories\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n dashboard_id = Column(Integer, nullable=False, unique=True)\n config_id = Column(String(36), ForeignKey(\"git_server_configs.id\"), nullable=False)\n remote_url = Column(String(255), nullable=False)\n local_path = Column(String(255), nullable=False)\n current_branch = Column(String(255), default=\"dev\")\n sync_status = Column(Enum(SyncStatus), default=SyncStatus.CLEAN)\n# [/DEF:GitRepository:Class]\n" + "anchor_syntax": "region", + "body": "# #region GitRepository [C:1] [TYPE Class]\n# @BRIEF Tracking for a local Git repository linked to a dashboard.\nclass GitRepository(Base):\n __tablename__ = \"git_repositories\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n dashboard_id = Column(Integer, nullable=False, unique=True)\n config_id = Column(String(36), ForeignKey(\"git_server_configs.id\"), nullable=False)\n remote_url = Column(String(255), nullable=False)\n local_path = Column(String(255), nullable=False)\n current_branch = Column(String(255), default=\"dev\")\n sync_status = Column(Enum(SyncStatus), default=SyncStatus.CLEAN)\n# #endregion GitRepository\n" }, { "contract_id": "DeploymentEnvironment", "contract_type": "Class", "file_path": "backend/src/models/git.py", - "start_line": 56, + "start_line": 57, "end_line": 67, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Target Superset environments for dashboard deployment." }, "relations": [], @@ -60676,27 +48920,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DeploymentEnvironment:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Target Superset environments for dashboard deployment.\nclass DeploymentEnvironment(Base):\n __tablename__ = \"deployment_environments\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n superset_url = Column(String(255), nullable=False)\n superset_token = Column(String(255), nullable=False)\n is_active = Column(Boolean, default=True)\n# [/DEF:DeploymentEnvironment:Class]\n" + "anchor_syntax": "region", + "body": "# #region DeploymentEnvironment [C:1] [TYPE Class]\n# @BRIEF Target Superset environments for dashboard deployment.\nclass DeploymentEnvironment(Base):\n __tablename__ = \"deployment_environments\"\n\n id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String(255), nullable=False)\n superset_url = Column(String(255), nullable=False)\n superset_token = Column(String(255), nullable=False)\n is_active = Column(Boolean, default=True)\n# #endregion DeploymentEnvironment\n" }, { "contract_id": "LlmModels", "contract_type": "Module", "file_path": "backend/src/models/llm.py", "start_line": 1, - "end_line": 68, + "end_line": 69, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", - "PURPOSE": "SQLAlchemy models for LLM provider configuration and validation results.", - "SEMANTICS": [ - "llm", - "models", - "sqlalchemy", - "persistence" - ] + "PURPOSE": "SQLAlchemy models for LLM provider configuration and validation results." }, "relations": [ { @@ -60726,15 +48964,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LlmModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: llm, models, sqlalchemy, persistence\n# @PURPOSE: SQLAlchemy models for LLM provider configuration and validation results.\n# @LAYER: Domain\n# @RELATION: INHERITS_FROM -> MappingModels:Base\n\nfrom sqlalchemy import Column, String, Boolean, DateTime, JSON, Text, Time, ForeignKey\nfrom datetime import datetime\nimport uuid\nfrom .mapping import Base\n\ndef generate_uuid():\n return str(uuid.uuid4())\n\n# [DEF:ValidationPolicy:Class]\n# @PURPOSE: Defines a scheduled rule for validating a group of dashboards within an execution window.\nclass ValidationPolicy(Base):\n __tablename__ = \"validation_policies\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n dashboard_ids = Column(JSON, nullable=False) # Array of dashboard IDs\n schedule_days = Column(JSON, nullable=False) # Array of integers (0-6)\n window_start = Column(Time, nullable=False)\n window_end = Column(Time, nullable=False)\n notify_owners = Column(Boolean, default=True)\n custom_channels = Column(JSON, nullable=True) # List of external channels\n alert_condition = Column(String, default=\"FAIL_ONLY\") # FAIL_ONLY, WARN_AND_FAIL, ALWAYS\n created_at = Column(DateTime, default=datetime.utcnow)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)\n# [/DEF:ValidationPolicy:Class]\n\n# [DEF:LLMProvider:Class]\n# @PURPOSE: SQLAlchemy model for LLM provider configuration.\nclass LLMProvider(Base):\n __tablename__ = \"llm_providers\"\n \n id = Column(String, primary_key=True, default=generate_uuid)\n provider_type = Column(String, nullable=False) # openai, openrouter, kilo\n name = Column(String, nullable=False)\n base_url = Column(String, nullable=False)\n api_key = Column(String, nullable=False) # Should be encrypted\n default_model = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n# [/DEF:LLMProvider:Class]\n\n# [DEF:ValidationRecord:Class]\n# @PURPOSE: SQLAlchemy model for dashboard validation history.\nclass ValidationRecord(Base):\n __tablename__ = \"llm_validation_results\"\n \n id = Column(String, primary_key=True, default=generate_uuid)\n task_id = Column(String, nullable=True, index=True) # Reference to TaskRecord\n dashboard_id = Column(String, nullable=False, index=True)\n environment_id = Column(String, nullable=True, index=True)\n timestamp = Column(DateTime, default=datetime.utcnow)\n status = Column(String, nullable=False) # PASS, WARN, FAIL, UNKNOWN\n screenshot_path = Column(String, nullable=True)\n issues = Column(JSON, nullable=False)\n summary = Column(Text, nullable=False)\n raw_response = Column(Text, nullable=True)\n# [/DEF:ValidationRecord:Class]\n\n# [/DEF:LlmModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region LlmModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, llm, model, schema, validate, provider]\n# @BRIEF SQLAlchemy models for LLM provider configuration and validation results.\n# @LAYER: Domain\n# @RELATION INHERITS_FROM -> MappingModels:Base\n\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import JSON, Boolean, Column, DateTime, String, Text, Time\n\nfrom .mapping import Base\n\n\ndef generate_uuid():\n return str(uuid.uuid4())\n\n# #region ValidationPolicy [TYPE Class]\n# @BRIEF Defines a scheduled rule for validating a group of dashboards within an execution window.\nclass ValidationPolicy(Base):\n __tablename__ = \"validation_policies\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n dashboard_ids = Column(JSON, nullable=False) # Array of dashboard IDs\n schedule_days = Column(JSON, nullable=False) # Array of integers (0-6)\n window_start = Column(Time, nullable=False)\n window_end = Column(Time, nullable=False)\n notify_owners = Column(Boolean, default=True)\n custom_channels = Column(JSON, nullable=True) # List of external channels\n alert_condition = Column(String, default=\"FAIL_ONLY\") # FAIL_ONLY, WARN_AND_FAIL, ALWAYS\n created_at = Column(DateTime, default=datetime.utcnow)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)\n# #endregion ValidationPolicy\n\n# #region LLMProvider [TYPE Class]\n# @BRIEF SQLAlchemy model for LLM provider configuration.\nclass LLMProvider(Base):\n __tablename__ = \"llm_providers\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n provider_type = Column(String, nullable=False) # openai, openrouter, kilo\n name = Column(String, nullable=False)\n base_url = Column(String, nullable=False)\n api_key = Column(String, nullable=False) # Should be encrypted\n default_model = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n# #endregion LLMProvider\n\n# #region ValidationRecord [TYPE Class]\n# @BRIEF SQLAlchemy model for dashboard validation history.\nclass ValidationRecord(Base):\n __tablename__ = \"llm_validation_results\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n task_id = Column(String, nullable=True, index=True) # Reference to TaskRecord\n dashboard_id = Column(String, nullable=False, index=True)\n environment_id = Column(String, nullable=True, index=True)\n timestamp = Column(DateTime, default=datetime.utcnow)\n status = Column(String, nullable=False) # PASS, WARN, FAIL, UNKNOWN\n screenshot_path = Column(String, nullable=True)\n issues = Column(JSON, nullable=False)\n summary = Column(Text, nullable=False)\n raw_response = Column(Text, nullable=True)\n# #endregion ValidationRecord\n\n# #endregion LlmModels\n" }, { "contract_id": "ValidationPolicy", "contract_type": "Class", "file_path": "backend/src/models/llm.py", - "start_line": 16, - "end_line": 34, + "start_line": 17, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -60752,15 +48990,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationPolicy:Class]\n# @PURPOSE: Defines a scheduled rule for validating a group of dashboards within an execution window.\nclass ValidationPolicy(Base):\n __tablename__ = \"validation_policies\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n dashboard_ids = Column(JSON, nullable=False) # Array of dashboard IDs\n schedule_days = Column(JSON, nullable=False) # Array of integers (0-6)\n window_start = Column(Time, nullable=False)\n window_end = Column(Time, nullable=False)\n notify_owners = Column(Boolean, default=True)\n custom_channels = Column(JSON, nullable=True) # List of external channels\n alert_condition = Column(String, default=\"FAIL_ONLY\") # FAIL_ONLY, WARN_AND_FAIL, ALWAYS\n created_at = Column(DateTime, default=datetime.utcnow)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)\n# [/DEF:ValidationPolicy:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationPolicy [TYPE Class]\n# @BRIEF Defines a scheduled rule for validating a group of dashboards within an execution window.\nclass ValidationPolicy(Base):\n __tablename__ = \"validation_policies\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n dashboard_ids = Column(JSON, nullable=False) # Array of dashboard IDs\n schedule_days = Column(JSON, nullable=False) # Array of integers (0-6)\n window_start = Column(Time, nullable=False)\n window_end = Column(Time, nullable=False)\n notify_owners = Column(Boolean, default=True)\n custom_channels = Column(JSON, nullable=True) # List of external channels\n alert_condition = Column(String, default=\"FAIL_ONLY\") # FAIL_ONLY, WARN_AND_FAIL, ALWAYS\n created_at = Column(DateTime, default=datetime.utcnow)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)\n# #endregion ValidationPolicy\n" }, { "contract_id": "LLMProvider", "contract_type": "Class", "file_path": "backend/src/models/llm.py", - "start_line": 36, - "end_line": 49, + "start_line": 37, + "end_line": 50, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -60778,15 +49016,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LLMProvider:Class]\n# @PURPOSE: SQLAlchemy model for LLM provider configuration.\nclass LLMProvider(Base):\n __tablename__ = \"llm_providers\"\n \n id = Column(String, primary_key=True, default=generate_uuid)\n provider_type = Column(String, nullable=False) # openai, openrouter, kilo\n name = Column(String, nullable=False)\n base_url = Column(String, nullable=False)\n api_key = Column(String, nullable=False) # Should be encrypted\n default_model = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n# [/DEF:LLMProvider:Class]\n" + "anchor_syntax": "region", + "body": "# #region LLMProvider [TYPE Class]\n# @BRIEF SQLAlchemy model for LLM provider configuration.\nclass LLMProvider(Base):\n __tablename__ = \"llm_providers\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n provider_type = Column(String, nullable=False) # openai, openrouter, kilo\n name = Column(String, nullable=False)\n base_url = Column(String, nullable=False)\n api_key = Column(String, nullable=False) # Should be encrypted\n default_model = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n# #endregion LLMProvider\n" }, { "contract_id": "ValidationRecord", "contract_type": "Class", "file_path": "backend/src/models/llm.py", - "start_line": 51, - "end_line": 66, + "start_line": 52, + "end_line": 67, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -60804,31 +49042,23 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationRecord:Class]\n# @PURPOSE: SQLAlchemy model for dashboard validation history.\nclass ValidationRecord(Base):\n __tablename__ = \"llm_validation_results\"\n \n id = Column(String, primary_key=True, default=generate_uuid)\n task_id = Column(String, nullable=True, index=True) # Reference to TaskRecord\n dashboard_id = Column(String, nullable=False, index=True)\n environment_id = Column(String, nullable=True, index=True)\n timestamp = Column(DateTime, default=datetime.utcnow)\n status = Column(String, nullable=False) # PASS, WARN, FAIL, UNKNOWN\n screenshot_path = Column(String, nullable=True)\n issues = Column(JSON, nullable=False)\n summary = Column(Text, nullable=False)\n raw_response = Column(Text, nullable=True)\n# [/DEF:ValidationRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationRecord [TYPE Class]\n# @BRIEF SQLAlchemy model for dashboard validation history.\nclass ValidationRecord(Base):\n __tablename__ = \"llm_validation_results\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n task_id = Column(String, nullable=True, index=True) # Reference to TaskRecord\n dashboard_id = Column(String, nullable=False, index=True)\n environment_id = Column(String, nullable=True, index=True)\n timestamp = Column(DateTime, default=datetime.utcnow)\n status = Column(String, nullable=False) # PASS, WARN, FAIL, UNKNOWN\n screenshot_path = Column(String, nullable=True)\n issues = Column(JSON, nullable=False)\n summary = Column(Text, nullable=False)\n raw_response = Column(Text, nullable=True)\n# #endregion ValidationRecord\n" }, { "contract_id": "MappingModels", "contract_type": "Module", "file_path": "backend/src/models/mapping.py", "start_line": 1, - "end_line": 104, + "end_line": 96, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "CONSTRAINT": "source_env_id and target_env_id must be valid environment IDs.", "INVARIANT": "All primary keys are UUID strings.", "LAYER": "Domain", - "PURPOSE": "Defines the database schema for environment metadata and database mappings using SQLAlchemy.", - "SEMANTICS": [ - "database", - "mapping", - "environment", - "migration", - "sqlalchemy", - "sqlite" - ] + "PURPOSE": "Defines the database schema for environment metadata and database mappings using SQLAlchemy." }, "relations": [ { @@ -60855,19 +49085,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MappingModels:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: database, mapping, environment, migration, sqlalchemy, sqlite\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, Enum as SQLEnum\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.sql import func\nimport uuid\nimport enum\n# [/SECTION]\n\nBase = declarative_base()\n\n# [DEF:ResourceType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of possible Superset resource types for ID mapping.\nclass ResourceType(str, enum.Enum):\n CHART = \"chart\"\n DATASET = \"dataset\"\n DASHBOARD = \"dashboard\"\n# [/DEF:ResourceType:Class]\n\n\n# [DEF:MigrationStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of possible migration job statuses.\nclass MigrationStatus(enum.Enum):\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n COMPLETED = \"COMPLETED\"\n FAILED = \"FAILED\"\n AWAITING_MAPPING = \"AWAITING_MAPPING\"\n# [/DEF:MigrationStatus:Class]\n\n# [DEF:Environment:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:Environment:Class]\n\n# [DEF:DatabaseMapping:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:DatabaseMapping:Class]\n\n# [DEF:MigrationJob:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:MigrationJob:Class]\n\n# [DEF:ResourceMapping:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:ResourceMapping:Class]\n\n# [/DEF:MappingModels:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ResourceType", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 23, - "end_line": 30, + "start_line": 21, + "end_line": 27, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Enumeration of possible Superset resource types for ID mapping." }, "relations": [], @@ -60882,19 +49112,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ResourceType:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of possible Superset resource types for ID mapping.\nclass ResourceType(str, enum.Enum):\n CHART = \"chart\"\n DATASET = \"dataset\"\n DASHBOARD = \"dashboard\"\n# [/DEF:ResourceType:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "MigrationStatus", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 33, - "end_line": 42, + "start_line": 30, + "end_line": 38, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Enumeration of possible migration job statuses." }, "relations": [], @@ -60909,36 +49139,36 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationStatus:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of possible migration job statuses.\nclass MigrationStatus(enum.Enum):\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n COMPLETED = \"COMPLETED\"\n FAILED = \"FAILED\"\n AWAITING_MAPPING = \"AWAITING_MAPPING\"\n# [/DEF:MigrationStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "MigrationJob", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 73, - "end_line": 85, + "start_line": 67, + "end_line": 78, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Represents a single migration execution job." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:MigrationJob:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:MigrationJob:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ResourceMapping", "contract_type": "Class", "file_path": "backend/src/models/mapping.py", - "start_line": 87, - "end_line": 102, + "start_line": 80, + "end_line": 94, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Maps a universal UUID for a resource to its actual ID on a specific environment.", "TEST_DATA": "resource_mapping_record -> {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}" }, @@ -60958,32 +49188,22 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:ResourceMapping:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:ResourceMapping:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ProfileModels", "contract_type": "Module", "file_path": "backend/src/models/profile.py", "start_line": 1, - "end_line": 61, + "end_line": 58, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Sensitive Git token is stored encrypted and never returned in plaintext.", "LAYER": "Domain", - "PURPOSE": "Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences.", - "SEMANTICS": [ - "profile", - "preferences", - "persistence", - "user", - "dashboard-filter", - "git", - "ui-preferences", - "sqlalchemy" - ] + "PURPOSE": "Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences." }, "relations": [ { @@ -61028,19 +49248,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ProfileModels:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: profile, preferences, persistence, user, dashboard-filter, git, ui-preferences, sqlalchemy\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nimport uuid\nfrom datetime import datetime\nfrom sqlalchemy import Column, String, Boolean, DateTime, ForeignKey\nfrom sqlalchemy.orm import relationship\nfrom .mapping import Base\n# [/SECTION]\n\n\n# [DEF:UserDashboardPreference:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:UserDashboardPreference:Class]\n\n# [/DEF:ProfileModels:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "UserDashboardPreference", "contract_type": "Class", "file_path": "backend/src/models/profile.py", - "start_line": 22, - "end_line": 59, + "start_line": 20, + "end_line": 56, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Stores Superset username binding and default \"my dashboards\" toggle for one authenticated user." }, "relations": [ @@ -61052,38 +49272,31 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:UserDashboardPreference:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:UserDashboardPreference:Class]\n" + "anchor_syntax": "region", + "body": "# #region UserDashboardPreference [C:3] [TYPE Class]\n# @BRIEF Stores Superset username binding and default \"my dashboards\" toggle for one authenticated user.\n# @RELATION INHERITS -> MappingModels:Base\nclass UserDashboardPreference(Base):\n __tablename__ = \"user_dashboard_preferences\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False, unique=True, index=True)\n\n superset_username = Column(String, nullable=True)\n superset_username_normalized = Column(String, nullable=True, index=True)\n\n show_only_my_dashboards = Column(Boolean, nullable=False, default=False)\n show_only_slug_dashboards = Column(Boolean, nullable=False, default=True)\n\n git_username = Column(String, nullable=True)\n git_email = Column(String, nullable=True)\n git_personal_access_token_encrypted = Column(String, nullable=True)\n\n start_page = Column(String, nullable=False, default=\"dashboards\")\n auto_open_task_drawer = Column(Boolean, nullable=False, default=True)\n dashboards_table_density = Column(String, nullable=False, default=\"comfortable\")\n\n telegram_id = Column(String, nullable=True)\n email_address = Column(String, nullable=True)\n notify_on_fail = Column(Boolean, nullable=False, default=True)\n\n created_at = Column(DateTime, nullable=False, default=datetime.utcnow)\n updated_at = Column(\n DateTime,\n nullable=False,\n default=datetime.utcnow,\n onupdate=datetime.utcnow,\n )\n\n user = relationship(\"User\")\n# #endregion UserDashboardPreference\n" }, { "contract_id": "ReportModels", "contract_type": "Module", "file_path": "backend/src/models/report.py", "start_line": 1, - "end_line": 256, + "end_line": 238, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "DATA_CONTRACT": "Model[TaskReport, ReportCollection, ReportDetailView]", "INVARIANT": "Canonical report fields are always present for every report item.", "LAYER": "Domain", "POST": "Provides validated schemas for cross-plugin reporting and UI consumption.", "PRE": "Pydantic library and task manager models are available.", "PURPOSE": "Canonical report schemas for unified task reporting across heterogeneous task types.", - "SEMANTICS": [ - "reports", - "models", - "pydantic", - "normalization", - "pagination" - ], "SIDE_EFFECT": "None (schema definition)." }, "relations": [ { "source_id": "ReportModels", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskModels", "target_ref": "[TaskModels]" } @@ -61133,46 +49346,23 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReportModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: reports, models, pydantic, normalization, pagination\n# @PURPOSE: Canonical report schemas for unified task reporting across heterogeneous task types.\n# @LAYER: Domain\n# @PRE: Pydantic library and task manager models are available.\n# @POST: Provides validated schemas for cross-plugin reporting and UI consumption.\n# @SIDE_EFFECT: None (schema definition).\n# @DATA_CONTRACT: Model[TaskReport, ReportCollection, ReportDetailView]\n# @RELATION: [DEPENDS_ON] -> [TaskModels]\n# @INVARIANT: Canonical report fields are always present for every report item.\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\n# [/SECTION]\n\n\n# [DEF:TaskType:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Must contain valid generic task type mappings.\n# @RELATION: DEPENDS_ON -> ReportModels\n# @SEMANTICS: enum, type, task\n# @PURPOSE: Supported normalized task report types.\nclass TaskType(str, Enum):\n LLM_VERIFICATION = \"llm_verification\"\n BACKUP = \"backup\"\n MIGRATION = \"migration\"\n DOCUMENTATION = \"documentation\"\n CLEAN_RELEASE = \"clean_release\"\n UNKNOWN = \"unknown\"\n\n\n# [/DEF:TaskType:Class]\n\n\n# [DEF:ReportStatus:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: TaskStatus enum mapping logic holds.\n# @SEMANTICS: enum, status, task\n# @PURPOSE: Supported normalized report status values.\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ReportStatus(str, Enum):\n SUCCESS = \"success\"\n FAILED = \"failed\"\n IN_PROGRESS = \"in_progress\"\n PARTIAL = \"partial\"\n\n\n# [/DEF:ReportStatus:Class]\n\n\n# [DEF:ErrorContext:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: The properties accurately describe error state.\n# @SEMANTICS: error, context, payload\n# @PURPOSE: Error and recovery context for failed/partial reports.\n#\n# @TEST_CONTRACT: ErrorContextModel ->\n# {\n# required_fields: {\n# message: str\n# },\n# optional_fields: {\n# code: str,\n# next_actions: list[str]\n# }\n# }\n# @TEST_FIXTURE: basic_error -> {\"message\": \"Connection timeout\", \"code\": \"ERR_504\", \"next_actions\": [\"retry\"]}\n# @TEST_EDGE: missing_message -> {\"code\": \"ERR_504\"}\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ErrorContext(BaseModel):\n code: Optional[str] = None\n message: str\n next_actions: List[str] = Field(default_factory=list)\n\n\n# [/DEF:ErrorContext:Class]\n\n\n# [DEF:TaskReport:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Must represent canonical task record attributes.\n# @SEMANTICS: report, model, summary\n# @PURPOSE: Canonical normalized report envelope for one task execution.\n#\n# @TEST_CONTRACT: TaskReportModel ->\n# {\n# required_fields: {\n# report_id: str,\n# task_id: str,\n# task_type: TaskType,\n# status: ReportStatus,\n# updated_at: datetime,\n# summary: str\n# },\n# invariants: [\n# \"report_id is a non-empty string\",\n# \"task_id is a non-empty string\",\n# \"summary is a non-empty string\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_report ->\n# {\n# report_id: \"rep-123\",\n# task_id: \"task-456\",\n# task_type: \"migration\",\n# status: \"success\",\n# updated_at: \"2026-02-26T12:00:00Z\",\n# summary: \"Migration completed successfully\"\n# }\n# @TEST_EDGE: empty_report_id -> {\"report_id\": \" \", \"task_id\": \"task-456\", \"task_type\": \"migration\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}\n# @TEST_EDGE: empty_summary -> {\"report_id\": \"rep-123\", \"task_id\": \"task-456\", \"task_type\": \"migration\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"\"}\n# @TEST_EDGE: invalid_task_type -> {\"report_id\": \"rep-123\", \"task_id\": \"task-456\", \"task_type\": \"invalid_type\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}\n# @TEST_INVARIANT: non_empty_validators -> verifies: [empty_report_id, empty_summary]\n# @RELATION: DEPENDS_ON -> ReportModels\nclass TaskReport(BaseModel):\n report_id: str\n task_id: str\n task_type: TaskType\n status: ReportStatus\n started_at: Optional[datetime] = None\n updated_at: datetime\n summary: str\n details: Optional[Dict[str, Any]] = None\n validation_record: Optional[Dict[str, Any]] = None # Extended for US2\n error_context: Optional[ErrorContext] = None\n source_ref: Optional[Dict[str, Any]] = None\n\n @field_validator(\"report_id\", \"task_id\", \"summary\")\n @classmethod\n def _non_empty_str(cls, value: str) -> str:\n if not isinstance(value, str) or not value.strip():\n raise ValueError(\"Value must be a non-empty string\")\n return value.strip()\n\n\n# [/DEF:TaskReport:Class]\n\n\n# [DEF:ReportQuery:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Time and pagination queries are mutually consistent.\n# @SEMANTICS: query, filter, search\n# @PURPOSE: Query object for server-side report filtering, sorting, and pagination.\n#\n# @TEST_CONTRACT: ReportQueryModel ->\n# {\n# optional_fields: {\n# page: int, page_size: int, task_types: list[TaskType], statuses: list[ReportStatus],\n# time_from: datetime, time_to: datetime, search: str, sort_by: str, sort_order: str\n# },\n# invariants: [\n# \"page >= 1\", \"1 <= page_size <= 100\",\n# \"sort_by in {'updated_at', 'status', 'task_type'}\",\n# \"sort_order in {'asc', 'desc'}\",\n# \"time_from <= time_to if both exist\"\n# ]\n# }\n# @TEST_FIXTURE: valid_query -> {\"page\": 1, \"page_size\":20, \"sort_by\": \"updated_at\", \"sort_order\": \"desc\"}\n# @TEST_EDGE: invalid_page_size_large -> {\"page_size\": 150}\n# @TEST_EDGE: invalid_sort_by -> {\"sort_by\": \"unknown_field\"}\n# @TEST_EDGE: invalid_time_range -> {\"time_from\": \"2026-02-26T12:00:00Z\", \"time_to\": \"2026-02-25T12:00:00Z\"}\n# @TEST_INVARIANT: attribute_constraints_enforced -> verifies: [invalid_page_size_large, invalid_sort_by, invalid_time_range]\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ReportQuery(BaseModel):\n page: int = Field(default=1, ge=1)\n page_size: int = Field(default=20, ge=1, le=100)\n task_types: List[TaskType] = Field(default_factory=list)\n statuses: List[ReportStatus] = Field(default_factory=list)\n time_from: Optional[datetime] = None\n time_to: Optional[datetime] = None\n search: Optional[str] = Field(default=None, max_length=200)\n sort_by: str = Field(default=\"updated_at\")\n sort_order: str = Field(default=\"desc\")\n\n @field_validator(\"sort_by\")\n @classmethod\n def _validate_sort_by(cls, value: str) -> str:\n allowed = {\"updated_at\", \"status\", \"task_type\"}\n if value not in allowed:\n raise ValueError(f\"sort_by must be one of: {', '.join(sorted(allowed))}\")\n return value\n\n @field_validator(\"sort_order\")\n @classmethod\n def _validate_sort_order(cls, value: str) -> str:\n if value not in {\"asc\", \"desc\"}:\n raise ValueError(\"sort_order must be 'asc' or 'desc'\")\n return value\n\n @model_validator(mode=\"after\")\n def _validate_time_range(self):\n if self.time_from and self.time_to and self.time_from > self.time_to:\n raise ValueError(\"time_from must be less than or equal to time_to\")\n return self\n\n\n# [/DEF:ReportQuery:Class]\n\n\n# [DEF:ReportCollection:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Represents paginated data correctly.\n# @SEMANTICS: collection, pagination\n# @PURPOSE: Paginated collection of normalized task reports.\n#\n# @TEST_CONTRACT: ReportCollectionModel ->\n# {\n# required_fields: {\n# items: list[TaskReport], total: int, page: int, page_size: int, has_next: bool, applied_filters: ReportQuery\n# },\n# invariants: [\"total >= 0\", \"page >= 1\", \"page_size >= 1\"]\n# }\n# @TEST_FIXTURE: empty_collection -> {\"items\": [], \"total\": 0, \"page\": 1, \"page_size\": 20, \"has_next\": False, \"applied_filters\": {}}\n# @TEST_EDGE: negative_total -> {\"items\": [], \"total\": -5, \"page\": 1, \"page_size\": 20, \"has_next\": False, \"applied_filters\": {}}\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ReportCollection(BaseModel):\n items: List[TaskReport]\n total: int = Field(ge=0)\n page: int = Field(ge=1)\n page_size: int = Field(ge=1)\n has_next: bool\n applied_filters: ReportQuery\n\n\n# [/DEF:ReportCollection:Class]\n\n\n# [DEF:ReportDetailView:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Incorporates a report and logs correctly.\n# @SEMANTICS: view, detail, logs\n# @PURPOSE: Detailed report representation including diagnostics and recovery actions.\n#\n# @TEST_CONTRACT: ReportDetailViewModel ->\n# {\n# required_fields: {report: TaskReport},\n# optional_fields: {timeline: list[dict], diagnostics: dict, next_actions: list[str]}\n# }\n# @TEST_FIXTURE: valid_detail -> {\"report\": {\"report_id\": \"rep-1\", \"task_id\": \"task-1\", \"task_type\": \"backup\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}}\n# @TEST_EDGE: missing_report -> {}\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ReportDetailView(BaseModel):\n report: TaskReport\n timeline: List[Dict[str, Any]] = Field(default_factory=list)\n diagnostics: Optional[Dict[str, Any]] = None\n next_actions: List[str] = Field(default_factory=list)\n\n\n# [/DEF:ReportDetailView:Class]\n\n# [/DEF:ReportModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region ReportModels [C:3] [TYPE Module] [SEMANTICS pydantic, report, model, schema, task, task-type]\n# @BRIEF Canonical report schemas for unified task reporting across heterogeneous task types.\n# @LAYER: Domain\n# @PRE: Pydantic library and task manager models are available.\n# @POST: Provides validated schemas for cross-plugin reporting and UI consumption.\n# @SIDE_EFFECT: None (schema definition).\n# @DATA_CONTRACT: Model[TaskReport, ReportCollection, ReportDetailView]\n# @RELATION DEPENDS_ON -> [TaskModels]\n# @INVARIANT: Canonical report fields are always present for every report item.\n\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\n\n\n# #region TaskType [C:3] [TYPE Class] [SEMANTICS enum, type, task]\n# @INVARIANT: Must contain valid generic task type mappings.\n# @RELATION DEPENDS_ON -> ReportModels\n# @BRIEF Supported normalized task report types.\nclass TaskType(str, Enum):\n LLM_VERIFICATION = \"llm_verification\"\n BACKUP = \"backup\"\n MIGRATION = \"migration\"\n DOCUMENTATION = \"documentation\"\n CLEAN_RELEASE = \"clean_release\"\n UNKNOWN = \"unknown\"\n\n\n# #endregion TaskType\n\n\n# #region ReportStatus [C:3] [TYPE Class] [SEMANTICS enum, status, task]\n# @INVARIANT: TaskStatus enum mapping logic holds.\n# @BRIEF Supported normalized report status values.\n# @RELATION DEPENDS_ON -> ReportModels\nclass ReportStatus(str, Enum):\n SUCCESS = \"success\"\n FAILED = \"failed\"\n IN_PROGRESS = \"in_progress\"\n PARTIAL = \"partial\"\n\n\n# #endregion ReportStatus\n\n\n# #region ErrorContext [C:3] [TYPE Class] [SEMANTICS error, context, payload]\n# @INVARIANT: The properties accurately describe error state.\n# @BRIEF Error and recovery context for failed/partial reports.\n#\n# @TEST_CONTRACT: ErrorContextModel ->\n# {\n# required_fields: {\n# message: str\n# },\n# optional_fields: {\n# code: str,\n# next_actions: list[str]\n# }\n# }\n# @TEST_FIXTURE: basic_error -> {\"message\": \"Connection timeout\", \"code\": \"ERR_504\", \"next_actions\": [\"retry\"]}\n# @TEST_EDGE: missing_message -> {\"code\": \"ERR_504\"}\n# @RELATION DEPENDS_ON -> ReportModels\nclass ErrorContext(BaseModel):\n code: str | None = None\n message: str\n next_actions: list[str] = Field(default_factory=list)\n\n\n# #endregion ErrorContext\n\n\n# #region TaskReport [C:3] [TYPE Class] [SEMANTICS report, model, summary]\n# @INVARIANT: Must represent canonical task record attributes.\n# @BRIEF Canonical normalized report envelope for one task execution.\n#\n# @TEST_CONTRACT: TaskReportModel ->\n# {\n# required_fields: {\n# report_id: str,\n# task_id: str,\n# task_type: TaskType,\n# status: ReportStatus,\n# updated_at: datetime,\n# summary: str\n# },\n# invariants: [\n# \"report_id is a non-empty string\",\n# \"task_id is a non-empty string\",\n# \"summary is a non-empty string\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_report ->\n# {\n# report_id: \"rep-123\",\n# task_id: \"task-456\",\n# task_type: \"migration\",\n# status: \"success\",\n# updated_at: \"2026-02-26T12:00:00Z\",\n# summary: \"Migration completed successfully\"\n# }\n# @TEST_EDGE: empty_report_id -> {\"report_id\": \" \", \"task_id\": \"task-456\", \"task_type\": \"migration\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}\n# @TEST_EDGE: empty_summary -> {\"report_id\": \"rep-123\", \"task_id\": \"task-456\", \"task_type\": \"migration\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"\"}\n# @TEST_EDGE: invalid_task_type -> {\"report_id\": \"rep-123\", \"task_id\": \"task-456\", \"task_type\": \"invalid_type\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}\n# @TEST_INVARIANT: non_empty_validators -> verifies: [empty_report_id, empty_summary]\n# @RELATION DEPENDS_ON -> ReportModels\nclass TaskReport(BaseModel):\n report_id: str\n task_id: str\n task_type: TaskType\n status: ReportStatus\n started_at: datetime | None = None\n updated_at: datetime\n summary: str\n details: dict[str, Any] | None = None\n validation_record: dict[str, Any] | None = None # Extended for US2\n error_context: ErrorContext | None = None\n source_ref: dict[str, Any] | None = None\n\n @field_validator(\"report_id\", \"task_id\", \"summary\")\n @classmethod\n def _non_empty_str(cls, value: str) -> str:\n if not isinstance(value, str) or not value.strip():\n raise ValueError(\"Value must be a non-empty string\")\n return value.strip()\n\n\n# #endregion TaskReport\n\n\n# #region ReportQuery [C:3] [TYPE Class] [SEMANTICS query, filter, search]\n# @INVARIANT: Time and pagination queries are mutually consistent.\n# @BRIEF Query object for server-side report filtering, sorting, and pagination.\n#\n# @TEST_CONTRACT: ReportQueryModel ->\n# {\n# optional_fields: {\n# page: int, page_size: int, task_types: list[TaskType], statuses: list[ReportStatus],\n# time_from: datetime, time_to: datetime, search: str, sort_by: str, sort_order: str\n# },\n# invariants: [\n# \"page >= 1\", \"1 <= page_size <= 100\",\n# \"sort_by in {'updated_at', 'status', 'task_type'}\",\n# \"sort_order in {'asc', 'desc'}\",\n# \"time_from <= time_to if both exist\"\n# ]\n# }\n# @TEST_FIXTURE: valid_query -> {\"page\": 1, \"page_size\":20, \"sort_by\": \"updated_at\", \"sort_order\": \"desc\"}\n# @TEST_EDGE: invalid_page_size_large -> {\"page_size\": 150}\n# @TEST_EDGE: invalid_sort_by -> {\"sort_by\": \"unknown_field\"}\n# @TEST_EDGE: invalid_time_range -> {\"time_from\": \"2026-02-26T12:00:00Z\", \"time_to\": \"2026-02-25T12:00:00Z\"}\n# @TEST_INVARIANT: attribute_constraints_enforced -> verifies: [invalid_page_size_large, invalid_sort_by, invalid_time_range]\n# @RELATION DEPENDS_ON -> ReportModels\nclass ReportQuery(BaseModel):\n page: int = Field(default=1, ge=1)\n page_size: int = Field(default=20, ge=1, le=100)\n task_types: list[TaskType] = Field(default_factory=list)\n statuses: list[ReportStatus] = Field(default_factory=list)\n time_from: datetime | None = None\n time_to: datetime | None = None\n search: str | None = Field(default=None, max_length=200)\n sort_by: str = Field(default=\"updated_at\")\n sort_order: str = Field(default=\"desc\")\n\n @field_validator(\"sort_by\")\n @classmethod\n def _validate_sort_by(cls, value: str) -> str:\n allowed = {\"updated_at\", \"status\", \"task_type\"}\n if value not in allowed:\n raise ValueError(f\"sort_by must be one of: {', '.join(sorted(allowed))}\")\n return value\n\n @field_validator(\"sort_order\")\n @classmethod\n def _validate_sort_order(cls, value: str) -> str:\n if value not in {\"asc\", \"desc\"}:\n raise ValueError(\"sort_order must be 'asc' or 'desc'\")\n return value\n\n @model_validator(mode=\"after\")\n def _validate_time_range(self):\n if self.time_from and self.time_to and self.time_from > self.time_to:\n raise ValueError(\"time_from must be less than or equal to time_to\")\n return self\n\n\n# #endregion ReportQuery\n\n\n# #region ReportCollection [C:3] [TYPE Class] [SEMANTICS collection, pagination]\n# @INVARIANT: Represents paginated data correctly.\n# @BRIEF Paginated collection of normalized task reports.\n#\n# @TEST_CONTRACT: ReportCollectionModel ->\n# {\n# required_fields: {\n# items: list[TaskReport], total: int, page: int, page_size: int, has_next: bool, applied_filters: ReportQuery\n# },\n# invariants: [\"total >= 0\", \"page >= 1\", \"page_size >= 1\"]\n# }\n# @TEST_FIXTURE: empty_collection -> {\"items\": [], \"total\": 0, \"page\": 1, \"page_size\": 20, \"has_next\": False, \"applied_filters\": {}}\n# @TEST_EDGE: negative_total -> {\"items\": [], \"total\": -5, \"page\": 1, \"page_size\": 20, \"has_next\": False, \"applied_filters\": {}}\n# @RELATION DEPENDS_ON -> ReportModels\nclass ReportCollection(BaseModel):\n items: list[TaskReport]\n total: int = Field(ge=0)\n page: int = Field(ge=1)\n page_size: int = Field(ge=1)\n has_next: bool\n applied_filters: ReportQuery\n\n\n# #endregion ReportCollection\n\n\n# #region ReportDetailView [C:3] [TYPE Class] [SEMANTICS view, detail, logs]\n# @INVARIANT: Incorporates a report and logs correctly.\n# @BRIEF Detailed report representation including diagnostics and recovery actions.\n#\n# @TEST_CONTRACT: ReportDetailViewModel ->\n# {\n# required_fields: {report: TaskReport},\n# optional_fields: {timeline: list[dict], diagnostics: dict, next_actions: list[str]}\n# }\n# @TEST_FIXTURE: valid_detail -> {\"report\": {\"report_id\": \"rep-1\", \"task_id\": \"task-1\", \"task_type\": \"backup\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}}\n# @TEST_EDGE: missing_report -> {}\n# @RELATION DEPENDS_ON -> ReportModels\nclass ReportDetailView(BaseModel):\n report: TaskReport\n timeline: list[dict[str, Any]] = Field(default_factory=list)\n diagnostics: dict[str, Any] | None = None\n next_actions: list[str] = Field(default_factory=list)\n\n\n# #endregion ReportDetailView\n\n# #endregion ReportModels\n" }, { "contract_id": "TaskType", "contract_type": "Class", "file_path": "backend/src/models/report.py", - "start_line": 22, - "end_line": 37, + "start_line": 18, + "end_line": 31, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Must contain valid generic task type mappings.", - "PURPOSE": "Supported normalized task report types.", - "SEMANTICS": [ - "enum", - "type", - "task" - ] + "PURPOSE": "Supported normalized task report types." }, "relations": [ { @@ -61191,51 +49381,23 @@ "actual_complexity": 3, "contract_type": "Class" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskType:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Must contain valid generic task type mappings.\n# @RELATION: DEPENDS_ON -> ReportModels\n# @SEMANTICS: enum, type, task\n# @PURPOSE: Supported normalized task report types.\nclass TaskType(str, Enum):\n LLM_VERIFICATION = \"llm_verification\"\n BACKUP = \"backup\"\n MIGRATION = \"migration\"\n DOCUMENTATION = \"documentation\"\n CLEAN_RELEASE = \"clean_release\"\n UNKNOWN = \"unknown\"\n\n\n# [/DEF:TaskType:Class]\n" + "anchor_syntax": "region", + "body": "# #region TaskType [C:3] [TYPE Class] [SEMANTICS enum, type, task]\n# @INVARIANT: Must contain valid generic task type mappings.\n# @RELATION DEPENDS_ON -> ReportModels\n# @BRIEF Supported normalized task report types.\nclass TaskType(str, Enum):\n LLM_VERIFICATION = \"llm_verification\"\n BACKUP = \"backup\"\n MIGRATION = \"migration\"\n DOCUMENTATION = \"documentation\"\n CLEAN_RELEASE = \"clean_release\"\n UNKNOWN = \"unknown\"\n\n\n# #endregion TaskType\n" }, { "contract_id": "ReportStatus", "contract_type": "Class", "file_path": "backend/src/models/report.py", - "start_line": 40, - "end_line": 53, + "start_line": 34, + "end_line": 45, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "TaskStatus enum mapping logic holds.", - "PURPOSE": "Supported normalized report status values.", - "SEMANTICS": [ - "enum", - "status", - "task" - ] + "PURPOSE": "Supported normalized report status values." }, "relations": [ { @@ -61254,51 +49416,23 @@ "actual_complexity": 3, "contract_type": "Class" } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReportStatus:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: TaskStatus enum mapping logic holds.\n# @SEMANTICS: enum, status, task\n# @PURPOSE: Supported normalized report status values.\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ReportStatus(str, Enum):\n SUCCESS = \"success\"\n FAILED = \"failed\"\n IN_PROGRESS = \"in_progress\"\n PARTIAL = \"partial\"\n\n\n# [/DEF:ReportStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region ReportStatus [C:3] [TYPE Class] [SEMANTICS enum, status, task]\n# @INVARIANT: TaskStatus enum mapping logic holds.\n# @BRIEF Supported normalized report status values.\n# @RELATION DEPENDS_ON -> ReportModels\nclass ReportStatus(str, Enum):\n SUCCESS = \"success\"\n FAILED = \"failed\"\n IN_PROGRESS = \"in_progress\"\n PARTIAL = \"partial\"\n\n\n# #endregion ReportStatus\n" }, { "contract_id": "ErrorContext", "contract_type": "Class", "file_path": "backend/src/models/report.py", - "start_line": 56, - "end_line": 81, + "start_line": 48, + "end_line": 71, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "The properties accurately describe error state.", "PURPOSE": "Error and recovery context for failed/partial reports.", - "SEMANTICS": [ - "error", - "context", - "payload" - ], "TEST_CONTRACT": "ErrorContextModel ->" }, "relations": [], @@ -61312,29 +49446,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -61366,26 +49477,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ErrorContext:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: The properties accurately describe error state.\n# @SEMANTICS: error, context, payload\n# @PURPOSE: Error and recovery context for failed/partial reports.\n#\n# @TEST_CONTRACT: ErrorContextModel ->\n# {\n# required_fields: {\n# message: str\n# },\n# optional_fields: {\n# code: str,\n# next_actions: list[str]\n# }\n# }\n# @TEST_FIXTURE: basic_error -> {\"message\": \"Connection timeout\", \"code\": \"ERR_504\", \"next_actions\": [\"retry\"]}\n# @TEST_EDGE: missing_message -> {\"code\": \"ERR_504\"}\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ErrorContext(BaseModel):\n code: Optional[str] = None\n message: str\n next_actions: List[str] = Field(default_factory=list)\n\n\n# [/DEF:ErrorContext:Class]\n" + "anchor_syntax": "region", + "body": "# #region ErrorContext [C:3] [TYPE Class] [SEMANTICS error, context, payload]\n# @INVARIANT: The properties accurately describe error state.\n# @BRIEF Error and recovery context for failed/partial reports.\n#\n# @TEST_CONTRACT: ErrorContextModel ->\n# {\n# required_fields: {\n# message: str\n# },\n# optional_fields: {\n# code: str,\n# next_actions: list[str]\n# }\n# }\n# @TEST_FIXTURE: basic_error -> {\"message\": \"Connection timeout\", \"code\": \"ERR_504\", \"next_actions\": [\"retry\"]}\n# @TEST_EDGE: missing_message -> {\"code\": \"ERR_504\"}\n# @RELATION DEPENDS_ON -> ReportModels\nclass ErrorContext(BaseModel):\n code: str | None = None\n message: str\n next_actions: list[str] = Field(default_factory=list)\n\n\n# #endregion ErrorContext\n" }, { "contract_id": "TaskReport", "contract_type": "Class", "file_path": "backend/src/models/report.py", - "start_line": 84, - "end_line": 141, + "start_line": 74, + "end_line": 129, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Must represent canonical task record attributes.", "PURPOSE": "Canonical normalized report envelope for one task execution.", - "SEMANTICS": [ - "report", - "model", - "summary" - ], "TEST_CONTRACT": "TaskReportModel ->" }, "relations": [], @@ -61399,29 +49505,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -61453,26 +49536,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskReport:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Must represent canonical task record attributes.\n# @SEMANTICS: report, model, summary\n# @PURPOSE: Canonical normalized report envelope for one task execution.\n#\n# @TEST_CONTRACT: TaskReportModel ->\n# {\n# required_fields: {\n# report_id: str,\n# task_id: str,\n# task_type: TaskType,\n# status: ReportStatus,\n# updated_at: datetime,\n# summary: str\n# },\n# invariants: [\n# \"report_id is a non-empty string\",\n# \"task_id is a non-empty string\",\n# \"summary is a non-empty string\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_report ->\n# {\n# report_id: \"rep-123\",\n# task_id: \"task-456\",\n# task_type: \"migration\",\n# status: \"success\",\n# updated_at: \"2026-02-26T12:00:00Z\",\n# summary: \"Migration completed successfully\"\n# }\n# @TEST_EDGE: empty_report_id -> {\"report_id\": \" \", \"task_id\": \"task-456\", \"task_type\": \"migration\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}\n# @TEST_EDGE: empty_summary -> {\"report_id\": \"rep-123\", \"task_id\": \"task-456\", \"task_type\": \"migration\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"\"}\n# @TEST_EDGE: invalid_task_type -> {\"report_id\": \"rep-123\", \"task_id\": \"task-456\", \"task_type\": \"invalid_type\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}\n# @TEST_INVARIANT: non_empty_validators -> verifies: [empty_report_id, empty_summary]\n# @RELATION: DEPENDS_ON -> ReportModels\nclass TaskReport(BaseModel):\n report_id: str\n task_id: str\n task_type: TaskType\n status: ReportStatus\n started_at: Optional[datetime] = None\n updated_at: datetime\n summary: str\n details: Optional[Dict[str, Any]] = None\n validation_record: Optional[Dict[str, Any]] = None # Extended for US2\n error_context: Optional[ErrorContext] = None\n source_ref: Optional[Dict[str, Any]] = None\n\n @field_validator(\"report_id\", \"task_id\", \"summary\")\n @classmethod\n def _non_empty_str(cls, value: str) -> str:\n if not isinstance(value, str) or not value.strip():\n raise ValueError(\"Value must be a non-empty string\")\n return value.strip()\n\n\n# [/DEF:TaskReport:Class]\n" + "anchor_syntax": "region", + "body": "# #region TaskReport [C:3] [TYPE Class] [SEMANTICS report, model, summary]\n# @INVARIANT: Must represent canonical task record attributes.\n# @BRIEF Canonical normalized report envelope for one task execution.\n#\n# @TEST_CONTRACT: TaskReportModel ->\n# {\n# required_fields: {\n# report_id: str,\n# task_id: str,\n# task_type: TaskType,\n# status: ReportStatus,\n# updated_at: datetime,\n# summary: str\n# },\n# invariants: [\n# \"report_id is a non-empty string\",\n# \"task_id is a non-empty string\",\n# \"summary is a non-empty string\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task_report ->\n# {\n# report_id: \"rep-123\",\n# task_id: \"task-456\",\n# task_type: \"migration\",\n# status: \"success\",\n# updated_at: \"2026-02-26T12:00:00Z\",\n# summary: \"Migration completed successfully\"\n# }\n# @TEST_EDGE: empty_report_id -> {\"report_id\": \" \", \"task_id\": \"task-456\", \"task_type\": \"migration\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}\n# @TEST_EDGE: empty_summary -> {\"report_id\": \"rep-123\", \"task_id\": \"task-456\", \"task_type\": \"migration\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"\"}\n# @TEST_EDGE: invalid_task_type -> {\"report_id\": \"rep-123\", \"task_id\": \"task-456\", \"task_type\": \"invalid_type\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}\n# @TEST_INVARIANT: non_empty_validators -> verifies: [empty_report_id, empty_summary]\n# @RELATION DEPENDS_ON -> ReportModels\nclass TaskReport(BaseModel):\n report_id: str\n task_id: str\n task_type: TaskType\n status: ReportStatus\n started_at: datetime | None = None\n updated_at: datetime\n summary: str\n details: dict[str, Any] | None = None\n validation_record: dict[str, Any] | None = None # Extended for US2\n error_context: ErrorContext | None = None\n source_ref: dict[str, Any] | None = None\n\n @field_validator(\"report_id\", \"task_id\", \"summary\")\n @classmethod\n def _non_empty_str(cls, value: str) -> str:\n if not isinstance(value, str) or not value.strip():\n raise ValueError(\"Value must be a non-empty string\")\n return value.strip()\n\n\n# #endregion TaskReport\n" }, { "contract_id": "ReportQuery", "contract_type": "Class", "file_path": "backend/src/models/report.py", - "start_line": 144, - "end_line": 202, + "start_line": 132, + "end_line": 188, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Time and pagination queries are mutually consistent.", "PURPOSE": "Query object for server-side report filtering, sorting, and pagination.", - "SEMANTICS": [ - "query", - "filter", - "search" - ], "TEST_CONTRACT": "ReportQueryModel ->" }, "relations": [], @@ -61486,29 +49564,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -61540,25 +49595,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReportQuery:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Time and pagination queries are mutually consistent.\n# @SEMANTICS: query, filter, search\n# @PURPOSE: Query object for server-side report filtering, sorting, and pagination.\n#\n# @TEST_CONTRACT: ReportQueryModel ->\n# {\n# optional_fields: {\n# page: int, page_size: int, task_types: list[TaskType], statuses: list[ReportStatus],\n# time_from: datetime, time_to: datetime, search: str, sort_by: str, sort_order: str\n# },\n# invariants: [\n# \"page >= 1\", \"1 <= page_size <= 100\",\n# \"sort_by in {'updated_at', 'status', 'task_type'}\",\n# \"sort_order in {'asc', 'desc'}\",\n# \"time_from <= time_to if both exist\"\n# ]\n# }\n# @TEST_FIXTURE: valid_query -> {\"page\": 1, \"page_size\":20, \"sort_by\": \"updated_at\", \"sort_order\": \"desc\"}\n# @TEST_EDGE: invalid_page_size_large -> {\"page_size\": 150}\n# @TEST_EDGE: invalid_sort_by -> {\"sort_by\": \"unknown_field\"}\n# @TEST_EDGE: invalid_time_range -> {\"time_from\": \"2026-02-26T12:00:00Z\", \"time_to\": \"2026-02-25T12:00:00Z\"}\n# @TEST_INVARIANT: attribute_constraints_enforced -> verifies: [invalid_page_size_large, invalid_sort_by, invalid_time_range]\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ReportQuery(BaseModel):\n page: int = Field(default=1, ge=1)\n page_size: int = Field(default=20, ge=1, le=100)\n task_types: List[TaskType] = Field(default_factory=list)\n statuses: List[ReportStatus] = Field(default_factory=list)\n time_from: Optional[datetime] = None\n time_to: Optional[datetime] = None\n search: Optional[str] = Field(default=None, max_length=200)\n sort_by: str = Field(default=\"updated_at\")\n sort_order: str = Field(default=\"desc\")\n\n @field_validator(\"sort_by\")\n @classmethod\n def _validate_sort_by(cls, value: str) -> str:\n allowed = {\"updated_at\", \"status\", \"task_type\"}\n if value not in allowed:\n raise ValueError(f\"sort_by must be one of: {', '.join(sorted(allowed))}\")\n return value\n\n @field_validator(\"sort_order\")\n @classmethod\n def _validate_sort_order(cls, value: str) -> str:\n if value not in {\"asc\", \"desc\"}:\n raise ValueError(\"sort_order must be 'asc' or 'desc'\")\n return value\n\n @model_validator(mode=\"after\")\n def _validate_time_range(self):\n if self.time_from and self.time_to and self.time_from > self.time_to:\n raise ValueError(\"time_from must be less than or equal to time_to\")\n return self\n\n\n# [/DEF:ReportQuery:Class]\n" + "anchor_syntax": "region", + "body": "# #region ReportQuery [C:3] [TYPE Class] [SEMANTICS query, filter, search]\n# @INVARIANT: Time and pagination queries are mutually consistent.\n# @BRIEF Query object for server-side report filtering, sorting, and pagination.\n#\n# @TEST_CONTRACT: ReportQueryModel ->\n# {\n# optional_fields: {\n# page: int, page_size: int, task_types: list[TaskType], statuses: list[ReportStatus],\n# time_from: datetime, time_to: datetime, search: str, sort_by: str, sort_order: str\n# },\n# invariants: [\n# \"page >= 1\", \"1 <= page_size <= 100\",\n# \"sort_by in {'updated_at', 'status', 'task_type'}\",\n# \"sort_order in {'asc', 'desc'}\",\n# \"time_from <= time_to if both exist\"\n# ]\n# }\n# @TEST_FIXTURE: valid_query -> {\"page\": 1, \"page_size\":20, \"sort_by\": \"updated_at\", \"sort_order\": \"desc\"}\n# @TEST_EDGE: invalid_page_size_large -> {\"page_size\": 150}\n# @TEST_EDGE: invalid_sort_by -> {\"sort_by\": \"unknown_field\"}\n# @TEST_EDGE: invalid_time_range -> {\"time_from\": \"2026-02-26T12:00:00Z\", \"time_to\": \"2026-02-25T12:00:00Z\"}\n# @TEST_INVARIANT: attribute_constraints_enforced -> verifies: [invalid_page_size_large, invalid_sort_by, invalid_time_range]\n# @RELATION DEPENDS_ON -> ReportModels\nclass ReportQuery(BaseModel):\n page: int = Field(default=1, ge=1)\n page_size: int = Field(default=20, ge=1, le=100)\n task_types: list[TaskType] = Field(default_factory=list)\n statuses: list[ReportStatus] = Field(default_factory=list)\n time_from: datetime | None = None\n time_to: datetime | None = None\n search: str | None = Field(default=None, max_length=200)\n sort_by: str = Field(default=\"updated_at\")\n sort_order: str = Field(default=\"desc\")\n\n @field_validator(\"sort_by\")\n @classmethod\n def _validate_sort_by(cls, value: str) -> str:\n allowed = {\"updated_at\", \"status\", \"task_type\"}\n if value not in allowed:\n raise ValueError(f\"sort_by must be one of: {', '.join(sorted(allowed))}\")\n return value\n\n @field_validator(\"sort_order\")\n @classmethod\n def _validate_sort_order(cls, value: str) -> str:\n if value not in {\"asc\", \"desc\"}:\n raise ValueError(\"sort_order must be 'asc' or 'desc'\")\n return value\n\n @model_validator(mode=\"after\")\n def _validate_time_range(self):\n if self.time_from and self.time_to and self.time_from > self.time_to:\n raise ValueError(\"time_from must be less than or equal to time_to\")\n return self\n\n\n# #endregion ReportQuery\n" }, { "contract_id": "ReportCollection", "contract_type": "Class", "file_path": "backend/src/models/report.py", - "start_line": 205, - "end_line": 230, + "start_line": 191, + "end_line": 214, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Represents paginated data correctly.", "PURPOSE": "Paginated collection of normalized task reports.", - "SEMANTICS": [ - "collection", - "pagination" - ], "TEST_CONTRACT": "ReportCollectionModel ->" }, "relations": [], @@ -61572,29 +49623,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -61626,26 +49654,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReportCollection:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Represents paginated data correctly.\n# @SEMANTICS: collection, pagination\n# @PURPOSE: Paginated collection of normalized task reports.\n#\n# @TEST_CONTRACT: ReportCollectionModel ->\n# {\n# required_fields: {\n# items: list[TaskReport], total: int, page: int, page_size: int, has_next: bool, applied_filters: ReportQuery\n# },\n# invariants: [\"total >= 0\", \"page >= 1\", \"page_size >= 1\"]\n# }\n# @TEST_FIXTURE: empty_collection -> {\"items\": [], \"total\": 0, \"page\": 1, \"page_size\": 20, \"has_next\": False, \"applied_filters\": {}}\n# @TEST_EDGE: negative_total -> {\"items\": [], \"total\": -5, \"page\": 1, \"page_size\": 20, \"has_next\": False, \"applied_filters\": {}}\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ReportCollection(BaseModel):\n items: List[TaskReport]\n total: int = Field(ge=0)\n page: int = Field(ge=1)\n page_size: int = Field(ge=1)\n has_next: bool\n applied_filters: ReportQuery\n\n\n# [/DEF:ReportCollection:Class]\n" + "anchor_syntax": "region", + "body": "# #region ReportCollection [C:3] [TYPE Class] [SEMANTICS collection, pagination]\n# @INVARIANT: Represents paginated data correctly.\n# @BRIEF Paginated collection of normalized task reports.\n#\n# @TEST_CONTRACT: ReportCollectionModel ->\n# {\n# required_fields: {\n# items: list[TaskReport], total: int, page: int, page_size: int, has_next: bool, applied_filters: ReportQuery\n# },\n# invariants: [\"total >= 0\", \"page >= 1\", \"page_size >= 1\"]\n# }\n# @TEST_FIXTURE: empty_collection -> {\"items\": [], \"total\": 0, \"page\": 1, \"page_size\": 20, \"has_next\": False, \"applied_filters\": {}}\n# @TEST_EDGE: negative_total -> {\"items\": [], \"total\": -5, \"page\": 1, \"page_size\": 20, \"has_next\": False, \"applied_filters\": {}}\n# @RELATION DEPENDS_ON -> ReportModels\nclass ReportCollection(BaseModel):\n items: list[TaskReport]\n total: int = Field(ge=0)\n page: int = Field(ge=1)\n page_size: int = Field(ge=1)\n has_next: bool\n applied_filters: ReportQuery\n\n\n# #endregion ReportCollection\n" }, { "contract_id": "ReportDetailView", "contract_type": "Class", "file_path": "backend/src/models/report.py", - "start_line": 233, - "end_line": 254, + "start_line": 217, + "end_line": 236, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Incorporates a report and logs correctly.", "PURPOSE": "Detailed report representation including diagnostics and recovery actions.", - "SEMANTICS": [ - "view", - "detail", - "logs" - ], "TEST_CONTRACT": "ReportDetailViewModel ->" }, "relations": [], @@ -61659,29 +49682,6 @@ "contract_type": "Class" } }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Class'", - "detail": { - "actual_type": "Class", - "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Class' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Class" - } - }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -61713,33 +49713,33 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReportDetailView:Class]\n# @COMPLEXITY: 3\n# @INVARIANT: Incorporates a report and logs correctly.\n# @SEMANTICS: view, detail, logs\n# @PURPOSE: Detailed report representation including diagnostics and recovery actions.\n#\n# @TEST_CONTRACT: ReportDetailViewModel ->\n# {\n# required_fields: {report: TaskReport},\n# optional_fields: {timeline: list[dict], diagnostics: dict, next_actions: list[str]}\n# }\n# @TEST_FIXTURE: valid_detail -> {\"report\": {\"report_id\": \"rep-1\", \"task_id\": \"task-1\", \"task_type\": \"backup\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}}\n# @TEST_EDGE: missing_report -> {}\n# @RELATION: DEPENDS_ON -> ReportModels\nclass ReportDetailView(BaseModel):\n report: TaskReport\n timeline: List[Dict[str, Any]] = Field(default_factory=list)\n diagnostics: Optional[Dict[str, Any]] = None\n next_actions: List[str] = Field(default_factory=list)\n\n\n# [/DEF:ReportDetailView:Class]\n" + "anchor_syntax": "region", + "body": "# #region ReportDetailView [C:3] [TYPE Class] [SEMANTICS view, detail, logs]\n# @INVARIANT: Incorporates a report and logs correctly.\n# @BRIEF Detailed report representation including diagnostics and recovery actions.\n#\n# @TEST_CONTRACT: ReportDetailViewModel ->\n# {\n# required_fields: {report: TaskReport},\n# optional_fields: {timeline: list[dict], diagnostics: dict, next_actions: list[str]}\n# }\n# @TEST_FIXTURE: valid_detail -> {\"report\": {\"report_id\": \"rep-1\", \"task_id\": \"task-1\", \"task_type\": \"backup\", \"status\": \"success\", \"updated_at\": \"2026-02-26T12:00:00Z\", \"summary\": \"Done\"}}\n# @TEST_EDGE: missing_report -> {}\n# @RELATION DEPENDS_ON -> ReportModels\nclass ReportDetailView(BaseModel):\n report: TaskReport\n timeline: list[dict[str, Any]] = Field(default_factory=list)\n diagnostics: dict[str, Any] | None = None\n next_actions: list[str] = Field(default_factory=list)\n\n\n# #endregion ReportDetailView\n" }, { "contract_id": "StorageModels", "contract_type": "Module", "file_path": "backend/src/models/storage.py", "start_line": 1, - "end_line": 40, + "end_line": 38, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:StorageModels:Module]\n\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Optional\nfrom pydantic import BaseModel, Field\n\n# [DEF:FileCategory:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of supported file categories in the storage system.\nclass FileCategory(str, Enum):\n BACKUP = \"backups\"\n REPOSITORY = \"repositorys\"\n# [/DEF:FileCategory:Class]\n\n# [DEF:StorageConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Configuration model for the storage system, defining paths and naming patterns.\nclass StorageConfig(BaseModel):\n root_path: str = Field(default=\"backups\", description=\"Absolute path to the storage root directory.\")\n backup_path: str = Field(default=\"backups\", description=\"Subpath for backups.\")\n repo_path: str = Field(default=\"repositorys\", description=\"Subpath for repositories.\")\n backup_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for backup directory structure.\")\n repo_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for repository directory structure.\")\n filename_pattern: str = Field(default=\"{name}_{timestamp}\", description=\"Pattern for filenames.\")\n# [/DEF:StorageConfig:Class]\n\n# [DEF:StoredFile:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Data model representing metadata for a file stored in the system.\nclass StoredFile(BaseModel):\n name: str = Field(..., description=\"Name of the file (including extension).\")\n path: str = Field(..., description=\"Relative path from storage root.\")\n size: int = Field(..., ge=0, description=\"Size of the file in bytes.\")\n created_at: datetime = Field(..., description=\"Creation timestamp.\")\n category: FileCategory = Field(..., description=\"Category of the file.\")\n mime_type: Optional[str] = Field(None, description=\"MIME type of the file.\")\n# [/DEF:StoredFile:Class]\n\n# [/DEF:StorageModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region StorageModels [TYPE Module] [SEMANTICS pydantic, storage, model, backup, config]\n\nfrom datetime import datetime\nfrom enum import Enum\n\nfrom pydantic import BaseModel, Field\n\n\n# #region FileCategory [C:1] [TYPE Class]\n# @BRIEF Enumeration of supported file categories in the storage system.\nclass FileCategory(str, Enum):\n BACKUP = \"backups\"\n REPOSITORY = \"repositorys\"\n# #endregion FileCategory\n\n# #region StorageConfig [C:1] [TYPE Class]\n# @BRIEF Configuration model for the storage system, defining paths and naming patterns.\nclass StorageConfig(BaseModel):\n root_path: str = Field(default=\"backups\", description=\"Absolute path to the storage root directory.\")\n backup_path: str = Field(default=\"backups\", description=\"Subpath for backups.\")\n repo_path: str = Field(default=\"repositorys\", description=\"Subpath for repositories.\")\n backup_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for backup directory structure.\")\n repo_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for repository directory structure.\")\n filename_pattern: str = Field(default=\"{name}_{timestamp}\", description=\"Pattern for filenames.\")\n# #endregion StorageConfig\n\n# #region StoredFile [C:1] [TYPE Class]\n# @BRIEF Data model representing metadata for a file stored in the system.\nclass StoredFile(BaseModel):\n name: str = Field(..., description=\"Name of the file (including extension).\")\n path: str = Field(..., description=\"Relative path from storage root.\")\n size: int = Field(..., ge=0, description=\"Size of the file in bytes.\")\n created_at: datetime = Field(..., description=\"Creation timestamp.\")\n category: FileCategory = Field(..., description=\"Category of the file.\")\n mime_type: str | None = Field(None, description=\"MIME type of the file.\")\n# #endregion StoredFile\n\n# #endregion StorageModels\n" }, { "contract_id": "FileCategory", "contract_type": "Class", "file_path": "backend/src/models/storage.py", - "start_line": 8, + "start_line": 9, "end_line": 14, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Enumeration of supported file categories in the storage system." }, "relations": [], @@ -61754,19 +49754,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:FileCategory:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Enumeration of supported file categories in the storage system.\nclass FileCategory(str, Enum):\n BACKUP = \"backups\"\n REPOSITORY = \"repositorys\"\n# [/DEF:FileCategory:Class]\n" + "anchor_syntax": "region", + "body": "# #region FileCategory [C:1] [TYPE Class]\n# @BRIEF Enumeration of supported file categories in the storage system.\nclass FileCategory(str, Enum):\n BACKUP = \"backups\"\n REPOSITORY = \"repositorys\"\n# #endregion FileCategory\n" }, { "contract_id": "StorageConfig", "contract_type": "Class", "file_path": "backend/src/models/storage.py", "start_line": 16, - "end_line": 26, + "end_line": 25, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Configuration model for the storage system, defining paths and naming patterns." }, "relations": [], @@ -61781,19 +49781,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:StorageConfig:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Configuration model for the storage system, defining paths and naming patterns.\nclass StorageConfig(BaseModel):\n root_path: str = Field(default=\"backups\", description=\"Absolute path to the storage root directory.\")\n backup_path: str = Field(default=\"backups\", description=\"Subpath for backups.\")\n repo_path: str = Field(default=\"repositorys\", description=\"Subpath for repositories.\")\n backup_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for backup directory structure.\")\n repo_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for repository directory structure.\")\n filename_pattern: str = Field(default=\"{name}_{timestamp}\", description=\"Pattern for filenames.\")\n# [/DEF:StorageConfig:Class]\n" + "anchor_syntax": "region", + "body": "# #region StorageConfig [C:1] [TYPE Class]\n# @BRIEF Configuration model for the storage system, defining paths and naming patterns.\nclass StorageConfig(BaseModel):\n root_path: str = Field(default=\"backups\", description=\"Absolute path to the storage root directory.\")\n backup_path: str = Field(default=\"backups\", description=\"Subpath for backups.\")\n repo_path: str = Field(default=\"repositorys\", description=\"Subpath for repositories.\")\n backup_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for backup directory structure.\")\n repo_structure_pattern: str = Field(default=\"{category}/\", description=\"Pattern for repository directory structure.\")\n filename_pattern: str = Field(default=\"{name}_{timestamp}\", description=\"Pattern for filenames.\")\n# #endregion StorageConfig\n" }, { "contract_id": "StoredFile", "contract_type": "Class", "file_path": "backend/src/models/storage.py", - "start_line": 28, - "end_line": 38, + "start_line": 27, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Data model representing metadata for a file stored in the system." }, "relations": [], @@ -61808,29 +49808,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:StoredFile:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Data model representing metadata for a file stored in the system.\nclass StoredFile(BaseModel):\n name: str = Field(..., description=\"Name of the file (including extension).\")\n path: str = Field(..., description=\"Relative path from storage root.\")\n size: int = Field(..., ge=0, description=\"Size of the file in bytes.\")\n created_at: datetime = Field(..., description=\"Creation timestamp.\")\n category: FileCategory = Field(..., description=\"Category of the file.\")\n mime_type: Optional[str] = Field(None, description=\"MIME type of the file.\")\n# [/DEF:StoredFile:Class]\n" + "anchor_syntax": "region", + "body": "# #region StoredFile [C:1] [TYPE Class]\n# @BRIEF Data model representing metadata for a file stored in the system.\nclass StoredFile(BaseModel):\n name: str = Field(..., description=\"Name of the file (including extension).\")\n path: str = Field(..., description=\"Relative path from storage root.\")\n size: int = Field(..., ge=0, description=\"Size of the file in bytes.\")\n created_at: datetime = Field(..., description=\"Creation timestamp.\")\n category: FileCategory = Field(..., description=\"Category of the file.\")\n mime_type: str | None = Field(None, description=\"MIME type of the file.\")\n# #endregion StoredFile\n" }, { "contract_id": "TaskModels", "contract_type": "Module", "file_path": "backend/src/models/task.py", "start_line": 1, - "end_line": 116, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "INVARIANT": "All primary keys are UUID strings.", "LAYER": "Domain", - "PURPOSE": "Defines the database schema for task execution records.", - "SEMANTICS": [ - "database", - "task", - "record", - "sqlalchemy", - "sqlite" - ] + "PURPOSE": "Defines the database schema for task execution records." }, "relations": [ { @@ -61869,19 +49862,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskModels:Module]\n#\n# @COMPLEXITY: 1\n# @SEMANTICS: database, task, record, sqlalchemy, sqlite\n# @PURPOSE: Defines the database schema for task execution records.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> sqlalchemy\n#\n# @INVARIANT: All primary keys are UUID strings.\n\n# [SECTION: IMPORTS]\nfrom sqlalchemy import Column, String, DateTime, JSON, ForeignKey, Text, Integer, Index\nfrom sqlalchemy.sql import func\nfrom .mapping import Base\nimport uuid\n# [/SECTION]\n\n# [DEF:TaskRecord:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a persistent record of a task execution.\nclass TaskRecord(Base):\n __tablename__ = \"task_records\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n type = Column(String, nullable=False) # e.g., \"backup\", \"migration\"\n status = Column(String, nullable=False) # Enum: \"PENDING\", \"RUNNING\", \"SUCCESS\", \"FAILED\"\n environment_id = Column(String, ForeignKey(\"environments.id\"), nullable=True)\n started_at = Column(DateTime(timezone=True), nullable=True)\n finished_at = Column(DateTime(timezone=True), nullable=True)\n logs = Column(JSON, nullable=True) # Store structured logs as JSON (legacy, kept for backward compatibility)\n error = Column(String, nullable=True)\n result = Column(JSON, nullable=True)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n params = Column(JSON, nullable=True)\n# [/DEF:TaskRecord:Class]\n\n# [DEF:TaskLogRecord:Class]\n# @PURPOSE: Represents a single persistent log entry for a task.\n# @COMPLEXITY: 3\n# @RELATION: DEPENDS_ON -> TaskRecord\n# @INVARIANT: Each log entry belongs to exactly one task.\n#\n# @TEST_CONTRACT: TaskLogCreate ->\n# {\n# required_fields: {\n# task_id: str,\n# timestamp: datetime,\n# level: str,\n# source: str,\n# message: str\n# },\n# optional_fields: {\n# metadata_json: str,\n# id: int\n# },\n# invariants: [\n# \"task_id matches an existing TaskRecord.id\"\n# ]\n# }\n#\n# @TEST_FIXTURE: basic_info_log ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"INFO\",\n# source: \"system\",\n# message: \"Task initialization complete\"\n# }\n#\n# @TEST_EDGE: missing_required_field ->\n# {\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"ERROR\",\n# source: \"system\",\n# message: \"Missing task_id\"\n# }\n#\n# @TEST_EDGE: invalid_type ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: 500,\n# source: \"system\",\n# message: \"Integer level\"\n# }\n#\n# @TEST_EDGE: empty_message ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"DEBUG\",\n# source: \"system\",\n# message: \"\"\n# }\n#\n# @TEST_INVARIANT: exact_one_task_association -> verifies: [basic_info_log, missing_required_field]\nclass TaskLogRecord(Base):\n __tablename__ = \"task_logs\"\n\n id = Column(Integer, primary_key=True, autoincrement=True)\n task_id = Column(String, ForeignKey(\"task_records.id\", ondelete=\"CASCADE\"), nullable=False, index=True)\n timestamp = Column(DateTime(timezone=True), nullable=False, index=True)\n level = Column(String(16), nullable=False) # INFO, WARNING, ERROR, DEBUG\n source = Column(String(64), nullable=False, default=\"system\") # plugin, superset_api, git, etc.\n message = Column(Text, nullable=False)\n metadata_json = Column(Text, nullable=True) # JSON string for additional metadata\n\n # Composite indexes for efficient filtering\n __table_args__ = (\n Index('ix_task_logs_task_timestamp', 'task_id', 'timestamp'),\n Index('ix_task_logs_task_level', 'task_id', 'level'),\n Index('ix_task_logs_task_source', 'task_id', 'source'),\n )\n# [/DEF:TaskLogRecord:Class]\n\n# [/DEF:TaskModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region TaskModels [C:1] [TYPE Module] [SEMANTICS sqlalchemy, task, model, schema, execution, task-record]\n#\n# @BRIEF Defines the database schema for task execution records.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> sqlalchemy\n#\n# @INVARIANT: All primary keys are UUID strings.\n\nimport uuid\n\nfrom sqlalchemy import JSON, Column, DateTime, ForeignKey, Index, Integer, String, Text\nfrom sqlalchemy.sql import func\n\nfrom .mapping import Base\n\n\n# #region TaskRecord [C:1] [TYPE Class]\n# @BRIEF Represents a persistent record of a task execution.\nclass TaskRecord(Base):\n __tablename__ = \"task_records\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n type = Column(String, nullable=False) # e.g., \"backup\", \"migration\"\n status = Column(String, nullable=False) # Enum: \"PENDING\", \"RUNNING\", \"SUCCESS\", \"FAILED\"\n environment_id = Column(String, ForeignKey(\"environments.id\"), nullable=True)\n started_at = Column(DateTime(timezone=True), nullable=True)\n finished_at = Column(DateTime(timezone=True), nullable=True)\n logs = Column(JSON, nullable=True) # Store structured logs as JSON (legacy, kept for backward compatibility)\n error = Column(String, nullable=True)\n result = Column(JSON, nullable=True)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n params = Column(JSON, nullable=True)\n# #endregion TaskRecord\n\n# #region TaskLogRecord [C:3] [TYPE Class]\n# @BRIEF Represents a single persistent log entry for a task.\n# @RELATION DEPENDS_ON -> TaskRecord\n# @INVARIANT: Each log entry belongs to exactly one task.\n#\n# @TEST_CONTRACT: TaskLogCreate ->\n# {\n# required_fields: {\n# task_id: str,\n# timestamp: datetime,\n# level: str,\n# source: str,\n# message: str\n# },\n# optional_fields: {\n# metadata_json: str,\n# id: int\n# },\n# invariants: [\n# \"task_id matches an existing TaskRecord.id\"\n# ]\n# }\n#\n# @TEST_FIXTURE: basic_info_log ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"INFO\",\n# source: \"system\",\n# message: \"Task initialization complete\"\n# }\n#\n# @TEST_EDGE: missing_required_field ->\n# {\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"ERROR\",\n# source: \"system\",\n# message: \"Missing task_id\"\n# }\n#\n# @TEST_EDGE: invalid_type ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: 500,\n# source: \"system\",\n# message: \"Integer level\"\n# }\n#\n# @TEST_EDGE: empty_message ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"DEBUG\",\n# source: \"system\",\n# message: \"\"\n# }\n#\n# @TEST_INVARIANT: exact_one_task_association -> verifies: [basic_info_log, missing_required_field]\nclass TaskLogRecord(Base):\n __tablename__ = \"task_logs\"\n\n id = Column(Integer, primary_key=True, autoincrement=True)\n task_id = Column(String, ForeignKey(\"task_records.id\", ondelete=\"CASCADE\"), nullable=False, index=True)\n timestamp = Column(DateTime(timezone=True), nullable=False, index=True)\n level = Column(String(16), nullable=False) # INFO, WARNING, ERROR, DEBUG\n source = Column(String(64), nullable=False, default=\"system\") # plugin, superset_api, git, etc.\n message = Column(Text, nullable=False)\n metadata_json = Column(Text, nullable=True) # JSON string for additional metadata\n\n # Composite indexes for efficient filtering\n __table_args__ = (\n Index('ix_task_logs_task_timestamp', 'task_id', 'timestamp'),\n Index('ix_task_logs_task_level', 'task_id', 'level'),\n Index('ix_task_logs_task_source', 'task_id', 'source'),\n )\n# #endregion TaskLogRecord\n\n# #endregion TaskModels\n" }, { "contract_id": "TaskRecord", "contract_type": "Class", "file_path": "backend/src/models/task.py", - "start_line": 18, - "end_line": 35, + "start_line": 17, + "end_line": 33, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Represents a persistent record of a task execution." }, "relations": [], @@ -61896,19 +49889,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskRecord:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a persistent record of a task execution.\nclass TaskRecord(Base):\n __tablename__ = \"task_records\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n type = Column(String, nullable=False) # e.g., \"backup\", \"migration\"\n status = Column(String, nullable=False) # Enum: \"PENDING\", \"RUNNING\", \"SUCCESS\", \"FAILED\"\n environment_id = Column(String, ForeignKey(\"environments.id\"), nullable=True)\n started_at = Column(DateTime(timezone=True), nullable=True)\n finished_at = Column(DateTime(timezone=True), nullable=True)\n logs = Column(JSON, nullable=True) # Store structured logs as JSON (legacy, kept for backward compatibility)\n error = Column(String, nullable=True)\n result = Column(JSON, nullable=True)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n params = Column(JSON, nullable=True)\n# [/DEF:TaskRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region TaskRecord [C:1] [TYPE Class]\n# @BRIEF Represents a persistent record of a task execution.\nclass TaskRecord(Base):\n __tablename__ = \"task_records\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n type = Column(String, nullable=False) # e.g., \"backup\", \"migration\"\n status = Column(String, nullable=False) # Enum: \"PENDING\", \"RUNNING\", \"SUCCESS\", \"FAILED\"\n environment_id = Column(String, ForeignKey(\"environments.id\"), nullable=True)\n started_at = Column(DateTime(timezone=True), nullable=True)\n finished_at = Column(DateTime(timezone=True), nullable=True)\n logs = Column(JSON, nullable=True) # Store structured logs as JSON (legacy, kept for backward compatibility)\n error = Column(String, nullable=True)\n result = Column(JSON, nullable=True)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n params = Column(JSON, nullable=True)\n# #endregion TaskRecord\n" }, { "contract_id": "TaskLogRecord", "contract_type": "Class", "file_path": "backend/src/models/task.py", - "start_line": 37, - "end_line": 114, + "start_line": 35, + "end_line": 111, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Each log entry belongs to exactly one task.", "PURPOSE": "Represents a single persistent log entry for a task.", "TEST_CONTRACT": "TaskLogCreate ->" @@ -61953,234 +49946,384 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TaskLogRecord:Class]\n# @PURPOSE: Represents a single persistent log entry for a task.\n# @COMPLEXITY: 3\n# @RELATION: DEPENDS_ON -> TaskRecord\n# @INVARIANT: Each log entry belongs to exactly one task.\n#\n# @TEST_CONTRACT: TaskLogCreate ->\n# {\n# required_fields: {\n# task_id: str,\n# timestamp: datetime,\n# level: str,\n# source: str,\n# message: str\n# },\n# optional_fields: {\n# metadata_json: str,\n# id: int\n# },\n# invariants: [\n# \"task_id matches an existing TaskRecord.id\"\n# ]\n# }\n#\n# @TEST_FIXTURE: basic_info_log ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"INFO\",\n# source: \"system\",\n# message: \"Task initialization complete\"\n# }\n#\n# @TEST_EDGE: missing_required_field ->\n# {\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"ERROR\",\n# source: \"system\",\n# message: \"Missing task_id\"\n# }\n#\n# @TEST_EDGE: invalid_type ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: 500,\n# source: \"system\",\n# message: \"Integer level\"\n# }\n#\n# @TEST_EDGE: empty_message ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"DEBUG\",\n# source: \"system\",\n# message: \"\"\n# }\n#\n# @TEST_INVARIANT: exact_one_task_association -> verifies: [basic_info_log, missing_required_field]\nclass TaskLogRecord(Base):\n __tablename__ = \"task_logs\"\n\n id = Column(Integer, primary_key=True, autoincrement=True)\n task_id = Column(String, ForeignKey(\"task_records.id\", ondelete=\"CASCADE\"), nullable=False, index=True)\n timestamp = Column(DateTime(timezone=True), nullable=False, index=True)\n level = Column(String(16), nullable=False) # INFO, WARNING, ERROR, DEBUG\n source = Column(String(64), nullable=False, default=\"system\") # plugin, superset_api, git, etc.\n message = Column(Text, nullable=False)\n metadata_json = Column(Text, nullable=True) # JSON string for additional metadata\n\n # Composite indexes for efficient filtering\n __table_args__ = (\n Index('ix_task_logs_task_timestamp', 'task_id', 'timestamp'),\n Index('ix_task_logs_task_level', 'task_id', 'level'),\n Index('ix_task_logs_task_source', 'task_id', 'source'),\n )\n# [/DEF:TaskLogRecord:Class]\n" + "anchor_syntax": "region", + "body": "# #region TaskLogRecord [C:3] [TYPE Class]\n# @BRIEF Represents a single persistent log entry for a task.\n# @RELATION DEPENDS_ON -> TaskRecord\n# @INVARIANT: Each log entry belongs to exactly one task.\n#\n# @TEST_CONTRACT: TaskLogCreate ->\n# {\n# required_fields: {\n# task_id: str,\n# timestamp: datetime,\n# level: str,\n# source: str,\n# message: str\n# },\n# optional_fields: {\n# metadata_json: str,\n# id: int\n# },\n# invariants: [\n# \"task_id matches an existing TaskRecord.id\"\n# ]\n# }\n#\n# @TEST_FIXTURE: basic_info_log ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"INFO\",\n# source: \"system\",\n# message: \"Task initialization complete\"\n# }\n#\n# @TEST_EDGE: missing_required_field ->\n# {\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"ERROR\",\n# source: \"system\",\n# message: \"Missing task_id\"\n# }\n#\n# @TEST_EDGE: invalid_type ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: 500,\n# source: \"system\",\n# message: \"Integer level\"\n# }\n#\n# @TEST_EDGE: empty_message ->\n# {\n# task_id: \"00000000-0000-0000-0000-000000000000\",\n# timestamp: \"2026-02-26T12:00:00Z\",\n# level: \"DEBUG\",\n# source: \"system\",\n# message: \"\"\n# }\n#\n# @TEST_INVARIANT: exact_one_task_association -> verifies: [basic_info_log, missing_required_field]\nclass TaskLogRecord(Base):\n __tablename__ = \"task_logs\"\n\n id = Column(Integer, primary_key=True, autoincrement=True)\n task_id = Column(String, ForeignKey(\"task_records.id\", ondelete=\"CASCADE\"), nullable=False, index=True)\n timestamp = Column(DateTime(timezone=True), nullable=False, index=True)\n level = Column(String(16), nullable=False) # INFO, WARNING, ERROR, DEBUG\n source = Column(String(64), nullable=False, default=\"system\") # plugin, superset_api, git, etc.\n message = Column(Text, nullable=False)\n metadata_json = Column(Text, nullable=True) # JSON string for additional metadata\n\n # Composite indexes for efficient filtering\n __table_args__ = (\n Index('ix_task_logs_task_timestamp', 'task_id', 'timestamp'),\n Index('ix_task_logs_task_level', 'task_id', 'level'),\n Index('ix_task_logs_task_source', 'task_id', 'source'),\n )\n# #endregion TaskLogRecord\n" }, { "contract_id": "TranslateModels", "contract_type": "Module", "file_path": "backend/src/models/translate.py", "start_line": 1, - "end_line": 283, - "tier": "TIER_1", - "complexity": 2, + "end_line": 295, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "COMPLEXITY": 2, + "COMPLEXITY": 3, "LAYER": "Domain", "PURPOSE": "SQLAlchemy ORM models for LLM-based SQL/dashboard translation across dialects." }, - "relations": [], - "schema_warnings": [], + "relations": [ + { + "source_id": "TranslateModels", + "relation_type": "INHERITS_FROM", + "target_id": "MappingModels", + "target_ref": "MappingModels:Base" + } + ], + "schema_warnings": [ + { + "code": "invalid_relation_predicate", + "tag": "RELATION", + "message": "Predicate INHERITS_FROM is not in allowed_predicates", + "detail": { + "allowed": [ + "DEPENDS_ON", + "CALLS", + "INHERITS", + "IMPLEMENTS", + "DISPATCHES", + "BINDS_TO", + "CALLED_BY", + "VERIFIES" + ], + "predicate": "INHERITS_FROM" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslateModels [C:2] [TYPE Module]\n# @BRIEF SQLAlchemy ORM models for LLM-based SQL/dashboard translation across dialects.\n# @LAYER Domain\n\nfrom sqlalchemy import (\n Column, String, Boolean, DateTime, JSON, Text,\n ForeignKey, Integer, UniqueConstraint, Index\n)\nfrom sqlalchemy.dialects.postgresql import JSON as PG_JSON\nfrom datetime import datetime, timezone\nimport uuid\nimport hashlib\nfrom .mapping import Base\n\n\n# #region generate_uuid [C:1] [TYPE Function]\ndef generate_uuid():\n return str(uuid.uuid4())\n# #endregion generate_uuid\n\n\n# #region TranslationJob [C:1] [TYPE Class]\nclass TranslationJob(Base):\n __tablename__ = \"translation_jobs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n database_dialect = Column(String, nullable=True, comment=\"Detected dialect from Superset connection at save time\")\n status = Column(String, nullable=False, default=\"DRAFT\") # DRAFT, READY, RUNNING, COMPLETED, FAILED, CANCELLED\n\n # Datasource & target table configuration\n source_datasource_id = Column(String, nullable=True, comment=\"Superset datasource ID\")\n source_table = Column(String, nullable=True, comment=\"Source table name resolved from datasource\")\n target_schema = Column(String, nullable=True, comment=\"Target table schema\")\n target_table = Column(String, nullable=True, comment=\"Target table name\")\n\n # Column mapping\n source_key_cols = Column(JSON, nullable=True, comment=\"Source key column names for composite key\")\n target_key_cols = Column(JSON, nullable=True, comment=\"Target key column names for composite key\")\n translation_column = Column(String, nullable=True, comment=\"The column whose values will be translated\")\n context_columns = Column(JSON, nullable=True, comment=\"Context column names included in LLM prompt\")\n\n # LLM & processing settings\n target_language = Column(String, nullable=True, comment=\"Target language code (e.g. en, ru)\")\n provider_id = Column(String, nullable=True, comment=\"LLM provider ID\")\n batch_size = Column(Integer, nullable=False, default=50, comment=\"Records per batch\")\n upsert_strategy = Column(String, nullable=False, default=\"MERGE\", comment=\"MERGE, INSERT, UPDATE\")\n\n # Environment association\n environment_id = Column(String, nullable=True, comment=\"Superset environment ID for datasource access\")\n target_database_id = Column(String, nullable=True, comment=\"Superset database ID for INSERT via SQL Lab\")\n\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TranslationJob\n\n\n# #region TranslationRun [C:1] [TYPE Class]\nclass TranslationRun(Base):\n __tablename__ = \"translation_runs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED\n trigger_type = Column(String, nullable=True, comment=\"manual, scheduled, retry, baseline_expired\")\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n error_message = Column(Text, nullable=True)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n insert_status = Column(String, nullable=True, comment=\"Status of the Superset insert/update operation\")\n superset_execution_id = Column(String, nullable=True, comment=\"Superset execution/task ID\")\n superset_execution_log = Column(JSON, nullable=True, comment=\"Superset execution log output\")\n config_snapshot = Column(JSON, nullable=True, comment=\"Snapshot of job config at run creation time\")\n key_hash = Column(String, nullable=True, comment=\"Hash of source key fields for dedup\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at run time\")\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationRun\n\n\n# #region TranslationBatch [C:1] [TYPE Class]\nclass TranslationBatch(Base):\n __tablename__ = \"translation_batches\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n batch_index = Column(Integer, nullable=False)\n status = Column(String, nullable=False, default=\"PENDING\")\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationBatch\n\n\n# #region TranslationRecord [C:1] [TYPE Class]\nclass TranslationRecord(Base):\n __tablename__ = \"translation_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n batch_id = Column(String, ForeignKey(\"translation_batches.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, SUCCESS, FAILED, SKIPPED\n error_message = Column(Text, nullable=True)\n token_count_input = Column(Integer, nullable=True)\n token_count_output = Column(Integer, nullable=True)\n translation_duration_ms = Column(Integer, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Snapshot of source row key/context column values for INSERT phase\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n Index(\"ix_translation_records_run_status\", \"run_id\", \"status\"),\n )\n# #endregion TranslationRecord\n\n\n# #region TranslationEvent [C:1] [TYPE Class]\nclass TranslationEvent(Base):\n __tablename__ = \"translation_events\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n event_type = Column(String, nullable=False) # JOB_CREATED, JOB_STARTED, JOB_COMPLETED, JOB_FAILED, etc.\n event_data = Column(JSON, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationEvent\n\n\n# #region TranslationPreviewSession [C:1] [TYPE Class]\nclass TranslationPreviewSession(Base):\n __tablename__ = \"translation_preview_sessions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True)\n status = Column(String, nullable=False, default=\"ACTIVE\") # ACTIVE, APPLIED, DISCARDED, EXPIRED\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n expires_at = Column(DateTime, nullable=True)\n# #endregion TranslationPreviewSession\n\n\n# #region TranslationPreviewRecord [C:1] [TYPE Class]\nclass TranslationPreviewRecord(Base):\n __tablename__ = \"translation_preview_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n session_id = Column(String, ForeignKey(\"translation_preview_sessions.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True)\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, APPROVED, REJECTED\n feedback = Column(Text, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Snapshot of source row values for key/context columns\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationPreviewRecord\n\n\n# #region TerminologyDictionary [C:1] [TYPE Class]\nclass TerminologyDictionary(Base):\n __tablename__ = \"terminology_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TerminologyDictionary\n\n\n# #region DictionaryEntry [C:1] [TYPE Class]\nclass DictionaryEntry(Base):\n __tablename__ = \"dictionary_entries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n source_term = Column(String, nullable=False)\n source_term_normalized = Column(String, nullable=False)\n target_term = Column(String, nullable=False)\n context_notes = Column(Text, nullable=True)\n origin_run_id = Column(String, nullable=True, comment=\"Run ID from which this correction originated\")\n origin_row_key = Column(String, nullable=True, comment=\"Row key within the run that triggered this correction\")\n origin_user_id = Column(String, nullable=True, comment=\"User who submitted the correction\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n UniqueConstraint(\n \"dictionary_id\", \"source_term_normalized\",\n name=\"uq_dictionary_entry_term\"\n ),\n )\n# #endregion DictionaryEntry\n\n\n# #region TranslationSchedule [C:1] [TYPE Class]\nclass TranslationSchedule(Base):\n __tablename__ = \"translation_schedules\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n cron_expression = Column(String, nullable=False)\n timezone = Column(String, nullable=False, default=\"UTC\")\n is_active = Column(Boolean, default=True)\n last_run_at = Column(DateTime, nullable=True)\n next_run_at = Column(DateTime, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TranslationSchedule\n\n\n# #region TranslationJobDictionary [C:1] [TYPE Class]\nclass TranslationJobDictionary(Base):\n __tablename__ = \"translation_job_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n UniqueConstraint(\n \"job_id\", \"dictionary_id\",\n name=\"uq_job_dictionary\"\n ),\n )\n# #endregion TranslationJobDictionary\n\n\n# #region MetricSnapshot [C:1] [TYPE Class]\nclass MetricSnapshot(Base):\n __tablename__ = \"translation_metric_snapshots\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n key_hash = Column(String, nullable=False, comment=\"Hash of dimension key fields for aggregation\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at capture time\")\n covers_events_before = Column(DateTime, nullable=True, comment=\"Indicates snapshot covers events before this timestamp\")\n total_jobs = Column(Integer, default=0)\n total_runs = Column(Integer, default=0)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n avg_duration_ms = Column(Integer, nullable=True)\n p50_duration_ms = Column(Integer, nullable=True)\n p95_duration_ms = Column(Integer, nullable=True)\n p99_duration_ms = Column(Integer, nullable=True)\n snapshot_date = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n Index(\"ix_metric_snapshots_job_date\", \"job_id\", \"snapshot_date\"),\n )\n# #endregion MetricSnapshot\n\n# #endregion TranslateModels\n" - }, - { - "contract_id": "generate_uuid", - "contract_type": "Function", - "file_path": "backend/src/models/translate.py", - "start_line": 16, - "end_line": 19, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region generate_uuid [C:1] [TYPE Function]\ndef generate_uuid():\n return str(uuid.uuid4())\n# #endregion generate_uuid\n" + "body": "# #region TranslateModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, translate, model, schema, dashboard, llm]\n# @BRIEF SQLAlchemy ORM models for LLM-based SQL/dashboard translation across dialects.\n# @LAYER: Domain\n# @RELATION INHERITS_FROM -> MappingModels:Base\n\nimport uuid\nfrom datetime import UTC, datetime\n\nfrom sqlalchemy import JSON, Boolean, Column, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint\n\nfrom .mapping import Base\n\n\ndef generate_uuid():\n return str(uuid.uuid4())\n\n\n# #region TranslationJob [TYPE Class]\n# @BRIEF A translation job representing a multi-dialect conversion task with column mappings, LLM config, and dictionary attachments.\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.\nclass TranslationJob(Base):\n __tablename__ = \"translation_jobs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n database_dialect = Column(String, nullable=True, comment=\"Detected dialect from Superset connection at save time\")\n status = Column(String, nullable=False, default=\"DRAFT\") # DRAFT, READY, RUNNING, COMPLETED, FAILED, CANCELLED\n\n # Datasource & target table configuration\n source_datasource_id = Column(String, nullable=True, comment=\"Superset datasource ID\")\n source_table = Column(String, nullable=True, comment=\"Source table name resolved from datasource\")\n target_schema = Column(String, nullable=True, comment=\"Target table schema\")\n target_table = Column(String, nullable=True, comment=\"Target table name\")\n\n # Column mapping\n source_key_cols = Column(JSON, nullable=True, comment=\"Source key column names for composite key\")\n target_key_cols = Column(JSON, nullable=True, comment=\"Target key column names for composite key\")\n translation_column = Column(String, nullable=True, comment=\"Source column whose values will be translated\")\n target_column = Column(String, nullable=True, comment=\"Target column for translated output (defaults to translation_column)\")\n context_columns = Column(JSON, nullable=True, comment=\"Context column names included in LLM prompt\")\n\n # LLM & processing settings\n target_language = Column(String, nullable=True, comment=\"Target language code (e.g. en, ru)\")\n provider_id = Column(String, nullable=True, comment=\"LLM provider ID\")\n batch_size = Column(Integer, nullable=False, default=50, comment=\"Records per batch\")\n upsert_strategy = Column(String, nullable=False, default=\"MERGE\", comment=\"MERGE, INSERT, UPDATE\")\n\n # Environment association\n environment_id = Column(String, nullable=True, comment=\"Superset environment ID for datasource access\")\n target_database_id = Column(String, nullable=True, comment=\"Superset database ID for SQL Lab insert target\")\n\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n# #endregion TranslationJob\n\n\n# #region TranslationRun [TYPE Class]\n# @BRIEF Represents a single execution of a translation job, capturing status, timing, and Superset execution metadata.\nclass TranslationRun(Base):\n __tablename__ = \"translation_runs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED\n trigger_type = Column(String, nullable=True, comment=\"manual, scheduled, retry, baseline_expired\")\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n error_message = Column(Text, nullable=True)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n insert_status = Column(String, nullable=True, comment=\"Status of the Superset insert/update operation\")\n superset_execution_id = Column(String, nullable=True, comment=\"Superset execution/task ID\")\n superset_execution_log = Column(JSON, nullable=True, comment=\"Superset execution log output\")\n config_snapshot = Column(JSON, nullable=True, comment=\"Snapshot of job config at run creation time\")\n key_hash = Column(String, nullable=True, comment=\"Hash of source key fields for dedup\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at run time\")\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion TranslationRun\n\n\n# #region TranslationBatch [TYPE Class]\n# @BRIEF Groups translation records within a run into manageable batches with timing and record counts.\nclass TranslationBatch(Base):\n __tablename__ = \"translation_batches\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n batch_index = Column(Integer, nullable=False)\n status = Column(String, nullable=False, default=\"PENDING\")\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion TranslationBatch\n\n\n# #region TranslationRecord [TYPE Class]\n# @BRIEF Individual translation result for a single SQL statement or dashboard element, tracking source, target, and error state.\nclass TranslationRecord(Base):\n __tablename__ = \"translation_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n batch_id = Column(String, ForeignKey(\"translation_batches.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Original source row key columns for upsert matching\")\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, SUCCESS, FAILED, SKIPPED\n error_message = Column(Text, nullable=True)\n token_count_input = Column(Integer, nullable=True)\n token_count_output = Column(Integer, nullable=True)\n translation_duration_ms = Column(Integer, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n\n __table_args__ = (\n Index(\"ix_translation_records_run_status\", \"run_id\", \"status\"),\n )\n# #endregion TranslationRecord\n\n\n# #region TranslationEvent [TYPE Class]\n# @BRIEF Audit/event log for translation operations, with optional run_id for context.\nclass TranslationEvent(Base):\n __tablename__ = \"translation_events\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n event_type = Column(String, nullable=False) # JOB_CREATED, JOB_STARTED, JOB_COMPLETED, JOB_FAILED, etc.\n event_data = Column(JSON, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion TranslationEvent\n\n\n# #region TranslationPreviewSession [TYPE Class]\n# @BRIEF A preview session allowing users to review proposed translations before applying them.\nclass TranslationPreviewSession(Base):\n __tablename__ = \"translation_preview_sessions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True)\n status = Column(String, nullable=False, default=\"ACTIVE\") # ACTIVE, APPLIED, DISCARDED, EXPIRED\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n expires_at = Column(DateTime, nullable=True)\n# #endregion TranslationPreviewSession\n\n\n# #region TranslationPreviewRecord [TYPE Class]\n# @BRIEF Individual preview entry within a preview session, showing original and translated content side by side.\nclass TranslationPreviewRecord(Base):\n __tablename__ = \"translation_preview_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n session_id = Column(String, ForeignKey(\"translation_preview_sessions.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True)\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Original source row key columns for upsert matching\")\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, APPROVED, REJECTED\n feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion TranslationPreviewRecord\n\n\n# #region TerminologyDictionary [TYPE Class]\n# @BRIEF A named collection of terminology mappings used during translation to ensure consistent term translation.\nclass TerminologyDictionary(Base):\n __tablename__ = \"terminology_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n# #endregion TerminologyDictionary\n\n\n# #region DictionaryEntry [TYPE Class]\n# @BRIEF A single term mapping entry with case-insensitive unique constraint on source_term_normalized and origin tracking.\nclass DictionaryEntry(Base):\n __tablename__ = \"dictionary_entries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n source_term = Column(String, nullable=False)\n source_term_normalized = Column(String, nullable=False)\n target_term = Column(String, nullable=False)\n context_notes = Column(Text, nullable=True)\n origin_run_id = Column(String, nullable=True, comment=\"Run ID from which this correction originated\")\n origin_row_key = Column(String, nullable=True, comment=\"Row key within the run that triggered this correction\")\n origin_user_id = Column(String, nullable=True, comment=\"User who submitted the correction\")\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n\n __table_args__ = (\n UniqueConstraint(\n \"dictionary_id\", \"source_term_normalized\",\n name=\"uq_dictionary_entry_term\"\n ),\n )\n# #endregion DictionaryEntry\n\n\n# #region TranslationSchedule [TYPE Class]\n# @BRIEF Defines a cron-based schedule for recurring translation jobs.\nclass TranslationSchedule(Base):\n __tablename__ = \"translation_schedules\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n cron_expression = Column(String, nullable=False)\n timezone = Column(String, nullable=False, default=\"UTC\")\n is_active = Column(Boolean, default=True)\n last_run_at = Column(DateTime, nullable=True)\n next_run_at = Column(DateTime, nullable=True)\n execution_mode = Column(String, nullable=False, default=\"full\", comment=\"full, new_key_only\")\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n# #endregion TranslationSchedule\n\n\n# #region TranslationJobDictionary [TYPE Class]\n# @BRIEF Many-to-many association between translation jobs and terminology dictionaries.\nclass TranslationJobDictionary(Base):\n __tablename__ = \"translation_job_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n\n __table_args__ = (\n UniqueConstraint(\n \"job_id\", \"dictionary_id\",\n name=\"uq_job_dictionary\"\n ),\n )\n# #endregion TranslationJobDictionary\n\n\n# #region MetricSnapshot [TYPE Class]\n# @BRIEF Captures translation performance metrics at a point in time with config/content/dictionary hash keys for aggregation.\nclass MetricSnapshot(Base):\n __tablename__ = \"translation_metric_snapshots\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n key_hash = Column(String, nullable=False, comment=\"Hash of dimension key fields for aggregation\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at capture time\")\n covers_events_before = Column(DateTime, nullable=True, comment=\"Indicates snapshot covers events before this timestamp\")\n total_jobs = Column(Integer, default=0)\n total_runs = Column(Integer, default=0)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n avg_duration_ms = Column(Integer, nullable=True)\n p50_duration_ms = Column(Integer, nullable=True)\n p95_duration_ms = Column(Integer, nullable=True)\n p99_duration_ms = Column(Integer, nullable=True)\n snapshot_date = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n\n __table_args__ = (\n Index(\"ix_metric_snapshots_job_date\", \"job_id\", \"snapshot_date\"),\n )\n# #endregion MetricSnapshot\n\n# #endregion TranslateModels\n" }, { "contract_id": "TranslationJob", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 22, + "start_line": 18, "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "A translation job representing a multi-dialect conversion task with column mappings, LLM config, and dictionary attachments.", + "RATIONALE": "Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.", + "REJECTED": "Invalidating in-progress runs on config edit would break scheduled run continuity." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationJob [C:1] [TYPE Class]\nclass TranslationJob(Base):\n __tablename__ = \"translation_jobs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n database_dialect = Column(String, nullable=True, comment=\"Detected dialect from Superset connection at save time\")\n status = Column(String, nullable=False, default=\"DRAFT\") # DRAFT, READY, RUNNING, COMPLETED, FAILED, CANCELLED\n\n # Datasource & target table configuration\n source_datasource_id = Column(String, nullable=True, comment=\"Superset datasource ID\")\n source_table = Column(String, nullable=True, comment=\"Source table name resolved from datasource\")\n target_schema = Column(String, nullable=True, comment=\"Target table schema\")\n target_table = Column(String, nullable=True, comment=\"Target table name\")\n\n # Column mapping\n source_key_cols = Column(JSON, nullable=True, comment=\"Source key column names for composite key\")\n target_key_cols = Column(JSON, nullable=True, comment=\"Target key column names for composite key\")\n translation_column = Column(String, nullable=True, comment=\"The column whose values will be translated\")\n context_columns = Column(JSON, nullable=True, comment=\"Context column names included in LLM prompt\")\n\n # LLM & processing settings\n target_language = Column(String, nullable=True, comment=\"Target language code (e.g. en, ru)\")\n provider_id = Column(String, nullable=True, comment=\"LLM provider ID\")\n batch_size = Column(Integer, nullable=False, default=50, comment=\"Records per batch\")\n upsert_strategy = Column(String, nullable=False, default=\"MERGE\", comment=\"MERGE, INSERT, UPDATE\")\n\n # Environment association\n environment_id = Column(String, nullable=True, comment=\"Superset environment ID for datasource access\")\n target_database_id = Column(String, nullable=True, comment=\"Superset database ID for INSERT via SQL Lab\")\n\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TranslationJob\n" + "body": "# #region TranslationJob [TYPE Class]\n# @BRIEF A translation job representing a multi-dialect conversion task with column mappings, LLM config, and dictionary attachments.\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.\nclass TranslationJob(Base):\n __tablename__ = \"translation_jobs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n database_dialect = Column(String, nullable=True, comment=\"Detected dialect from Superset connection at save time\")\n status = Column(String, nullable=False, default=\"DRAFT\") # DRAFT, READY, RUNNING, COMPLETED, FAILED, CANCELLED\n\n # Datasource & target table configuration\n source_datasource_id = Column(String, nullable=True, comment=\"Superset datasource ID\")\n source_table = Column(String, nullable=True, comment=\"Source table name resolved from datasource\")\n target_schema = Column(String, nullable=True, comment=\"Target table schema\")\n target_table = Column(String, nullable=True, comment=\"Target table name\")\n\n # Column mapping\n source_key_cols = Column(JSON, nullable=True, comment=\"Source key column names for composite key\")\n target_key_cols = Column(JSON, nullable=True, comment=\"Target key column names for composite key\")\n translation_column = Column(String, nullable=True, comment=\"Source column whose values will be translated\")\n target_column = Column(String, nullable=True, comment=\"Target column for translated output (defaults to translation_column)\")\n context_columns = Column(JSON, nullable=True, comment=\"Context column names included in LLM prompt\")\n\n # LLM & processing settings\n target_language = Column(String, nullable=True, comment=\"Target language code (e.g. en, ru)\")\n provider_id = Column(String, nullable=True, comment=\"LLM provider ID\")\n batch_size = Column(Integer, nullable=False, default=50, comment=\"Records per batch\")\n upsert_strategy = Column(String, nullable=False, default=\"MERGE\", comment=\"MERGE, INSERT, UPDATE\")\n\n # Environment association\n environment_id = Column(String, nullable=True, comment=\"Superset environment ID for datasource access\")\n target_database_id = Column(String, nullable=True, comment=\"Superset database ID for SQL Lab insert target\")\n\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n# #endregion TranslationJob\n" }, { "contract_id": "TranslationRun", "contract_type": "Class", "file_path": "backend/src/models/translate.py", "start_line": 62, - "end_line": 86, + "end_line": 87, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Represents a single execution of a translation job, capturing status, timing, and Superset execution metadata." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationRun [C:1] [TYPE Class]\nclass TranslationRun(Base):\n __tablename__ = \"translation_runs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED\n trigger_type = Column(String, nullable=True, comment=\"manual, scheduled, retry, baseline_expired\")\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n error_message = Column(Text, nullable=True)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n insert_status = Column(String, nullable=True, comment=\"Status of the Superset insert/update operation\")\n superset_execution_id = Column(String, nullable=True, comment=\"Superset execution/task ID\")\n superset_execution_log = Column(JSON, nullable=True, comment=\"Superset execution log output\")\n config_snapshot = Column(JSON, nullable=True, comment=\"Snapshot of job config at run creation time\")\n key_hash = Column(String, nullable=True, comment=\"Hash of source key fields for dedup\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at run time\")\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationRun\n" + "body": "# #region TranslationRun [TYPE Class]\n# @BRIEF Represents a single execution of a translation job, capturing status, timing, and Superset execution metadata.\nclass TranslationRun(Base):\n __tablename__ = \"translation_runs\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED\n trigger_type = Column(String, nullable=True, comment=\"manual, scheduled, retry, baseline_expired\")\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n error_message = Column(Text, nullable=True)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n insert_status = Column(String, nullable=True, comment=\"Status of the Superset insert/update operation\")\n superset_execution_id = Column(String, nullable=True, comment=\"Superset execution/task ID\")\n superset_execution_log = Column(JSON, nullable=True, comment=\"Superset execution log output\")\n config_snapshot = Column(JSON, nullable=True, comment=\"Snapshot of job config at run creation time\")\n key_hash = Column(String, nullable=True, comment=\"Hash of source key fields for dedup\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at run time\")\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion TranslationRun\n" }, { "contract_id": "TranslationBatch", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 89, - "end_line": 103, + "start_line": 90, + "end_line": 105, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Groups translation records within a run into manageable batches with timing and record counts." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationBatch [C:1] [TYPE Class]\nclass TranslationBatch(Base):\n __tablename__ = \"translation_batches\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n batch_index = Column(Integer, nullable=False)\n status = Column(String, nullable=False, default=\"PENDING\")\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationBatch\n" + "body": "# #region TranslationBatch [TYPE Class]\n# @BRIEF Groups translation records within a run into manageable batches with timing and record counts.\nclass TranslationBatch(Base):\n __tablename__ = \"translation_batches\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n batch_index = Column(Integer, nullable=False)\n status = Column(String, nullable=False, default=\"PENDING\")\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n started_at = Column(DateTime, nullable=True)\n completed_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion TranslationBatch\n" }, { "contract_id": "TranslationRecord", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 106, - "end_line": 129, + "start_line": 108, + "end_line": 132, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Individual translation result for a single SQL statement or dashboard element, tracking source, target, and error state." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationRecord [C:1] [TYPE Class]\nclass TranslationRecord(Base):\n __tablename__ = \"translation_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n batch_id = Column(String, ForeignKey(\"translation_batches.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, SUCCESS, FAILED, SKIPPED\n error_message = Column(Text, nullable=True)\n token_count_input = Column(Integer, nullable=True)\n token_count_output = Column(Integer, nullable=True)\n translation_duration_ms = Column(Integer, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Snapshot of source row key/context column values for INSERT phase\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n Index(\"ix_translation_records_run_status\", \"run_id\", \"status\"),\n )\n# #endregion TranslationRecord\n" + "body": "# #region TranslationRecord [TYPE Class]\n# @BRIEF Individual translation result for a single SQL statement or dashboard element, tracking source, target, and error state.\nclass TranslationRecord(Base):\n __tablename__ = \"translation_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n batch_id = Column(String, ForeignKey(\"translation_batches.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Original source row key columns for upsert matching\")\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, SUCCESS, FAILED, SKIPPED\n error_message = Column(Text, nullable=True)\n token_count_input = Column(Integer, nullable=True)\n token_count_output = Column(Integer, nullable=True)\n translation_duration_ms = Column(Integer, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n\n __table_args__ = (\n Index(\"ix_translation_records_run_status\", \"run_id\", \"status\"),\n )\n# #endregion TranslationRecord\n" }, { "contract_id": "TranslationEvent", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 132, - "end_line": 143, + "start_line": 135, + "end_line": 147, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Audit/event log for translation operations, with optional run_id for context." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationEvent [C:1] [TYPE Class]\nclass TranslationEvent(Base):\n __tablename__ = \"translation_events\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n event_type = Column(String, nullable=False) # JOB_CREATED, JOB_STARTED, JOB_COMPLETED, JOB_FAILED, etc.\n event_data = Column(JSON, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationEvent\n" + "body": "# #region TranslationEvent [TYPE Class]\n# @BRIEF Audit/event log for translation operations, with optional run_id for context.\nclass TranslationEvent(Base):\n __tablename__ = \"translation_events\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n event_type = Column(String, nullable=False) # JOB_CREATED, JOB_STARTED, JOB_COMPLETED, JOB_FAILED, etc.\n event_data = Column(JSON, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion TranslationEvent\n" }, { "contract_id": "TranslationPreviewSession", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 146, - "end_line": 157, + "start_line": 150, + "end_line": 162, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "A preview session allowing users to review proposed translations before applying them." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationPreviewSession [C:1] [TYPE Class]\nclass TranslationPreviewSession(Base):\n __tablename__ = \"translation_preview_sessions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True)\n status = Column(String, nullable=False, default=\"ACTIVE\") # ACTIVE, APPLIED, DISCARDED, EXPIRED\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n expires_at = Column(DateTime, nullable=True)\n# #endregion TranslationPreviewSession\n" + "body": "# #region TranslationPreviewSession [TYPE Class]\n# @BRIEF A preview session allowing users to review proposed translations before applying them.\nclass TranslationPreviewSession(Base):\n __tablename__ = \"translation_preview_sessions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True)\n status = Column(String, nullable=False, default=\"ACTIVE\") # ACTIVE, APPLIED, DISCARDED, EXPIRED\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n expires_at = Column(DateTime, nullable=True)\n# #endregion TranslationPreviewSession\n" }, { "contract_id": "TranslationPreviewRecord", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 160, - "end_line": 175, + "start_line": 165, + "end_line": 181, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Individual preview entry within a preview session, showing original and translated content side by side." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationPreviewRecord [C:1] [TYPE Class]\nclass TranslationPreviewRecord(Base):\n __tablename__ = \"translation_preview_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n session_id = Column(String, ForeignKey(\"translation_preview_sessions.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True)\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, APPROVED, REJECTED\n feedback = Column(Text, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Snapshot of source row values for key/context columns\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n# #endregion TranslationPreviewRecord\n" + "body": "# #region TranslationPreviewRecord [TYPE Class]\n# @BRIEF Individual preview entry within a preview session, showing original and translated content side by side.\nclass TranslationPreviewRecord(Base):\n __tablename__ = \"translation_preview_records\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n session_id = Column(String, ForeignKey(\"translation_preview_sessions.id\"), nullable=False, index=True)\n source_sql = Column(Text, nullable=True)\n target_sql = Column(Text, nullable=True)\n source_object_type = Column(String, nullable=True)\n source_object_id = Column(String, nullable=True)\n source_object_name = Column(String, nullable=True)\n source_data = Column(JSON, nullable=True, comment=\"Original source row key columns for upsert matching\")\n status = Column(String, nullable=False, default=\"PENDING\") # PENDING, APPROVED, REJECTED\n feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion TranslationPreviewRecord\n" }, { "contract_id": "TerminologyDictionary", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 178, - "end_line": 191, + "start_line": 184, + "end_line": 198, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "A named collection of terminology mappings used during translation to ensure consistent term translation." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TerminologyDictionary [C:1] [TYPE Class]\nclass TerminologyDictionary(Base):\n __tablename__ = \"terminology_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TerminologyDictionary\n" + "body": "# #region TerminologyDictionary [TYPE Class]\n# @BRIEF A named collection of terminology mappings used during translation to ensure consistent term translation.\nclass TerminologyDictionary(Base):\n __tablename__ = \"terminology_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, nullable=False)\n description = Column(Text, nullable=True)\n source_dialect = Column(String, nullable=False)\n target_dialect = Column(String, nullable=False)\n is_active = Column(Boolean, default=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n# #endregion TerminologyDictionary\n" }, { "contract_id": "DictionaryEntry", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 194, - "end_line": 216, + "start_line": 201, + "end_line": 224, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "A single term mapping entry with case-insensitive unique constraint on source_term_normalized and origin tracking." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DictionaryEntry [C:1] [TYPE Class]\nclass DictionaryEntry(Base):\n __tablename__ = \"dictionary_entries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n source_term = Column(String, nullable=False)\n source_term_normalized = Column(String, nullable=False)\n target_term = Column(String, nullable=False)\n context_notes = Column(Text, nullable=True)\n origin_run_id = Column(String, nullable=True, comment=\"Run ID from which this correction originated\")\n origin_row_key = Column(String, nullable=True, comment=\"Row key within the run that triggered this correction\")\n origin_user_id = Column(String, nullable=True, comment=\"User who submitted the correction\")\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n UniqueConstraint(\n \"dictionary_id\", \"source_term_normalized\",\n name=\"uq_dictionary_entry_term\"\n ),\n )\n# #endregion DictionaryEntry\n" + "body": "# #region DictionaryEntry [TYPE Class]\n# @BRIEF A single term mapping entry with case-insensitive unique constraint on source_term_normalized and origin tracking.\nclass DictionaryEntry(Base):\n __tablename__ = \"dictionary_entries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n source_term = Column(String, nullable=False)\n source_term_normalized = Column(String, nullable=False)\n target_term = Column(String, nullable=False)\n context_notes = Column(Text, nullable=True)\n origin_run_id = Column(String, nullable=True, comment=\"Run ID from which this correction originated\")\n origin_row_key = Column(String, nullable=True, comment=\"Row key within the run that triggered this correction\")\n origin_user_id = Column(String, nullable=True, comment=\"User who submitted the correction\")\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n\n __table_args__ = (\n UniqueConstraint(\n \"dictionary_id\", \"source_term_normalized\",\n name=\"uq_dictionary_entry_term\"\n ),\n )\n# #endregion DictionaryEntry\n" }, { "contract_id": "TranslationSchedule", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 219, - "end_line": 233, + "start_line": 227, + "end_line": 243, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Defines a cron-based schedule for recurring translation jobs." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationSchedule [C:1] [TYPE Class]\nclass TranslationSchedule(Base):\n __tablename__ = \"translation_schedules\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n cron_expression = Column(String, nullable=False)\n timezone = Column(String, nullable=False, default=\"UTC\")\n is_active = Column(Boolean, default=True)\n last_run_at = Column(DateTime, nullable=True)\n next_run_at = Column(DateTime, nullable=True)\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))\n# #endregion TranslationSchedule\n" + "body": "# #region TranslationSchedule [TYPE Class]\n# @BRIEF Defines a cron-based schedule for recurring translation jobs.\nclass TranslationSchedule(Base):\n __tablename__ = \"translation_schedules\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n cron_expression = Column(String, nullable=False)\n timezone = Column(String, nullable=False, default=\"UTC\")\n is_active = Column(Boolean, default=True)\n last_run_at = Column(DateTime, nullable=True)\n next_run_at = Column(DateTime, nullable=True)\n execution_mode = Column(String, nullable=False, default=\"full\", comment=\"full, new_key_only\")\n created_by = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n# #endregion TranslationSchedule\n" }, { "contract_id": "TranslationJobDictionary", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 236, - "end_line": 251, + "start_line": 246, + "end_line": 262, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Many-to-many association between translation jobs and terminology dictionaries." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationJobDictionary [C:1] [TYPE Class]\nclass TranslationJobDictionary(Base):\n __tablename__ = \"translation_job_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n UniqueConstraint(\n \"job_id\", \"dictionary_id\",\n name=\"uq_job_dictionary\"\n ),\n )\n# #endregion TranslationJobDictionary\n" + "body": "# #region TranslationJobDictionary [TYPE Class]\n# @BRIEF Many-to-many association between translation jobs and terminology dictionaries.\nclass TranslationJobDictionary(Base):\n __tablename__ = \"translation_job_dictionaries\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n dictionary_id = Column(String, ForeignKey(\"terminology_dictionaries.id\"), nullable=False, index=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n\n __table_args__ = (\n UniqueConstraint(\n \"job_id\", \"dictionary_id\",\n name=\"uq_job_dictionary\"\n ),\n )\n# #endregion TranslationJobDictionary\n" }, { "contract_id": "MetricSnapshot", "contract_type": "Class", "file_path": "backend/src/models/translate.py", - "start_line": 254, - "end_line": 281, + "start_line": 265, + "end_line": 293, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Captures translation performance metrics at a point in time with config/content/dictionary hash keys for aggregation." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region MetricSnapshot [C:1] [TYPE Class]\nclass MetricSnapshot(Base):\n __tablename__ = \"translation_metric_snapshots\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n key_hash = Column(String, nullable=False, comment=\"Hash of dimension key fields for aggregation\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at capture time\")\n covers_events_before = Column(DateTime, nullable=True, comment=\"Indicates snapshot covers events before this timestamp\")\n total_jobs = Column(Integer, default=0)\n total_runs = Column(Integer, default=0)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n avg_duration_ms = Column(Integer, nullable=True)\n p50_duration_ms = Column(Integer, nullable=True)\n p95_duration_ms = Column(Integer, nullable=True)\n p99_duration_ms = Column(Integer, nullable=True)\n snapshot_date = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))\n\n __table_args__ = (\n Index(\"ix_metric_snapshots_job_date\", \"job_id\", \"snapshot_date\"),\n )\n# #endregion MetricSnapshot\n" + "body": "# #region MetricSnapshot [TYPE Class]\n# @BRIEF Captures translation performance metrics at a point in time with config/content/dictionary hash keys for aggregation.\nclass MetricSnapshot(Base):\n __tablename__ = \"translation_metric_snapshots\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n job_id = Column(String, ForeignKey(\"translation_jobs.id\"), nullable=False, index=True)\n run_id = Column(String, ForeignKey(\"translation_runs.id\"), nullable=True, index=True)\n key_hash = Column(String, nullable=False, comment=\"Hash of dimension key fields for aggregation\")\n config_hash = Column(String, nullable=True, comment=\"Hash of translation configuration state\")\n dict_snapshot_hash = Column(String, nullable=True, comment=\"Hash of dictionary state at capture time\")\n covers_events_before = Column(DateTime, nullable=True, comment=\"Indicates snapshot covers events before this timestamp\")\n total_jobs = Column(Integer, default=0)\n total_runs = Column(Integer, default=0)\n total_records = Column(Integer, default=0)\n successful_records = Column(Integer, default=0)\n failed_records = Column(Integer, default=0)\n skipped_records = Column(Integer, default=0)\n avg_duration_ms = Column(Integer, nullable=True)\n p50_duration_ms = Column(Integer, nullable=True)\n p95_duration_ms = Column(Integer, nullable=True)\n p99_duration_ms = Column(Integer, nullable=True)\n snapshot_date = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n\n __table_args__ = (\n Index(\"ix_metric_snapshots_job_date\", \"job_id\", \"snapshot_date\"),\n )\n# #endregion MetricSnapshot\n" }, { "contract_id": "src.plugins", @@ -62223,27 +50366,20 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:src.plugins:Package]\n# @PURPOSE: Plugin package root for dynamic discovery and runtime imports.\n# [/DEF:src.plugins:Package]\n" + "anchor_syntax": "region", + "body": "# #region src.plugins [TYPE Package] [SEMANTICS plugin, package, discovery, runtime]\n# @BRIEF Plugin package root for dynamic discovery and runtime imports.\n# #endregion src.plugins\n" }, { "contract_id": "BackupPlugin", "contract_type": "Module", "file_path": "backend/src/plugins/backup.py", "start_line": 1, - "end_line": 261, + "end_line": 256, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "App", - "PURPOSE": "A plugin that provides functionality to back up Superset dashboards.", - "SEMANTICS": [ - "backup", - "superset", - "automation", - "dashboard", - "plugin" - ] + "PURPOSE": "A plugin that provides functionality to back up Superset dashboards." }, "relations": [ { @@ -62309,27 +50445,20 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:BackupPlugin:Module]\n# @SEMANTICS: backup, superset, automation, dashboard, plugin\n# @PURPOSE: A plugin that provides functionality to back up Superset dashboards.\n# @LAYER: App\n# @RELATION: IMPLEMENTS -> PluginBase\n# @RELATION: DEPENDS_ON -> superset_tool.client\n# @RELATION: DEPENDS_ON -> superset_tool.utils\n# @RELATION: USES -> TaskContext\n\nfrom typing import Dict, Any, Optional\nfrom pathlib import Path\nfrom requests.exceptions import RequestException\n\nfrom ..core.plugin_base import PluginBase\nfrom ..core.logger import belief_scope, logger as app_logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.utils.network import SupersetAPIError\nfrom ..core.utils.fileio import (\n save_and_unpack_dashboard,\n archive_exports,\n sanitize_filename,\n consolidate_archive_folders,\n remove_empty_directories,\n RetentionPolicy\n)\nfrom ..dependencies import get_config_manager\nfrom ..core.task_manager.context import TaskContext\n\n# [DEF:BackupPlugin:Class]\n# @PURPOSE: Implementation of the backup plugin logic.\nclass BackupPlugin(PluginBase):\n \"\"\"\n A plugin to back up Superset dashboards.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the backup plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"superset-backup\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"superset-backup\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the backup plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Superset Dashboard Backup\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the backup plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Backs up all dashboards from a Superset instance.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the backup plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the backup plugin.\n # @RETURN: str - \"/tools/backups\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/backups\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for backup plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n config_manager = get_config_manager()\n envs = [e.name for e in config_manager.get_environments()]\n config_manager.get_config().settings.storage.root_path\n \n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment to back up.\",\n \"enum\": envs if envs else [],\n },\n },\n \"required\": [\"env\"],\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the dashboard backup logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Backup parameters (env, backup_path, dashboard_ids).\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Target environment must be configured. params must be a dictionary.\n # @POST: All dashboards are exported and archived.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None):\n with belief_scope(\"execute\"):\n config_manager = get_config_manager()\n \n # Support both parameter names: environment_id (for task creation) and env (for direct calls)\n env_id = params.get(\"environment_id\") or params.get(\"env\")\n dashboard_ids = params.get(\"dashboard_ids\") or params.get(\"dashboards\")\n \n # Log the incoming parameters for debugging\n log = context.logger if context else app_logger\n log.info(f\"Backup parameters received: env_id={env_id}, dashboard_ids={dashboard_ids}\")\n \n # Resolve environment name if environment_id is provided\n if env_id:\n env_config = next((e for e in config_manager.get_environments() if e.id == env_id), None)\n if env_config:\n params[\"env\"] = env_config.name\n \n env = params.get(\"env\")\n if not env:\n raise KeyError(\"env\")\n \n log.info(f\"Backup started for environment: {env}, selected dashboards: {dashboard_ids}\")\n\n storage_settings = config_manager.get_config().settings.storage\n # Use 'backups' subfolder within the storage root\n backup_path = Path(storage_settings.root_path) / \"backups\"\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 superset_log = log.with_source(\"superset_api\") if context else log\n storage_log = log.with_source(\"storage\") if context else log\n \n log.info(f\"Starting backup for environment: {env}\")\n\n try:\n config_manager = get_config_manager()\n if not config_manager.has_environments():\n raise ValueError(\"No Superset environments configured. Please add an environment in Settings.\")\n \n env_config = config_manager.get_environment(env)\n if not env_config:\n raise ValueError(f\"Environment '{env}' not found in configuration.\")\n \n client = SupersetClient(env_config)\n \n # Get all dashboards\n all_dashboard_count, all_dashboard_meta = client.get_dashboards()\n superset_log.info(f\"Found {all_dashboard_count} total dashboards in environment\")\n \n # Filter dashboards if specific IDs are provided\n if dashboard_ids:\n dashboard_ids_int = [int(did) for did in dashboard_ids]\n dashboard_meta = [db for db in all_dashboard_meta if db.get('id') in dashboard_ids_int]\n dashboard_count = len(dashboard_meta)\n superset_log.info(f\"Filtered to {dashboard_count} selected dashboards: {dashboard_ids_int}\")\n else:\n dashboard_count = all_dashboard_count\n superset_log.info(\"No dashboard filter applied - backing up all dashboards\")\n dashboard_meta = all_dashboard_meta\n\n if dashboard_count == 0:\n log.info(\"No dashboards to back up\")\n return {\n \"status\": \"NO_DASHBOARDS\",\n \"environment\": env,\n \"backup_root\": str(backup_path / env.upper()),\n \"total_dashboards\": 0,\n \"backed_up_dashboards\": 0,\n \"failed_dashboards\": 0,\n \"dashboards\": [],\n \"failures\": []\n }\n\n total = len(dashboard_meta)\n backed_up_dashboards = []\n failed_dashboards = []\n for idx, db in enumerate(dashboard_meta, 1):\n dashboard_id = db.get('id')\n dashboard_title = db.get('dashboard_title', 'Unknown Dashboard')\n if not dashboard_id:\n continue\n\n # Report progress\n progress_pct = (idx / total) * 100\n log.progress(f\"Backing up dashboard: {dashboard_title}\", percent=progress_pct)\n\n try:\n dashboard_base_dir_name = sanitize_filename(f\"{dashboard_title}\")\n dashboard_dir = backup_path / env.upper() / dashboard_base_dir_name\n dashboard_dir.mkdir(parents=True, exist_ok=True)\n\n zip_content, filename = client.export_dashboard(dashboard_id)\n superset_log.debug(f\"Exported dashboard: {dashboard_title}\")\n\n save_and_unpack_dashboard(\n zip_content=zip_content,\n original_filename=filename,\n output_dir=dashboard_dir,\n unpack=False\n )\n\n archive_exports(str(dashboard_dir), policy=RetentionPolicy())\n storage_log.debug(f\"Archived dashboard: {dashboard_title}\")\n backed_up_dashboards.append({\n \"id\": dashboard_id,\n \"title\": dashboard_title,\n \"path\": str(dashboard_dir)\n })\n\n except (SupersetAPIError, RequestException, IOError, OSError) as db_error:\n log.error(f\"Failed to export dashboard {dashboard_title} (ID: {dashboard_id}): {db_error}\")\n failed_dashboards.append({\n \"id\": dashboard_id,\n \"title\": dashboard_title,\n \"error\": str(db_error)\n })\n continue\n \n consolidate_archive_folders(backup_path / env.upper())\n remove_empty_directories(str(backup_path / env.upper()))\n\n log.info(f\"Backup completed successfully for {env}\")\n return {\n \"status\": \"SUCCESS\" if not failed_dashboards else \"PARTIAL_SUCCESS\",\n \"environment\": env,\n \"backup_root\": str(backup_path / env.upper()),\n \"total_dashboards\": total,\n \"backed_up_dashboards\": len(backed_up_dashboards),\n \"failed_dashboards\": len(failed_dashboards),\n \"dashboards\": backed_up_dashboards,\n \"failures\": failed_dashboards\n }\n\n except (RequestException, IOError, KeyError) as e:\n log.error(f\"Fatal error during backup for {env}: {e}\")\n raise e\n # [/DEF:execute:Function]\n# [/DEF:BackupPlugin:Class]\n# [/DEF:BackupPlugin:Module]\n" + "anchor_syntax": "region", + "body": "# #region BackupPlugin [TYPE Module] [SEMANTICS backup, export, archive, superset, dashboard]\n# @BRIEF A plugin that provides functionality to back up Superset dashboards.\n# @LAYER: App\n# @RELATION IMPLEMENTS -> PluginBase\n# @RELATION DEPENDS_ON -> superset_tool.client\n# @RELATION DEPENDS_ON -> superset_tool.utils\n# @RELATION USES -> TaskContext\n\nfrom pathlib import Path\nfrom typing import Any\n\nfrom requests.exceptions import RequestException\n\nfrom ..core.logger import belief_scope\nfrom ..core.logger import logger as app_logger\nfrom ..core.plugin_base import PluginBase\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager.context import TaskContext\nfrom ..core.utils.fileio import RetentionPolicy, archive_exports, consolidate_archive_folders, remove_empty_directories, sanitize_filename, save_and_unpack_dashboard\nfrom ..core.utils.network import SupersetAPIError\nfrom ..dependencies import get_config_manager\n\n\n# #region BackupPlugin [TYPE Class]\n# @BRIEF Implementation of the backup plugin logic.\nclass BackupPlugin(PluginBase):\n \"\"\"\n A plugin to back up Superset dashboards.\n \"\"\"\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the backup plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"superset-backup\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"superset-backup\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the backup plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Superset Dashboard Backup\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns a description of the backup plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Backs up all dashboards from a Superset instance.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the version of the backup plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the backup plugin.\n # @RETURN: str - \"/tools/backups\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/backups\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for backup plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"get_schema\"):\n config_manager = get_config_manager()\n envs = [e.name for e in config_manager.get_environments()]\n config_manager.get_config().settings.storage.root_path\n\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment to back up.\",\n \"enum\": envs if envs else [],\n },\n },\n \"required\": [\"env\"],\n }\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Executes the dashboard backup logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Backup parameters (env, backup_path, dashboard_ids).\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Target environment must be configured. params must be a dictionary.\n # @POST: All dashboards are exported and archived.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"execute\"):\n config_manager = get_config_manager()\n\n # Support both parameter names: environment_id (for task creation) and env (for direct calls)\n env_id = params.get(\"environment_id\") or params.get(\"env\")\n dashboard_ids = params.get(\"dashboard_ids\") or params.get(\"dashboards\")\n\n # Log the incoming parameters for debugging\n log = context.logger if context else app_logger\n log.info(f\"Backup parameters received: env_id={env_id}, dashboard_ids={dashboard_ids}\")\n\n # Resolve environment name if environment_id is provided\n if env_id:\n env_config = next((e for e in config_manager.get_environments() if e.id == env_id), None)\n if env_config:\n params[\"env\"] = env_config.name\n\n env = params.get(\"env\")\n if not env:\n raise KeyError(\"env\")\n\n log.info(f\"Backup started for environment: {env}, selected dashboards: {dashboard_ids}\")\n\n storage_settings = config_manager.get_config().settings.storage\n # Use 'backups' subfolder within the storage root\n backup_path = Path(storage_settings.root_path) / \"backups\"\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 superset_log = log.with_source(\"superset_api\") if context else log\n storage_log = log.with_source(\"storage\") if context else log\n\n log.info(f\"Starting backup for environment: {env}\")\n\n try:\n config_manager = get_config_manager()\n if not config_manager.has_environments():\n raise ValueError(\"No Superset environments configured. Please add an environment in Settings.\")\n\n env_config = config_manager.get_environment(env)\n if not env_config:\n raise ValueError(f\"Environment '{env}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n\n # Get all dashboards\n all_dashboard_count, all_dashboard_meta = client.get_dashboards()\n superset_log.info(f\"Found {all_dashboard_count} total dashboards in environment\")\n\n # Filter dashboards if specific IDs are provided\n if dashboard_ids:\n dashboard_ids_int = [int(did) for did in dashboard_ids]\n dashboard_meta = [db for db in all_dashboard_meta if db.get('id') in dashboard_ids_int]\n dashboard_count = len(dashboard_meta)\n superset_log.info(f\"Filtered to {dashboard_count} selected dashboards: {dashboard_ids_int}\")\n else:\n dashboard_count = all_dashboard_count\n superset_log.info(\"No dashboard filter applied - backing up all dashboards\")\n dashboard_meta = all_dashboard_meta\n\n if dashboard_count == 0:\n log.info(\"No dashboards to back up\")\n return {\n \"status\": \"NO_DASHBOARDS\",\n \"environment\": env,\n \"backup_root\": str(backup_path / env.upper()),\n \"total_dashboards\": 0,\n \"backed_up_dashboards\": 0,\n \"failed_dashboards\": 0,\n \"dashboards\": [],\n \"failures\": []\n }\n\n total = len(dashboard_meta)\n backed_up_dashboards = []\n failed_dashboards = []\n for idx, db in enumerate(dashboard_meta, 1):\n dashboard_id = db.get('id')\n dashboard_title = db.get('dashboard_title', 'Unknown Dashboard')\n if not dashboard_id:\n continue\n\n # Report progress\n progress_pct = (idx / total) * 100\n log.progress(f\"Backing up dashboard: {dashboard_title}\", percent=progress_pct)\n\n try:\n dashboard_base_dir_name = sanitize_filename(f\"{dashboard_title}\")\n dashboard_dir = backup_path / env.upper() / dashboard_base_dir_name\n dashboard_dir.mkdir(parents=True, exist_ok=True)\n\n zip_content, filename = client.export_dashboard(dashboard_id)\n superset_log.debug(f\"Exported dashboard: {dashboard_title}\")\n\n save_and_unpack_dashboard(\n zip_content=zip_content,\n original_filename=filename,\n output_dir=dashboard_dir,\n unpack=False\n )\n\n archive_exports(str(dashboard_dir), policy=RetentionPolicy())\n storage_log.debug(f\"Archived dashboard: {dashboard_title}\")\n backed_up_dashboards.append({\n \"id\": dashboard_id,\n \"title\": dashboard_title,\n \"path\": str(dashboard_dir)\n })\n\n except (SupersetAPIError, RequestException, OSError) as db_error:\n log.error(f\"Failed to export dashboard {dashboard_title} (ID: {dashboard_id}): {db_error}\")\n failed_dashboards.append({\n \"id\": dashboard_id,\n \"title\": dashboard_title,\n \"error\": str(db_error)\n })\n continue\n\n consolidate_archive_folders(backup_path / env.upper())\n remove_empty_directories(str(backup_path / env.upper()))\n\n log.info(f\"Backup completed successfully for {env}\")\n return {\n \"status\": \"SUCCESS\" if not failed_dashboards else \"PARTIAL_SUCCESS\",\n \"environment\": env,\n \"backup_root\": str(backup_path / env.upper()),\n \"total_dashboards\": total,\n \"backed_up_dashboards\": len(backed_up_dashboards),\n \"failed_dashboards\": len(failed_dashboards),\n \"dashboards\": backed_up_dashboards,\n \"failures\": failed_dashboards\n }\n\n except (OSError, RequestException, KeyError) as e:\n log.error(f\"Fatal error during backup for {env}: {e}\")\n raise e\n # endregion execute\n# #endregion BackupPlugin\n# #endregion BackupPlugin\n" }, { "contract_id": "DebugPluginModule", "contract_type": "Module", "file_path": "backend/src/plugins/debug.py", "start_line": 1, - "end_line": 215, + "end_line": 214, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "Plugins", - "PURPOSE": "Implements a plugin for system diagnostics and debugging Superset API responses.", - "SEMANTICS": [ - "plugin", - "debug", - "api", - "database", - "superset" - ] + "PURPOSE": "Implements a plugin for system diagnostics and debugging Superset API responses." }, "relations": [ { @@ -62383,15 +50512,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DebugPluginModule:Module]\n# @SEMANTICS: plugin, debug, api, database, superset\n# @PURPOSE: Implements a plugin for system diagnostics and debugging Superset API responses.\n# @LAYER: Plugins\n# @RELATION: Inherits from PluginBase. Uses SupersetClient from core.\n# @RELATION: USES -> TaskContext\n\n# [SECTION: IMPORTS]\nfrom typing import Dict, Any, Optional\nfrom ..core.plugin_base import PluginBase\nfrom ..core.superset_client import SupersetClient\nfrom ..core.logger import logger, belief_scope\nfrom ..core.task_manager.context import TaskContext\n# [/SECTION]\n\n# [DEF:DebugPlugin:Class]\n# @PURPOSE: Plugin for system diagnostics and debugging.\nclass DebugPlugin(PluginBase):\n \"\"\"\n Plugin for system diagnostics and debugging.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"system-debug\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"system-debug\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"System Debug\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Run system diagnostics and debug Superset API responses.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the debug plugin.\n # @RETURN: str - \"/tools/debug\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/debug\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the debug plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"action\": {\n \"type\": \"string\",\n \"title\": \"Action\",\n \"enum\": [\"test-db-api\", \"get-dataset-structure\"],\n \"default\": \"test-db-api\"\n },\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (for dataset structure).\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset (for dataset structure).\"\n },\n \"source_env\": {\n \"type\": \"string\",\n \"title\": \"Source Environment\",\n \"description\": \"Source env for DB API test.\"\n },\n \"target_env\": {\n \"type\": \"string\",\n \"title\": \"Target Environment\",\n \"description\": \"Target env for DB API test.\"\n }\n },\n \"required\": [\"action\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the debug logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Debug parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: action must be provided in params.\n # @POST: Debug action is executed and results returned.\n # @RETURN: Dict[str, Any] - Execution results.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"execute\"):\n action = params.get(\"action\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n debug_log = log.with_source(\"debug\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n \n debug_log.info(f\"Executing debug action: {action}\")\n \n if action == \"test-db-api\":\n return await self._test_db_api(params, superset_log)\n elif action == \"get-dataset-structure\":\n return await self._get_dataset_structure(params, superset_log)\n else:\n debug_log.error(f\"Unknown action: {action}\")\n raise ValueError(f\"Unknown action: {action}\")\n # [/DEF:execute:Function]\n\n # [DEF:_test_db_api:Function]\n # @PURPOSE: Tests database API connectivity for source and target environments.\n # @PRE: source_env and target_env params exist in params.\n # @POST: Returns DB counts for both envs.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Comparison results.\n async def _test_db_api(self, params: Dict[str, Any], log) -> Dict[str, Any]:\n with belief_scope(\"_test_db_api\"):\n source_env_name = params.get(\"source_env\")\n target_env_name = params.get(\"target_env\")\n \n if not source_env_name or not target_env_name:\n raise ValueError(\"source_env and target_env are required for test-db-api\")\n\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n \n results = {}\n for name in [source_env_name, target_env_name]:\n log.info(f\"Testing database API for environment: {name}\")\n env_config = config_manager.get_environment(name)\n if not env_config:\n log.error(f\"Environment '{name}' not found.\")\n raise ValueError(f\"Environment '{name}' not found.\")\n \n client = SupersetClient(env_config)\n client.authenticate()\n count, dbs = client.get_databases()\n log.debug(f\"Found {count} databases in {name}\")\n results[name] = {\n \"count\": count,\n \"databases\": dbs\n }\n \n return results\n # [/DEF:_test_db_api:Function]\n\n # [DEF:_get_dataset_structure:Function]\n # @PURPOSE: Retrieves the structure of a dataset.\n # @PRE: env and dataset_id params exist in params.\n # @POST: Returns dataset JSON structure.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Dataset structure.\n async def _get_dataset_structure(self, params: Dict[str, Any], log) -> Dict[str, Any]:\n with belief_scope(\"_get_dataset_structure\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n \n if not env_name or dataset_id is None:\n raise ValueError(\"env and dataset_id are required for get-dataset-structure\")\n\n log.info(f\"Fetching structure for dataset {dataset_id} in {env_name}\")\n \n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found.\")\n raise ValueError(f\"Environment '{env_name}' not found.\")\n \n client = SupersetClient(env_config)\n client.authenticate()\n \n dataset_response = client.get_dataset(dataset_id)\n log.debug(f\"Retrieved dataset structure for {dataset_id}\")\n return dataset_response.get('result') or {}\n # [/DEF:_get_dataset_structure:Function]\n\n# [/DEF:DebugPlugin:Class]\n# [/DEF:DebugPluginModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region DebugPluginModule [TYPE Module] [SEMANTICS debug, diagnostic, superset, api, system]\n# @BRIEF Implements a plugin for system diagnostics and debugging Superset API responses.\n# @LAYER: Plugins\n# @RELATION Inherits from PluginBase. Uses SupersetClient from core.\n# @RELATION USES -> TaskContext\n\nfrom typing import Any\n\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\n\n\n# #region DebugPlugin [TYPE Class]\n# @BRIEF Plugin for system diagnostics and debugging.\nclass DebugPlugin(PluginBase):\n \"\"\"\n Plugin for system diagnostics and debugging.\n \"\"\"\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"system-debug\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"system-debug\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"System Debug\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns a description of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Run system diagnostics and debug Superset API responses.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the version of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the debug plugin.\n # @RETURN: str - \"/tools/debug\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/debug\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for the debug plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"action\": {\n \"type\": \"string\",\n \"title\": \"Action\",\n \"enum\": [\"test-db-api\", \"get-dataset-structure\"],\n \"default\": \"test-db-api\"\n },\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (for dataset structure).\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset (for dataset structure).\"\n },\n \"source_env\": {\n \"type\": \"string\",\n \"title\": \"Source Environment\",\n \"description\": \"Source env for DB API test.\"\n },\n \"target_env\": {\n \"type\": \"string\",\n \"title\": \"Target Environment\",\n \"description\": \"Target env for DB API test.\"\n }\n },\n \"required\": [\"action\"]\n }\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Executes the debug logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Debug parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: action must be provided in params.\n # @POST: Debug action is executed and results returned.\n # @RETURN: Dict[str, Any] - Execution results.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None) -> dict[str, Any]:\n with belief_scope(\"execute\"):\n action = params.get(\"action\")\n\n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n debug_log = log.with_source(\"debug\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n\n debug_log.info(f\"Executing debug action: {action}\")\n\n if action == \"test-db-api\":\n return await self._test_db_api(params, superset_log)\n elif action == \"get-dataset-structure\":\n return await self._get_dataset_structure(params, superset_log)\n else:\n debug_log.error(f\"Unknown action: {action}\")\n raise ValueError(f\"Unknown action: {action}\")\n # endregion execute\n\n # region _test_db_api [TYPE Function]\n # @PURPOSE: Tests database API connectivity for source and target environments.\n # @PRE: source_env and target_env params exist in params.\n # @POST: Returns DB counts for both envs.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Comparison results.\n async def _test_db_api(self, params: dict[str, Any], log) -> dict[str, Any]:\n with belief_scope(\"_test_db_api\"):\n source_env_name = params.get(\"source_env\")\n target_env_name = params.get(\"target_env\")\n\n if not source_env_name or not target_env_name:\n raise ValueError(\"source_env and target_env are required for test-db-api\")\n\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n\n results = {}\n for name in [source_env_name, target_env_name]:\n log.info(f\"Testing database API for environment: {name}\")\n env_config = config_manager.get_environment(name)\n if not env_config:\n log.error(f\"Environment '{name}' not found.\")\n raise ValueError(f\"Environment '{name}' not found.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n count, dbs = client.get_databases()\n log.debug(f\"Found {count} databases in {name}\")\n results[name] = {\n \"count\": count,\n \"databases\": dbs\n }\n\n return results\n # endregion _test_db_api\n\n # region _get_dataset_structure [TYPE Function]\n # @PURPOSE: Retrieves the structure of a dataset.\n # @PRE: env and dataset_id params exist in params.\n # @POST: Returns dataset JSON structure.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Dataset structure.\n async def _get_dataset_structure(self, params: dict[str, Any], log) -> dict[str, Any]:\n with belief_scope(\"_get_dataset_structure\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n\n if not env_name or dataset_id is None:\n raise ValueError(\"env and dataset_id are required for get-dataset-structure\")\n\n log.info(f\"Fetching structure for dataset {dataset_id} in {env_name}\")\n\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found.\")\n raise ValueError(f\"Environment '{env_name}' not found.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n dataset_response = client.get_dataset(dataset_id)\n log.debug(f\"Retrieved dataset structure for {dataset_id}\")\n return dataset_response.get('result') or {}\n # endregion _get_dataset_structure\n\n# #endregion DebugPlugin\n# #endregion DebugPluginModule\n" }, { "contract_id": "DebugPlugin", "contract_type": "Class", "file_path": "backend/src/plugins/debug.py", - "start_line": 16, - "end_line": 214, + "start_line": 15, + "end_line": 213, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -62409,128 +50538,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DebugPlugin:Class]\n# @PURPOSE: Plugin for system diagnostics and debugging.\nclass DebugPlugin(PluginBase):\n \"\"\"\n Plugin for system diagnostics and debugging.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"system-debug\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"system-debug\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"System Debug\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Run system diagnostics and debug Superset API responses.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the debug plugin.\n # @RETURN: str - \"/tools/debug\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/debug\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the debug plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"action\": {\n \"type\": \"string\",\n \"title\": \"Action\",\n \"enum\": [\"test-db-api\", \"get-dataset-structure\"],\n \"default\": \"test-db-api\"\n },\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (for dataset structure).\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset (for dataset structure).\"\n },\n \"source_env\": {\n \"type\": \"string\",\n \"title\": \"Source Environment\",\n \"description\": \"Source env for DB API test.\"\n },\n \"target_env\": {\n \"type\": \"string\",\n \"title\": \"Target Environment\",\n \"description\": \"Target env for DB API test.\"\n }\n },\n \"required\": [\"action\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the debug logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Debug parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: action must be provided in params.\n # @POST: Debug action is executed and results returned.\n # @RETURN: Dict[str, Any] - Execution results.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"execute\"):\n action = params.get(\"action\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n debug_log = log.with_source(\"debug\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n \n debug_log.info(f\"Executing debug action: {action}\")\n \n if action == \"test-db-api\":\n return await self._test_db_api(params, superset_log)\n elif action == \"get-dataset-structure\":\n return await self._get_dataset_structure(params, superset_log)\n else:\n debug_log.error(f\"Unknown action: {action}\")\n raise ValueError(f\"Unknown action: {action}\")\n # [/DEF:execute:Function]\n\n # [DEF:_test_db_api:Function]\n # @PURPOSE: Tests database API connectivity for source and target environments.\n # @PRE: source_env and target_env params exist in params.\n # @POST: Returns DB counts for both envs.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Comparison results.\n async def _test_db_api(self, params: Dict[str, Any], log) -> Dict[str, Any]:\n with belief_scope(\"_test_db_api\"):\n source_env_name = params.get(\"source_env\")\n target_env_name = params.get(\"target_env\")\n \n if not source_env_name or not target_env_name:\n raise ValueError(\"source_env and target_env are required for test-db-api\")\n\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n \n results = {}\n for name in [source_env_name, target_env_name]:\n log.info(f\"Testing database API for environment: {name}\")\n env_config = config_manager.get_environment(name)\n if not env_config:\n log.error(f\"Environment '{name}' not found.\")\n raise ValueError(f\"Environment '{name}' not found.\")\n \n client = SupersetClient(env_config)\n client.authenticate()\n count, dbs = client.get_databases()\n log.debug(f\"Found {count} databases in {name}\")\n results[name] = {\n \"count\": count,\n \"databases\": dbs\n }\n \n return results\n # [/DEF:_test_db_api:Function]\n\n # [DEF:_get_dataset_structure:Function]\n # @PURPOSE: Retrieves the structure of a dataset.\n # @PRE: env and dataset_id params exist in params.\n # @POST: Returns dataset JSON structure.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Dataset structure.\n async def _get_dataset_structure(self, params: Dict[str, Any], log) -> Dict[str, Any]:\n with belief_scope(\"_get_dataset_structure\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n \n if not env_name or dataset_id is None:\n raise ValueError(\"env and dataset_id are required for get-dataset-structure\")\n\n log.info(f\"Fetching structure for dataset {dataset_id} in {env_name}\")\n \n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found.\")\n raise ValueError(f\"Environment '{env_name}' not found.\")\n \n client = SupersetClient(env_config)\n client.authenticate()\n \n dataset_response = client.get_dataset(dataset_id)\n log.debug(f\"Retrieved dataset structure for {dataset_id}\")\n return dataset_response.get('result') or {}\n # [/DEF:_get_dataset_structure:Function]\n\n# [/DEF:DebugPlugin:Class]\n" - }, - { - "contract_id": "_test_db_api", - "contract_type": "Function", - "file_path": "backend/src/plugins/debug.py", - "start_line": 144, - "end_line": 180, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "log - Logger instance for superset_api source.", - "POST": "Returns DB counts for both envs.", - "PRE": "source_env and target_env params exist in params.", - "PURPOSE": "Tests database API connectivity for source and target environments.", - "RETURN": "Dict - Comparison results." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_test_db_api:Function]\n # @PURPOSE: Tests database API connectivity for source and target environments.\n # @PRE: source_env and target_env params exist in params.\n # @POST: Returns DB counts for both envs.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Comparison results.\n async def _test_db_api(self, params: Dict[str, Any], log) -> Dict[str, Any]:\n with belief_scope(\"_test_db_api\"):\n source_env_name = params.get(\"source_env\")\n target_env_name = params.get(\"target_env\")\n \n if not source_env_name or not target_env_name:\n raise ValueError(\"source_env and target_env are required for test-db-api\")\n\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n \n results = {}\n for name in [source_env_name, target_env_name]:\n log.info(f\"Testing database API for environment: {name}\")\n env_config = config_manager.get_environment(name)\n if not env_config:\n log.error(f\"Environment '{name}' not found.\")\n raise ValueError(f\"Environment '{name}' not found.\")\n \n client = SupersetClient(env_config)\n client.authenticate()\n count, dbs = client.get_databases()\n log.debug(f\"Found {count} databases in {name}\")\n results[name] = {\n \"count\": count,\n \"databases\": dbs\n }\n \n return results\n # [/DEF:_test_db_api:Function]\n" - }, - { - "contract_id": "_get_dataset_structure", - "contract_type": "Function", - "file_path": "backend/src/plugins/debug.py", - "start_line": 182, - "end_line": 212, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "log - Logger instance for superset_api source.", - "POST": "Returns dataset JSON structure.", - "PRE": "env and dataset_id params exist in params.", - "PURPOSE": "Retrieves the structure of a dataset.", - "RETURN": "Dict - Dataset structure." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_dataset_structure:Function]\n # @PURPOSE: Retrieves the structure of a dataset.\n # @PRE: env and dataset_id params exist in params.\n # @POST: Returns dataset JSON structure.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Dataset structure.\n async def _get_dataset_structure(self, params: Dict[str, Any], log) -> Dict[str, Any]:\n with belief_scope(\"_get_dataset_structure\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n \n if not env_name or dataset_id is None:\n raise ValueError(\"env and dataset_id are required for get-dataset-structure\")\n\n log.info(f\"Fetching structure for dataset {dataset_id} in {env_name}\")\n \n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found.\")\n raise ValueError(f\"Environment '{env_name}' not found.\")\n \n client = SupersetClient(env_config)\n client.authenticate()\n \n dataset_response = client.get_dataset(dataset_id)\n log.debug(f\"Retrieved dataset structure for {dataset_id}\")\n return dataset_response.get('result') or {}\n # [/DEF:_get_dataset_structure:Function]\n" + "anchor_syntax": "region", + "body": "# #region DebugPlugin [TYPE Class]\n# @BRIEF Plugin for system diagnostics and debugging.\nclass DebugPlugin(PluginBase):\n \"\"\"\n Plugin for system diagnostics and debugging.\n \"\"\"\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"system-debug\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"system-debug\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"System Debug\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns a description of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Run system diagnostics and debug Superset API responses.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the version of the debug plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the debug plugin.\n # @RETURN: str - \"/tools/debug\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/debug\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for the debug plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"action\": {\n \"type\": \"string\",\n \"title\": \"Action\",\n \"enum\": [\"test-db-api\", \"get-dataset-structure\"],\n \"default\": \"test-db-api\"\n },\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (for dataset structure).\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset (for dataset structure).\"\n },\n \"source_env\": {\n \"type\": \"string\",\n \"title\": \"Source Environment\",\n \"description\": \"Source env for DB API test.\"\n },\n \"target_env\": {\n \"type\": \"string\",\n \"title\": \"Target Environment\",\n \"description\": \"Target env for DB API test.\"\n }\n },\n \"required\": [\"action\"]\n }\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Executes the debug logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Debug parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: action must be provided in params.\n # @POST: Debug action is executed and results returned.\n # @RETURN: Dict[str, Any] - Execution results.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None) -> dict[str, Any]:\n with belief_scope(\"execute\"):\n action = params.get(\"action\")\n\n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n debug_log = log.with_source(\"debug\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n\n debug_log.info(f\"Executing debug action: {action}\")\n\n if action == \"test-db-api\":\n return await self._test_db_api(params, superset_log)\n elif action == \"get-dataset-structure\":\n return await self._get_dataset_structure(params, superset_log)\n else:\n debug_log.error(f\"Unknown action: {action}\")\n raise ValueError(f\"Unknown action: {action}\")\n # endregion execute\n\n # region _test_db_api [TYPE Function]\n # @PURPOSE: Tests database API connectivity for source and target environments.\n # @PRE: source_env and target_env params exist in params.\n # @POST: Returns DB counts for both envs.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Comparison results.\n async def _test_db_api(self, params: dict[str, Any], log) -> dict[str, Any]:\n with belief_scope(\"_test_db_api\"):\n source_env_name = params.get(\"source_env\")\n target_env_name = params.get(\"target_env\")\n\n if not source_env_name or not target_env_name:\n raise ValueError(\"source_env and target_env are required for test-db-api\")\n\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n\n results = {}\n for name in [source_env_name, target_env_name]:\n log.info(f\"Testing database API for environment: {name}\")\n env_config = config_manager.get_environment(name)\n if not env_config:\n log.error(f\"Environment '{name}' not found.\")\n raise ValueError(f\"Environment '{name}' not found.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n count, dbs = client.get_databases()\n log.debug(f\"Found {count} databases in {name}\")\n results[name] = {\n \"count\": count,\n \"databases\": dbs\n }\n\n return results\n # endregion _test_db_api\n\n # region _get_dataset_structure [TYPE Function]\n # @PURPOSE: Retrieves the structure of a dataset.\n # @PRE: env and dataset_id params exist in params.\n # @POST: Returns dataset JSON structure.\n # @PARAM: params (Dict) - Plugin parameters.\n # @PARAM: log - Logger instance for superset_api source.\n # @RETURN: Dict - Dataset structure.\n async def _get_dataset_structure(self, params: dict[str, Any], log) -> dict[str, Any]:\n with belief_scope(\"_get_dataset_structure\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n\n if not env_name or dataset_id is None:\n raise ValueError(\"env and dataset_id are required for get-dataset-structure\")\n\n log.info(f\"Fetching structure for dataset {dataset_id} in {env_name}\")\n\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found.\")\n raise ValueError(f\"Environment '{env_name}' not found.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n dataset_response = client.get_dataset(dataset_id)\n log.debug(f\"Retrieved dataset structure for {dataset_id}\")\n return dataset_response.get('result') or {}\n # endregion _get_dataset_structure\n\n# #endregion DebugPlugin\n" }, { "contract_id": "GitPluginExt", @@ -62573,11 +50582,11 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitPluginExt:Package]\n# @PURPOSE: Git plugin extension package root.\n# [/DEF:GitPluginExt:Package]\n" + "anchor_syntax": "region", + "body": "# #region GitPluginExt [TYPE Package] [SEMANTICS git, plugin, extension, package]\n# @BRIEF Git plugin extension package root.\n# #endregion GitPluginExt\n" }, { - "contract_id": "backend/src/plugins/git/llm_extension", + "contract_id": "GitLLMExtensionModule", "contract_type": "Module", "file_path": "backend/src/plugins/git/llm_extension.py", "start_line": 1, @@ -62585,26 +50594,21 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", - "PURPOSE": "LLM-based extensions for the Git plugin, specifically for commit message generation.", - "SEMANTICS": [ - "git", - "llm", - "commit" - ] + "PURPOSE": "LLM-based extensions for the Git plugin, specifically for commit message generation." }, "relations": [ { - "source_id": "backend/src/plugins/git/llm_extension", + "source_id": "GitLLMExtensionModule", "relation_type": "DEPENDS_ON", "target_id": "LLMClient", "target_ref": "[LLMClient]" } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:backend/src/plugins/git/llm_extension:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: git, llm, commit\n# @PURPOSE: LLM-based extensions for the Git plugin, specifically for commit message generation.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [LLMClient]\n\nfrom typing import List\nfrom tenacity import retry, stop_after_attempt, wait_exponential\nfrom ..llm_analysis.service import LLMClient\nfrom ...core.logger import belief_scope, logger\nfrom ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt\n\n# [DEF:GitLLMExtension:Class]\n# @PURPOSE: Provides LLM capabilities to the Git plugin.\nclass GitLLMExtension:\n def __init__(self, client: LLMClient):\n self.client = client\n\n # [DEF:suggest_commit_message:Function]\n # @PURPOSE: Generates a suggested commit message based on a diff and history.\n # @PARAM: diff (str) - The git diff of staged changes.\n # @PARAM: history (List[str]) - Recent commit messages for context.\n # @RETURN: str - The suggested commit message.\n @retry(\n stop=stop_after_attempt(2),\n wait=wait_exponential(multiplier=1, min=2, max=10),\n reraise=True\n )\n async def suggest_commit_message(\n self,\n diff: str,\n history: List[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n ) -> str:\n with belief_scope(\"suggest_commit_message\"):\n history_text = \"\\n\".join(history)\n prompt = render_prompt(\n prompt_template,\n {\"history\": history_text, \"diff\": diff},\n )\n \n logger.debug(f\"[suggest_commit_message] Calling LLM with model: {self.client.default_model}\")\n response = await self.client.client.chat.completions.create(\n model=self.client.default_model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n temperature=0.7\n )\n \n logger.debug(f\"[suggest_commit_message] LLM Response: {response}\")\n \n if not response or not hasattr(response, 'choices') or not response.choices:\n error_info = getattr(response, 'error', 'No choices in response')\n logger.error(f\"[suggest_commit_message] Invalid LLM response. Error info: {error_info}\")\n \n # If it's a timeout/provider error, we might want to throw to trigger retry if decorated\n # but for now we return a safe fallback to avoid UI crash\n return \"Update dashboard configurations (LLM generation failed)\"\n\n return response.choices[0].message.content.strip()\n # [/DEF:suggest_commit_message:Function]\n# [/DEF:GitLLMExtension:Class]\n\n# [/DEF:backend/src/plugins/git/llm_extension:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitLLMExtensionModule [C:3] [TYPE Module] [SEMANTICS git, llm, commit, message, generation]\n# @BRIEF LLM-based extensions for the Git plugin, specifically for commit message generation.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [LLMClient]\n\n\nfrom tenacity import retry, stop_after_attempt, wait_exponential\n\nfrom ...core.logger import belief_scope, logger\nfrom ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt\nfrom ..llm_analysis.service import LLMClient\n\n\n# #region GitLLMExtension [TYPE Class]\n# @BRIEF Provides LLM capabilities to the Git plugin.\nclass GitLLMExtension:\n def __init__(self, client: LLMClient):\n self.client = client\n\n # region suggest_commit_message [TYPE Function]\n # @PURPOSE: Generates a suggested commit message based on a diff and history.\n # @PARAM: diff (str) - The git diff of staged changes.\n # @PARAM: history (List[str]) - Recent commit messages for context.\n # @RETURN: str - The suggested commit message.\n @retry(\n stop=stop_after_attempt(2),\n wait=wait_exponential(multiplier=1, min=2, max=10),\n reraise=True\n )\n async def suggest_commit_message(\n self,\n diff: str,\n history: list[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n ) -> str:\n with belief_scope(\"suggest_commit_message\"):\n history_text = \"\\n\".join(history)\n prompt = render_prompt(\n prompt_template,\n {\"history\": history_text, \"diff\": diff},\n )\n\n logger.debug(f\"[suggest_commit_message] Calling LLM with model: {self.client.default_model}\")\n response = await self.client.client.chat.completions.create(\n model=self.client.default_model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n temperature=0.7\n )\n\n logger.debug(f\"[suggest_commit_message] LLM Response: {response}\")\n\n if not response or not hasattr(response, 'choices') or not response.choices:\n error_info = getattr(response, 'error', 'No choices in response')\n logger.error(f\"[suggest_commit_message] Invalid LLM response. Error info: {error_info}\")\n\n # If it's a timeout/provider error, we might want to throw to trigger retry if decorated\n # but for now we return a safe fallback to avoid UI crash\n return \"Update dashboard configurations (LLM generation failed)\"\n\n return response.choices[0].message.content.strip()\n # endregion suggest_commit_message\n# #endregion GitLLMExtension\n\n# #endregion GitLLMExtensionModule\n" }, { "contract_id": "GitLLMExtension", @@ -62629,70 +50633,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitLLMExtension:Class]\n# @PURPOSE: Provides LLM capabilities to the Git plugin.\nclass GitLLMExtension:\n def __init__(self, client: LLMClient):\n self.client = client\n\n # [DEF:suggest_commit_message:Function]\n # @PURPOSE: Generates a suggested commit message based on a diff and history.\n # @PARAM: diff (str) - The git diff of staged changes.\n # @PARAM: history (List[str]) - Recent commit messages for context.\n # @RETURN: str - The suggested commit message.\n @retry(\n stop=stop_after_attempt(2),\n wait=wait_exponential(multiplier=1, min=2, max=10),\n reraise=True\n )\n async def suggest_commit_message(\n self,\n diff: str,\n history: List[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n ) -> str:\n with belief_scope(\"suggest_commit_message\"):\n history_text = \"\\n\".join(history)\n prompt = render_prompt(\n prompt_template,\n {\"history\": history_text, \"diff\": diff},\n )\n \n logger.debug(f\"[suggest_commit_message] Calling LLM with model: {self.client.default_model}\")\n response = await self.client.client.chat.completions.create(\n model=self.client.default_model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n temperature=0.7\n )\n \n logger.debug(f\"[suggest_commit_message] LLM Response: {response}\")\n \n if not response or not hasattr(response, 'choices') or not response.choices:\n error_info = getattr(response, 'error', 'No choices in response')\n logger.error(f\"[suggest_commit_message] Invalid LLM response. Error info: {error_info}\")\n \n # If it's a timeout/provider error, we might want to throw to trigger retry if decorated\n # but for now we return a safe fallback to avoid UI crash\n return \"Update dashboard configurations (LLM generation failed)\"\n\n return response.choices[0].message.content.strip()\n # [/DEF:suggest_commit_message:Function]\n# [/DEF:GitLLMExtension:Class]\n" - }, - { - "contract_id": "suggest_commit_message", - "contract_type": "Function", - "file_path": "backend/src/plugins/git/llm_extension.py", - "start_line": 20, - "end_line": 61, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "history (List[str]) - Recent commit messages for context.", - "PURPOSE": "Generates a suggested commit message based on a diff and history.", - "RETURN": "str - The suggested commit message." - }, - "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": "PURPOSE", - "message": "@PURPOSE 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": "def", - "body": " # [DEF:suggest_commit_message:Function]\n # @PURPOSE: Generates a suggested commit message based on a diff and history.\n # @PARAM: diff (str) - The git diff of staged changes.\n # @PARAM: history (List[str]) - Recent commit messages for context.\n # @RETURN: str - The suggested commit message.\n @retry(\n stop=stop_after_attempt(2),\n wait=wait_exponential(multiplier=1, min=2, max=10),\n reraise=True\n )\n async def suggest_commit_message(\n self,\n diff: str,\n history: List[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n ) -> str:\n with belief_scope(\"suggest_commit_message\"):\n history_text = \"\\n\".join(history)\n prompt = render_prompt(\n prompt_template,\n {\"history\": history_text, \"diff\": diff},\n )\n \n logger.debug(f\"[suggest_commit_message] Calling LLM with model: {self.client.default_model}\")\n response = await self.client.client.chat.completions.create(\n model=self.client.default_model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n temperature=0.7\n )\n \n logger.debug(f\"[suggest_commit_message] LLM Response: {response}\")\n \n if not response or not hasattr(response, 'choices') or not response.choices:\n error_info = getattr(response, 'error', 'No choices in response')\n logger.error(f\"[suggest_commit_message] Invalid LLM response. Error info: {error_info}\")\n \n # If it's a timeout/provider error, we might want to throw to trigger retry if decorated\n # but for now we return a safe fallback to avoid UI crash\n return \"Update dashboard configurations (LLM generation failed)\"\n\n return response.choices[0].message.content.strip()\n # [/DEF:suggest_commit_message:Function]\n" + "anchor_syntax": "region", + "body": "# #region GitLLMExtension [TYPE Class]\n# @BRIEF Provides LLM capabilities to the Git plugin.\nclass GitLLMExtension:\n def __init__(self, client: LLMClient):\n self.client = client\n\n # region suggest_commit_message [TYPE Function]\n # @PURPOSE: Generates a suggested commit message based on a diff and history.\n # @PARAM: diff (str) - The git diff of staged changes.\n # @PARAM: history (List[str]) - Recent commit messages for context.\n # @RETURN: str - The suggested commit message.\n @retry(\n stop=stop_after_attempt(2),\n wait=wait_exponential(multiplier=1, min=2, max=10),\n reraise=True\n )\n async def suggest_commit_message(\n self,\n diff: str,\n history: list[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n ) -> str:\n with belief_scope(\"suggest_commit_message\"):\n history_text = \"\\n\".join(history)\n prompt = render_prompt(\n prompt_template,\n {\"history\": history_text, \"diff\": diff},\n )\n\n logger.debug(f\"[suggest_commit_message] Calling LLM with model: {self.client.default_model}\")\n response = await self.client.client.chat.completions.create(\n model=self.client.default_model,\n messages=[{\"role\": \"user\", \"content\": prompt}],\n temperature=0.7\n )\n\n logger.debug(f\"[suggest_commit_message] LLM Response: {response}\")\n\n if not response or not hasattr(response, 'choices') or not response.choices:\n error_info = getattr(response, 'error', 'No choices in response')\n logger.error(f\"[suggest_commit_message] Invalid LLM response. Error info: {error_info}\")\n\n # If it's a timeout/provider error, we might want to throw to trigger retry if decorated\n # but for now we return a safe fallback to avoid UI crash\n return \"Update dashboard configurations (LLM generation failed)\"\n\n return response.choices[0].message.content.strip()\n # endregion suggest_commit_message\n# #endregion GitLLMExtension\n" }, { "contract_id": "GitPluginModule", "contract_type": "Module", "file_path": "backend/src/plugins/git_plugin.py", "start_line": 1, - "end_line": 404, + "end_line": 399, "tier": "TIER_1", "complexity": 1, "metadata": { "CONSTRAINT": "Плагин работает только с распакованными YAML-экспортами Superset.", "INVARIANT": "Все операции с Git должны выполняться через GitService.", "LAYER": "Plugin", - "PURPOSE": "Предоставляет плагин для версионирования и развертывания дашбордов Superset.", - "SEMANTICS": [ - "git", - "plugin", - "dashboard", - "version_control", - "sync", - "deploy" - ] + "PURPOSE": "Предоставляет плагин для версионирования и развертывания дашбордов Superset." }, "relations": [ { @@ -62851,15 +50807,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitPluginModule:Module]\n#\n# @SEMANTICS: git, plugin, dashboard, version_control, sync, deploy\n# @PURPOSE: Предоставляет плагин для версионирования и развертывания дашбордов 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\n# [SECTION: IMPORTS]\nimport os\nimport io\nimport shutil\nimport zipfile\nfrom pathlib import Path\nfrom typing import Dict, Any, Optional\nfrom src.core.plugin_base import PluginBase\nfrom src.services.git_service import GitService\nfrom src.core.logger import logger as app_logger, belief_scope\nfrom src.core.cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"GitPlugin\")\nfrom src.core.config_manager import ConfigManager\nfrom src.core.superset_client import SupersetClient\nfrom src.core.task_manager.context import TaskContext\nfrom src.core.database import SessionLocal\nfrom src.core.mapping_service import IdMappingService\n# [/SECTION]\n\n# [DEF:GitPlugin:Class]\n# @PURPOSE: Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n \n # [DEF:__init__:Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE: config.json exists or shared config_manager is available.\n # @POST: Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n log.reason(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n \n # Robust config path resolution:\n # 1. Try absolute path from src/dependencies.py style if possible\n # 2. Try relative paths based on common execution patterns\n if os.path.exists(\"../config.json\"):\n config_path = \"../config.json\"\n elif os.path.exists(\"config.json\"):\n config_path = \"config.json\"\n else:\n # Fallback to the one initialized in dependencies if we can import it\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n log.reflect(\"GitPlugin initialized using shared config_manager.\")\n return\n except Exception:\n config_path = \"config.json\"\n\n self.config_manager = ConfigManager(config_path)\n log.reflect(f\"GitPlugin initialized with {config_path}\")\n # [/DEF:__init__:Function]\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the plugin identifier.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns 'git-integration'.\n def id(self) -> str:\n with belief_scope(\"GitPlugin.id\"):\n return \"git-integration\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the plugin name.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the human-readable name.\n def name(self) -> str:\n with belief_scope(\"GitPlugin.name\"):\n return \"Git Integration\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns the plugin description.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the plugin's purpose description.\n def description(self) -> str:\n with belief_scope(\"GitPlugin.description\"):\n return \"Version control for Superset dashboards\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the plugin version.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the version string.\n def version(self) -> str:\n with belief_scope(\"GitPlugin.version\"):\n return \"0.1.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the git plugin.\n # @RETURN: str - \"/git\"\n def ui_route(self) -> str:\n with belief_scope(\"GitPlugin.ui_route\"):\n return \"/git\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns a JSON schema dictionary.\n # @RETURN: Dict[str, Any] - Схема параметров.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin.get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"operation\": {\"type\": \"string\", \"enum\": [\"sync\", \"deploy\", \"history\"]},\n \"dashboard_id\": {\"type\": \"integer\"},\n \"environment_id\": {\"type\": \"string\"},\n \"source_env_id\": {\"type\": \"string\"}\n },\n \"required\": [\"operation\", \"dashboard_id\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:initialize:Function]\n # @PURPOSE: Выполняет начальную настройку плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Плагин готов к выполнению задач.\n async def initialize(self):\n with belief_scope(\"GitPlugin.initialize\"):\n log.reason(\"Initializing Git Integration Plugin logic\")\n\n # [DEF:execute:Function]\n # @PURPOSE: Основной метод выполнения задач плагина с поддержкой TaskContext.\n # @PRE: task_data содержит 'operation' и 'dashboard_id'.\n # @POST: Возвращает результат выполнения операции.\n # @PARAM: task_data (Dict[str, Any]) - Данные задачи.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @RETURN: Dict[str, Any] - Статус и сообщение.\n # @RELATION: CALLS -> self._handle_sync\n # @RELATION: CALLS -> self._handle_deploy\n async def execute(self, task_data: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin.execute\"):\n operation = task_data.get(\"operation\")\n dashboard_id = task_data.get(\"dashboard_id\")\n \n # Use TaskContext logger if available, otherwise fall back to app_logger\n log = context.logger if context else app_logger\n \n # Create sub-loggers for different components\n git_log = log.with_source(\"git\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n \n log.info(f\"Executing operation: {operation} for dashboard {dashboard_id}\")\n \n if operation == \"sync\":\n source_env_id = task_data.get(\"source_env_id\")\n result = await self._handle_sync(dashboard_id, source_env_id, log, git_log, superset_log)\n elif operation == \"deploy\":\n env_id = task_data.get(\"environment_id\")\n result = await self._handle_deploy(dashboard_id, env_id, log, git_log, superset_log)\n elif operation == \"history\":\n result = {\"status\": \"success\", \"message\": \"History available via API\"}\n else:\n log.error(f\"Unknown operation: {operation}\")\n raise ValueError(f\"Unknown operation: {operation}\")\n \n log.info(f\"Operation {operation} completed.\")\n return result\n # [/DEF:execute:Function]\n\n # [DEF:_handle_sync:Function]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий.\n # @PRE: Репозиторий для дашборда должен существовать.\n # @POST: Файлы в репозитории обновлены до текущего состояния в Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: source_env_id (Optional[str]) - ID исходного окружения.\n # @RETURN: Dict[str, str] - Результат синхронизации.\n # @SIDE_EFFECT: Изменяет файлы в локальной рабочей директории репозитория.\n # @RELATION: CALLS -> src.services.git_service.GitService.get_repo\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.export_dashboard\n async def _handle_sync(self, dashboard_id: int, source_env_id: Optional[str] = None, log=None, git_log=None, superset_log=None) -> Dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Target repo path: {repo_path}\")\n\n # 2. Настройка клиента Superset\n env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n # 3. Экспорт дашборда\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n \n # 4. Распаковка с выравниванием структуры (flattening)\n git_log.info(f\"Unpacking export to {repo_path}\")\n \n # Список папок/файлов, которые мы ожидаем от Superset\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n \n # Очистка старых данных перед распаковкой, чтобы не оставалось \"призраков\"\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.rmtree(d_path)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n f_path.unlink()\n\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n # Superset экспортирует всё в подпапку dashboard_export_timestamp/\n # Нам нужно найти это имя папки\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n \n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n \n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n # Убираем префикс папки\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\n \n if member.is_dir():\n target_path.mkdir(parents=True, exist_ok=True)\n else:\n target_path.parent.mkdir(parents=True, exist_ok=True)\n with zf.open(member) as source, open(target_path, \"wb\") as target:\n shutil.copyfileobj(source, target)\n \n # 5. Автоматический staging изменений (не коммит, чтобы юзер мог проверить diff)\n try:\n repo.git.add(A=True)\n log.reason(\"Changes staged in git\")\n except Exception as ge:\n log.explore(\"Failed to stage changes\", error=str(ge))\n\n log.reflect(\"Dashboard synced successfully\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n log.explore(\"Sync failed\", error=str(e))\n raise\n # [/DEF:_handle_sync:Function]\n\n # [DEF:_handle_deploy:Function]\n # @PURPOSE: Упаковывает репозиторий в ZIP и импортирует в целевое окружение Superset.\n # @PRE: environment_id должен соответствовать настроенному окружению.\n # @POST: Дашборд импортирован в целевой Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: env_id (str) - ID целевого окружения.\n # @PARAM: log - Main logger instance.\n # @PARAM: git_log - Git-specific logger instance.\n # @PARAM: superset_log - Superset API-specific logger instance.\n # @RETURN: Dict[str, Any] - Результат деплоя.\n # @SIDE_EFFECT: Создает и удаляет временный ZIP-файл.\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.import_dashboard\n async def _handle_deploy(self, dashboard_id: int, env_id: str, log=None, git_log=None, superset_log=None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin._handle_deploy\"):\n try:\n if not env_id:\n raise ValueError(\"Target environment ID required for deployment\")\n\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n\n # 2. Упаковка в ZIP\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n \n # Superset expects a root directory in the ZIP (e.g., dashboard_export_20240101T000000/)\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\n \n with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(repo_path):\n if \".git\" in dirs:\n dirs.remove(\".git\")\n for file in files:\n if file == \".git\" or file.endswith(\".zip\"):\n continue\n file_path = Path(root) / file\n # Prepend the root directory name to the archive path\n arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n \n zip_buffer.seek(0)\n \n # 3. Настройка клиента Superset\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n \n client = SupersetClient(env)\n client.authenticate()\n\n # 4. Импорт\n temp_zip_path = repo_path / f\"deploy_{dashboard_id}.zip\"\n git_log.info(f\"Saving temporary zip to {temp_zip_path}\")\n with open(temp_zip_path, \"wb\") as f:\n f.write(zip_buffer.getvalue())\n \n try:\n log.reason(\"Importing dashboard\", payload={\"environment\": env.name})\n result = client.import_dashboard(temp_zip_path)\n log.reflect(\"Deployment successful\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": f\"Dashboard deployed to {env.name}\", \"details\": result}\n finally:\n if temp_zip_path.exists():\n os.remove(temp_zip_path)\n\n except Exception as e:\n log.explore(\"Deployment failed\", error=str(e))\n raise\n # [/DEF:_handle_deploy:Function]\n\n # [DEF:_get_env:Function]\n # @PURPOSE: Вспомогательный метод для получения конфигурации окружения.\n # @PARAM: env_id (Optional[str]) - ID окружения.\n # @PRE: env_id is a string or None.\n # @POST: Returns an Environment object from config or DB.\n # @RETURN: Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: Optional[str] = None):\n with belief_scope(\"GitPlugin._get_env\"):\n log.reason(\"Fetching environment\", payload={\"env_id\": env_id})\n \n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n log.reflect(\"Found environment by ID in ConfigManager\", payload={\"name\": env.name})\n return env\n \n # Priority 2: Database (DeploymentEnvironment)\n from src.core.database import SessionLocal\n from src.models.git import DeploymentEnvironment\n \n db = SessionLocal()\n try:\n if env_id:\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.id == env_id).first()\n else:\n # If no ID, try to find active or any environment in DB\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.is_active).first()\n if not db_env:\n db_env = db.query(DeploymentEnvironment).first()\n\n if db_env:\n log.reflect(\"Found environment in DB\", payload={\"name\": db_env.name})\n from src.core.config_models import Environment\n # Use token as password for SupersetClient\n return Environment(\n id=db_env.id,\n name=db_env.name,\n url=db_env.superset_url,\n username=\"admin\",\n password=db_env.superset_token,\n verify_ssl=True\n )\n finally:\n db.close()\n \n # Priority 3: ConfigManager Default (if no env_id provided)\n envs = self.config_manager.get_environments()\n if envs:\n if env_id:\n # If env_id was provided but not found in DB or specifically by ID in config,\n # but we have other envs, maybe it's one of them?\n env = next((e for e in envs if e.id == env_id), None)\n if env:\n log.reflect(\"Found environment in ConfigManager list\", payload={\"env_id\": env_id})\n return env\n \n if not env_id:\n log.reflect(\"Using first environment from ConfigManager\", payload={\"name\": envs[0].name})\n return envs[0]\n \n log.explore(\"No environments configured\", payload={\"env_id\": env_id}, error=\"No Superset environments found in config or database\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # [/DEF:_get_env:Function]\n\n # [/DEF:initialize:Function]\n# [/DEF:GitPlugin:Class]\n# [/DEF:GitPluginModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GitPlugin", "contract_type": "Class", "file_path": "backend/src/plugins/git_plugin.py", - "start_line": 35, - "end_line": 403, + "start_line": 30, + "end_line": 398, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -62877,295 +50833,11 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitPlugin:Class]\n# @PURPOSE: Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n \n # [DEF:__init__:Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE: config.json exists or shared config_manager is available.\n # @POST: Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n log.reason(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n \n # Robust config path resolution:\n # 1. Try absolute path from src/dependencies.py style if possible\n # 2. Try relative paths based on common execution patterns\n if os.path.exists(\"../config.json\"):\n config_path = \"../config.json\"\n elif os.path.exists(\"config.json\"):\n config_path = \"config.json\"\n else:\n # Fallback to the one initialized in dependencies if we can import it\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n log.reflect(\"GitPlugin initialized using shared config_manager.\")\n return\n except Exception:\n config_path = \"config.json\"\n\n self.config_manager = ConfigManager(config_path)\n log.reflect(f\"GitPlugin initialized with {config_path}\")\n # [/DEF:__init__:Function]\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the plugin identifier.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns 'git-integration'.\n def id(self) -> str:\n with belief_scope(\"GitPlugin.id\"):\n return \"git-integration\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the plugin name.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the human-readable name.\n def name(self) -> str:\n with belief_scope(\"GitPlugin.name\"):\n return \"Git Integration\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns the plugin description.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the plugin's purpose description.\n def description(self) -> str:\n with belief_scope(\"GitPlugin.description\"):\n return \"Version control for Superset dashboards\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the plugin version.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns the version string.\n def version(self) -> str:\n with belief_scope(\"GitPlugin.version\"):\n return \"0.1.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the git plugin.\n # @RETURN: str - \"/git\"\n def ui_route(self) -> str:\n with belief_scope(\"GitPlugin.ui_route\"):\n return \"/git\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Возвращает JSON-схему параметров для выполнения задач плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Returns a JSON schema dictionary.\n # @RETURN: Dict[str, Any] - Схема параметров.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin.get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"operation\": {\"type\": \"string\", \"enum\": [\"sync\", \"deploy\", \"history\"]},\n \"dashboard_id\": {\"type\": \"integer\"},\n \"environment_id\": {\"type\": \"string\"},\n \"source_env_id\": {\"type\": \"string\"}\n },\n \"required\": [\"operation\", \"dashboard_id\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:initialize:Function]\n # @PURPOSE: Выполняет начальную настройку плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Плагин готов к выполнению задач.\n async def initialize(self):\n with belief_scope(\"GitPlugin.initialize\"):\n log.reason(\"Initializing Git Integration Plugin logic\")\n\n # [DEF:execute:Function]\n # @PURPOSE: Основной метод выполнения задач плагина с поддержкой TaskContext.\n # @PRE: task_data содержит 'operation' и 'dashboard_id'.\n # @POST: Возвращает результат выполнения операции.\n # @PARAM: task_data (Dict[str, Any]) - Данные задачи.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @RETURN: Dict[str, Any] - Статус и сообщение.\n # @RELATION: CALLS -> self._handle_sync\n # @RELATION: CALLS -> self._handle_deploy\n async def execute(self, task_data: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin.execute\"):\n operation = task_data.get(\"operation\")\n dashboard_id = task_data.get(\"dashboard_id\")\n \n # Use TaskContext logger if available, otherwise fall back to app_logger\n log = context.logger if context else app_logger\n \n # Create sub-loggers for different components\n git_log = log.with_source(\"git\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n \n log.info(f\"Executing operation: {operation} for dashboard {dashboard_id}\")\n \n if operation == \"sync\":\n source_env_id = task_data.get(\"source_env_id\")\n result = await self._handle_sync(dashboard_id, source_env_id, log, git_log, superset_log)\n elif operation == \"deploy\":\n env_id = task_data.get(\"environment_id\")\n result = await self._handle_deploy(dashboard_id, env_id, log, git_log, superset_log)\n elif operation == \"history\":\n result = {\"status\": \"success\", \"message\": \"History available via API\"}\n else:\n log.error(f\"Unknown operation: {operation}\")\n raise ValueError(f\"Unknown operation: {operation}\")\n \n log.info(f\"Operation {operation} completed.\")\n return result\n # [/DEF:execute:Function]\n\n # [DEF:_handle_sync:Function]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий.\n # @PRE: Репозиторий для дашборда должен существовать.\n # @POST: Файлы в репозитории обновлены до текущего состояния в Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: source_env_id (Optional[str]) - ID исходного окружения.\n # @RETURN: Dict[str, str] - Результат синхронизации.\n # @SIDE_EFFECT: Изменяет файлы в локальной рабочей директории репозитория.\n # @RELATION: CALLS -> src.services.git_service.GitService.get_repo\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.export_dashboard\n async def _handle_sync(self, dashboard_id: int, source_env_id: Optional[str] = None, log=None, git_log=None, superset_log=None) -> Dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Target repo path: {repo_path}\")\n\n # 2. Настройка клиента Superset\n env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n # 3. Экспорт дашборда\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n \n # 4. Распаковка с выравниванием структуры (flattening)\n git_log.info(f\"Unpacking export to {repo_path}\")\n \n # Список папок/файлов, которые мы ожидаем от Superset\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n \n # Очистка старых данных перед распаковкой, чтобы не оставалось \"призраков\"\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.rmtree(d_path)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n f_path.unlink()\n\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n # Superset экспортирует всё в подпапку dashboard_export_timestamp/\n # Нам нужно найти это имя папки\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n \n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n \n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n # Убираем префикс папки\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\n \n if member.is_dir():\n target_path.mkdir(parents=True, exist_ok=True)\n else:\n target_path.parent.mkdir(parents=True, exist_ok=True)\n with zf.open(member) as source, open(target_path, \"wb\") as target:\n shutil.copyfileobj(source, target)\n \n # 5. Автоматический staging изменений (не коммит, чтобы юзер мог проверить diff)\n try:\n repo.git.add(A=True)\n log.reason(\"Changes staged in git\")\n except Exception as ge:\n log.explore(\"Failed to stage changes\", error=str(ge))\n\n log.reflect(\"Dashboard synced successfully\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n log.explore(\"Sync failed\", error=str(e))\n raise\n # [/DEF:_handle_sync:Function]\n\n # [DEF:_handle_deploy:Function]\n # @PURPOSE: Упаковывает репозиторий в ZIP и импортирует в целевое окружение Superset.\n # @PRE: environment_id должен соответствовать настроенному окружению.\n # @POST: Дашборд импортирован в целевой Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: env_id (str) - ID целевого окружения.\n # @PARAM: log - Main logger instance.\n # @PARAM: git_log - Git-specific logger instance.\n # @PARAM: superset_log - Superset API-specific logger instance.\n # @RETURN: Dict[str, Any] - Результат деплоя.\n # @SIDE_EFFECT: Создает и удаляет временный ZIP-файл.\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.import_dashboard\n async def _handle_deploy(self, dashboard_id: int, env_id: str, log=None, git_log=None, superset_log=None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin._handle_deploy\"):\n try:\n if not env_id:\n raise ValueError(\"Target environment ID required for deployment\")\n\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n\n # 2. Упаковка в ZIP\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n \n # Superset expects a root directory in the ZIP (e.g., dashboard_export_20240101T000000/)\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\n \n with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(repo_path):\n if \".git\" in dirs:\n dirs.remove(\".git\")\n for file in files:\n if file == \".git\" or file.endswith(\".zip\"):\n continue\n file_path = Path(root) / file\n # Prepend the root directory name to the archive path\n arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n \n zip_buffer.seek(0)\n \n # 3. Настройка клиента Superset\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n \n client = SupersetClient(env)\n client.authenticate()\n\n # 4. Импорт\n temp_zip_path = repo_path / f\"deploy_{dashboard_id}.zip\"\n git_log.info(f\"Saving temporary zip to {temp_zip_path}\")\n with open(temp_zip_path, \"wb\") as f:\n f.write(zip_buffer.getvalue())\n \n try:\n log.reason(\"Importing dashboard\", payload={\"environment\": env.name})\n result = client.import_dashboard(temp_zip_path)\n log.reflect(\"Deployment successful\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": f\"Dashboard deployed to {env.name}\", \"details\": result}\n finally:\n if temp_zip_path.exists():\n os.remove(temp_zip_path)\n\n except Exception as e:\n log.explore(\"Deployment failed\", error=str(e))\n raise\n # [/DEF:_handle_deploy:Function]\n\n # [DEF:_get_env:Function]\n # @PURPOSE: Вспомогательный метод для получения конфигурации окружения.\n # @PARAM: env_id (Optional[str]) - ID окружения.\n # @PRE: env_id is a string or None.\n # @POST: Returns an Environment object from config or DB.\n # @RETURN: Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: Optional[str] = None):\n with belief_scope(\"GitPlugin._get_env\"):\n log.reason(\"Fetching environment\", payload={\"env_id\": env_id})\n \n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n log.reflect(\"Found environment by ID in ConfigManager\", payload={\"name\": env.name})\n return env\n \n # Priority 2: Database (DeploymentEnvironment)\n from src.core.database import SessionLocal\n from src.models.git import DeploymentEnvironment\n \n db = SessionLocal()\n try:\n if env_id:\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.id == env_id).first()\n else:\n # If no ID, try to find active or any environment in DB\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.is_active).first()\n if not db_env:\n db_env = db.query(DeploymentEnvironment).first()\n\n if db_env:\n log.reflect(\"Found environment in DB\", payload={\"name\": db_env.name})\n from src.core.config_models import Environment\n # Use token as password for SupersetClient\n return Environment(\n id=db_env.id,\n name=db_env.name,\n url=db_env.superset_url,\n username=\"admin\",\n password=db_env.superset_token,\n verify_ssl=True\n )\n finally:\n db.close()\n \n # Priority 3: ConfigManager Default (if no env_id provided)\n envs = self.config_manager.get_environments()\n if envs:\n if env_id:\n # If env_id was provided but not found in DB or specifically by ID in config,\n # but we have other envs, maybe it's one of them?\n env = next((e for e in envs if e.id == env_id), None)\n if env:\n log.reflect(\"Found environment in ConfigManager list\", payload={\"env_id\": env_id})\n return env\n \n if not env_id:\n log.reflect(\"Using first environment from ConfigManager\", payload={\"name\": envs[0].name})\n return envs[0]\n \n log.explore(\"No environments configured\", payload={\"env_id\": env_id}, error=\"No Superset environments found in config or database\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # [/DEF:_get_env:Function]\n\n # [/DEF:initialize:Function]\n# [/DEF:GitPlugin:Class]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "initialize", - "contract_type": "Function", - "file_path": "backend/src/plugins/git_plugin.py", - "start_line": 137, - "end_line": 402, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Плагин готов к выполнению задач.", - "PRE": "GitPlugin is initialized.", - "PURPOSE": "Выполняет начальную настройку плагина." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:initialize:Function]\n # @PURPOSE: Выполняет начальную настройку плагина.\n # @PRE: GitPlugin is initialized.\n # @POST: Плагин готов к выполнению задач.\n async def initialize(self):\n with belief_scope(\"GitPlugin.initialize\"):\n log.reason(\"Initializing Git Integration Plugin logic\")\n\n # [DEF:execute:Function]\n # @PURPOSE: Основной метод выполнения задач плагина с поддержкой TaskContext.\n # @PRE: task_data содержит 'operation' и 'dashboard_id'.\n # @POST: Возвращает результат выполнения операции.\n # @PARAM: task_data (Dict[str, Any]) - Данные задачи.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @RETURN: Dict[str, Any] - Статус и сообщение.\n # @RELATION: CALLS -> self._handle_sync\n # @RELATION: CALLS -> self._handle_deploy\n async def execute(self, task_data: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin.execute\"):\n operation = task_data.get(\"operation\")\n dashboard_id = task_data.get(\"dashboard_id\")\n \n # Use TaskContext logger if available, otherwise fall back to app_logger\n log = context.logger if context else app_logger\n \n # Create sub-loggers for different components\n git_log = log.with_source(\"git\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n \n log.info(f\"Executing operation: {operation} for dashboard {dashboard_id}\")\n \n if operation == \"sync\":\n source_env_id = task_data.get(\"source_env_id\")\n result = await self._handle_sync(dashboard_id, source_env_id, log, git_log, superset_log)\n elif operation == \"deploy\":\n env_id = task_data.get(\"environment_id\")\n result = await self._handle_deploy(dashboard_id, env_id, log, git_log, superset_log)\n elif operation == \"history\":\n result = {\"status\": \"success\", \"message\": \"History available via API\"}\n else:\n log.error(f\"Unknown operation: {operation}\")\n raise ValueError(f\"Unknown operation: {operation}\")\n \n log.info(f\"Operation {operation} completed.\")\n return result\n # [/DEF:execute:Function]\n\n # [DEF:_handle_sync:Function]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий.\n # @PRE: Репозиторий для дашборда должен существовать.\n # @POST: Файлы в репозитории обновлены до текущего состояния в Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: source_env_id (Optional[str]) - ID исходного окружения.\n # @RETURN: Dict[str, str] - Результат синхронизации.\n # @SIDE_EFFECT: Изменяет файлы в локальной рабочей директории репозитория.\n # @RELATION: CALLS -> src.services.git_service.GitService.get_repo\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.export_dashboard\n async def _handle_sync(self, dashboard_id: int, source_env_id: Optional[str] = None, log=None, git_log=None, superset_log=None) -> Dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Target repo path: {repo_path}\")\n\n # 2. Настройка клиента Superset\n env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n # 3. Экспорт дашборда\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n \n # 4. Распаковка с выравниванием структуры (flattening)\n git_log.info(f\"Unpacking export to {repo_path}\")\n \n # Список папок/файлов, которые мы ожидаем от Superset\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n \n # Очистка старых данных перед распаковкой, чтобы не оставалось \"призраков\"\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.rmtree(d_path)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n f_path.unlink()\n\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n # Superset экспортирует всё в подпапку dashboard_export_timestamp/\n # Нам нужно найти это имя папки\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n \n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n \n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n # Убираем префикс папки\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\n \n if member.is_dir():\n target_path.mkdir(parents=True, exist_ok=True)\n else:\n target_path.parent.mkdir(parents=True, exist_ok=True)\n with zf.open(member) as source, open(target_path, \"wb\") as target:\n shutil.copyfileobj(source, target)\n \n # 5. Автоматический staging изменений (не коммит, чтобы юзер мог проверить diff)\n try:\n repo.git.add(A=True)\n log.reason(\"Changes staged in git\")\n except Exception as ge:\n log.explore(\"Failed to stage changes\", error=str(ge))\n\n log.reflect(\"Dashboard synced successfully\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n log.explore(\"Sync failed\", error=str(e))\n raise\n # [/DEF:_handle_sync:Function]\n\n # [DEF:_handle_deploy:Function]\n # @PURPOSE: Упаковывает репозиторий в ZIP и импортирует в целевое окружение Superset.\n # @PRE: environment_id должен соответствовать настроенному окружению.\n # @POST: Дашборд импортирован в целевой Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: env_id (str) - ID целевого окружения.\n # @PARAM: log - Main logger instance.\n # @PARAM: git_log - Git-specific logger instance.\n # @PARAM: superset_log - Superset API-specific logger instance.\n # @RETURN: Dict[str, Any] - Результат деплоя.\n # @SIDE_EFFECT: Создает и удаляет временный ZIP-файл.\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.import_dashboard\n async def _handle_deploy(self, dashboard_id: int, env_id: str, log=None, git_log=None, superset_log=None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin._handle_deploy\"):\n try:\n if not env_id:\n raise ValueError(\"Target environment ID required for deployment\")\n\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n\n # 2. Упаковка в ZIP\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n \n # Superset expects a root directory in the ZIP (e.g., dashboard_export_20240101T000000/)\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\n \n with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(repo_path):\n if \".git\" in dirs:\n dirs.remove(\".git\")\n for file in files:\n if file == \".git\" or file.endswith(\".zip\"):\n continue\n file_path = Path(root) / file\n # Prepend the root directory name to the archive path\n arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n \n zip_buffer.seek(0)\n \n # 3. Настройка клиента Superset\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n \n client = SupersetClient(env)\n client.authenticate()\n\n # 4. Импорт\n temp_zip_path = repo_path / f\"deploy_{dashboard_id}.zip\"\n git_log.info(f\"Saving temporary zip to {temp_zip_path}\")\n with open(temp_zip_path, \"wb\") as f:\n f.write(zip_buffer.getvalue())\n \n try:\n log.reason(\"Importing dashboard\", payload={\"environment\": env.name})\n result = client.import_dashboard(temp_zip_path)\n log.reflect(\"Deployment successful\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": f\"Dashboard deployed to {env.name}\", \"details\": result}\n finally:\n if temp_zip_path.exists():\n os.remove(temp_zip_path)\n\n except Exception as e:\n log.explore(\"Deployment failed\", error=str(e))\n raise\n # [/DEF:_handle_deploy:Function]\n\n # [DEF:_get_env:Function]\n # @PURPOSE: Вспомогательный метод для получения конфигурации окружения.\n # @PARAM: env_id (Optional[str]) - ID окружения.\n # @PRE: env_id is a string or None.\n # @POST: Returns an Environment object from config or DB.\n # @RETURN: Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: Optional[str] = None):\n with belief_scope(\"GitPlugin._get_env\"):\n log.reason(\"Fetching environment\", payload={\"env_id\": env_id})\n \n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n log.reflect(\"Found environment by ID in ConfigManager\", payload={\"name\": env.name})\n return env\n \n # Priority 2: Database (DeploymentEnvironment)\n from src.core.database import SessionLocal\n from src.models.git import DeploymentEnvironment\n \n db = SessionLocal()\n try:\n if env_id:\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.id == env_id).first()\n else:\n # If no ID, try to find active or any environment in DB\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.is_active).first()\n if not db_env:\n db_env = db.query(DeploymentEnvironment).first()\n\n if db_env:\n log.reflect(\"Found environment in DB\", payload={\"name\": db_env.name})\n from src.core.config_models import Environment\n # Use token as password for SupersetClient\n return Environment(\n id=db_env.id,\n name=db_env.name,\n url=db_env.superset_url,\n username=\"admin\",\n password=db_env.superset_token,\n verify_ssl=True\n )\n finally:\n db.close()\n \n # Priority 3: ConfigManager Default (if no env_id provided)\n envs = self.config_manager.get_environments()\n if envs:\n if env_id:\n # If env_id was provided but not found in DB or specifically by ID in config,\n # but we have other envs, maybe it's one of them?\n env = next((e for e in envs if e.id == env_id), None)\n if env:\n log.reflect(\"Found environment in ConfigManager list\", payload={\"env_id\": env_id})\n return env\n \n if not env_id:\n log.reflect(\"Using first environment from ConfigManager\", payload={\"name\": envs[0].name})\n return envs[0]\n \n log.explore(\"No environments configured\", payload={\"env_id\": env_id}, error=\"No Superset environments found in config or database\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # [/DEF:_get_env:Function]\n\n # [/DEF:initialize:Function]\n" - }, - { - "contract_id": "_handle_sync", - "contract_type": "Function", - "file_path": "backend/src/plugins/git_plugin.py", - "start_line": 184, - "end_line": 264, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "source_env_id (Optional[str]) - ID исходного окружения.", - "POST": "Файлы в репозитории обновлены до текущего состояния в Superset.", - "PRE": "Репозиторий для дашборда должен существовать.", - "PURPOSE": "Экспортирует дашборд из Superset и распаковывает в Git-репозиторий.", - "RETURN": "Dict[str, str] - Результат синхронизации.", - "SIDE_EFFECT": "Изменяет файлы в локальной рабочей директории репозитория." - }, - "relations": [ - { - "source_id": "_handle_sync", - "relation_type": "CALLS", - "target_id": "src.services.git_service.GitService.get_repo", - "target_ref": "src.services.git_service.GitService.get_repo" - }, - { - "source_id": "_handle_sync", - "relation_type": "CALLS", - "target_id": "src.core.superset_client.SupersetClient.export_dashboard", - "target_ref": "src.core.superset_client.SupersetClient.export_dashboard" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT 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" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_handle_sync:Function]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий.\n # @PRE: Репозиторий для дашборда должен существовать.\n # @POST: Файлы в репозитории обновлены до текущего состояния в Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: source_env_id (Optional[str]) - ID исходного окружения.\n # @RETURN: Dict[str, str] - Результат синхронизации.\n # @SIDE_EFFECT: Изменяет файлы в локальной рабочей директории репозитория.\n # @RELATION: CALLS -> src.services.git_service.GitService.get_repo\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.export_dashboard\n async def _handle_sync(self, dashboard_id: int, source_env_id: Optional[str] = None, log=None, git_log=None, superset_log=None) -> Dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Target repo path: {repo_path}\")\n\n # 2. Настройка клиента Superset\n env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n # 3. Экспорт дашборда\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n \n # 4. Распаковка с выравниванием структуры (flattening)\n git_log.info(f\"Unpacking export to {repo_path}\")\n \n # Список папок/файлов, которые мы ожидаем от Superset\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n \n # Очистка старых данных перед распаковкой, чтобы не оставалось \"призраков\"\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.rmtree(d_path)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n f_path.unlink()\n\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n # Superset экспортирует всё в подпапку dashboard_export_timestamp/\n # Нам нужно найти это имя папки\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n \n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n \n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n # Убираем префикс папки\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\n \n if member.is_dir():\n target_path.mkdir(parents=True, exist_ok=True)\n else:\n target_path.parent.mkdir(parents=True, exist_ok=True)\n with zf.open(member) as source, open(target_path, \"wb\") as target:\n shutil.copyfileobj(source, target)\n \n # 5. Автоматический staging изменений (не коммит, чтобы юзер мог проверить diff)\n try:\n repo.git.add(A=True)\n log.reason(\"Changes staged in git\")\n except Exception as ge:\n log.explore(\"Failed to stage changes\", error=str(ge))\n\n log.reflect(\"Dashboard synced successfully\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n log.explore(\"Sync failed\", error=str(e))\n raise\n # [/DEF:_handle_sync:Function]\n" - }, - { - "contract_id": "_handle_deploy", - "contract_type": "Function", - "file_path": "backend/src/plugins/git_plugin.py", - "start_line": 266, - "end_line": 335, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "superset_log - Superset API-specific logger instance.", - "POST": "Дашборд импортирован в целевой Superset.", - "PRE": "environment_id должен соответствовать настроенному окружению.", - "PURPOSE": "Упаковывает репозиторий в ZIP и импортирует в целевое окружение Superset.", - "RETURN": "Dict[str, Any] - Результат деплоя.", - "SIDE_EFFECT": "Создает и удаляет временный ZIP-файл." - }, - "relations": [ - { - "source_id": "_handle_deploy", - "relation_type": "CALLS", - "target_id": "src.core.superset_client.SupersetClient.import_dashboard", - "target_ref": "src.core.superset_client.SupersetClient.import_dashboard" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT 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" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_handle_deploy:Function]\n # @PURPOSE: Упаковывает репозиторий в ZIP и импортирует в целевое окружение Superset.\n # @PRE: environment_id должен соответствовать настроенному окружению.\n # @POST: Дашборд импортирован в целевой Superset.\n # @PARAM: dashboard_id (int) - ID дашборда.\n # @PARAM: env_id (str) - ID целевого окружения.\n # @PARAM: log - Main logger instance.\n # @PARAM: git_log - Git-specific logger instance.\n # @PARAM: superset_log - Superset API-specific logger instance.\n # @RETURN: Dict[str, Any] - Результат деплоя.\n # @SIDE_EFFECT: Создает и удаляет временный ZIP-файл.\n # @RELATION: CALLS -> src.core.superset_client.SupersetClient.import_dashboard\n async def _handle_deploy(self, dashboard_id: int, env_id: str, log=None, git_log=None, superset_log=None) -> Dict[str, Any]:\n with belief_scope(\"GitPlugin._handle_deploy\"):\n try:\n if not env_id:\n raise ValueError(\"Target environment ID required for deployment\")\n\n # 1. Получение репозитория\n repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n\n # 2. Упаковка в ZIP\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n \n # Superset expects a root directory in the ZIP (e.g., dashboard_export_20240101T000000/)\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\n \n with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n for root, dirs, files in os.walk(repo_path):\n if \".git\" in dirs:\n dirs.remove(\".git\")\n for file in files:\n if file == \".git\" or file.endswith(\".zip\"):\n continue\n file_path = Path(root) / file\n # Prepend the root directory name to the archive path\n arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n \n zip_buffer.seek(0)\n \n # 3. Настройка клиента Superset\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment {env_id} not found\")\n \n client = SupersetClient(env)\n client.authenticate()\n\n # 4. Импорт\n temp_zip_path = repo_path / f\"deploy_{dashboard_id}.zip\"\n git_log.info(f\"Saving temporary zip to {temp_zip_path}\")\n with open(temp_zip_path, \"wb\") as f:\n f.write(zip_buffer.getvalue())\n \n try:\n log.reason(\"Importing dashboard\", payload={\"environment\": env.name})\n result = client.import_dashboard(temp_zip_path)\n log.reflect(\"Deployment successful\", payload={\"dashboard_id\": dashboard_id})\n return {\"status\": \"success\", \"message\": f\"Dashboard deployed to {env.name}\", \"details\": result}\n finally:\n if temp_zip_path.exists():\n os.remove(temp_zip_path)\n\n except Exception as e:\n log.explore(\"Deployment failed\", error=str(e))\n raise\n # [/DEF:_handle_deploy:Function]\n" - }, - { - "contract_id": "_get_env", - "contract_type": "Function", - "file_path": "backend/src/plugins/git_plugin.py", - "start_line": 337, - "end_line": 400, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "env_id (Optional[str]) - ID окружения.", - "POST": "Returns an Environment object from config or DB.", - "PRE": "env_id is a string or None.", - "PURPOSE": "Вспомогательный метод для получения конфигурации окружения.", - "RETURN": "Environment - Объект конфигурации окружения." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_env:Function]\n # @PURPOSE: Вспомогательный метод для получения конфигурации окружения.\n # @PARAM: env_id (Optional[str]) - ID окружения.\n # @PRE: env_id is a string or None.\n # @POST: Returns an Environment object from config or DB.\n # @RETURN: Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: Optional[str] = None):\n with belief_scope(\"GitPlugin._get_env\"):\n log.reason(\"Fetching environment\", payload={\"env_id\": env_id})\n \n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n log.reflect(\"Found environment by ID in ConfigManager\", payload={\"name\": env.name})\n return env\n \n # Priority 2: Database (DeploymentEnvironment)\n from src.core.database import SessionLocal\n from src.models.git import DeploymentEnvironment\n \n db = SessionLocal()\n try:\n if env_id:\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.id == env_id).first()\n else:\n # If no ID, try to find active or any environment in DB\n db_env = db.query(DeploymentEnvironment).filter(DeploymentEnvironment.is_active).first()\n if not db_env:\n db_env = db.query(DeploymentEnvironment).first()\n\n if db_env:\n log.reflect(\"Found environment in DB\", payload={\"name\": db_env.name})\n from src.core.config_models import Environment\n # Use token as password for SupersetClient\n return Environment(\n id=db_env.id,\n name=db_env.name,\n url=db_env.superset_url,\n username=\"admin\",\n password=db_env.superset_token,\n verify_ssl=True\n )\n finally:\n db.close()\n \n # Priority 3: ConfigManager Default (if no env_id provided)\n envs = self.config_manager.get_environments()\n if envs:\n if env_id:\n # If env_id was provided but not found in DB or specifically by ID in config,\n # but we have other envs, maybe it's one of them?\n env = next((e for e in envs if e.id == env_id), None)\n if env:\n log.reflect(\"Found environment in ConfigManager list\", payload={\"env_id\": env_id})\n return env\n \n if not env_id:\n log.reflect(\"Using first environment from ConfigManager\", payload={\"name\": envs[0].name})\n return envs[0]\n \n log.explore(\"No environments configured\", payload={\"env_id\": env_id}, error=\"No Superset environments found in config or database\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # [/DEF:_get_env:Function]\n" - }, - { - "contract_id": "backend/src/plugins/llm_analysis/__init__.py", + "contract_id": "LLMAnalysisPackage", "contract_type": "Module", "file_path": "backend/src/plugins/llm_analysis/__init__.py", "start_line": 1, @@ -63175,685 +50847,21 @@ "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:backend/src/plugins/llm_analysis/__init__.py:Module]\n\n\"\"\"\nLLM Analysis Plugin for automated dashboard validation and dataset documentation.\n\"\"\"\n\nfrom .plugin import DashboardValidationPlugin, DocumentationPlugin\n\n__all__ = ['DashboardValidationPlugin', 'DocumentationPlugin']\n\n# [/DEF:backend/src/plugins/llm_analysis/__init__.py:Module]\n" - }, - { - "contract_id": "TestClientHeaders", - "contract_type": "Module", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_client_headers.py", - "start_line": 1, - "end_line": 32, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Verify OpenRouter client initialization includes provider-specific headers.", - "SEMANTICS": [ - "tests", - "llm-client", - "openrouter", - "headers" - ] - }, - "relations": [ - { - "source_id": "TestClientHeaders", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestClientHeaders:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, llm-client, openrouter, headers\n# @PURPOSE: Verify OpenRouter client initialization includes provider-specific headers.\n\nfrom src.plugins.llm_analysis.models import LLMProviderType\nfrom src.plugins.llm_analysis.service import LLMClient\n\n\n# [DEF:test_openrouter_client_includes_referer_and_title_headers:Function]\n# @RELATION: BINDS_TO -> TestClientHeaders\n# @PURPOSE: OpenRouter requests should carry site/app attribution headers for compatibility.\n# @PRE: Client is initialized for OPENROUTER provider.\n# @POST: Async client headers include Authorization, HTTP-Referer, and X-Title.\ndef test_openrouter_client_includes_referer_and_title_headers(monkeypatch):\n monkeypatch.setenv(\"OPENROUTER_SITE_URL\", \"http://localhost:8000\")\n monkeypatch.setenv(\"OPENROUTER_APP_NAME\", \"ss-tools-test\")\n\n client = LLMClient(\n provider_type=LLMProviderType.OPENROUTER,\n api_key=\"sk-test-provider-key-123456\",\n base_url=\"https://openrouter.ai/api/v1\",\n default_model=\"nvidia/nemotron-nano-12b-v2-vl:free\",\n )\n\n headers = dict(client.client.default_headers)\n assert headers[\"Authorization\"] == \"Bearer sk-test-provider-key-123456\"\n assert headers[\"HTTP-Referer\"] == \"http://localhost:8000\"\n assert headers[\"X-Title\"] == \"ss-tools-test\"\n# [/DEF:test_openrouter_client_includes_referer_and_title_headers:Function]\n# [/DEF:TestClientHeaders:Module]\n" - }, - { - "contract_id": "test_openrouter_client_includes_referer_and_title_headers", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_client_headers.py", - "start_line": 11, - "end_line": 31, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Async client headers include Authorization, HTTP-Referer, and X-Title.", - "PRE": "Client is initialized for OPENROUTER provider.", - "PURPOSE": "OpenRouter requests should carry site/app attribution headers for compatibility." - }, - "relations": [ - { - "source_id": "test_openrouter_client_includes_referer_and_title_headers", - "relation_type": "BINDS_TO", - "target_id": "TestClientHeaders", - "target_ref": "TestClientHeaders" - } - ], - "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": "def", - "body": "# [DEF:test_openrouter_client_includes_referer_and_title_headers:Function]\n# @RELATION: BINDS_TO -> TestClientHeaders\n# @PURPOSE: OpenRouter requests should carry site/app attribution headers for compatibility.\n# @PRE: Client is initialized for OPENROUTER provider.\n# @POST: Async client headers include Authorization, HTTP-Referer, and X-Title.\ndef test_openrouter_client_includes_referer_and_title_headers(monkeypatch):\n monkeypatch.setenv(\"OPENROUTER_SITE_URL\", \"http://localhost:8000\")\n monkeypatch.setenv(\"OPENROUTER_APP_NAME\", \"ss-tools-test\")\n\n client = LLMClient(\n provider_type=LLMProviderType.OPENROUTER,\n api_key=\"sk-test-provider-key-123456\",\n base_url=\"https://openrouter.ai/api/v1\",\n default_model=\"nvidia/nemotron-nano-12b-v2-vl:free\",\n )\n\n headers = dict(client.client.default_headers)\n assert headers[\"Authorization\"] == \"Bearer sk-test-provider-key-123456\"\n assert headers[\"HTTP-Referer\"] == \"http://localhost:8000\"\n assert headers[\"X-Title\"] == \"ss-tools-test\"\n# [/DEF:test_openrouter_client_includes_referer_and_title_headers:Function]\n" - }, - { - "contract_id": "TestScreenshotService", - "contract_type": "Module", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py", - "start_line": 1, - "end_line": 404, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Protect dashboard screenshot navigation from brittle networkidle waits.", - "SEMANTICS": [ - "tests", - "screenshot-service", - "navigation", - "timeout-regression" - ] - }, - "relations": [ - { - "source_id": "TestScreenshotService", - "relation_type": "VERIFIES", - "target_id": "src.plugins.llm_analysis.service.ScreenshotService", - "target_ref": "[src.plugins.llm_analysis.service.ScreenshotService]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TestScreenshotService:Module]\n# @RELATION: VERIFIES ->[src.plugins.llm_analysis.service.ScreenshotService]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, screenshot-service, navigation, timeout-regression\n# @PURPOSE: Protect dashboard screenshot navigation from brittle networkidle waits.\n\nimport pytest\n\nfrom src.plugins.llm_analysis.service import ScreenshotService\n\n\n# [DEF:test_iter_login_roots_includes_child_frames:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Login discovery must search embedded auth frames, not only the main page.\n# @PRE: Page exposes child frames list.\n# @POST: Returned roots include page plus child frames in order.\ndef test_iter_login_roots_includes_child_frames():\n frame_a = object()\n frame_b = object()\n fake_page = type(\"FakePage\", (), {\"frames\": [frame_a, frame_b]})()\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n roots = service._iter_login_roots(fake_page)\n\n assert roots == [fake_page, frame_a, frame_b]\n\n\n# [/DEF:test_iter_login_roots_includes_child_frames:Function]\n\n\n# [DEF:test_response_looks_like_login_page_detects_login_markup:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Direct login fallback must reject responses that render the login screen again.\n# @PRE: Response body contains stable login-page markers.\n# @POST: Helper returns True so caller treats fallback as failed authentication.\ndef test_response_looks_like_login_page_detects_login_markup():\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n result = service._response_looks_like_login_page(\n \"\"\"\n \n \n

Enter your login and password below

\n \n \n \n \n \n \"\"\"\n )\n\n assert result is True\n\n\n# [/DEF:test_response_looks_like_login_page_detects_login_markup:Function]\n\n\n# [DEF:test_find_first_visible_locator_skips_hidden_first_match:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Locator helper must not reject a selector collection just because its first element is hidden.\n# @PRE: First matched element is hidden and second matched element is visible.\n# @POST: Helper returns the second visible candidate.\n@pytest.mark.anyio\nasync def test_find_first_visible_locator_skips_hidden_first_match():\n class _FakeElement:\n def __init__(self, visible, label):\n self._visible = visible\n self.label = label\n\n async def is_visible(self):\n return self._visible\n\n class _FakeLocator:\n def __init__(self, elements):\n self._elements = elements\n\n async def count(self):\n return len(self._elements)\n\n def nth(self, index):\n return self._elements[index]\n\n service = ScreenshotService(env=type(\"Env\", (), {})())\n hidden_then_visible = _FakeLocator(\n [\n _FakeElement(False, \"hidden\"),\n _FakeElement(True, \"visible\"),\n ]\n )\n\n result = await service._find_first_visible_locator([hidden_then_visible])\n\n assert result.label == \"visible\"\n\n\n# [/DEF:test_find_first_visible_locator_skips_hidden_first_match:Function]\n\n\n# [DEF:test_submit_login_via_form_post_uses_browser_context_request:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must submit hidden fields and credentials through the context request cookie jar.\n# @PRE: Login DOM exposes csrf hidden field and request context returns authenticated HTML.\n# @POST: Helper returns True and request payload contains csrf_token plus credentials plus request options.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_uses_browser_context_request():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 200\n url = \"https://example.test/welcome/\"\n\n async def text(self):\n return \"Welcome\"\n\n class _FakeRequest:\n def __init__(self):\n self.calls = []\n\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n self.calls.append(\n {\n \"url\": url,\n \"form\": dict(form or {}),\n \"headers\": dict(headers or {}),\n \"timeout\": timeout,\n \"fail_on_status_code\": fail_on_status_code,\n \"max_redirects\": max_redirects,\n }\n )\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator(\n [\n _FakeInput(\"csrf_token\", \"csrf-123\"),\n _FakeInput(\"next\", \"/superset/welcome/\"),\n ]\n )\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n page = _FakePage()\n\n result = await service._submit_login_via_form_post(\n page, \"https://example.test/login/\"\n )\n\n assert result is True\n assert page.context.request.calls == [\n {\n \"url\": \"https://example.test/login/\",\n \"form\": {\n \"csrf_token\": \"csrf-123\",\n \"next\": \"/superset/welcome/\",\n \"username\": \"admin\",\n \"password\": \"secret\",\n },\n \"headers\": {\n \"Origin\": \"https://example.test\",\n \"Referer\": \"https://example.test/login/\",\n },\n \"timeout\": 10000,\n \"fail_on_status_code\": False,\n \"max_redirects\": 0,\n }\n ]\n\n\n# [/DEF:test_submit_login_via_form_post_uses_browser_context_request:Function]\n\n\n# [DEF:test_submit_login_via_form_post_accepts_authenticated_redirect:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must treat non-login 302 redirect as success without waiting for redirect target.\n# @PRE: Request response is 302 with Location outside login path.\n# @POST: Helper returns True.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_accepts_authenticated_redirect():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 302\n url = \"https://example.test/login/\"\n headers = {\"location\": \"/superset/welcome/\"}\n\n async def text(self):\n return \"\"\n\n class _FakeRequest:\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator([_FakeInput(\"csrf_token\", \"csrf-123\")])\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n\n result = await service._submit_login_via_form_post(\n _FakePage(), \"https://example.test/login/\"\n )\n\n assert result is True\n\n\n# [/DEF:test_submit_login_via_form_post_accepts_authenticated_redirect:Function]\n\n\n# [DEF:test_submit_login_via_form_post_rejects_login_markup_response:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must fail when POST response still contains login form content.\n# @PRE: Login DOM exposes csrf hidden field and request response renders login markup.\n# @POST: Helper returns False.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_rejects_login_markup_response():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 200\n url = \"https://example.test/login/\"\n\n async def text(self):\n return \"\"\"\n \n \n Enter your login and password below\n Username:\n Password:\n Sign in\n \n \n \"\"\"\n\n class _FakeRequest:\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator([_FakeInput(\"csrf_token\", \"csrf-123\")])\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n\n result = await service._submit_login_via_form_post(\n _FakePage(), \"https://example.test/login/\"\n )\n\n assert result is False\n\n\n# [/DEF:test_submit_login_via_form_post_rejects_login_markup_response:Function]\n\n\n# [DEF:test_goto_resilient_falls_back_from_domcontentloaded_to_load:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Pages with unstable primary wait must retry with fallback wait strategy.\n# @PRE: First page.goto call raises; second succeeds.\n# @POST: Helper returns second response and attempts both wait modes in order.\n@pytest.mark.anyio\nasync def test_goto_resilient_falls_back_from_domcontentloaded_to_load():\n class _FakePage:\n def __init__(self):\n self.calls = []\n\n async def goto(self, url, wait_until, timeout):\n self.calls.append((url, wait_until, timeout))\n if wait_until == \"domcontentloaded\":\n raise RuntimeError(\"primary wait failed\")\n return {\"ok\": True, \"url\": url, \"wait_until\": wait_until}\n\n page = _FakePage()\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n response = await service._goto_resilient(\n page,\n \"https://example.test/dashboard\",\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=1234,\n )\n\n assert response[\"ok\"] is True\n assert page.calls == [\n (\"https://example.test/dashboard\", \"domcontentloaded\", 1234),\n (\"https://example.test/dashboard\", \"load\", 1234),\n ]\n\n\n# [/DEF:test_goto_resilient_falls_back_from_domcontentloaded_to_load:Function]\n# [/DEF:TestScreenshotService:Module]\n" - }, - { - "contract_id": "test_iter_login_roots_includes_child_frames", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py", - "start_line": 12, - "end_line": 28, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returned roots include page plus child frames in order.", - "PRE": "Page exposes child frames list.", - "PURPOSE": "Login discovery must search embedded auth frames, not only the main page." - }, - "relations": [ - { - "source_id": "test_iter_login_roots_includes_child_frames", - "relation_type": "BINDS_TO", - "target_id": "TestScreenshotService", - "target_ref": "[TestScreenshotService]" - } - ], - "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": "def", - "body": "# [DEF:test_iter_login_roots_includes_child_frames:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Login discovery must search embedded auth frames, not only the main page.\n# @PRE: Page exposes child frames list.\n# @POST: Returned roots include page plus child frames in order.\ndef test_iter_login_roots_includes_child_frames():\n frame_a = object()\n frame_b = object()\n fake_page = type(\"FakePage\", (), {\"frames\": [frame_a, frame_b]})()\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n roots = service._iter_login_roots(fake_page)\n\n assert roots == [fake_page, frame_a, frame_b]\n\n\n# [/DEF:test_iter_login_roots_includes_child_frames:Function]\n" - }, - { - "contract_id": "test_response_looks_like_login_page_detects_login_markup", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py", - "start_line": 31, - "end_line": 55, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Helper returns True so caller treats fallback as failed authentication.", - "PRE": "Response body contains stable login-page markers.", - "PURPOSE": "Direct login fallback must reject responses that render the login screen again." - }, - "relations": [ - { - "source_id": "test_response_looks_like_login_page_detects_login_markup", - "relation_type": "BINDS_TO", - "target_id": "TestScreenshotService", - "target_ref": "[TestScreenshotService]" - } - ], - "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": "def", - "body": "# [DEF:test_response_looks_like_login_page_detects_login_markup:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Direct login fallback must reject responses that render the login screen again.\n# @PRE: Response body contains stable login-page markers.\n# @POST: Helper returns True so caller treats fallback as failed authentication.\ndef test_response_looks_like_login_page_detects_login_markup():\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n result = service._response_looks_like_login_page(\n \"\"\"\n \n \n

Enter your login and password below

\n \n \n \n \n \n \"\"\"\n )\n\n assert result is True\n\n\n# [/DEF:test_response_looks_like_login_page_detects_login_markup:Function]\n" - }, - { - "contract_id": "test_find_first_visible_locator_skips_hidden_first_match", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py", - "start_line": 58, - "end_line": 96, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Helper returns the second visible candidate.", - "PRE": "First matched element is hidden and second matched element is visible.", - "PURPOSE": "Locator helper must not reject a selector collection just because its first element is hidden." - }, - "relations": [ - { - "source_id": "test_find_first_visible_locator_skips_hidden_first_match", - "relation_type": "BINDS_TO", - "target_id": "TestScreenshotService", - "target_ref": "[TestScreenshotService]" - } - ], - "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": "def", - "body": "# [DEF:test_find_first_visible_locator_skips_hidden_first_match:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Locator helper must not reject a selector collection just because its first element is hidden.\n# @PRE: First matched element is hidden and second matched element is visible.\n# @POST: Helper returns the second visible candidate.\n@pytest.mark.anyio\nasync def test_find_first_visible_locator_skips_hidden_first_match():\n class _FakeElement:\n def __init__(self, visible, label):\n self._visible = visible\n self.label = label\n\n async def is_visible(self):\n return self._visible\n\n class _FakeLocator:\n def __init__(self, elements):\n self._elements = elements\n\n async def count(self):\n return len(self._elements)\n\n def nth(self, index):\n return self._elements[index]\n\n service = ScreenshotService(env=type(\"Env\", (), {})())\n hidden_then_visible = _FakeLocator(\n [\n _FakeElement(False, \"hidden\"),\n _FakeElement(True, \"visible\"),\n ]\n )\n\n result = await service._find_first_visible_locator([hidden_then_visible])\n\n assert result.label == \"visible\"\n\n\n# [/DEF:test_find_first_visible_locator_skips_hidden_first_match:Function]\n" - }, - { - "contract_id": "test_submit_login_via_form_post_uses_browser_context_request", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py", - "start_line": 99, - "end_line": 207, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Helper returns True and request payload contains csrf_token plus credentials plus request options.", - "PRE": "Login DOM exposes csrf hidden field and request context returns authenticated HTML.", - "PURPOSE": "Fallback login must submit hidden fields and credentials through the context request cookie jar." - }, - "relations": [ - { - "source_id": "test_submit_login_via_form_post_uses_browser_context_request", - "relation_type": "BINDS_TO", - "target_id": "TestScreenshotService", - "target_ref": "[TestScreenshotService]" - } - ], - "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": "def", - "body": "# [DEF:test_submit_login_via_form_post_uses_browser_context_request:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must submit hidden fields and credentials through the context request cookie jar.\n# @PRE: Login DOM exposes csrf hidden field and request context returns authenticated HTML.\n# @POST: Helper returns True and request payload contains csrf_token plus credentials plus request options.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_uses_browser_context_request():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 200\n url = \"https://example.test/welcome/\"\n\n async def text(self):\n return \"Welcome\"\n\n class _FakeRequest:\n def __init__(self):\n self.calls = []\n\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n self.calls.append(\n {\n \"url\": url,\n \"form\": dict(form or {}),\n \"headers\": dict(headers or {}),\n \"timeout\": timeout,\n \"fail_on_status_code\": fail_on_status_code,\n \"max_redirects\": max_redirects,\n }\n )\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator(\n [\n _FakeInput(\"csrf_token\", \"csrf-123\"),\n _FakeInput(\"next\", \"/superset/welcome/\"),\n ]\n )\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n page = _FakePage()\n\n result = await service._submit_login_via_form_post(\n page, \"https://example.test/login/\"\n )\n\n assert result is True\n assert page.context.request.calls == [\n {\n \"url\": \"https://example.test/login/\",\n \"form\": {\n \"csrf_token\": \"csrf-123\",\n \"next\": \"/superset/welcome/\",\n \"username\": \"admin\",\n \"password\": \"secret\",\n },\n \"headers\": {\n \"Origin\": \"https://example.test\",\n \"Referer\": \"https://example.test/login/\",\n },\n \"timeout\": 10000,\n \"fail_on_status_code\": False,\n \"max_redirects\": 0,\n }\n ]\n\n\n# [/DEF:test_submit_login_via_form_post_uses_browser_context_request:Function]\n" - }, - { - "contract_id": "test_submit_login_via_form_post_accepts_authenticated_redirect", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py", - "start_line": 210, - "end_line": 282, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Helper returns True.", - "PRE": "Request response is 302 with Location outside login path.", - "PURPOSE": "Fallback login must treat non-login 302 redirect as success without waiting for redirect target." - }, - "relations": [ - { - "source_id": "test_submit_login_via_form_post_accepts_authenticated_redirect", - "relation_type": "BINDS_TO", - "target_id": "TestScreenshotService", - "target_ref": "[TestScreenshotService]" - } - ], - "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": "def", - "body": "# [DEF:test_submit_login_via_form_post_accepts_authenticated_redirect:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must treat non-login 302 redirect as success without waiting for redirect target.\n# @PRE: Request response is 302 with Location outside login path.\n# @POST: Helper returns True.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_accepts_authenticated_redirect():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 302\n url = \"https://example.test/login/\"\n headers = {\"location\": \"/superset/welcome/\"}\n\n async def text(self):\n return \"\"\n\n class _FakeRequest:\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator([_FakeInput(\"csrf_token\", \"csrf-123\")])\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n\n result = await service._submit_login_via_form_post(\n _FakePage(), \"https://example.test/login/\"\n )\n\n assert result is True\n\n\n# [/DEF:test_submit_login_via_form_post_accepts_authenticated_redirect:Function]\n" - }, - { - "contract_id": "test_submit_login_via_form_post_rejects_login_markup_response", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py", - "start_line": 285, - "end_line": 365, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Helper returns False.", - "PRE": "Login DOM exposes csrf hidden field and request response renders login markup.", - "PURPOSE": "Fallback login must fail when POST response still contains login form content." - }, - "relations": [ - { - "source_id": "test_submit_login_via_form_post_rejects_login_markup_response", - "relation_type": "BINDS_TO", - "target_id": "TestScreenshotService", - "target_ref": "[TestScreenshotService]" - } - ], - "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": "def", - "body": "# [DEF:test_submit_login_via_form_post_rejects_login_markup_response:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Fallback login must fail when POST response still contains login form content.\n# @PRE: Login DOM exposes csrf hidden field and request response renders login markup.\n# @POST: Helper returns False.\n@pytest.mark.anyio\nasync def test_submit_login_via_form_post_rejects_login_markup_response():\n class _FakeInput:\n def __init__(self, name, value):\n self._name = name\n self._value = value\n\n async def get_attribute(self, attr_name):\n return self._name if attr_name == \"name\" else None\n\n async def input_value(self):\n return self._value\n\n class _FakeLocator:\n def __init__(self, items):\n self._items = items\n\n async def count(self):\n return len(self._items)\n\n def nth(self, index):\n return self._items[index]\n\n class _FakeResponse:\n status = 200\n url = \"https://example.test/login/\"\n\n async def text(self):\n return \"\"\"\n \n \n Enter your login and password below\n Username:\n Password:\n Sign in\n \n \n \"\"\"\n\n class _FakeRequest:\n async def post(\n self,\n url,\n form=None,\n headers=None,\n timeout=None,\n fail_on_status_code=None,\n max_redirects=None,\n ):\n return _FakeResponse()\n\n class _FakeContext:\n def __init__(self):\n self.request = _FakeRequest()\n\n class _FakePage:\n def __init__(self):\n self.frames = []\n self.context = _FakeContext()\n\n def locator(self, selector):\n if selector == \"input[type='hidden'][name]\":\n return _FakeLocator([_FakeInput(\"csrf_token\", \"csrf-123\")])\n return _FakeLocator([])\n\n env = type(\"Env\", (), {\"username\": \"admin\", \"password\": \"secret\"})()\n service = ScreenshotService(env=env)\n\n result = await service._submit_login_via_form_post(\n _FakePage(), \"https://example.test/login/\"\n )\n\n assert result is False\n\n\n# [/DEF:test_submit_login_via_form_post_rejects_login_markup_response:Function]\n" - }, - { - "contract_id": "test_goto_resilient_falls_back_from_domcontentloaded_to_load", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py", - "start_line": 368, - "end_line": 403, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Helper returns second response and attempts both wait modes in order.", - "PRE": "First page.goto call raises; second succeeds.", - "PURPOSE": "Pages with unstable primary wait must retry with fallback wait strategy." - }, - "relations": [ - { - "source_id": "test_goto_resilient_falls_back_from_domcontentloaded_to_load", - "relation_type": "BINDS_TO", - "target_id": "TestScreenshotService", - "target_ref": "[TestScreenshotService]" - } - ], - "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": "def", - "body": "# [DEF:test_goto_resilient_falls_back_from_domcontentloaded_to_load:Function]\n# @RELATION: BINDS_TO ->[TestScreenshotService]\n# @PURPOSE: Pages with unstable primary wait must retry with fallback wait strategy.\n# @PRE: First page.goto call raises; second succeeds.\n# @POST: Helper returns second response and attempts both wait modes in order.\n@pytest.mark.anyio\nasync def test_goto_resilient_falls_back_from_domcontentloaded_to_load():\n class _FakePage:\n def __init__(self):\n self.calls = []\n\n async def goto(self, url, wait_until, timeout):\n self.calls.append((url, wait_until, timeout))\n if wait_until == \"domcontentloaded\":\n raise RuntimeError(\"primary wait failed\")\n return {\"ok\": True, \"url\": url, \"wait_until\": wait_until}\n\n page = _FakePage()\n service = ScreenshotService(env=type(\"Env\", (), {})())\n\n response = await service._goto_resilient(\n page,\n \"https://example.test/dashboard\",\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=1234,\n )\n\n assert response[\"ok\"] is True\n assert page.calls == [\n (\"https://example.test/dashboard\", \"domcontentloaded\", 1234),\n (\"https://example.test/dashboard\", \"load\", 1234),\n ]\n\n\n# [/DEF:test_goto_resilient_falls_back_from_domcontentloaded_to_load:Function]\n" - }, - { - "contract_id": "TestService", - "contract_type": "Module", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_service.py", - "start_line": 1, - "end_line": 70, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results.", - "SEMANTICS": [ - "tests", - "llm-analysis", - "fallback", - "provider-error", - "unknown-status" - ] - }, - "relations": [ - { - "source_id": "TestService", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestService:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, llm-analysis, fallback, provider-error, unknown-status\n# @PURPOSE: Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results.\n\nimport pytest\n\nfrom src.plugins.llm_analysis.models import LLMProviderType\nfrom src.plugins.llm_analysis.service import LLMClient\n\n\n# [DEF:test_test_runtime_connection_uses_json_completion_transport:Function]\n# @RELATION: BINDS_TO -> TestService\n# @PURPOSE: Provider self-test must exercise the same chat completion transport as runtime analysis.\n# @PRE: get_json_completion is available on initialized client.\n# @POST: Self-test forwards a lightweight user message into get_json_completion and returns its payload.\n@pytest.mark.anyio\nasync def test_test_runtime_connection_uses_json_completion_transport(monkeypatch):\n client = LLMClient(\n provider_type=LLMProviderType.OPENROUTER,\n api_key=\"sk-test-provider-key-123456\",\n base_url=\"https://openrouter.ai/api/v1\",\n default_model=\"nvidia/nemotron-nano-12b-v2-vl:free\",\n )\n recorded = {}\n\n async def _fake_get_json_completion(messages):\n recorded[\"messages\"] = messages\n return {\"ok\": True}\n\n monkeypatch.setattr(client, \"get_json_completion\", _fake_get_json_completion)\n\n result = await client.test_runtime_connection()\n\n assert result == {\"ok\": True}\n assert recorded[\"messages\"][0][\"role\"] == \"user\"\n assert \"Return exactly this JSON object\" in recorded[\"messages\"][0][\"content\"]\n# [/DEF:test_test_runtime_connection_uses_json_completion_transport:Function]\n\n\n# [DEF:test_analyze_dashboard_provider_error_maps_to_unknown:Function]\n# @RELATION: BINDS_TO -> TestService\n# @PURPOSE: Infrastructure/provider failures must produce UNKNOWN analysis status rather than FAIL.\n# @PRE: LLMClient.get_json_completion raises provider/auth exception.\n# @POST: Returned payload uses status=UNKNOWN and issue severity UNKNOWN.\n@pytest.mark.anyio\nasync def test_analyze_dashboard_provider_error_maps_to_unknown(monkeypatch, tmp_path):\n screenshot_path = tmp_path / \"shot.jpg\"\n screenshot_path.write_bytes(b\"fake-image\")\n\n client = LLMClient(\n provider_type=LLMProviderType.OPENROUTER,\n api_key=\"sk-test-provider-key-123456\",\n base_url=\"https://openrouter.ai/api/v1\",\n default_model=\"nvidia/nemotron-nano-12b-v2-vl:free\",\n )\n\n async def _raise_provider_error(_messages):\n raise RuntimeError(\"Error code: 401 - {'error': {'message': 'User not found.', 'code': 401}}\")\n\n monkeypatch.setattr(client, \"get_json_completion\", _raise_provider_error)\n\n result = await client.analyze_dashboard(str(screenshot_path), logs=[\"line-1\"])\n\n assert result[\"status\"] == \"UNKNOWN\"\n assert \"Failed to get response from LLM\" in result[\"summary\"]\n assert result[\"issues\"][0][\"severity\"] == \"UNKNOWN\"\n# [/DEF:test_analyze_dashboard_provider_error_maps_to_unknown:Function]\n# [/DEF:TestService:Module]\n" - }, - { - "contract_id": "test_test_runtime_connection_uses_json_completion_transport", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_service.py", - "start_line": 13, - "end_line": 39, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Self-test forwards a lightweight user message into get_json_completion and returns its payload.", - "PRE": "get_json_completion is available on initialized client.", - "PURPOSE": "Provider self-test must exercise the same chat completion transport as runtime analysis." - }, - "relations": [ - { - "source_id": "test_test_runtime_connection_uses_json_completion_transport", - "relation_type": "BINDS_TO", - "target_id": "TestService", - "target_ref": "TestService" - } - ], - "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": "def", - "body": "# [DEF:test_test_runtime_connection_uses_json_completion_transport:Function]\n# @RELATION: BINDS_TO -> TestService\n# @PURPOSE: Provider self-test must exercise the same chat completion transport as runtime analysis.\n# @PRE: get_json_completion is available on initialized client.\n# @POST: Self-test forwards a lightweight user message into get_json_completion and returns its payload.\n@pytest.mark.anyio\nasync def test_test_runtime_connection_uses_json_completion_transport(monkeypatch):\n client = LLMClient(\n provider_type=LLMProviderType.OPENROUTER,\n api_key=\"sk-test-provider-key-123456\",\n base_url=\"https://openrouter.ai/api/v1\",\n default_model=\"nvidia/nemotron-nano-12b-v2-vl:free\",\n )\n recorded = {}\n\n async def _fake_get_json_completion(messages):\n recorded[\"messages\"] = messages\n return {\"ok\": True}\n\n monkeypatch.setattr(client, \"get_json_completion\", _fake_get_json_completion)\n\n result = await client.test_runtime_connection()\n\n assert result == {\"ok\": True}\n assert recorded[\"messages\"][0][\"role\"] == \"user\"\n assert \"Return exactly this JSON object\" in recorded[\"messages\"][0][\"content\"]\n# [/DEF:test_test_runtime_connection_uses_json_completion_transport:Function]\n" - }, - { - "contract_id": "test_analyze_dashboard_provider_error_maps_to_unknown", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/__tests__/test_service.py", - "start_line": 42, - "end_line": 69, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returned payload uses status=UNKNOWN and issue severity UNKNOWN.", - "PRE": "LLMClient.get_json_completion raises provider/auth exception.", - "PURPOSE": "Infrastructure/provider failures must produce UNKNOWN analysis status rather than FAIL." - }, - "relations": [ - { - "source_id": "test_analyze_dashboard_provider_error_maps_to_unknown", - "relation_type": "BINDS_TO", - "target_id": "TestService", - "target_ref": "TestService" - } - ], - "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": "def", - "body": "# [DEF:test_analyze_dashboard_provider_error_maps_to_unknown:Function]\n# @RELATION: BINDS_TO -> TestService\n# @PURPOSE: Infrastructure/provider failures must produce UNKNOWN analysis status rather than FAIL.\n# @PRE: LLMClient.get_json_completion raises provider/auth exception.\n# @POST: Returned payload uses status=UNKNOWN and issue severity UNKNOWN.\n@pytest.mark.anyio\nasync def test_analyze_dashboard_provider_error_maps_to_unknown(monkeypatch, tmp_path):\n screenshot_path = tmp_path / \"shot.jpg\"\n screenshot_path.write_bytes(b\"fake-image\")\n\n client = LLMClient(\n provider_type=LLMProviderType.OPENROUTER,\n api_key=\"sk-test-provider-key-123456\",\n base_url=\"https://openrouter.ai/api/v1\",\n default_model=\"nvidia/nemotron-nano-12b-v2-vl:free\",\n )\n\n async def _raise_provider_error(_messages):\n raise RuntimeError(\"Error code: 401 - {'error': {'message': 'User not found.', 'code': 401}}\")\n\n monkeypatch.setattr(client, \"get_json_completion\", _raise_provider_error)\n\n result = await client.analyze_dashboard(str(screenshot_path), logs=[\"line-1\"])\n\n assert result[\"status\"] == \"UNKNOWN\"\n assert \"Failed to get response from LLM\" in result[\"summary\"]\n assert result[\"issues\"][0][\"severity\"] == \"UNKNOWN\"\n# [/DEF:test_analyze_dashboard_provider_error_maps_to_unknown:Function]\n" + "anchor_syntax": "region", + "body": "# #region LLMAnalysisPackage [TYPE Module] [SEMANTICS llm, analysis, plugin, package, export]\n\n\"\"\"\nLLM Analysis Plugin for automated dashboard validation and dataset documentation.\n\"\"\"\n\nfrom .plugin import DashboardValidationPlugin, DocumentationPlugin\n\n__all__ = ['DashboardValidationPlugin', 'DocumentationPlugin']\n\n# #endregion LLMAnalysisPackage\n" }, { "contract_id": "LLMAnalysisModels", "contract_type": "Module", "file_path": "backend/src/plugins/llm_analysis/models.py", "start_line": 1, - "end_line": 81, + "end_line": 64, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", - "PURPOSE": "Define Pydantic models for LLM Analysis plugin.", - "SEMANTICS": [ - "pydantic", - "models", - "llm" - ] + "PURPOSE": "Define Pydantic models for LLM Analysis plugin." }, "relations": [ { @@ -63876,15 +50884,15 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:LLMAnalysisModels:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: pydantic, models, llm\n# @PURPOSE: Define Pydantic models for LLM Analysis plugin.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> pydantic\n# @RELATION: DEPENDS_on -> pydantic\n# @RELATION: DEPENDs_on -> pydantic\n\nfrom typing import List, Optional\nfrom pydantic import BaseModel, Field\nfrom datetime import datetime\nfrom enum import Enum\n\n# [DEF:LLMProviderType:Class]\n# @PURPOSE: Enum for supported LLM providers.\nclass LLMProviderType(str, Enum):\n OPENAI = \"openai\"\n OPENROUTER = \"openrouter\"\n KILO = \"kilo\"\n# [/DEF:LLMProviderType:Class]\n\n# [DEF:FetchModelsRequest:Class]\n# @PURPOSE: Request model to fetch available models from a provider.\nclass FetchModelsRequest(BaseModel):\n base_url: str = Field(description=\"Provider base URL (e.g. https://api.openai.com/v1)\")\n api_key: Optional[str] = Field(None, description=\"Optional API key for authenticated providers\")\n provider_type: LLMProviderType = Field(LLMProviderType.OPENAI, description=\"Provider type for auth headers\")\n provider_id: Optional[str] = Field(None, description=\"Existing provider ID — resolves api_key from DB if not provided directly\")\n# [/DEF:FetchModelsRequest:Class]\n\n# [DEF:FetchModelsResponse:Class]\n# @PURPOSE: Response with available model IDs from a provider.\nclass FetchModelsResponse(BaseModel):\n models: List[str] = Field(description=\"List of available model IDs\")\n total: int = Field(description=\"Total number of models found\")\n# [/DEF:FetchModelsResponse:Class]\n\n# [DEF:LLMProviderConfig:Class]\n# @PURPOSE: Configuration for an LLM provider.\nclass LLMProviderConfig(BaseModel):\n id: Optional[str] = None\n provider_type: LLMProviderType\n name: str\n base_url: str\n api_key: Optional[str] = None\n default_model: str\n is_active: bool = True\n# [/DEF:LLMProviderConfig:Class]\n\n# [DEF:ValidationStatus:Class]\n# @PURPOSE: Enum for dashboard validation status.\nclass ValidationStatus(str, Enum):\n PASS = \"PASS\"\n WARN = \"WARN\"\n FAIL = \"FAIL\"\n UNKNOWN = \"UNKNOWN\"\n# [/DEF:ValidationStatus:Class]\n\n# [DEF:DetectedIssue:Class]\n# @PURPOSE: Model for a single issue detected during validation.\nclass DetectedIssue(BaseModel):\n severity: ValidationStatus\n message: str\n location: Optional[str] = None\n# [/DEF:DetectedIssue:Class]\n\n# [DEF:ValidationResult:Class]\n# @PURPOSE: Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: Optional[str] = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n status: ValidationStatus\n screenshot_path: Optional[str] = None\n issues: List[DetectedIssue]\n summary: str\n raw_response: Optional[str] = None\n# [/DEF:ValidationResult:Class]\n\n# [/DEF:LLMAnalysisModels:Module]\n" + "anchor_syntax": "region", + "body": "# #region LLMAnalysisModels [C:3] [TYPE Module] [SEMANTICS pydantic, llm, plugin, model, provider-type]\n# @BRIEF Define Pydantic models for LLM Analysis plugin.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> pydantic\n# @RELATION DEPENDS_on -> pydantic\n# @RELATION DEPENDs_on -> pydantic\n\nfrom datetime import datetime\nfrom enum import Enum\n\nfrom pydantic import BaseModel, Field\n\n\n# #region LLMProviderType [TYPE Class]\n# @BRIEF Enum for supported LLM providers.\nclass LLMProviderType(str, Enum):\n OPENAI = \"openai\"\n OPENROUTER = \"openrouter\"\n KILO = \"kilo\"\n# #endregion LLMProviderType\n\n# #region LLMProviderConfig [TYPE Class]\n# @BRIEF Configuration for an LLM provider.\nclass LLMProviderConfig(BaseModel):\n id: str | None = None\n provider_type: LLMProviderType\n name: str\n base_url: str\n api_key: str | None = None\n default_model: str\n is_active: bool = True\n# #endregion LLMProviderConfig\n\n# #region ValidationStatus [TYPE Class]\n# @BRIEF Enum for dashboard validation status.\nclass ValidationStatus(str, Enum):\n PASS = \"PASS\"\n WARN = \"WARN\"\n FAIL = \"FAIL\"\n UNKNOWN = \"UNKNOWN\"\n# #endregion ValidationStatus\n\n# #region DetectedIssue [TYPE Class]\n# @BRIEF Model for a single issue detected during validation.\nclass DetectedIssue(BaseModel):\n severity: ValidationStatus\n message: str\n location: str | None = None\n# #endregion DetectedIssue\n\n# #region ValidationResult [TYPE Class]\n# @BRIEF Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: str | None = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n status: ValidationStatus\n screenshot_path: str | None = None\n issues: list[DetectedIssue]\n summary: str\n raw_response: str | None = None\n# #endregion ValidationResult\n\n# #endregion LLMAnalysisModels\n" }, { "contract_id": "LLMProviderType", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/models.py", - "start_line": 15, - "end_line": 21, + "start_line": 14, + "end_line": 20, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -63902,67 +50910,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LLMProviderType:Class]\n# @PURPOSE: Enum for supported LLM providers.\nclass LLMProviderType(str, Enum):\n OPENAI = \"openai\"\n OPENROUTER = \"openrouter\"\n KILO = \"kilo\"\n# [/DEF:LLMProviderType:Class]\n" - }, - { - "contract_id": "FetchModelsRequest", - "contract_type": "Class", - "file_path": "backend/src/plugins/llm_analysis/models.py", - "start_line": 23, - "end_line": 30, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Request model to fetch available models from a provider." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:FetchModelsRequest:Class]\n# @PURPOSE: Request model to fetch available models from a provider.\nclass FetchModelsRequest(BaseModel):\n base_url: str = Field(description=\"Provider base URL (e.g. https://api.openai.com/v1)\")\n api_key: Optional[str] = Field(None, description=\"Optional API key for authenticated providers\")\n provider_type: LLMProviderType = Field(LLMProviderType.OPENAI, description=\"Provider type for auth headers\")\n provider_id: Optional[str] = Field(None, description=\"Existing provider ID — resolves api_key from DB if not provided directly\")\n# [/DEF:FetchModelsRequest:Class]\n" - }, - { - "contract_id": "FetchModelsResponse", - "contract_type": "Class", - "file_path": "backend/src/plugins/llm_analysis/models.py", - "start_line": 32, - "end_line": 37, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Response with available model IDs from a provider." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:FetchModelsResponse:Class]\n# @PURPOSE: Response with available model IDs from a provider.\nclass FetchModelsResponse(BaseModel):\n models: List[str] = Field(description=\"List of available model IDs\")\n total: int = Field(description=\"Total number of models found\")\n# [/DEF:FetchModelsResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region LLMProviderType [TYPE Class]\n# @BRIEF Enum for supported LLM providers.\nclass LLMProviderType(str, Enum):\n OPENAI = \"openai\"\n OPENROUTER = \"openrouter\"\n KILO = \"kilo\"\n# #endregion LLMProviderType\n" }, { "contract_id": "LLMProviderConfig", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/models.py", - "start_line": 39, - "end_line": 49, + "start_line": 22, + "end_line": 32, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -63980,15 +50936,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LLMProviderConfig:Class]\n# @PURPOSE: Configuration for an LLM provider.\nclass LLMProviderConfig(BaseModel):\n id: Optional[str] = None\n provider_type: LLMProviderType\n name: str\n base_url: str\n api_key: Optional[str] = None\n default_model: str\n is_active: bool = True\n# [/DEF:LLMProviderConfig:Class]\n" + "anchor_syntax": "region", + "body": "# #region LLMProviderConfig [TYPE Class]\n# @BRIEF Configuration for an LLM provider.\nclass LLMProviderConfig(BaseModel):\n id: str | None = None\n provider_type: LLMProviderType\n name: str\n base_url: str\n api_key: str | None = None\n default_model: str\n is_active: bool = True\n# #endregion LLMProviderConfig\n" }, { "contract_id": "ValidationStatus", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/models.py", - "start_line": 51, - "end_line": 58, + "start_line": 34, + "end_line": 41, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -64006,15 +50962,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationStatus:Class]\n# @PURPOSE: Enum for dashboard validation status.\nclass ValidationStatus(str, Enum):\n PASS = \"PASS\"\n WARN = \"WARN\"\n FAIL = \"FAIL\"\n UNKNOWN = \"UNKNOWN\"\n# [/DEF:ValidationStatus:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationStatus [TYPE Class]\n# @BRIEF Enum for dashboard validation status.\nclass ValidationStatus(str, Enum):\n PASS = \"PASS\"\n WARN = \"WARN\"\n FAIL = \"FAIL\"\n UNKNOWN = \"UNKNOWN\"\n# #endregion ValidationStatus\n" }, { "contract_id": "DetectedIssue", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/models.py", - "start_line": 60, - "end_line": 66, + "start_line": 43, + "end_line": 49, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -64032,15 +50988,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DetectedIssue:Class]\n# @PURPOSE: Model for a single issue detected during validation.\nclass DetectedIssue(BaseModel):\n severity: ValidationStatus\n message: str\n location: Optional[str] = None\n# [/DEF:DetectedIssue:Class]\n" + "anchor_syntax": "region", + "body": "# #region DetectedIssue [TYPE Class]\n# @BRIEF Model for a single issue detected during validation.\nclass DetectedIssue(BaseModel):\n severity: ValidationStatus\n message: str\n location: str | None = None\n# #endregion DetectedIssue\n" }, { "contract_id": "ValidationResult", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/models.py", - "start_line": 68, - "end_line": 79, + "start_line": 51, + "end_line": 62, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -64058,11 +51014,11 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationResult:Class]\n# @PURPOSE: Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: Optional[str] = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n status: ValidationStatus\n screenshot_path: Optional[str] = None\n issues: List[DetectedIssue]\n summary: str\n raw_response: Optional[str] = None\n# [/DEF:ValidationResult:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationResult [TYPE Class]\n# @BRIEF Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: str | None = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n status: ValidationStatus\n screenshot_path: str | None = None\n issues: list[DetectedIssue]\n summary: str\n raw_response: str | None = None\n# #endregion ValidationResult\n" }, { - "contract_id": "backend/src/plugins/llm_analysis/plugin.py", + "contract_id": "LLMAnalysisPlugin", "contract_type": "Module", "file_path": "backend/src/plugins/llm_analysis/plugin.py", "start_line": 1, @@ -64070,44 +51026,38 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "All LLM interactions must be executed as asynchronous tasks.", "LAYER": "Domain", - "PURPOSE": "Implements DashboardValidationPlugin and DocumentationPlugin.", - "SEMANTICS": [ - "plugin", - "llm", - "analysis", - "documentation" - ] + "PURPOSE": "Implements DashboardValidationPlugin and DocumentationPlugin." }, "relations": [ { - "source_id": "backend/src/plugins/llm_analysis/plugin.py", + "source_id": "LLMAnalysisPlugin", "relation_type": "INHERITS", "target_id": "PluginBase", "target_ref": "[PluginBase]" }, { - "source_id": "backend/src/plugins/llm_analysis/plugin.py", + "source_id": "LLMAnalysisPlugin", "relation_type": "CALLS", "target_id": "ScreenshotService", "target_ref": "[ScreenshotService]" }, { - "source_id": "backend/src/plugins/llm_analysis/plugin.py", + "source_id": "LLMAnalysisPlugin", "relation_type": "CALLS", "target_id": "LLMClient", "target_ref": "[LLMClient]" }, { - "source_id": "backend/src/plugins/llm_analysis/plugin.py", + "source_id": "LLMAnalysisPlugin", "relation_type": "CALLS", "target_id": "LLMProviderService", "target_ref": "[LLMProviderService]" }, { - "source_id": "backend/src/plugins/llm_analysis/plugin.py", + "source_id": "LLMAnalysisPlugin", "relation_type": "USES", "target_id": "TaskContext", "target_ref": "TaskContext" @@ -64142,8 +51092,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:backend/src/plugins/llm_analysis/plugin.py:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: plugin, llm, analysis, documentation\n# @PURPOSE: 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\nfrom typing import Dict, Any, Optional\nimport os\nimport json\nfrom datetime import datetime, timedelta\nfrom ...core.plugin_base import PluginBase\nfrom ...core.logger import belief_scope, logger\nfrom ...core.database import SessionLocal\nfrom ...services.llm_provider import LLMProviderService\nfrom ...core.superset_client import SupersetClient\nfrom .service import ScreenshotService, LLMClient\nfrom .models import LLMProviderType, ValidationStatus, ValidationResult, DetectedIssue\nfrom ...models.llm import ValidationRecord, ValidationPolicy\nfrom ...core.task_manager.context import TaskContext\nfrom ...services.notifications.service import NotificationService\nfrom ...services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n is_multimodal_model,\n normalize_llm_settings,\n render_prompt,\n)\n\n# [DEF:_is_masked_or_invalid_api_key:Function]\n# @PURPOSE: 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: Optional[str]) -> 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# [/DEF:_is_masked_or_invalid_api_key:Function]\n\n# [DEF:_json_safe_value:Function]\n# @PURPOSE: 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# [/DEF:_json_safe_value:Function]\n\n# [DEF:DashboardValidationPlugin:Class]\n# @PURPOSE: 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 # [DEF:DashboardValidationPlugin.execute: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: Optional[TaskContext] = 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: {str(e)}\"]\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 # [/DEF:DashboardValidationPlugin.execute:Function]\n# [/DEF:DashboardValidationPlugin:Class]\n\n# [DEF:DocumentationPlugin:Class]\n# @PURPOSE: 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 # [DEF:DocumentationPlugin.execute: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: Optional[TaskContext] = 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 # [/DEF:DocumentationPlugin.execute:Function]\n# [/DEF:DocumentationPlugin:Class]\n\n# [/DEF:backend/src/plugins/llm_analysis/plugin.py:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_is_masked_or_invalid_api_key", @@ -64188,8 +51138,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_is_masked_or_invalid_api_key:Function]\n# @PURPOSE: 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: Optional[str]) -> 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# [/DEF:_is_masked_or_invalid_api_key:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_json_safe_value", @@ -64234,8 +51184,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_json_safe_value:Function]\n# @PURPOSE: 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# [/DEF:_json_safe_value:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DashboardValidationPlugin", @@ -64276,71 +51226,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardValidationPlugin:Class]\n# @PURPOSE: 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 # [DEF:DashboardValidationPlugin.execute: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: Optional[TaskContext] = 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: {str(e)}\"]\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 # [/DEF:DashboardValidationPlugin.execute:Function]\n# [/DEF:DashboardValidationPlugin:Class]\n" - }, - { - "contract_id": "DashboardValidationPlugin.execute", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/plugin.py", - "start_line": 93, - "end_line": 322, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "context (Optional[TaskContext]) - Task context for logging with source attribution.", - "POST": "Returns a dictionary with validation results and persists them to the database.", - "PRE": "params contains dashboard_id, environment_id, and provider_id.", - "PURPOSE": "Executes the dashboard validation task with TaskContext support.", - "SIDE_EFFECT": "Captures a screenshot, calls LLM API, and writes to the database." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:DashboardValidationPlugin.execute: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: Optional[TaskContext] = 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: {str(e)}\"]\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 # [/DEF:DashboardValidationPlugin.execute:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "DocumentationPlugin", @@ -64381,123 +51268,48 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DocumentationPlugin:Class]\n# @PURPOSE: 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 # [DEF:DocumentationPlugin.execute: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: Optional[TaskContext] = 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 # [/DEF:DocumentationPlugin.execute:Function]\n# [/DEF:DocumentationPlugin:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { - "contract_id": "DocumentationPlugin.execute", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/plugin.py", - "start_line": 356, - "end_line": 478, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "context (Optional[TaskContext]) - Task context for logging with source attribution.", - "POST": "Returns generated documentation and updates the dataset in Superset.", - "PRE": "params contains dataset_id, environment_id, and provider_id.", - "PURPOSE": "Executes the dataset documentation task with TaskContext support.", - "SIDE_EFFECT": "Calls LLM API and updates dataset metadata in Superset." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:DocumentationPlugin.execute: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: Optional[TaskContext] = 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 # [/DEF:DocumentationPlugin.execute:Function]\n" - }, - { - "contract_id": "backend/src/plugins/llm_analysis/scheduler.py", + "contract_id": "LLMAnalysisScheduler", "contract_type": "Module", "file_path": "backend/src/plugins/llm_analysis/scheduler.py", "start_line": 1, - "end_line": 62, + "end_line": 57, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", - "PURPOSE": "Provides helper functions to schedule LLM-based validation tasks.", - "SEMANTICS": [ - "scheduler", - "task", - "automation" - ] + "PURPOSE": "Provides helper functions to schedule LLM-based validation tasks." }, "relations": [ { - "source_id": "backend/src/plugins/llm_analysis/scheduler.py", + "source_id": "LLMAnalysisScheduler", "relation_type": "DEPENDS_ON", "target_id": "SchedulerService", "target_ref": "[SchedulerService]" } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:backend/src/plugins/llm_analysis/scheduler.py:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: scheduler, task, automation\n# @PURPOSE: Provides helper functions to schedule LLM-based validation tasks.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [SchedulerService]\n\nfrom typing import Dict, Any\nfrom ...dependencies import get_task_manager, get_scheduler_service\nfrom ...core.logger import belief_scope, logger\n\n# [DEF:schedule_dashboard_validation:Function]\n# @PURPOSE: Schedules a recurring dashboard validation task.\n# @PARAM: dashboard_id (str) - ID of the dashboard to validate.\n# @PARAM: cron_expression (str) - Standard cron expression for scheduling.\n# @PARAM: params (Dict[str, Any]) - Task parameters (environment_id, provider_id).\n# @SIDE_EFFECT: Adds a job to the scheduler service.\ndef schedule_dashboard_validation(dashboard_id: str, cron_expression: str, params: Dict[str, Any]):\n with belief_scope(\"schedule_dashboard_validation\", f\"dashboard_id={dashboard_id}\"):\n scheduler = get_scheduler_service()\n task_manager = get_task_manager()\n \n job_id = f\"llm_val_{dashboard_id}\"\n \n async def job_func():\n await task_manager.create_task(\n plugin_id=\"llm_dashboard_validation\",\n params={\n \"dashboard_id\": dashboard_id,\n **params\n }\n )\n\n scheduler.add_job(\n job_func,\n \"cron\",\n id=job_id,\n replace_existing=True,\n **_parse_cron(cron_expression)\n )\n logger.info(f\"Scheduled validation for dashboard {dashboard_id} with cron {cron_expression}\")\n# [/DEF:schedule_dashboard_validation:Function]\n\n# [DEF:_parse_cron:Function]\n# @PURPOSE: Basic cron parser placeholder.\n# @PARAM: cron (str) - Cron expression.\n# @RETURN: Dict[str, str] - Parsed cron parts.\ndef _parse_cron(cron: str) -> Dict[str, str]:\n # Basic cron parser placeholder\n parts = cron.split()\n if len(parts) != 5:\n return {}\n return {\n \"minute\": parts[0],\n \"hour\": parts[1],\n \"day\": parts[2],\n \"month\": parts[3],\n \"day_of_week\": parts[4]\n }\n# [/DEF:_parse_cron:Function]\n\n# [/DEF:backend/src/plugins/llm_analysis/scheduler.py:Module]\n" + "anchor_syntax": "region", + "body": "# #region LLMAnalysisScheduler [C:3] [TYPE Module] [SEMANTICS scheduler, llm, validation, task, cron]\n# @BRIEF Provides helper functions to schedule LLM-based validation tasks.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [SchedulerService]\n\nfrom typing import Any\n\nfrom ...core.logger import belief_scope, logger\nfrom ...dependencies import get_scheduler_service, get_task_manager\n\n\n# #region schedule_dashboard_validation [TYPE Function]\n# @BRIEF Schedules a recurring dashboard validation task.\n# @SIDE_EFFECT: Adds a job to the scheduler service.\ndef schedule_dashboard_validation(dashboard_id: str, cron_expression: str, params: dict[str, Any]):\n with belief_scope(\"schedule_dashboard_validation\", f\"dashboard_id={dashboard_id}\"):\n scheduler = get_scheduler_service()\n task_manager = get_task_manager()\n\n job_id = f\"llm_val_{dashboard_id}\"\n\n async def job_func():\n await task_manager.create_task(\n plugin_id=\"llm_dashboard_validation\",\n params={\n \"dashboard_id\": dashboard_id,\n **params\n }\n )\n\n scheduler.add_job(\n job_func,\n \"cron\",\n id=job_id,\n replace_existing=True,\n **_parse_cron(cron_expression)\n )\n logger.info(f\"Scheduled validation for dashboard {dashboard_id} with cron {cron_expression}\")\n# #endregion schedule_dashboard_validation\n\n# #region _parse_cron [TYPE Function]\n# @BRIEF Basic cron parser placeholder.\ndef _parse_cron(cron: str) -> dict[str, str]:\n # Basic cron parser placeholder\n parts = cron.split()\n if len(parts) != 5:\n return {}\n return {\n \"minute\": parts[0],\n \"hour\": parts[1],\n \"day\": parts[2],\n \"month\": parts[3],\n \"day_of_week\": parts[4]\n }\n# #endregion _parse_cron\n\n# #endregion LLMAnalysisScheduler\n" }, { "contract_id": "schedule_dashboard_validation", "contract_type": "Function", "file_path": "backend/src/plugins/llm_analysis/scheduler.py", "start_line": 12, - "end_line": 42, + "end_line": 39, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "params (Dict[str, Any]) - Task parameters (environment_id, provider_id).", "PURPOSE": "Schedules a recurring dashboard validation task.", "SIDE_EFFECT": "Adds a job to the scheduler service." }, "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": "PURPOSE", @@ -64517,30 +51329,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:schedule_dashboard_validation:Function]\n# @PURPOSE: Schedules a recurring dashboard validation task.\n# @PARAM: dashboard_id (str) - ID of the dashboard to validate.\n# @PARAM: cron_expression (str) - Standard cron expression for scheduling.\n# @PARAM: params (Dict[str, Any]) - Task parameters (environment_id, provider_id).\n# @SIDE_EFFECT: Adds a job to the scheduler service.\ndef schedule_dashboard_validation(dashboard_id: str, cron_expression: str, params: Dict[str, Any]):\n with belief_scope(\"schedule_dashboard_validation\", f\"dashboard_id={dashboard_id}\"):\n scheduler = get_scheduler_service()\n task_manager = get_task_manager()\n \n job_id = f\"llm_val_{dashboard_id}\"\n \n async def job_func():\n await task_manager.create_task(\n plugin_id=\"llm_dashboard_validation\",\n params={\n \"dashboard_id\": dashboard_id,\n **params\n }\n )\n\n scheduler.add_job(\n job_func,\n \"cron\",\n id=job_id,\n replace_existing=True,\n **_parse_cron(cron_expression)\n )\n logger.info(f\"Scheduled validation for dashboard {dashboard_id} with cron {cron_expression}\")\n# [/DEF:schedule_dashboard_validation:Function]\n" + "anchor_syntax": "region", + "body": "# #region schedule_dashboard_validation [TYPE Function]\n# @BRIEF Schedules a recurring dashboard validation task.\n# @SIDE_EFFECT: Adds a job to the scheduler service.\ndef schedule_dashboard_validation(dashboard_id: str, cron_expression: str, params: dict[str, Any]):\n with belief_scope(\"schedule_dashboard_validation\", f\"dashboard_id={dashboard_id}\"):\n scheduler = get_scheduler_service()\n task_manager = get_task_manager()\n\n job_id = f\"llm_val_{dashboard_id}\"\n\n async def job_func():\n await task_manager.create_task(\n plugin_id=\"llm_dashboard_validation\",\n params={\n \"dashboard_id\": dashboard_id,\n **params\n }\n )\n\n scheduler.add_job(\n job_func,\n \"cron\",\n id=job_id,\n replace_existing=True,\n **_parse_cron(cron_expression)\n )\n logger.info(f\"Scheduled validation for dashboard {dashboard_id} with cron {cron_expression}\")\n# #endregion schedule_dashboard_validation\n" }, { "contract_id": "_parse_cron", "contract_type": "Function", "file_path": "backend/src/plugins/llm_analysis/scheduler.py", - "start_line": 44, - "end_line": 60, + "start_line": 41, + "end_line": 55, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "cron (str) - Cron expression.", - "PURPOSE": "Basic cron parser placeholder.", - "RETURN": "Dict[str, str] - Parsed cron parts." + "PURPOSE": "Basic cron parser placeholder." }, "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": "PURPOSE", @@ -64549,53 +51353,40 @@ "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": "def", - "body": "# [DEF:_parse_cron:Function]\n# @PURPOSE: Basic cron parser placeholder.\n# @PARAM: cron (str) - Cron expression.\n# @RETURN: Dict[str, str] - Parsed cron parts.\ndef _parse_cron(cron: str) -> Dict[str, str]:\n # Basic cron parser placeholder\n parts = cron.split()\n if len(parts) != 5:\n return {}\n return {\n \"minute\": parts[0],\n \"hour\": parts[1],\n \"day\": parts[2],\n \"month\": parts[3],\n \"day_of_week\": parts[4]\n }\n# [/DEF:_parse_cron:Function]\n" + "anchor_syntax": "region", + "body": "# #region _parse_cron [TYPE Function]\n# @BRIEF Basic cron parser placeholder.\ndef _parse_cron(cron: str) -> dict[str, str]:\n # Basic cron parser placeholder\n parts = cron.split()\n if len(parts) != 5:\n return {}\n return {\n \"minute\": parts[0],\n \"hour\": parts[1],\n \"day\": parts[2],\n \"month\": parts[3],\n \"day_of_week\": parts[4]\n }\n# #endregion _parse_cron\n" }, { - "contract_id": "backend/src/plugins/llm_analysis/service.py", + "contract_id": "LLMAnalysisService", "contract_type": "Module", "file_path": "backend/src/plugins/llm_analysis/service.py", "start_line": 1, - "end_line": 953, + "end_line": 978, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Screenshots must be 1920px width and capture full page height.", "LAYER": "Domain", - "PURPOSE": "Services for LLM interaction and dashboard screenshots.", - "SEMANTICS": [ - "service", - "llm", - "screenshot", - "playwright", - "openai" - ] + "PURPOSE": "Services for LLM interaction and dashboard screenshots." }, "relations": [ { - "source_id": "backend/src/plugins/llm_analysis/service.py", + "source_id": "LLMAnalysisService", "relation_type": "DEPENDS_ON", "target_id": "playwright", "target_ref": "playwright" }, { - "source_id": "backend/src/plugins/llm_analysis/service.py", + "source_id": "LLMAnalysisService", "relation_type": "DEPENDS_ON", "target_id": "openai", "target_ref": "openai" }, { - "source_id": "backend/src/plugins/llm_analysis/service.py", + "source_id": "LLMAnalysisService", "relation_type": "DEPENDS_ON", "target_id": "tenacity", "target_ref": "tenacity" @@ -64612,15 +51403,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:backend/src/plugins/llm_analysis/service.py:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: service, llm, screenshot, playwright, openai\n# @PURPOSE: 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 json\nimport io\nimport os\nfrom urllib.parse import urlsplit\nfrom typing import List, Dict, Any\nimport httpx\nfrom PIL import Image\nfrom playwright.async_api import async_playwright\nfrom openai import AsyncOpenAI, RateLimitError, AuthenticationError as OpenAIAuthenticationError\nfrom tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception\nfrom .models import LLMProviderType\nfrom ...core.logger import belief_scope, logger\nfrom ...core.config_models import Environment\nfrom ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt\n\n# [DEF:ScreenshotService:Class]\n# @PURPOSE: Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # [DEF:ScreenshotService.__init__:Function]\n # @PURPOSE: Initializes the ScreenshotService with environment configuration.\n # @PRE: env is a valid Environment object.\n def __init__(self, env: Environment):\n self.env = env\n # [/DEF:ScreenshotService.__init__:Function]\n\n # [DEF:ScreenshotService._find_first_visible_locator:Function]\n # @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies.\n # @PRE: candidates is a non-empty list of locator-like objects.\n # @POST: Returns a locator ready for interaction or None when nothing matches.\n async def _find_first_visible_locator(self, candidates) -> Any:\n for locator in candidates:\n try:\n match_count = await locator.count()\n for index in range(match_count):\n candidate = locator.nth(index)\n if await candidate.is_visible():\n return candidate\n except Exception:\n continue\n return None\n # [/DEF:ScreenshotService._find_first_visible_locator:Function]\n\n # [DEF:ScreenshotService._iter_login_roots:Function]\n # @PURPOSE: Enumerate page and child frames where login controls may be rendered.\n # @PRE: page is a Playwright page-like object.\n # @POST: Returns ordered roots starting with main page followed by frames.\n def _iter_login_roots(self, page) -> List[Any]:\n roots = [page]\n page_frames = getattr(page, \"frames\", [])\n try:\n for frame in page_frames:\n if frame not in roots:\n roots.append(frame)\n except Exception:\n pass\n return roots\n # [/DEF:ScreenshotService._iter_login_roots:Function]\n\n # [DEF:ScreenshotService._extract_hidden_login_fields:Function]\n # @PURPOSE: Collect hidden form fields required for direct login POST fallback.\n # @PRE: Login page is loaded.\n # @POST: Returns hidden input name/value mapping aggregated from page and child frames.\n async def _extract_hidden_login_fields(self, page) -> Dict[str, str]:\n hidden_fields: Dict[str, str] = {}\n for root in self._iter_login_roots(page):\n try:\n locator = root.locator(\"input[type='hidden'][name]\")\n count = await locator.count()\n for index in range(count):\n candidate = locator.nth(index)\n field_name = str(await candidate.get_attribute(\"name\") or \"\").strip()\n if not field_name or field_name in hidden_fields:\n continue\n hidden_fields[field_name] = str(await candidate.input_value()).strip()\n except Exception:\n continue\n return hidden_fields\n # [/DEF:ScreenshotService._extract_hidden_login_fields:Function]\n\n # [DEF:ScreenshotService._extract_csrf_token:Function]\n # @PURPOSE: Resolve CSRF token value from main page or embedded login frame.\n # @PRE: Login page is loaded.\n # @POST: Returns first non-empty csrf token or empty string.\n async def _extract_csrf_token(self, page) -> str:\n hidden_fields = await self._extract_hidden_login_fields(page)\n return str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n # [/DEF:ScreenshotService._extract_csrf_token:Function]\n\n # [DEF:ScreenshotService._response_looks_like_login_page:Function]\n # @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page.\n # @PRE: response_text is normalized HTML or text from login POST response.\n # @POST: Returns True when login-page markers dominate the response body.\n def _response_looks_like_login_page(self, response_text: str) -> bool:\n normalized = str(response_text or \"\").strip().lower()\n if not normalized:\n return False\n\n markers = [\n \"enter your login and password below\",\n \"username:\",\n \"password:\",\n \"sign in\",\n 'name=\"csrf_token\"',\n ]\n return sum(marker in normalized for marker in markers) >= 3\n # [/DEF:ScreenshotService._response_looks_like_login_page:Function]\n\n # [DEF:ScreenshotService._redirect_looks_authenticated:Function]\n # @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target.\n # @PRE: redirect_location may be empty or relative.\n # @POST: Returns True when redirect target does not point back to login flow.\n def _redirect_looks_authenticated(self, redirect_location: str) -> bool:\n normalized = str(redirect_location or \"\").strip().lower()\n if not normalized:\n return True\n return \"/login\" not in normalized\n # [/DEF:ScreenshotService._redirect_looks_authenticated:Function]\n\n # [DEF:ScreenshotService._submit_login_via_form_post:Function]\n # @PURPOSE: Fallback login path that submits credentials directly with csrf token.\n # @PRE: login_url is same-origin and csrf token can be read from DOM.\n # @POST: Browser context receives authenticated cookies when login succeeds.\n async def _submit_login_via_form_post(self, page, login_url: str) -> bool:\n hidden_fields = await self._extract_hidden_login_fields(page)\n csrf_token = str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n if not csrf_token:\n logger.warning(\"[DEBUG] Direct form login fallback skipped: csrf_token not found\")\n return False\n\n try:\n request_context = page.context.request\n except Exception as context_error:\n logger.warning(f\"[DEBUG] Direct form login fallback skipped: request context unavailable: {context_error}\")\n return False\n\n parsed_url = urlsplit(login_url)\n origin = f\"{parsed_url.scheme}://{parsed_url.netloc}\" if parsed_url.scheme and parsed_url.netloc else login_url\n payload = dict(hidden_fields)\n payload[\"username\"] = self.env.username\n payload[\"password\"] = self.env.password\n logger.info(\n f\"[DEBUG] Attempting direct form login fallback via browser context request with hidden fields: {sorted(hidden_fields.keys())}\"\n )\n\n response = await request_context.post(\n login_url,\n form=payload,\n headers={\n \"Origin\": origin,\n \"Referer\": login_url,\n },\n timeout=10000,\n fail_on_status_code=False,\n max_redirects=0,\n )\n response_url = str(getattr(response, \"url\", \"\") or \"\")\n response_status = int(getattr(response, \"status\", 0) or 0)\n response_headers = dict(getattr(response, \"headers\", {}) or {})\n redirect_location = str(\n response_headers.get(\"location\")\n or response_headers.get(\"Location\")\n or \"\"\n ).strip()\n redirect_statuses = {301, 302, 303, 307, 308}\n if response_status in redirect_statuses:\n redirect_authenticated = self._redirect_looks_authenticated(redirect_location)\n logger.info(\n f\"[DEBUG] Direct form login fallback redirect response: status={response_status} url={response_url} location={redirect_location!r} authenticated={redirect_authenticated}\"\n )\n return redirect_authenticated\n\n response_text = await response.text()\n text_snippet = \" \".join(response_text.split())[:200]\n looks_like_login_page = self._response_looks_like_login_page(response_text)\n logger.info(\n f\"[DEBUG] Direct form login fallback response: status={response_status} url={response_url} login_markup={looks_like_login_page} snippet={text_snippet!r}\"\n )\n return not looks_like_login_page\n # [/DEF:ScreenshotService._submit_login_via_form_post:Function]\n\n # [DEF:ScreenshotService._find_login_field_locator:Function]\n # @PURPOSE: Resolve login form input using semantic label text plus generic visible-input fallbacks.\n # @PRE: field_name is `username` or `password`.\n # @POST: Returns a locator for the corresponding input or None.\n async def _find_login_field_locator(self, page, field_name: str) -> Any:\n normalized = str(field_name or \"\").strip().lower()\n for root in self._iter_login_roots(page):\n if normalized == \"username\":\n input_candidates = [\n root.get_by_label(\"Username\", exact=False),\n root.get_by_label(\"Login\", exact=False),\n root.locator(\"label:text-matches('Username|Login', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Username|Login/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='username']\"),\n root.locator(\"input#username\"),\n root.locator(\"input[placeholder*='Username']\"),\n root.locator(\"input[type='text']\"),\n root.locator(\"input:not([type='password'])\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n if normalized == \"password\":\n input_candidates = [\n root.get_by_label(\"Password\", exact=False),\n root.locator(\"label:text-matches('Password', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Password/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='password']\"),\n root.locator(\"input#password\"),\n root.locator(\"input[placeholder*='Password']\"),\n root.locator(\"input[type='password']\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n return None\n # [/DEF:ScreenshotService._find_login_field_locator:Function]\n\n # [DEF:ScreenshotService._find_submit_locator:Function]\n # @PURPOSE: Resolve login submit button from main page or embedded auth frame.\n # @PRE: page is ready for login interaction.\n # @POST: Returns visible submit locator or None.\n async def _find_submit_locator(self, page) -> Any:\n selectors = [\n lambda root: root.get_by_role(\"button\", name=\"Sign in\", exact=False),\n lambda root: root.get_by_role(\"button\", name=\"Login\", exact=False),\n lambda root: root.locator(\"button[type='submit']\"),\n lambda root: root.locator(\"button#submit\"),\n lambda root: root.locator(\".btn-primary\"),\n lambda root: root.locator(\"input[type='submit']\"),\n ]\n for root in self._iter_login_roots(page):\n locator = await self._find_first_visible_locator([factory(root) for factory in selectors])\n if locator:\n return locator\n return None\n # [/DEF:ScreenshotService._find_submit_locator:Function]\n\n # [DEF:ScreenshotService._goto_resilient:Function]\n # @PURPOSE: Navigate without relying on networkidle for pages with long-polling or persistent requests.\n # @PRE: page is a valid Playwright page and url is non-empty.\n # @POST: Returns last navigation response or raises when both primary and fallback waits fail.\n async def _goto_resilient(\n self,\n page,\n url: str,\n primary_wait_until: str = \"domcontentloaded\",\n fallback_wait_until: str = \"load\",\n timeout: int = 60000,\n ):\n try:\n return await page.goto(url, wait_until=primary_wait_until, timeout=timeout)\n except Exception as primary_error:\n logger.warning(\n f\"[ScreenshotService._goto_resilient] Primary navigation wait '{primary_wait_until}' failed for {url}: {primary_error}\"\n )\n return await page.goto(url, wait_until=fallback_wait_until, timeout=timeout)\n # [/DEF:ScreenshotService._goto_resilient:Function]\n\n # [DEF:ScreenshotService.capture_dashboard:Function]\n # @PURPOSE: Captures a full-page screenshot of a dashboard using Playwright and CDP.\n # @PRE: dashboard_id is a valid string, output_path is a writable path.\n # @POST: Returns True if screenshot is saved successfully.\n # @SIDE_EFFECT: Launches a browser, performs UI login, switches tabs, and writes a PNG file.\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n with belief_scope(\"capture_dashboard\", f\"dashboard_id={dashboard_id}\"):\n logger.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n async with async_playwright() as p:\n browser = await p.chromium.launch(\n headless=True,\n args=[\n \"--disable-blink-features=AutomationControlled\",\n \"--disable-infobars\",\n \"--no-sandbox\"\n ]\n )\n # Set a realistic user agent to avoid 403 Forbidden from OpenResty/WAF\n user_agent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"\n # Construct base UI URL from environment (strip /api/v1 suffix)\n base_ui_url = self.env.url.rstrip(\"/\")\n if base_ui_url.endswith(\"/api/v1\"):\n base_ui_url = base_ui_url[:-len(\"/api/v1\")]\n \n # Create browser context with realistic headers\n context = await browser.new_context(\n viewport={'width': 1280, 'height': 720},\n user_agent=user_agent,\n extra_http_headers={\n \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n \"Accept-Language\": \"ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7\",\n \"Upgrade-Insecure-Requests\": \"1\",\n \"Sec-Fetch-Dest\": \"document\",\n \"Sec-Fetch-Mode\": \"navigate\",\n \"Sec-Fetch-Site\": \"none\",\n \"Sec-Fetch-User\": \"?1\"\n }\n )\n logger.info(\"Browser context created successfully\")\n\n page = await context.new_page()\n # Bypass navigator.webdriver detection\n await page.add_init_script(\"delete Object.getPrototypeOf(navigator).webdriver\")\n\n # 1. Navigate to login page and authenticate\n login_url = f\"{base_ui_url.rstrip('/')}/login/\"\n logger.info(f\"[DEBUG] Navigating to login page: {login_url}\")\n \n response = await self._goto_resilient(\n page,\n login_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n if response:\n logger.info(f\"[DEBUG] Login page response status: {response.status}\")\n \n # Wait for login form to be ready\n await page.wait_for_load_state(\"domcontentloaded\")\n \n # More exhaustive list of selectors for various Superset versions/themes\n selectors = {\n \"username\": ['input[name=\"username\"]', 'input#username', 'input[placeholder*=\"Username\"]', 'input[type=\"text\"]'],\n \"password\": ['input[name=\"password\"]', 'input#password', 'input[placeholder*=\"Password\"]', 'input[type=\"password\"]'],\n \"submit\": ['button[type=\"submit\"]', 'button#submit', '.btn-primary', 'input[type=\"submit\"]']\n }\n logger.info(\"[DEBUG] Attempting to find login form elements...\")\n \n try:\n used_direct_form_login = False\n # Find and fill username\n username_locator = await self._find_login_field_locator(page, \"username\")\n\n if not username_locator:\n roots = self._iter_login_roots(page)\n logger.info(f\"[DEBUG] Found {len(roots)} login roots including child frames\")\n for root_index, root in enumerate(roots[:5]):\n all_inputs = await root.locator('input').all()\n logger.info(f\"[DEBUG] Root {root_index}: found {len(all_inputs)} input fields\")\n for i, inp in enumerate(all_inputs[:5]): # Log first 5\n inp_type = await inp.get_attribute('type')\n inp_name = await inp.get_attribute('name')\n inp_id = await inp.get_attribute('id')\n logger.info(f\"[DEBUG] Root {root_index} input {i}: type={inp_type}, name={inp_name}, id={inp_id}\")\n used_direct_form_login = await self._submit_login_via_form_post(page, login_url)\n if not used_direct_form_login:\n raise RuntimeError(\"Could not find username input field on login page\")\n username_locator = None\n \n if username_locator is not None:\n logger.info(\"[DEBUG] Filling username field\")\n await username_locator.fill(self.env.username)\n \n # Find and fill password\n password_locator = await self._find_login_field_locator(page, \"password\") if username_locator is not None else None\n\n if username_locator is not None and not password_locator:\n raise RuntimeError(\"Could not find password input field on login page\")\n \n if password_locator is not None:\n logger.info(\"[DEBUG] Filling password field\")\n await password_locator.fill(self.env.password)\n \n # Click submit\n submit_locator = await self._find_submit_locator(page) if username_locator is not None else None\n\n if username_locator is not None and not submit_locator:\n raise RuntimeError(\"Could not find submit button on login page\")\n\n if submit_locator is not None:\n logger.info(\"[DEBUG] Clicking submit button\")\n await submit_locator.click()\n \n # Wait for navigation after login\n if not used_direct_form_login:\n try:\n await page.wait_for_load_state(\"load\", timeout=30000)\n except Exception as load_wait_error:\n logger.warning(f\"[DEBUG] Login post-submit load wait timed out: {load_wait_error}\")\n \n # Check if login was successful\n if not used_direct_form_login and \"/login\" in page.url:\n # Check for error messages on page\n error_msg = await page.locator(\".alert-danger, .error-message\").text_content() if await page.locator(\".alert-danger, .error-message\").count() > 0 else \"Unknown error\"\n logger.error(f\"[DEBUG] Login failed. Still on login page. Error: {error_msg}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {error_msg}. Debug screenshot saved to {debug_path}\")\n \n logger.info(f\"[DEBUG] Login successful. Current URL: {page.url}\")\n \n # Check cookies after successful login\n page_cookies = await context.cookies()\n logger.info(f\"[DEBUG] Cookies after login: {len(page_cookies)}\")\n for c in page_cookies:\n logger.info(f\"[DEBUG] Cookie: name={c['name']}, domain={c['domain']}, value={c.get('value', '')[:20]}...\")\n \n except Exception as e:\n page_title = await page.title()\n logger.error(f\"UI Login failed. Page title: {page_title}, URL: {page.url}, Error: {str(e)}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {str(e)}. Debug screenshot saved to {debug_path}\")\n\n # 2. Navigate to dashboard\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n dashboard_url = f\"{base_ui_url.rstrip('/')}/superset/dashboard/{dashboard_id}/?standalone=true\"\n \n if base_ui_url.startswith(\"https://\") and dashboard_url.startswith(\"http://\"):\n dashboard_url = dashboard_url.replace(\"http://\", \"https://\")\n\n logger.info(f\"[DEBUG] Navigating to dashboard: {dashboard_url}\")\n \n # Dashboard pages can keep polling/network activity open indefinitely.\n response = await self._goto_resilient(\n page,\n dashboard_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n \n if response:\n logger.info(f\"[DEBUG] Dashboard navigation response status: {response.status}, URL: {response.url}\")\n\n if \"/login\" in page.url:\n debug_path = output_path.replace(\".png\", \"_debug_failed_dashboard_auth.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(\n f\"Dashboard navigation redirected to login page after authentication. Debug screenshot saved to {debug_path}\"\n )\n \n try:\n # Wait for the dashboard grid to be present\n await page.wait_for_selector('.dashboard-component, .dashboard-header, [data-test=\"dashboard-grid\"]', timeout=30000)\n logger.info(\"[DEBUG] Dashboard container loaded\")\n \n # Wait for charts to finish loading (Superset uses loading spinners/skeletons)\n # We wait until loading indicators disappear or a timeout occurs\n try:\n # Wait for loading indicators to disappear\n await page.wait_for_selector('.loading, .ant-skeleton, .spinner', state=\"hidden\", timeout=60000)\n logger.info(\"[DEBUG] Loading indicators hidden\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for loading indicators to hide\")\n\n # Wait for charts to actually render their content (e.g., ECharts, NVD3)\n # We look for common chart containers that should have content\n try:\n await page.wait_for_selector('.chart-container canvas, .slice_container svg, .superset-chart-canvas, .grid-content .chart-container', timeout=60000)\n logger.info(\"[DEBUG] Chart content detected\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for chart content\")\n\n # Additional check: wait for all chart containers to have non-empty content\n logger.info(\"[DEBUG] Waiting for all charts to have rendered content...\")\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container, .slice_container');\n if (charts.length === 0) return true; // No charts to wait for\n \n // Check if all charts have rendered content (canvas, svg, or non-empty div)\n return Array.from(charts).every(chart => {\n const hasCanvas = chart.querySelector('canvas') !== null;\n const hasSvg = chart.querySelector('svg') !== null;\n const hasContent = chart.innerText.trim().length > 0 || chart.children.length > 0;\n return hasCanvas || hasSvg || hasContent;\n });\n }\"\"\", timeout=60000)\n logger.info(\"[DEBUG] All charts have rendered content\")\n\n # Scroll to bottom and back to top to trigger lazy loading of all charts\n logger.info(\"[DEBUG] Scrolling to trigger lazy loading...\")\n await page.evaluate(\"\"\"async () => {\n const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n for (let i = 0; i < document.body.scrollHeight; i += 500) {\n window.scrollTo(0, i);\n await delay(100);\n }\n window.scrollTo(0, 0);\n await delay(500);\n }\"\"\")\n\n except Exception as e:\n logger.warning(f\"[DEBUG] Dashboard content wait failed: {e}, proceeding anyway after delay\")\n \n # Final stabilization delay - increased for complex dashboards\n logger.info(\"[DEBUG] Final stabilization delay...\")\n await asyncio.sleep(15)\n\n # Logic to handle tabs and full-page capture\n try:\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n processed_tabs = set()\n\n async def switch_tabs(depth=0):\n if depth > 3:\n return # Limit recursion depth\n \n tab_selectors = [\n '.ant-tabs-nav-list .ant-tabs-tab',\n '.dashboard-component-tabs .ant-tabs-tab',\n '[data-test=\"dashboard-component-tabs\"] .ant-tabs-tab'\n ]\n \n found_tabs = []\n for selector in tab_selectors:\n found_tabs = await page.locator(selector).all()\n if found_tabs:\n break\n \n if found_tabs:\n logger.info(f\"[DEBUG][TabSwitching] Found {len(found_tabs)} tabs at depth {depth}\")\n for i, tab in enumerate(found_tabs):\n try:\n tab_text = (await tab.inner_text()).strip()\n tab_id = f\"{depth}_{i}_{tab_text}\"\n \n if tab_id in processed_tabs:\n continue\n \n if await tab.is_visible():\n logger.info(f\"[DEBUG][TabSwitching] Switching to tab: {tab_text}\")\n processed_tabs.add(tab_id)\n \n is_active = \"ant-tabs-tab-active\" in (await tab.get_attribute(\"class\") or \"\")\n if not is_active:\n await tab.click()\n await asyncio.sleep(2) # Wait for content to render\n \n await switch_tabs(depth + 1)\n except Exception as tab_e:\n logger.warning(f\"[DEBUG][TabSwitching] Failed to process tab {i}: {tab_e}\")\n \n try:\n first_tab = found_tabs[0]\n if \"ant-tabs-tab-active\" not in (await first_tab.get_attribute(\"class\") or \"\"):\n await first_tab.click()\n await asyncio.sleep(1)\n except Exception:\n pass\n\n await switch_tabs()\n\n # 2. Calculate full height for screenshot\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n full_height = await page.evaluate(\"\"\"() => {\n const body = document.body;\n const html = document.documentElement;\n const dashboardContent = document.querySelector('.dashboard-content');\n \n return Math.max(\n body.scrollHeight, body.offsetHeight,\n html.clientHeight, html.scrollHeight, html.offsetHeight,\n dashboardContent ? dashboardContent.scrollHeight + 100 : 0\n );\n }\"\"\")\n logger.info(f\"[DEBUG] Calculated full height: {full_height}\")\n \n # DIAGNOSTIC: Count chart elements before resize\n chart_count_before = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements BEFORE viewport resize: {chart_count_before}\")\n \n # DIAGNOSTIC: Capture pre-resize screenshot for comparison\n pre_resize_path = output_path.replace(\".png\", \"_preresize.png\")\n try:\n await page.screenshot(path=pre_resize_path, full_page=False, timeout=10000)\n import os\n pre_resize_size = os.path.getsize(pre_resize_path) if os.path.exists(pre_resize_path) else 0\n logger.info(f\"[DIAGNOSTIC] Pre-resize screenshot saved: {pre_resize_path} ({pre_resize_size} bytes)\")\n except Exception as pre_e:\n logger.warning(f\"[DIAGNOSTIC] Failed to capture pre-resize screenshot: {pre_e}\")\n \n logger.info(f\"[DIAGNOSTIC] Resizing viewport from current to 1920x{int(full_height)}\")\n await page.set_viewport_size({\"width\": 1920, \"height\": int(full_height)})\n \n # DIAGNOSTIC: Increased wait time and log timing\n logger.info(\"[DIAGNOSTIC] Waiting 10 seconds after viewport resize for re-render...\")\n await asyncio.sleep(10)\n logger.info(\"[DIAGNOSTIC] Wait completed\")\n \n # DIAGNOSTIC: Count chart elements after resize and wait\n chart_count_after = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements AFTER viewport resize + wait: {chart_count_after}\")\n \n # DIAGNOSTIC: Check if any charts have error states\n chart_errors = await page.evaluate(\"\"\"() => {\n const errors = [];\n document.querySelectorAll('.chart-container, .slice_container').forEach((chart, i) => {\n const errorEl = chart.querySelector('.error, .alert-danger, .ant-alert-error');\n if (errorEl) {\n errors.push({index: i, text: errorEl.innerText.substring(0, 100)});\n }\n });\n return errors;\n }\"\"\")\n if chart_errors:\n logger.warning(f\"[DIAGNOSTIC] Charts with error states detected: {chart_errors}\")\n else:\n logger.info(\"[DIAGNOSTIC] No chart error states detected\")\n\n # 3. Take screenshot using CDP to bypass Playwright's font loading wait\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n logger.info(\"[DEBUG] Attempting full-page screenshot via CDP...\")\n cdp = await page.context.new_cdp_session(page)\n \n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n \n image_data = base64.b64decode(screenshot_data[\"data\"])\n \n with open(output_path, 'wb') as f:\n f.write(image_data)\n \n # DIAGNOSTIC: Verify screenshot file\n import os\n final_size = os.path.getsize(output_path) if os.path.exists(output_path) else 0\n logger.info(f\"[DIAGNOSTIC] Final screenshot saved: {output_path}\")\n logger.info(f\"[DIAGNOSTIC] Final screenshot size: {final_size} bytes ({final_size / 1024:.2f} KB)\")\n \n # DIAGNOSTIC: Get image dimensions\n try:\n with Image.open(output_path) as final_img:\n logger.info(f\"[DIAGNOSTIC] Final screenshot dimensions: {final_img.width}x{final_img.height}\")\n except Exception as img_err:\n logger.warning(f\"[DIAGNOSTIC] Could not read final image dimensions: {img_err}\")\n \n logger.info(f\"Full-page screenshot saved to {output_path} (via CDP)\")\n except Exception as e:\n logger.error(f\"[DEBUG] Full-page/Tab capture failed: {e}\")\n try:\n await page.screenshot(path=output_path, full_page=True, timeout=10000)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=5000)\n \n await browser.close()\n return True\n # [/DEF:ScreenshotService.capture_dashboard:Function]\n# [/DEF:ScreenshotService:Class]\n\n# [DEF:LLMClient:Class]\n# @PURPOSE: Wrapper for LLM provider APIs.\nclass LLMClient:\n # [DEF:LLMClient.__init__:Function]\n # @PURPOSE: Initializes the LLMClient with provider settings.\n # @PRE: api_key, base_url, and default_model are non-empty strings.\n def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):\n self.provider_type = provider_type\n normalized_key = (api_key or \"\").strip()\n if normalized_key.lower().startswith(\"bearer \"):\n normalized_key = normalized_key[7:].strip()\n self.api_key = normalized_key\n self.base_url = base_url\n self.default_model = default_model\n \n # DEBUG: Log initialization parameters (without exposing full API key)\n logger.info(\"[LLMClient.__init__] Initializing LLM client:\")\n logger.info(f\"[LLMClient.__init__] Provider Type: {provider_type}\")\n logger.info(f\"[LLMClient.__init__] Base URL: {base_url}\")\n logger.info(f\"[LLMClient.__init__] Default Model: {default_model}\")\n logger.info(f\"[LLMClient.__init__] API Key (first 8 chars): {self.api_key[:8] if self.api_key and len(self.api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n logger.info(f\"[LLMClient.__init__] API Key Length: {len(self.api_key) if self.api_key else 0}\")\n\n # Some OpenAI-compatible gateways are strict about auth header naming.\n default_headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n if self.provider_type == LLMProviderType.OPENROUTER:\n default_headers[\"HTTP-Referer\"] = (\n os.getenv(\"OPENROUTER_SITE_URL\", \"\").strip()\n or os.getenv(\"APP_BASE_URL\", \"\").strip()\n or \"http://localhost:8000\"\n )\n default_headers[\"X-Title\"] = os.getenv(\"OPENROUTER_APP_NAME\", \"\").strip() or \"ss-tools\"\n if self.provider_type == LLMProviderType.KILO:\n default_headers[\"Authentication\"] = f\"Bearer {self.api_key}\"\n default_headers[\"X-API-Key\"] = self.api_key\n\n http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)\n self.client = AsyncOpenAI(\n api_key=self.api_key,\n base_url=base_url,\n default_headers=default_headers,\n http_client=http_client,\n )\n # [/DEF:LLMClient.__init__:Function]\n\n # [DEF:LLMClient._supports_json_response_format:Function]\n # @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object.\n # @PRE: Client initialized with base_url and default_model.\n # @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors.\n def _supports_json_response_format(self) -> bool:\n base = (self.base_url or \"\").lower()\n model = (self.default_model or \"\").lower()\n\n # OpenRouter routes to many upstream providers; some models reject json_object mode.\n if \"openrouter.ai\" in base:\n incompatible_tokens = (\n \"stepfun/\",\n \"step-\",\n \":free\",\n )\n if any(token in model for token in incompatible_tokens):\n return False\n return True\n # [/DEF:LLMClient._supports_json_response_format:Function]\n\n # [DEF:LLMClient.get_json_completion:Function]\n # @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing.\n # @PRE: messages is a list of valid message dictionaries.\n # @POST: Returns a parsed JSON dictionary.\n # @SIDE_EFFECT: Calls external LLM API.\n def _should_retry(exception: Exception) -> bool:\n \"\"\"Custom retry predicate that excludes authentication errors.\"\"\"\n # Don't retry on authentication errors\n if isinstance(exception, OpenAIAuthenticationError):\n return False\n # Retry on rate limit errors and other exceptions\n return isinstance(exception, (RateLimitError, Exception))\n \n @retry(\n stop=stop_after_attempt(5),\n wait=wait_exponential(multiplier=2, min=5, max=60),\n retry=retry_if_exception(_should_retry),\n reraise=True\n )\n async def get_json_completion(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:\n with belief_scope(\"get_json_completion\"):\n response = None\n try:\n use_json_mode = self._supports_json_response_format()\n try:\n logger.info(\n f\"[get_json_completion] Attempting LLM call for model: {self.default_model} \"\n f\"(json_mode={'on' if use_json_mode else 'off'})\"\n )\n logger.info(f\"[get_json_completion] Base URL being used: {self.base_url}\")\n logger.info(f\"[get_json_completion] Number of messages: {len(messages)}\")\n logger.info(f\"[get_json_completion] API Key present: {bool(self.api_key and len(self.api_key) > 0)}\")\n\n if use_json_mode:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages,\n response_format={\"type\": \"json_object\"}\n )\n else:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n except Exception as e:\n if use_json_mode and (\n \"JSON mode is not enabled\" in str(e)\n or \"json_object is not supported\" in str(e).lower()\n or \"response_format\" in str(e).lower()\n or \"400\" in str(e)\n ):\n logger.warning(f\"[get_json_completion] JSON mode failed or not supported: {str(e)}. Falling back to plain text response.\")\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n else:\n raise e\n \n logger.debug(f\"[get_json_completion] LLM Response: {response}\")\n except OpenAIAuthenticationError as e:\n logger.error(f\"[get_json_completion] Authentication error: {str(e)}\")\n # Do not retry on auth errors - re-raise to stop retry\n raise\n except RateLimitError as e:\n logger.warning(f\"[get_json_completion] Rate limit hit: {str(e)}\")\n \n # Extract retry_delay from error metadata if available\n retry_delay = 5.0 # Default fallback\n try:\n # Based on logs, the raw response is in e.body or e.response.json()\n # The logs show 'metadata': {'raw': '...'} which suggests a proxy or specific client wrapper\n # Let's try to find the 'retryDelay' in the error message or response\n import re\n \n # Try to find \"retryDelay\": \"XXs\" in the string representation of the error\n error_str = str(e)\n match = re.search(r'\"retryDelay\":\\s*\"(\\d+)s\"', error_str)\n if match:\n retry_delay = float(match.group(1))\n else:\n # Try to parse from response if it's a standard OpenAI-like error with body\n if hasattr(e, 'body') and isinstance(e.body, dict):\n # Some providers put it in details\n details = e.body.get('error', {}).get('details', [])\n for detail in details:\n if detail.get('@type') == 'type.googleapis.com/google.rpc.RetryInfo':\n delay_str = detail.get('retryDelay', '5s')\n retry_delay = float(delay_str.rstrip('s'))\n break\n except Exception as parse_e:\n logger.debug(f\"[get_json_completion] Failed to parse retry delay: {parse_e}\")\n \n # Add a small safety margin (0.5s) as requested\n wait_time = retry_delay + 0.5\n logger.info(f\"[get_json_completion] Waiting for {wait_time}s before retry...\")\n await asyncio.sleep(wait_time)\n raise\n except Exception as e:\n logger.error(f\"[get_json_completion] LLM call failed: {str(e)}\")\n raise\n\n if not response or not hasattr(response, 'choices') or not response.choices:\n raise RuntimeError(f\"Invalid LLM response: {response}\")\n\n content = response.choices[0].message.content\n logger.debug(f\"[get_json_completion] Raw content to parse: {content}\")\n \n try:\n return json.loads(content)\n except json.JSONDecodeError:\n logger.warning(\"[get_json_completion] Failed to parse JSON directly, attempting to extract from code blocks\")\n if \"```json\" in content:\n json_str = content.split(\"```json\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n elif \"```\" in content:\n json_str = content.split(\"```\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n else:\n raise\n # [/DEF:LLMClient.get_json_completion:Function]\n\n # [DEF:LLMClient.test_runtime_connection:Function]\n # @PURPOSE: Validate provider credentials using the same chat completions transport as runtime analysis.\n # @PRE: Client is initialized with provider credentials and default_model.\n # @POST: Returns lightweight JSON payload when runtime auth/model path is valid.\n # @SIDE_EFFECT: Calls external LLM API.\n async def test_runtime_connection(self) -> Dict[str, Any]:\n with belief_scope(\"test_runtime_connection\"):\n messages = [\n {\n \"role\": \"user\",\n \"content\": 'Return exactly this JSON object and nothing else: {\"ok\": true}',\n }\n ]\n return await self.get_json_completion(messages)\n # [/DEF:LLMClient.test_runtime_connection:Function]\n\n # [DEF:LLMClient.analyze_dashboard:Function]\n # @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.\n # @PRE: screenshot_path exists, logs is a list of strings.\n # @POST: Returns a structured analysis dictionary (status, summary, issues).\n # @SIDE_EFFECT: Reads screenshot file and calls external LLM API.\n async def analyze_dashboard(\n self,\n screenshot_path: str,\n logs: List[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n ) -> Dict[str, Any]:\n with belief_scope(\"analyze_dashboard\"):\n # Optimize image to reduce token count (US1 / T023)\n # Gemini/Gemma models have limits on input tokens, and large images contribute significantly.\n try:\n with Image.open(screenshot_path) as img:\n # Convert to RGB if necessary\n if img.mode in (\"RGBA\", \"P\"):\n img = img.convert(\"RGB\")\n \n # Resize if too large (max 1024px width while maintaining aspect ratio)\n # We reduce width further to 1024px to stay within token limits for long dashboards\n max_width = 1024\n if img.width > max_width or img.height > 2048:\n # Calculate scaling factor to fit within 1024x2048\n scale = min(max_width / img.width, 2048 / img.height)\n if scale < 1.0:\n new_width = int(img.width * scale)\n new_height = int(img.height * scale)\n img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)\n logger.info(f\"[analyze_dashboard] Resized image from {img.width}x{img.height} to {new_width}x{new_height}\")\n \n # Compress and convert to base64\n buffer = io.BytesIO()\n # Lower quality to 60% to further reduce payload size\n img.save(buffer, format=\"JPEG\", quality=60, optimize=True)\n base_64_image = base64.b64encode(buffer.getvalue()).decode('utf-8')\n logger.info(f\"[analyze_dashboard] Optimized image size: {len(buffer.getvalue()) / 1024:.2f} KB\")\n except Exception as img_e:\n logger.warning(f\"[analyze_dashboard] Image optimization failed: {img_e}. Using raw image.\")\n with open(screenshot_path, \"rb\") as image_file:\n base_64_image = base64.b64encode(image_file.read()).decode('utf-8')\n\n log_text = \"\\n\".join(logs)\n prompt = render_prompt(prompt_template, {\"logs\": log_text})\n \n messages = [\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": prompt},\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": f\"data:image/jpeg;base64,{base_64_image}\"\n }\n }\n ]\n }\n ]\n \n try:\n return await self.get_json_completion(messages)\n except Exception as e:\n logger.error(f\"[analyze_dashboard] Failed to get analysis: {str(e)}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {str(e)}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # [/DEF:LLMClient.analyze_dashboard:Function]\n# [/DEF:LLMClient:Class]\n\n# [/DEF:backend/src/plugins/llm_analysis/service.py:Module]\n" + "anchor_syntax": "region", + "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\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": 28, - "end_line": 675, + "start_line": 30, + "end_line": 677, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -64638,588 +51429,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ScreenshotService:Class]\n# @PURPOSE: Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # [DEF:ScreenshotService.__init__:Function]\n # @PURPOSE: Initializes the ScreenshotService with environment configuration.\n # @PRE: env is a valid Environment object.\n def __init__(self, env: Environment):\n self.env = env\n # [/DEF:ScreenshotService.__init__:Function]\n\n # [DEF:ScreenshotService._find_first_visible_locator:Function]\n # @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies.\n # @PRE: candidates is a non-empty list of locator-like objects.\n # @POST: Returns a locator ready for interaction or None when nothing matches.\n async def _find_first_visible_locator(self, candidates) -> Any:\n for locator in candidates:\n try:\n match_count = await locator.count()\n for index in range(match_count):\n candidate = locator.nth(index)\n if await candidate.is_visible():\n return candidate\n except Exception:\n continue\n return None\n # [/DEF:ScreenshotService._find_first_visible_locator:Function]\n\n # [DEF:ScreenshotService._iter_login_roots:Function]\n # @PURPOSE: Enumerate page and child frames where login controls may be rendered.\n # @PRE: page is a Playwright page-like object.\n # @POST: Returns ordered roots starting with main page followed by frames.\n def _iter_login_roots(self, page) -> List[Any]:\n roots = [page]\n page_frames = getattr(page, \"frames\", [])\n try:\n for frame in page_frames:\n if frame not in roots:\n roots.append(frame)\n except Exception:\n pass\n return roots\n # [/DEF:ScreenshotService._iter_login_roots:Function]\n\n # [DEF:ScreenshotService._extract_hidden_login_fields:Function]\n # @PURPOSE: Collect hidden form fields required for direct login POST fallback.\n # @PRE: Login page is loaded.\n # @POST: Returns hidden input name/value mapping aggregated from page and child frames.\n async def _extract_hidden_login_fields(self, page) -> Dict[str, str]:\n hidden_fields: Dict[str, str] = {}\n for root in self._iter_login_roots(page):\n try:\n locator = root.locator(\"input[type='hidden'][name]\")\n count = await locator.count()\n for index in range(count):\n candidate = locator.nth(index)\n field_name = str(await candidate.get_attribute(\"name\") or \"\").strip()\n if not field_name or field_name in hidden_fields:\n continue\n hidden_fields[field_name] = str(await candidate.input_value()).strip()\n except Exception:\n continue\n return hidden_fields\n # [/DEF:ScreenshotService._extract_hidden_login_fields:Function]\n\n # [DEF:ScreenshotService._extract_csrf_token:Function]\n # @PURPOSE: Resolve CSRF token value from main page or embedded login frame.\n # @PRE: Login page is loaded.\n # @POST: Returns first non-empty csrf token or empty string.\n async def _extract_csrf_token(self, page) -> str:\n hidden_fields = await self._extract_hidden_login_fields(page)\n return str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n # [/DEF:ScreenshotService._extract_csrf_token:Function]\n\n # [DEF:ScreenshotService._response_looks_like_login_page:Function]\n # @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page.\n # @PRE: response_text is normalized HTML or text from login POST response.\n # @POST: Returns True when login-page markers dominate the response body.\n def _response_looks_like_login_page(self, response_text: str) -> bool:\n normalized = str(response_text or \"\").strip().lower()\n if not normalized:\n return False\n\n markers = [\n \"enter your login and password below\",\n \"username:\",\n \"password:\",\n \"sign in\",\n 'name=\"csrf_token\"',\n ]\n return sum(marker in normalized for marker in markers) >= 3\n # [/DEF:ScreenshotService._response_looks_like_login_page:Function]\n\n # [DEF:ScreenshotService._redirect_looks_authenticated:Function]\n # @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target.\n # @PRE: redirect_location may be empty or relative.\n # @POST: Returns True when redirect target does not point back to login flow.\n def _redirect_looks_authenticated(self, redirect_location: str) -> bool:\n normalized = str(redirect_location or \"\").strip().lower()\n if not normalized:\n return True\n return \"/login\" not in normalized\n # [/DEF:ScreenshotService._redirect_looks_authenticated:Function]\n\n # [DEF:ScreenshotService._submit_login_via_form_post:Function]\n # @PURPOSE: Fallback login path that submits credentials directly with csrf token.\n # @PRE: login_url is same-origin and csrf token can be read from DOM.\n # @POST: Browser context receives authenticated cookies when login succeeds.\n async def _submit_login_via_form_post(self, page, login_url: str) -> bool:\n hidden_fields = await self._extract_hidden_login_fields(page)\n csrf_token = str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n if not csrf_token:\n logger.warning(\"[DEBUG] Direct form login fallback skipped: csrf_token not found\")\n return False\n\n try:\n request_context = page.context.request\n except Exception as context_error:\n logger.warning(f\"[DEBUG] Direct form login fallback skipped: request context unavailable: {context_error}\")\n return False\n\n parsed_url = urlsplit(login_url)\n origin = f\"{parsed_url.scheme}://{parsed_url.netloc}\" if parsed_url.scheme and parsed_url.netloc else login_url\n payload = dict(hidden_fields)\n payload[\"username\"] = self.env.username\n payload[\"password\"] = self.env.password\n logger.info(\n f\"[DEBUG] Attempting direct form login fallback via browser context request with hidden fields: {sorted(hidden_fields.keys())}\"\n )\n\n response = await request_context.post(\n login_url,\n form=payload,\n headers={\n \"Origin\": origin,\n \"Referer\": login_url,\n },\n timeout=10000,\n fail_on_status_code=False,\n max_redirects=0,\n )\n response_url = str(getattr(response, \"url\", \"\") or \"\")\n response_status = int(getattr(response, \"status\", 0) or 0)\n response_headers = dict(getattr(response, \"headers\", {}) or {})\n redirect_location = str(\n response_headers.get(\"location\")\n or response_headers.get(\"Location\")\n or \"\"\n ).strip()\n redirect_statuses = {301, 302, 303, 307, 308}\n if response_status in redirect_statuses:\n redirect_authenticated = self._redirect_looks_authenticated(redirect_location)\n logger.info(\n f\"[DEBUG] Direct form login fallback redirect response: status={response_status} url={response_url} location={redirect_location!r} authenticated={redirect_authenticated}\"\n )\n return redirect_authenticated\n\n response_text = await response.text()\n text_snippet = \" \".join(response_text.split())[:200]\n looks_like_login_page = self._response_looks_like_login_page(response_text)\n logger.info(\n f\"[DEBUG] Direct form login fallback response: status={response_status} url={response_url} login_markup={looks_like_login_page} snippet={text_snippet!r}\"\n )\n return not looks_like_login_page\n # [/DEF:ScreenshotService._submit_login_via_form_post:Function]\n\n # [DEF:ScreenshotService._find_login_field_locator:Function]\n # @PURPOSE: Resolve login form input using semantic label text plus generic visible-input fallbacks.\n # @PRE: field_name is `username` or `password`.\n # @POST: Returns a locator for the corresponding input or None.\n async def _find_login_field_locator(self, page, field_name: str) -> Any:\n normalized = str(field_name or \"\").strip().lower()\n for root in self._iter_login_roots(page):\n if normalized == \"username\":\n input_candidates = [\n root.get_by_label(\"Username\", exact=False),\n root.get_by_label(\"Login\", exact=False),\n root.locator(\"label:text-matches('Username|Login', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Username|Login/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='username']\"),\n root.locator(\"input#username\"),\n root.locator(\"input[placeholder*='Username']\"),\n root.locator(\"input[type='text']\"),\n root.locator(\"input:not([type='password'])\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n if normalized == \"password\":\n input_candidates = [\n root.get_by_label(\"Password\", exact=False),\n root.locator(\"label:text-matches('Password', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Password/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='password']\"),\n root.locator(\"input#password\"),\n root.locator(\"input[placeholder*='Password']\"),\n root.locator(\"input[type='password']\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n return None\n # [/DEF:ScreenshotService._find_login_field_locator:Function]\n\n # [DEF:ScreenshotService._find_submit_locator:Function]\n # @PURPOSE: Resolve login submit button from main page or embedded auth frame.\n # @PRE: page is ready for login interaction.\n # @POST: Returns visible submit locator or None.\n async def _find_submit_locator(self, page) -> Any:\n selectors = [\n lambda root: root.get_by_role(\"button\", name=\"Sign in\", exact=False),\n lambda root: root.get_by_role(\"button\", name=\"Login\", exact=False),\n lambda root: root.locator(\"button[type='submit']\"),\n lambda root: root.locator(\"button#submit\"),\n lambda root: root.locator(\".btn-primary\"),\n lambda root: root.locator(\"input[type='submit']\"),\n ]\n for root in self._iter_login_roots(page):\n locator = await self._find_first_visible_locator([factory(root) for factory in selectors])\n if locator:\n return locator\n return None\n # [/DEF:ScreenshotService._find_submit_locator:Function]\n\n # [DEF:ScreenshotService._goto_resilient:Function]\n # @PURPOSE: Navigate without relying on networkidle for pages with long-polling or persistent requests.\n # @PRE: page is a valid Playwright page and url is non-empty.\n # @POST: Returns last navigation response or raises when both primary and fallback waits fail.\n async def _goto_resilient(\n self,\n page,\n url: str,\n primary_wait_until: str = \"domcontentloaded\",\n fallback_wait_until: str = \"load\",\n timeout: int = 60000,\n ):\n try:\n return await page.goto(url, wait_until=primary_wait_until, timeout=timeout)\n except Exception as primary_error:\n logger.warning(\n f\"[ScreenshotService._goto_resilient] Primary navigation wait '{primary_wait_until}' failed for {url}: {primary_error}\"\n )\n return await page.goto(url, wait_until=fallback_wait_until, timeout=timeout)\n # [/DEF:ScreenshotService._goto_resilient:Function]\n\n # [DEF:ScreenshotService.capture_dashboard:Function]\n # @PURPOSE: Captures a full-page screenshot of a dashboard using Playwright and CDP.\n # @PRE: dashboard_id is a valid string, output_path is a writable path.\n # @POST: Returns True if screenshot is saved successfully.\n # @SIDE_EFFECT: Launches a browser, performs UI login, switches tabs, and writes a PNG file.\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n with belief_scope(\"capture_dashboard\", f\"dashboard_id={dashboard_id}\"):\n logger.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n async with async_playwright() as p:\n browser = await p.chromium.launch(\n headless=True,\n args=[\n \"--disable-blink-features=AutomationControlled\",\n \"--disable-infobars\",\n \"--no-sandbox\"\n ]\n )\n # Set a realistic user agent to avoid 403 Forbidden from OpenResty/WAF\n user_agent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"\n # Construct base UI URL from environment (strip /api/v1 suffix)\n base_ui_url = self.env.url.rstrip(\"/\")\n if base_ui_url.endswith(\"/api/v1\"):\n base_ui_url = base_ui_url[:-len(\"/api/v1\")]\n \n # Create browser context with realistic headers\n context = await browser.new_context(\n viewport={'width': 1280, 'height': 720},\n user_agent=user_agent,\n extra_http_headers={\n \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n \"Accept-Language\": \"ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7\",\n \"Upgrade-Insecure-Requests\": \"1\",\n \"Sec-Fetch-Dest\": \"document\",\n \"Sec-Fetch-Mode\": \"navigate\",\n \"Sec-Fetch-Site\": \"none\",\n \"Sec-Fetch-User\": \"?1\"\n }\n )\n logger.info(\"Browser context created successfully\")\n\n page = await context.new_page()\n # Bypass navigator.webdriver detection\n await page.add_init_script(\"delete Object.getPrototypeOf(navigator).webdriver\")\n\n # 1. Navigate to login page and authenticate\n login_url = f\"{base_ui_url.rstrip('/')}/login/\"\n logger.info(f\"[DEBUG] Navigating to login page: {login_url}\")\n \n response = await self._goto_resilient(\n page,\n login_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n if response:\n logger.info(f\"[DEBUG] Login page response status: {response.status}\")\n \n # Wait for login form to be ready\n await page.wait_for_load_state(\"domcontentloaded\")\n \n # More exhaustive list of selectors for various Superset versions/themes\n selectors = {\n \"username\": ['input[name=\"username\"]', 'input#username', 'input[placeholder*=\"Username\"]', 'input[type=\"text\"]'],\n \"password\": ['input[name=\"password\"]', 'input#password', 'input[placeholder*=\"Password\"]', 'input[type=\"password\"]'],\n \"submit\": ['button[type=\"submit\"]', 'button#submit', '.btn-primary', 'input[type=\"submit\"]']\n }\n logger.info(\"[DEBUG] Attempting to find login form elements...\")\n \n try:\n used_direct_form_login = False\n # Find and fill username\n username_locator = await self._find_login_field_locator(page, \"username\")\n\n if not username_locator:\n roots = self._iter_login_roots(page)\n logger.info(f\"[DEBUG] Found {len(roots)} login roots including child frames\")\n for root_index, root in enumerate(roots[:5]):\n all_inputs = await root.locator('input').all()\n logger.info(f\"[DEBUG] Root {root_index}: found {len(all_inputs)} input fields\")\n for i, inp in enumerate(all_inputs[:5]): # Log first 5\n inp_type = await inp.get_attribute('type')\n inp_name = await inp.get_attribute('name')\n inp_id = await inp.get_attribute('id')\n logger.info(f\"[DEBUG] Root {root_index} input {i}: type={inp_type}, name={inp_name}, id={inp_id}\")\n used_direct_form_login = await self._submit_login_via_form_post(page, login_url)\n if not used_direct_form_login:\n raise RuntimeError(\"Could not find username input field on login page\")\n username_locator = None\n \n if username_locator is not None:\n logger.info(\"[DEBUG] Filling username field\")\n await username_locator.fill(self.env.username)\n \n # Find and fill password\n password_locator = await self._find_login_field_locator(page, \"password\") if username_locator is not None else None\n\n if username_locator is not None and not password_locator:\n raise RuntimeError(\"Could not find password input field on login page\")\n \n if password_locator is not None:\n logger.info(\"[DEBUG] Filling password field\")\n await password_locator.fill(self.env.password)\n \n # Click submit\n submit_locator = await self._find_submit_locator(page) if username_locator is not None else None\n\n if username_locator is not None and not submit_locator:\n raise RuntimeError(\"Could not find submit button on login page\")\n\n if submit_locator is not None:\n logger.info(\"[DEBUG] Clicking submit button\")\n await submit_locator.click()\n \n # Wait for navigation after login\n if not used_direct_form_login:\n try:\n await page.wait_for_load_state(\"load\", timeout=30000)\n except Exception as load_wait_error:\n logger.warning(f\"[DEBUG] Login post-submit load wait timed out: {load_wait_error}\")\n \n # Check if login was successful\n if not used_direct_form_login and \"/login\" in page.url:\n # Check for error messages on page\n error_msg = await page.locator(\".alert-danger, .error-message\").text_content() if await page.locator(\".alert-danger, .error-message\").count() > 0 else \"Unknown error\"\n logger.error(f\"[DEBUG] Login failed. Still on login page. Error: {error_msg}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {error_msg}. Debug screenshot saved to {debug_path}\")\n \n logger.info(f\"[DEBUG] Login successful. Current URL: {page.url}\")\n \n # Check cookies after successful login\n page_cookies = await context.cookies()\n logger.info(f\"[DEBUG] Cookies after login: {len(page_cookies)}\")\n for c in page_cookies:\n logger.info(f\"[DEBUG] Cookie: name={c['name']}, domain={c['domain']}, value={c.get('value', '')[:20]}...\")\n \n except Exception as e:\n page_title = await page.title()\n logger.error(f\"UI Login failed. Page title: {page_title}, URL: {page.url}, Error: {str(e)}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {str(e)}. Debug screenshot saved to {debug_path}\")\n\n # 2. Navigate to dashboard\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n dashboard_url = f\"{base_ui_url.rstrip('/')}/superset/dashboard/{dashboard_id}/?standalone=true\"\n \n if base_ui_url.startswith(\"https://\") and dashboard_url.startswith(\"http://\"):\n dashboard_url = dashboard_url.replace(\"http://\", \"https://\")\n\n logger.info(f\"[DEBUG] Navigating to dashboard: {dashboard_url}\")\n \n # Dashboard pages can keep polling/network activity open indefinitely.\n response = await self._goto_resilient(\n page,\n dashboard_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n \n if response:\n logger.info(f\"[DEBUG] Dashboard navigation response status: {response.status}, URL: {response.url}\")\n\n if \"/login\" in page.url:\n debug_path = output_path.replace(\".png\", \"_debug_failed_dashboard_auth.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(\n f\"Dashboard navigation redirected to login page after authentication. Debug screenshot saved to {debug_path}\"\n )\n \n try:\n # Wait for the dashboard grid to be present\n await page.wait_for_selector('.dashboard-component, .dashboard-header, [data-test=\"dashboard-grid\"]', timeout=30000)\n logger.info(\"[DEBUG] Dashboard container loaded\")\n \n # Wait for charts to finish loading (Superset uses loading spinners/skeletons)\n # We wait until loading indicators disappear or a timeout occurs\n try:\n # Wait for loading indicators to disappear\n await page.wait_for_selector('.loading, .ant-skeleton, .spinner', state=\"hidden\", timeout=60000)\n logger.info(\"[DEBUG] Loading indicators hidden\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for loading indicators to hide\")\n\n # Wait for charts to actually render their content (e.g., ECharts, NVD3)\n # We look for common chart containers that should have content\n try:\n await page.wait_for_selector('.chart-container canvas, .slice_container svg, .superset-chart-canvas, .grid-content .chart-container', timeout=60000)\n logger.info(\"[DEBUG] Chart content detected\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for chart content\")\n\n # Additional check: wait for all chart containers to have non-empty content\n logger.info(\"[DEBUG] Waiting for all charts to have rendered content...\")\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container, .slice_container');\n if (charts.length === 0) return true; // No charts to wait for\n \n // Check if all charts have rendered content (canvas, svg, or non-empty div)\n return Array.from(charts).every(chart => {\n const hasCanvas = chart.querySelector('canvas') !== null;\n const hasSvg = chart.querySelector('svg') !== null;\n const hasContent = chart.innerText.trim().length > 0 || chart.children.length > 0;\n return hasCanvas || hasSvg || hasContent;\n });\n }\"\"\", timeout=60000)\n logger.info(\"[DEBUG] All charts have rendered content\")\n\n # Scroll to bottom and back to top to trigger lazy loading of all charts\n logger.info(\"[DEBUG] Scrolling to trigger lazy loading...\")\n await page.evaluate(\"\"\"async () => {\n const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n for (let i = 0; i < document.body.scrollHeight; i += 500) {\n window.scrollTo(0, i);\n await delay(100);\n }\n window.scrollTo(0, 0);\n await delay(500);\n }\"\"\")\n\n except Exception as e:\n logger.warning(f\"[DEBUG] Dashboard content wait failed: {e}, proceeding anyway after delay\")\n \n # Final stabilization delay - increased for complex dashboards\n logger.info(\"[DEBUG] Final stabilization delay...\")\n await asyncio.sleep(15)\n\n # Logic to handle tabs and full-page capture\n try:\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n processed_tabs = set()\n\n async def switch_tabs(depth=0):\n if depth > 3:\n return # Limit recursion depth\n \n tab_selectors = [\n '.ant-tabs-nav-list .ant-tabs-tab',\n '.dashboard-component-tabs .ant-tabs-tab',\n '[data-test=\"dashboard-component-tabs\"] .ant-tabs-tab'\n ]\n \n found_tabs = []\n for selector in tab_selectors:\n found_tabs = await page.locator(selector).all()\n if found_tabs:\n break\n \n if found_tabs:\n logger.info(f\"[DEBUG][TabSwitching] Found {len(found_tabs)} tabs at depth {depth}\")\n for i, tab in enumerate(found_tabs):\n try:\n tab_text = (await tab.inner_text()).strip()\n tab_id = f\"{depth}_{i}_{tab_text}\"\n \n if tab_id in processed_tabs:\n continue\n \n if await tab.is_visible():\n logger.info(f\"[DEBUG][TabSwitching] Switching to tab: {tab_text}\")\n processed_tabs.add(tab_id)\n \n is_active = \"ant-tabs-tab-active\" in (await tab.get_attribute(\"class\") or \"\")\n if not is_active:\n await tab.click()\n await asyncio.sleep(2) # Wait for content to render\n \n await switch_tabs(depth + 1)\n except Exception as tab_e:\n logger.warning(f\"[DEBUG][TabSwitching] Failed to process tab {i}: {tab_e}\")\n \n try:\n first_tab = found_tabs[0]\n if \"ant-tabs-tab-active\" not in (await first_tab.get_attribute(\"class\") or \"\"):\n await first_tab.click()\n await asyncio.sleep(1)\n except Exception:\n pass\n\n await switch_tabs()\n\n # 2. Calculate full height for screenshot\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n full_height = await page.evaluate(\"\"\"() => {\n const body = document.body;\n const html = document.documentElement;\n const dashboardContent = document.querySelector('.dashboard-content');\n \n return Math.max(\n body.scrollHeight, body.offsetHeight,\n html.clientHeight, html.scrollHeight, html.offsetHeight,\n dashboardContent ? dashboardContent.scrollHeight + 100 : 0\n );\n }\"\"\")\n logger.info(f\"[DEBUG] Calculated full height: {full_height}\")\n \n # DIAGNOSTIC: Count chart elements before resize\n chart_count_before = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements BEFORE viewport resize: {chart_count_before}\")\n \n # DIAGNOSTIC: Capture pre-resize screenshot for comparison\n pre_resize_path = output_path.replace(\".png\", \"_preresize.png\")\n try:\n await page.screenshot(path=pre_resize_path, full_page=False, timeout=10000)\n import os\n pre_resize_size = os.path.getsize(pre_resize_path) if os.path.exists(pre_resize_path) else 0\n logger.info(f\"[DIAGNOSTIC] Pre-resize screenshot saved: {pre_resize_path} ({pre_resize_size} bytes)\")\n except Exception as pre_e:\n logger.warning(f\"[DIAGNOSTIC] Failed to capture pre-resize screenshot: {pre_e}\")\n \n logger.info(f\"[DIAGNOSTIC] Resizing viewport from current to 1920x{int(full_height)}\")\n await page.set_viewport_size({\"width\": 1920, \"height\": int(full_height)})\n \n # DIAGNOSTIC: Increased wait time and log timing\n logger.info(\"[DIAGNOSTIC] Waiting 10 seconds after viewport resize for re-render...\")\n await asyncio.sleep(10)\n logger.info(\"[DIAGNOSTIC] Wait completed\")\n \n # DIAGNOSTIC: Count chart elements after resize and wait\n chart_count_after = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements AFTER viewport resize + wait: {chart_count_after}\")\n \n # DIAGNOSTIC: Check if any charts have error states\n chart_errors = await page.evaluate(\"\"\"() => {\n const errors = [];\n document.querySelectorAll('.chart-container, .slice_container').forEach((chart, i) => {\n const errorEl = chart.querySelector('.error, .alert-danger, .ant-alert-error');\n if (errorEl) {\n errors.push({index: i, text: errorEl.innerText.substring(0, 100)});\n }\n });\n return errors;\n }\"\"\")\n if chart_errors:\n logger.warning(f\"[DIAGNOSTIC] Charts with error states detected: {chart_errors}\")\n else:\n logger.info(\"[DIAGNOSTIC] No chart error states detected\")\n\n # 3. Take screenshot using CDP to bypass Playwright's font loading wait\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n logger.info(\"[DEBUG] Attempting full-page screenshot via CDP...\")\n cdp = await page.context.new_cdp_session(page)\n \n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n \n image_data = base64.b64decode(screenshot_data[\"data\"])\n \n with open(output_path, 'wb') as f:\n f.write(image_data)\n \n # DIAGNOSTIC: Verify screenshot file\n import os\n final_size = os.path.getsize(output_path) if os.path.exists(output_path) else 0\n logger.info(f\"[DIAGNOSTIC] Final screenshot saved: {output_path}\")\n logger.info(f\"[DIAGNOSTIC] Final screenshot size: {final_size} bytes ({final_size / 1024:.2f} KB)\")\n \n # DIAGNOSTIC: Get image dimensions\n try:\n with Image.open(output_path) as final_img:\n logger.info(f\"[DIAGNOSTIC] Final screenshot dimensions: {final_img.width}x{final_img.height}\")\n except Exception as img_err:\n logger.warning(f\"[DIAGNOSTIC] Could not read final image dimensions: {img_err}\")\n \n logger.info(f\"Full-page screenshot saved to {output_path} (via CDP)\")\n except Exception as e:\n logger.error(f\"[DEBUG] Full-page/Tab capture failed: {e}\")\n try:\n await page.screenshot(path=output_path, full_page=True, timeout=10000)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=5000)\n \n await browser.close()\n return True\n # [/DEF:ScreenshotService.capture_dashboard:Function]\n# [/DEF:ScreenshotService:Class]\n" - }, - { - "contract_id": "ScreenshotService.__init__", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 31, - "end_line": 36, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PRE": "env is a valid Environment object.", - "PURPOSE": "Initializes the ScreenshotService with environment configuration." - }, - "relations": [], - "schema_warnings": [ - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService.__init__:Function]\n # @PURPOSE: Initializes the ScreenshotService with environment configuration.\n # @PRE: env is a valid Environment object.\n def __init__(self, env: Environment):\n self.env = env\n # [/DEF:ScreenshotService.__init__:Function]\n" - }, - { - "contract_id": "ScreenshotService._find_first_visible_locator", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 38, - "end_line": 53, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns a locator ready for interaction or None when nothing matches.", - "PRE": "candidates is a non-empty list of locator-like objects.", - "PURPOSE": "Resolve the first visible locator from multiple Playwright locator strategies." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._find_first_visible_locator:Function]\n # @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies.\n # @PRE: candidates is a non-empty list of locator-like objects.\n # @POST: Returns a locator ready for interaction or None when nothing matches.\n async def _find_first_visible_locator(self, candidates) -> Any:\n for locator in candidates:\n try:\n match_count = await locator.count()\n for index in range(match_count):\n candidate = locator.nth(index)\n if await candidate.is_visible():\n return candidate\n except Exception:\n continue\n return None\n # [/DEF:ScreenshotService._find_first_visible_locator:Function]\n" - }, - { - "contract_id": "ScreenshotService._iter_login_roots", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 55, - "end_line": 69, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns ordered roots starting with main page followed by frames.", - "PRE": "page is a Playwright page-like object.", - "PURPOSE": "Enumerate page and child frames where login controls may be rendered." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._iter_login_roots:Function]\n # @PURPOSE: Enumerate page and child frames where login controls may be rendered.\n # @PRE: page is a Playwright page-like object.\n # @POST: Returns ordered roots starting with main page followed by frames.\n def _iter_login_roots(self, page) -> List[Any]:\n roots = [page]\n page_frames = getattr(page, \"frames\", [])\n try:\n for frame in page_frames:\n if frame not in roots:\n roots.append(frame)\n except Exception:\n pass\n return roots\n # [/DEF:ScreenshotService._iter_login_roots:Function]\n" - }, - { - "contract_id": "ScreenshotService._extract_hidden_login_fields", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 71, - "end_line": 90, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns hidden input name/value mapping aggregated from page and child frames.", - "PRE": "Login page is loaded.", - "PURPOSE": "Collect hidden form fields required for direct login POST fallback." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._extract_hidden_login_fields:Function]\n # @PURPOSE: Collect hidden form fields required for direct login POST fallback.\n # @PRE: Login page is loaded.\n # @POST: Returns hidden input name/value mapping aggregated from page and child frames.\n async def _extract_hidden_login_fields(self, page) -> Dict[str, str]:\n hidden_fields: Dict[str, str] = {}\n for root in self._iter_login_roots(page):\n try:\n locator = root.locator(\"input[type='hidden'][name]\")\n count = await locator.count()\n for index in range(count):\n candidate = locator.nth(index)\n field_name = str(await candidate.get_attribute(\"name\") or \"\").strip()\n if not field_name or field_name in hidden_fields:\n continue\n hidden_fields[field_name] = str(await candidate.input_value()).strip()\n except Exception:\n continue\n return hidden_fields\n # [/DEF:ScreenshotService._extract_hidden_login_fields:Function]\n" - }, - { - "contract_id": "ScreenshotService._extract_csrf_token", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 92, - "end_line": 99, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns first non-empty csrf token or empty string.", - "PRE": "Login page is loaded.", - "PURPOSE": "Resolve CSRF token value from main page or embedded login frame." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._extract_csrf_token:Function]\n # @PURPOSE: Resolve CSRF token value from main page or embedded login frame.\n # @PRE: Login page is loaded.\n # @POST: Returns first non-empty csrf token or empty string.\n async def _extract_csrf_token(self, page) -> str:\n hidden_fields = await self._extract_hidden_login_fields(page)\n return str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n # [/DEF:ScreenshotService._extract_csrf_token:Function]\n" - }, - { - "contract_id": "ScreenshotService._response_looks_like_login_page", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 101, - "end_line": 118, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns True when login-page markers dominate the response body.", - "PRE": "response_text is normalized HTML or text from login POST response.", - "PURPOSE": "Detect when fallback login POST returned the login form again instead of an authenticated page." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._response_looks_like_login_page:Function]\n # @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page.\n # @PRE: response_text is normalized HTML or text from login POST response.\n # @POST: Returns True when login-page markers dominate the response body.\n def _response_looks_like_login_page(self, response_text: str) -> bool:\n normalized = str(response_text or \"\").strip().lower()\n if not normalized:\n return False\n\n markers = [\n \"enter your login and password below\",\n \"username:\",\n \"password:\",\n \"sign in\",\n 'name=\"csrf_token\"',\n ]\n return sum(marker in normalized for marker in markers) >= 3\n # [/DEF:ScreenshotService._response_looks_like_login_page:Function]\n" - }, - { - "contract_id": "ScreenshotService._redirect_looks_authenticated", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 120, - "end_line": 129, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns True when redirect target does not point back to login flow.", - "PRE": "redirect_location may be empty or relative.", - "PURPOSE": "Treat non-login redirects after form POST as successful authentication without waiting for redirect target." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._redirect_looks_authenticated:Function]\n # @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target.\n # @PRE: redirect_location may be empty or relative.\n # @POST: Returns True when redirect target does not point back to login flow.\n def _redirect_looks_authenticated(self, redirect_location: str) -> bool:\n normalized = str(redirect_location or \"\").strip().lower()\n if not normalized:\n return True\n return \"/login\" not in normalized\n # [/DEF:ScreenshotService._redirect_looks_authenticated:Function]\n" - }, - { - "contract_id": "ScreenshotService._submit_login_via_form_post", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 131, - "end_line": 191, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Browser context receives authenticated cookies when login succeeds.", - "PRE": "login_url is same-origin and csrf token can be read from DOM.", - "PURPOSE": "Fallback login path that submits credentials directly with csrf token." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._submit_login_via_form_post:Function]\n # @PURPOSE: Fallback login path that submits credentials directly with csrf token.\n # @PRE: login_url is same-origin and csrf token can be read from DOM.\n # @POST: Browser context receives authenticated cookies when login succeeds.\n async def _submit_login_via_form_post(self, page, login_url: str) -> bool:\n hidden_fields = await self._extract_hidden_login_fields(page)\n csrf_token = str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n if not csrf_token:\n logger.warning(\"[DEBUG] Direct form login fallback skipped: csrf_token not found\")\n return False\n\n try:\n request_context = page.context.request\n except Exception as context_error:\n logger.warning(f\"[DEBUG] Direct form login fallback skipped: request context unavailable: {context_error}\")\n return False\n\n parsed_url = urlsplit(login_url)\n origin = f\"{parsed_url.scheme}://{parsed_url.netloc}\" if parsed_url.scheme and parsed_url.netloc else login_url\n payload = dict(hidden_fields)\n payload[\"username\"] = self.env.username\n payload[\"password\"] = self.env.password\n logger.info(\n f\"[DEBUG] Attempting direct form login fallback via browser context request with hidden fields: {sorted(hidden_fields.keys())}\"\n )\n\n response = await request_context.post(\n login_url,\n form=payload,\n headers={\n \"Origin\": origin,\n \"Referer\": login_url,\n },\n timeout=10000,\n fail_on_status_code=False,\n max_redirects=0,\n )\n response_url = str(getattr(response, \"url\", \"\") or \"\")\n response_status = int(getattr(response, \"status\", 0) or 0)\n response_headers = dict(getattr(response, \"headers\", {}) or {})\n redirect_location = str(\n response_headers.get(\"location\")\n or response_headers.get(\"Location\")\n or \"\"\n ).strip()\n redirect_statuses = {301, 302, 303, 307, 308}\n if response_status in redirect_statuses:\n redirect_authenticated = self._redirect_looks_authenticated(redirect_location)\n logger.info(\n f\"[DEBUG] Direct form login fallback redirect response: status={response_status} url={response_url} location={redirect_location!r} authenticated={redirect_authenticated}\"\n )\n return redirect_authenticated\n\n response_text = await response.text()\n text_snippet = \" \".join(response_text.split())[:200]\n looks_like_login_page = self._response_looks_like_login_page(response_text)\n logger.info(\n f\"[DEBUG] Direct form login fallback response: status={response_status} url={response_url} login_markup={looks_like_login_page} snippet={text_snippet!r}\"\n )\n return not looks_like_login_page\n # [/DEF:ScreenshotService._submit_login_via_form_post:Function]\n" - }, - { - "contract_id": "ScreenshotService._find_login_field_locator", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 193, - "end_line": 231, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns a locator for the corresponding input or None.", - "PRE": "field_name is `username` or `password`.", - "PURPOSE": "Resolve login form input using semantic label text plus generic visible-input fallbacks." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._find_login_field_locator:Function]\n # @PURPOSE: Resolve login form input using semantic label text plus generic visible-input fallbacks.\n # @PRE: field_name is `username` or `password`.\n # @POST: Returns a locator for the corresponding input or None.\n async def _find_login_field_locator(self, page, field_name: str) -> Any:\n normalized = str(field_name or \"\").strip().lower()\n for root in self._iter_login_roots(page):\n if normalized == \"username\":\n input_candidates = [\n root.get_by_label(\"Username\", exact=False),\n root.get_by_label(\"Login\", exact=False),\n root.locator(\"label:text-matches('Username|Login', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Username|Login/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='username']\"),\n root.locator(\"input#username\"),\n root.locator(\"input[placeholder*='Username']\"),\n root.locator(\"input[type='text']\"),\n root.locator(\"input:not([type='password'])\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n if normalized == \"password\":\n input_candidates = [\n root.get_by_label(\"Password\", exact=False),\n root.locator(\"label:text-matches('Password', 'i')\").locator(\"xpath=following::input[1]\"),\n root.locator(\"text=/Password/i\").locator(\"xpath=following::input[1]\"),\n root.locator(\"input[name='password']\"),\n root.locator(\"input#password\"),\n root.locator(\"input[placeholder*='Password']\"),\n root.locator(\"input[type='password']\"),\n ]\n locator = await self._find_first_visible_locator(input_candidates)\n if locator:\n return locator\n\n return None\n # [/DEF:ScreenshotService._find_login_field_locator:Function]\n" - }, - { - "contract_id": "ScreenshotService._find_submit_locator", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 233, - "end_line": 251, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns visible submit locator or None.", - "PRE": "page is ready for login interaction.", - "PURPOSE": "Resolve login submit button from main page or embedded auth frame." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._find_submit_locator:Function]\n # @PURPOSE: Resolve login submit button from main page or embedded auth frame.\n # @PRE: page is ready for login interaction.\n # @POST: Returns visible submit locator or None.\n async def _find_submit_locator(self, page) -> Any:\n selectors = [\n lambda root: root.get_by_role(\"button\", name=\"Sign in\", exact=False),\n lambda root: root.get_by_role(\"button\", name=\"Login\", exact=False),\n lambda root: root.locator(\"button[type='submit']\"),\n lambda root: root.locator(\"button#submit\"),\n lambda root: root.locator(\".btn-primary\"),\n lambda root: root.locator(\"input[type='submit']\"),\n ]\n for root in self._iter_login_roots(page):\n locator = await self._find_first_visible_locator([factory(root) for factory in selectors])\n if locator:\n return locator\n return None\n # [/DEF:ScreenshotService._find_submit_locator:Function]\n" - }, - { - "contract_id": "ScreenshotService._goto_resilient", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 253, - "end_line": 272, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns last navigation response or raises when both primary and fallback waits fail.", - "PRE": "page is a valid Playwright page and url is non-empty.", - "PURPOSE": "Navigate without relying on networkidle for pages with long-polling or persistent requests." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService._goto_resilient:Function]\n # @PURPOSE: Navigate without relying on networkidle for pages with long-polling or persistent requests.\n # @PRE: page is a valid Playwright page and url is non-empty.\n # @POST: Returns last navigation response or raises when both primary and fallback waits fail.\n async def _goto_resilient(\n self,\n page,\n url: str,\n primary_wait_until: str = \"domcontentloaded\",\n fallback_wait_until: str = \"load\",\n timeout: int = 60000,\n ):\n try:\n return await page.goto(url, wait_until=primary_wait_until, timeout=timeout)\n except Exception as primary_error:\n logger.warning(\n f\"[ScreenshotService._goto_resilient] Primary navigation wait '{primary_wait_until}' failed for {url}: {primary_error}\"\n )\n return await page.goto(url, wait_until=fallback_wait_until, timeout=timeout)\n # [/DEF:ScreenshotService._goto_resilient:Function]\n" - }, - { - "contract_id": "ScreenshotService.capture_dashboard", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 274, - "end_line": 674, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns True if screenshot is saved successfully.", - "PRE": "dashboard_id is a valid string, output_path is a writable path.", - "PURPOSE": "Captures a full-page screenshot of a dashboard using Playwright and CDP.", - "SIDE_EFFECT": "Launches a browser, performs UI login, switches tabs, and writes a PNG file.", - "UX_STATE": "[Capturing] -> Executing CDP screenshot" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "UX_STATE", - "message": "@UX_STATE is not allowed for contract type 'Function'", - "detail": { - "actual_type": "Function", - "allowed_types": [ - "Component" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "UX_STATE", - "message": "@UX_STATE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ScreenshotService.capture_dashboard:Function]\n # @PURPOSE: Captures a full-page screenshot of a dashboard using Playwright and CDP.\n # @PRE: dashboard_id is a valid string, output_path is a writable path.\n # @POST: Returns True if screenshot is saved successfully.\n # @SIDE_EFFECT: Launches a browser, performs UI login, switches tabs, and writes a PNG file.\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n with belief_scope(\"capture_dashboard\", f\"dashboard_id={dashboard_id}\"):\n logger.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n async with async_playwright() as p:\n browser = await p.chromium.launch(\n headless=True,\n args=[\n \"--disable-blink-features=AutomationControlled\",\n \"--disable-infobars\",\n \"--no-sandbox\"\n ]\n )\n # Set a realistic user agent to avoid 403 Forbidden from OpenResty/WAF\n user_agent = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"\n # Construct base UI URL from environment (strip /api/v1 suffix)\n base_ui_url = self.env.url.rstrip(\"/\")\n if base_ui_url.endswith(\"/api/v1\"):\n base_ui_url = base_ui_url[:-len(\"/api/v1\")]\n \n # Create browser context with realistic headers\n context = await browser.new_context(\n viewport={'width': 1280, 'height': 720},\n user_agent=user_agent,\n extra_http_headers={\n \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n \"Accept-Language\": \"ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7\",\n \"Upgrade-Insecure-Requests\": \"1\",\n \"Sec-Fetch-Dest\": \"document\",\n \"Sec-Fetch-Mode\": \"navigate\",\n \"Sec-Fetch-Site\": \"none\",\n \"Sec-Fetch-User\": \"?1\"\n }\n )\n logger.info(\"Browser context created successfully\")\n\n page = await context.new_page()\n # Bypass navigator.webdriver detection\n await page.add_init_script(\"delete Object.getPrototypeOf(navigator).webdriver\")\n\n # 1. Navigate to login page and authenticate\n login_url = f\"{base_ui_url.rstrip('/')}/login/\"\n logger.info(f\"[DEBUG] Navigating to login page: {login_url}\")\n \n response = await self._goto_resilient(\n page,\n login_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n if response:\n logger.info(f\"[DEBUG] Login page response status: {response.status}\")\n \n # Wait for login form to be ready\n await page.wait_for_load_state(\"domcontentloaded\")\n \n # More exhaustive list of selectors for various Superset versions/themes\n selectors = {\n \"username\": ['input[name=\"username\"]', 'input#username', 'input[placeholder*=\"Username\"]', 'input[type=\"text\"]'],\n \"password\": ['input[name=\"password\"]', 'input#password', 'input[placeholder*=\"Password\"]', 'input[type=\"password\"]'],\n \"submit\": ['button[type=\"submit\"]', 'button#submit', '.btn-primary', 'input[type=\"submit\"]']\n }\n logger.info(\"[DEBUG] Attempting to find login form elements...\")\n \n try:\n used_direct_form_login = False\n # Find and fill username\n username_locator = await self._find_login_field_locator(page, \"username\")\n\n if not username_locator:\n roots = self._iter_login_roots(page)\n logger.info(f\"[DEBUG] Found {len(roots)} login roots including child frames\")\n for root_index, root in enumerate(roots[:5]):\n all_inputs = await root.locator('input').all()\n logger.info(f\"[DEBUG] Root {root_index}: found {len(all_inputs)} input fields\")\n for i, inp in enumerate(all_inputs[:5]): # Log first 5\n inp_type = await inp.get_attribute('type')\n inp_name = await inp.get_attribute('name')\n inp_id = await inp.get_attribute('id')\n logger.info(f\"[DEBUG] Root {root_index} input {i}: type={inp_type}, name={inp_name}, id={inp_id}\")\n used_direct_form_login = await self._submit_login_via_form_post(page, login_url)\n if not used_direct_form_login:\n raise RuntimeError(\"Could not find username input field on login page\")\n username_locator = None\n \n if username_locator is not None:\n logger.info(\"[DEBUG] Filling username field\")\n await username_locator.fill(self.env.username)\n \n # Find and fill password\n password_locator = await self._find_login_field_locator(page, \"password\") if username_locator is not None else None\n\n if username_locator is not None and not password_locator:\n raise RuntimeError(\"Could not find password input field on login page\")\n \n if password_locator is not None:\n logger.info(\"[DEBUG] Filling password field\")\n await password_locator.fill(self.env.password)\n \n # Click submit\n submit_locator = await self._find_submit_locator(page) if username_locator is not None else None\n\n if username_locator is not None and not submit_locator:\n raise RuntimeError(\"Could not find submit button on login page\")\n\n if submit_locator is not None:\n logger.info(\"[DEBUG] Clicking submit button\")\n await submit_locator.click()\n \n # Wait for navigation after login\n if not used_direct_form_login:\n try:\n await page.wait_for_load_state(\"load\", timeout=30000)\n except Exception as load_wait_error:\n logger.warning(f\"[DEBUG] Login post-submit load wait timed out: {load_wait_error}\")\n \n # Check if login was successful\n if not used_direct_form_login and \"/login\" in page.url:\n # Check for error messages on page\n error_msg = await page.locator(\".alert-danger, .error-message\").text_content() if await page.locator(\".alert-danger, .error-message\").count() > 0 else \"Unknown error\"\n logger.error(f\"[DEBUG] Login failed. Still on login page. Error: {error_msg}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {error_msg}. Debug screenshot saved to {debug_path}\")\n \n logger.info(f\"[DEBUG] Login successful. Current URL: {page.url}\")\n \n # Check cookies after successful login\n page_cookies = await context.cookies()\n logger.info(f\"[DEBUG] Cookies after login: {len(page_cookies)}\")\n for c in page_cookies:\n logger.info(f\"[DEBUG] Cookie: name={c['name']}, domain={c['domain']}, value={c.get('value', '')[:20]}...\")\n \n except Exception as e:\n page_title = await page.title()\n logger.error(f\"UI Login failed. Page title: {page_title}, URL: {page.url}, Error: {str(e)}\")\n debug_path = output_path.replace(\".png\", \"_debug_failed_login.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(f\"Login failed: {str(e)}. Debug screenshot saved to {debug_path}\")\n\n # 2. Navigate to dashboard\n # @UX_STATE: [Navigating] -> Loading dashboard UI\n dashboard_url = f\"{base_ui_url.rstrip('/')}/superset/dashboard/{dashboard_id}/?standalone=true\"\n \n if base_ui_url.startswith(\"https://\") and dashboard_url.startswith(\"http://\"):\n dashboard_url = dashboard_url.replace(\"http://\", \"https://\")\n\n logger.info(f\"[DEBUG] Navigating to dashboard: {dashboard_url}\")\n \n # Dashboard pages can keep polling/network activity open indefinitely.\n response = await self._goto_resilient(\n page,\n dashboard_url,\n primary_wait_until=\"domcontentloaded\",\n fallback_wait_until=\"load\",\n timeout=60000,\n )\n \n if response:\n logger.info(f\"[DEBUG] Dashboard navigation response status: {response.status}, URL: {response.url}\")\n\n if \"/login\" in page.url:\n debug_path = output_path.replace(\".png\", \"_debug_failed_dashboard_auth.png\")\n await page.screenshot(path=debug_path)\n raise RuntimeError(\n f\"Dashboard navigation redirected to login page after authentication. Debug screenshot saved to {debug_path}\"\n )\n \n try:\n # Wait for the dashboard grid to be present\n await page.wait_for_selector('.dashboard-component, .dashboard-header, [data-test=\"dashboard-grid\"]', timeout=30000)\n logger.info(\"[DEBUG] Dashboard container loaded\")\n \n # Wait for charts to finish loading (Superset uses loading spinners/skeletons)\n # We wait until loading indicators disappear or a timeout occurs\n try:\n # Wait for loading indicators to disappear\n await page.wait_for_selector('.loading, .ant-skeleton, .spinner', state=\"hidden\", timeout=60000)\n logger.info(\"[DEBUG] Loading indicators hidden\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for loading indicators to hide\")\n\n # Wait for charts to actually render their content (e.g., ECharts, NVD3)\n # We look for common chart containers that should have content\n try:\n await page.wait_for_selector('.chart-container canvas, .slice_container svg, .superset-chart-canvas, .grid-content .chart-container', timeout=60000)\n logger.info(\"[DEBUG] Chart content detected\")\n except Exception:\n logger.warning(\"[DEBUG] Timeout waiting for chart content\")\n\n # Additional check: wait for all chart containers to have non-empty content\n logger.info(\"[DEBUG] Waiting for all charts to have rendered content...\")\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container, .slice_container');\n if (charts.length === 0) return true; // No charts to wait for\n \n // Check if all charts have rendered content (canvas, svg, or non-empty div)\n return Array.from(charts).every(chart => {\n const hasCanvas = chart.querySelector('canvas') !== null;\n const hasSvg = chart.querySelector('svg') !== null;\n const hasContent = chart.innerText.trim().length > 0 || chart.children.length > 0;\n return hasCanvas || hasSvg || hasContent;\n });\n }\"\"\", timeout=60000)\n logger.info(\"[DEBUG] All charts have rendered content\")\n\n # Scroll to bottom and back to top to trigger lazy loading of all charts\n logger.info(\"[DEBUG] Scrolling to trigger lazy loading...\")\n await page.evaluate(\"\"\"async () => {\n const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n for (let i = 0; i < document.body.scrollHeight; i += 500) {\n window.scrollTo(0, i);\n await delay(100);\n }\n window.scrollTo(0, 0);\n await delay(500);\n }\"\"\")\n\n except Exception as e:\n logger.warning(f\"[DEBUG] Dashboard content wait failed: {e}, proceeding anyway after delay\")\n \n # Final stabilization delay - increased for complex dashboards\n logger.info(\"[DEBUG] Final stabilization delay...\")\n await asyncio.sleep(15)\n\n # Logic to handle tabs and full-page capture\n try:\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE: [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading\n processed_tabs = set()\n\n async def switch_tabs(depth=0):\n if depth > 3:\n return # Limit recursion depth\n \n tab_selectors = [\n '.ant-tabs-nav-list .ant-tabs-tab',\n '.dashboard-component-tabs .ant-tabs-tab',\n '[data-test=\"dashboard-component-tabs\"] .ant-tabs-tab'\n ]\n \n found_tabs = []\n for selector in tab_selectors:\n found_tabs = await page.locator(selector).all()\n if found_tabs:\n break\n \n if found_tabs:\n logger.info(f\"[DEBUG][TabSwitching] Found {len(found_tabs)} tabs at depth {depth}\")\n for i, tab in enumerate(found_tabs):\n try:\n tab_text = (await tab.inner_text()).strip()\n tab_id = f\"{depth}_{i}_{tab_text}\"\n \n if tab_id in processed_tabs:\n continue\n \n if await tab.is_visible():\n logger.info(f\"[DEBUG][TabSwitching] Switching to tab: {tab_text}\")\n processed_tabs.add(tab_id)\n \n is_active = \"ant-tabs-tab-active\" in (await tab.get_attribute(\"class\") or \"\")\n if not is_active:\n await tab.click()\n await asyncio.sleep(2) # Wait for content to render\n \n await switch_tabs(depth + 1)\n except Exception as tab_e:\n logger.warning(f\"[DEBUG][TabSwitching] Failed to process tab {i}: {tab_e}\")\n \n try:\n first_tab = found_tabs[0]\n if \"ant-tabs-tab-active\" not in (await first_tab.get_attribute(\"class\") or \"\"):\n await first_tab.click()\n await asyncio.sleep(1)\n except Exception:\n pass\n\n await switch_tabs()\n\n # 2. Calculate full height for screenshot\n # @UX_STATE: [CalculatingHeight] -> Determining dashboard dimensions\n full_height = await page.evaluate(\"\"\"() => {\n const body = document.body;\n const html = document.documentElement;\n const dashboardContent = document.querySelector('.dashboard-content');\n \n return Math.max(\n body.scrollHeight, body.offsetHeight,\n html.clientHeight, html.scrollHeight, html.offsetHeight,\n dashboardContent ? dashboardContent.scrollHeight + 100 : 0\n );\n }\"\"\")\n logger.info(f\"[DEBUG] Calculated full height: {full_height}\")\n \n # DIAGNOSTIC: Count chart elements before resize\n chart_count_before = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements BEFORE viewport resize: {chart_count_before}\")\n \n # DIAGNOSTIC: Capture pre-resize screenshot for comparison\n pre_resize_path = output_path.replace(\".png\", \"_preresize.png\")\n try:\n await page.screenshot(path=pre_resize_path, full_page=False, timeout=10000)\n import os\n pre_resize_size = os.path.getsize(pre_resize_path) if os.path.exists(pre_resize_path) else 0\n logger.info(f\"[DIAGNOSTIC] Pre-resize screenshot saved: {pre_resize_path} ({pre_resize_size} bytes)\")\n except Exception as pre_e:\n logger.warning(f\"[DIAGNOSTIC] Failed to capture pre-resize screenshot: {pre_e}\")\n \n logger.info(f\"[DIAGNOSTIC] Resizing viewport from current to 1920x{int(full_height)}\")\n await page.set_viewport_size({\"width\": 1920, \"height\": int(full_height)})\n \n # DIAGNOSTIC: Increased wait time and log timing\n logger.info(\"[DIAGNOSTIC] Waiting 10 seconds after viewport resize for re-render...\")\n await asyncio.sleep(10)\n logger.info(\"[DIAGNOSTIC] Wait completed\")\n \n # DIAGNOSTIC: Count chart elements after resize and wait\n chart_count_after = await page.evaluate(\"\"\"() => {\n return {\n chartContainers: document.querySelectorAll('.chart-container, .slice_container').length,\n canvasElements: document.querySelectorAll('canvas').length,\n svgElements: document.querySelectorAll('.chart-container svg, .slice_container svg').length,\n visibleCharts: document.querySelectorAll('.chart-container:visible, .slice_container:visible').length\n };\n }\"\"\")\n logger.info(f\"[DIAGNOSTIC] Chart elements AFTER viewport resize + wait: {chart_count_after}\")\n \n # DIAGNOSTIC: Check if any charts have error states\n chart_errors = await page.evaluate(\"\"\"() => {\n const errors = [];\n document.querySelectorAll('.chart-container, .slice_container').forEach((chart, i) => {\n const errorEl = chart.querySelector('.error, .alert-danger, .ant-alert-error');\n if (errorEl) {\n errors.push({index: i, text: errorEl.innerText.substring(0, 100)});\n }\n });\n return errors;\n }\"\"\")\n if chart_errors:\n logger.warning(f\"[DIAGNOSTIC] Charts with error states detected: {chart_errors}\")\n else:\n logger.info(\"[DIAGNOSTIC] No chart error states detected\")\n\n # 3. Take screenshot using CDP to bypass Playwright's font loading wait\n # @UX_STATE: [Capturing] -> Executing CDP screenshot\n logger.info(\"[DEBUG] Attempting full-page screenshot via CDP...\")\n cdp = await page.context.new_cdp_session(page)\n \n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n \n image_data = base64.b64decode(screenshot_data[\"data\"])\n \n with open(output_path, 'wb') as f:\n f.write(image_data)\n \n # DIAGNOSTIC: Verify screenshot file\n import os\n final_size = os.path.getsize(output_path) if os.path.exists(output_path) else 0\n logger.info(f\"[DIAGNOSTIC] Final screenshot saved: {output_path}\")\n logger.info(f\"[DIAGNOSTIC] Final screenshot size: {final_size} bytes ({final_size / 1024:.2f} KB)\")\n \n # DIAGNOSTIC: Get image dimensions\n try:\n with Image.open(output_path) as final_img:\n logger.info(f\"[DIAGNOSTIC] Final screenshot dimensions: {final_img.width}x{final_img.height}\")\n except Exception as img_err:\n logger.warning(f\"[DIAGNOSTIC] Could not read final image dimensions: {img_err}\")\n \n logger.info(f\"Full-page screenshot saved to {output_path} (via CDP)\")\n except Exception as e:\n logger.error(f\"[DEBUG] Full-page/Tab capture failed: {e}\")\n try:\n await page.screenshot(path=output_path, full_page=True, timeout=10000)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=5000)\n \n await browser.close()\n return True\n # [/DEF:ScreenshotService.capture_dashboard:Function]\n" + "anchor_syntax": "region", + "body": "# #region ScreenshotService [TYPE Class]\n# @BRIEF Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # region ScreenshotService.__init__ [TYPE Function]\n # @PURPOSE: Initializes the ScreenshotService with environment configuration.\n # @PRE: env is a valid Environment object.\n def __init__(self, env: Environment):\n self.env = env\n # endregion ScreenshotService.__init__\n\n # region ScreenshotService._find_first_visible_locator [TYPE Function]\n # @PURPOSE: Resolve the first visible locator from multiple Playwright locator strategies.\n # @PRE: candidates is a non-empty list of locator-like objects.\n # @POST: Returns a locator ready for interaction or None when nothing matches.\n async def _find_first_visible_locator(self, candidates) -> Any:\n for locator in candidates:\n try:\n match_count = await locator.count()\n for index in range(match_count):\n candidate = locator.nth(index)\n if await candidate.is_visible():\n return candidate\n except Exception:\n continue\n return None\n # endregion ScreenshotService._find_first_visible_locator\n\n # region ScreenshotService._iter_login_roots [TYPE Function]\n # @PURPOSE: Enumerate page and child frames where login controls may be rendered.\n # @PRE: page is a Playwright page-like object.\n # @POST: Returns ordered roots starting with main page followed by frames.\n def _iter_login_roots(self, page) -> list[Any]:\n roots = [page]\n page_frames = getattr(page, \"frames\", [])\n try:\n for frame in page_frames:\n if frame not in roots:\n roots.append(frame)\n except Exception:\n pass\n return roots\n # endregion ScreenshotService._iter_login_roots\n\n # region ScreenshotService._extract_hidden_login_fields [TYPE Function]\n # @PURPOSE: Collect hidden form fields required for direct login POST fallback.\n # @PRE: Login page is loaded.\n # @POST: Returns hidden input name/value mapping aggregated from page and child frames.\n async def _extract_hidden_login_fields(self, page) -> dict[str, str]:\n hidden_fields: dict[str, str] = {}\n for root in self._iter_login_roots(page):\n try:\n locator = root.locator(\"input[type='hidden'][name]\")\n count = await locator.count()\n for index in range(count):\n candidate = locator.nth(index)\n field_name = str(await candidate.get_attribute(\"name\") or \"\").strip()\n if not field_name or field_name in hidden_fields:\n continue\n hidden_fields[field_name] = str(await candidate.input_value()).strip()\n except Exception:\n continue\n return hidden_fields\n # endregion ScreenshotService._extract_hidden_login_fields\n\n # region ScreenshotService._extract_csrf_token [TYPE Function]\n # @PURPOSE: Resolve CSRF token value from main page or embedded login frame.\n # @PRE: Login page is loaded.\n # @POST: Returns first non-empty csrf token or empty string.\n async def _extract_csrf_token(self, page) -> str:\n hidden_fields = await self._extract_hidden_login_fields(page)\n return str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n # endregion ScreenshotService._extract_csrf_token\n\n # region ScreenshotService._response_looks_like_login_page [TYPE Function]\n # @PURPOSE: Detect when fallback login POST returned the login form again instead of an authenticated page.\n # @PRE: response_text is normalized HTML or text from login POST response.\n # @POST: Returns True when login-page markers dominate the response body.\n def _response_looks_like_login_page(self, response_text: str) -> bool:\n normalized = str(response_text or \"\").strip().lower()\n if not normalized:\n return False\n\n markers = [\n \"enter your login and password below\",\n \"username:\",\n \"password:\",\n \"sign in\",\n 'name=\"csrf_token\"',\n ]\n return sum(marker in normalized for marker in markers) >= 3\n # endregion ScreenshotService._response_looks_like_login_page\n\n # region ScreenshotService._redirect_looks_authenticated [TYPE Function]\n # @PURPOSE: Treat non-login redirects after form POST as successful authentication without waiting for redirect target.\n # @PRE: redirect_location may be empty or relative.\n # @POST: Returns True when redirect target does not point back to login flow.\n def _redirect_looks_authenticated(self, redirect_location: str) -> bool:\n normalized = str(redirect_location or \"\").strip().lower()\n if not normalized:\n return True\n return \"/login\" not in normalized\n # endregion ScreenshotService._redirect_looks_authenticated\n\n # region ScreenshotService._submit_login_via_form_post [TYPE Function]\n # @PURPOSE: Fallback login path that submits credentials directly with csrf token.\n # @PRE: login_url is same-origin and csrf token can be read from DOM.\n # @POST: Browser context receives authenticated cookies when login succeeds.\n async def _submit_login_via_form_post(self, page, login_url: str) -> bool:\n hidden_fields = await self._extract_hidden_login_fields(page)\n csrf_token = str(hidden_fields.get(\"csrf_token\") or \"\").strip()\n if not csrf_token:\n logger.warning(\"[DEBUG] Direct form login fallback skipped: csrf_token not found\")\n return False\n\n try:\n request_context = page.context.request\n except Exception as context_error:\n logger.warning(f\"[DEBUG] Direct form login fallback skipped: request context unavailable: {context_error}\")\n return False\n\n parsed_url = urlsplit(login_url)\n origin = f\"{parsed_url.scheme}://{parsed_url.netloc}\" if parsed_url.scheme and parsed_url.netloc else login_url\n payload = dict(hidden_fields)\n payload[\"username\"] = self.env.username\n payload[\"password\"] = self.env.password\n logger.info(\n f\"[DEBUG] Attempting direct form login fallback via browser context request with hidden fields: {sorted(hidden_fields.keys())}\"\n )\n\n response = await request_context.post(\n login_url,\n form=payload,\n headers={\n \"Origin\": origin,\n \"Referer\": login_url,\n },\n timeout=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" }, { "contract_id": "LLMClient", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 677, - "end_line": 951, + "start_line": 679, + "end_line": 976, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -65237,277 +51455,20 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:LLMClient:Class]\n# @PURPOSE: Wrapper for LLM provider APIs.\nclass LLMClient:\n # [DEF:LLMClient.__init__:Function]\n # @PURPOSE: Initializes the LLMClient with provider settings.\n # @PRE: api_key, base_url, and default_model are non-empty strings.\n def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):\n self.provider_type = provider_type\n normalized_key = (api_key or \"\").strip()\n if normalized_key.lower().startswith(\"bearer \"):\n normalized_key = normalized_key[7:].strip()\n self.api_key = normalized_key\n self.base_url = base_url\n self.default_model = default_model\n \n # DEBUG: Log initialization parameters (without exposing full API key)\n logger.info(\"[LLMClient.__init__] Initializing LLM client:\")\n logger.info(f\"[LLMClient.__init__] Provider Type: {provider_type}\")\n logger.info(f\"[LLMClient.__init__] Base URL: {base_url}\")\n logger.info(f\"[LLMClient.__init__] Default Model: {default_model}\")\n logger.info(f\"[LLMClient.__init__] API Key (first 8 chars): {self.api_key[:8] if self.api_key and len(self.api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n logger.info(f\"[LLMClient.__init__] API Key Length: {len(self.api_key) if self.api_key else 0}\")\n\n # Some OpenAI-compatible gateways are strict about auth header naming.\n default_headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n if self.provider_type == LLMProviderType.OPENROUTER:\n default_headers[\"HTTP-Referer\"] = (\n os.getenv(\"OPENROUTER_SITE_URL\", \"\").strip()\n or os.getenv(\"APP_BASE_URL\", \"\").strip()\n or \"http://localhost:8000\"\n )\n default_headers[\"X-Title\"] = os.getenv(\"OPENROUTER_APP_NAME\", \"\").strip() or \"ss-tools\"\n if self.provider_type == LLMProviderType.KILO:\n default_headers[\"Authentication\"] = f\"Bearer {self.api_key}\"\n default_headers[\"X-API-Key\"] = self.api_key\n\n http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)\n self.client = AsyncOpenAI(\n api_key=self.api_key,\n base_url=base_url,\n default_headers=default_headers,\n http_client=http_client,\n )\n # [/DEF:LLMClient.__init__:Function]\n\n # [DEF:LLMClient._supports_json_response_format:Function]\n # @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object.\n # @PRE: Client initialized with base_url and default_model.\n # @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors.\n def _supports_json_response_format(self) -> bool:\n base = (self.base_url or \"\").lower()\n model = (self.default_model or \"\").lower()\n\n # OpenRouter routes to many upstream providers; some models reject json_object mode.\n if \"openrouter.ai\" in base:\n incompatible_tokens = (\n \"stepfun/\",\n \"step-\",\n \":free\",\n )\n if any(token in model for token in incompatible_tokens):\n return False\n return True\n # [/DEF:LLMClient._supports_json_response_format:Function]\n\n # [DEF:LLMClient.get_json_completion:Function]\n # @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing.\n # @PRE: messages is a list of valid message dictionaries.\n # @POST: Returns a parsed JSON dictionary.\n # @SIDE_EFFECT: Calls external LLM API.\n def _should_retry(exception: Exception) -> bool:\n \"\"\"Custom retry predicate that excludes authentication errors.\"\"\"\n # Don't retry on authentication errors\n if isinstance(exception, OpenAIAuthenticationError):\n return False\n # Retry on rate limit errors and other exceptions\n return isinstance(exception, (RateLimitError, Exception))\n \n @retry(\n stop=stop_after_attempt(5),\n wait=wait_exponential(multiplier=2, min=5, max=60),\n retry=retry_if_exception(_should_retry),\n reraise=True\n )\n async def get_json_completion(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:\n with belief_scope(\"get_json_completion\"):\n response = None\n try:\n use_json_mode = self._supports_json_response_format()\n try:\n logger.info(\n f\"[get_json_completion] Attempting LLM call for model: {self.default_model} \"\n f\"(json_mode={'on' if use_json_mode else 'off'})\"\n )\n logger.info(f\"[get_json_completion] Base URL being used: {self.base_url}\")\n logger.info(f\"[get_json_completion] Number of messages: {len(messages)}\")\n logger.info(f\"[get_json_completion] API Key present: {bool(self.api_key and len(self.api_key) > 0)}\")\n\n if use_json_mode:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages,\n response_format={\"type\": \"json_object\"}\n )\n else:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n except Exception as e:\n if use_json_mode and (\n \"JSON mode is not enabled\" in str(e)\n or \"json_object is not supported\" in str(e).lower()\n or \"response_format\" in str(e).lower()\n or \"400\" in str(e)\n ):\n logger.warning(f\"[get_json_completion] JSON mode failed or not supported: {str(e)}. Falling back to plain text response.\")\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n else:\n raise e\n \n logger.debug(f\"[get_json_completion] LLM Response: {response}\")\n except OpenAIAuthenticationError as e:\n logger.error(f\"[get_json_completion] Authentication error: {str(e)}\")\n # Do not retry on auth errors - re-raise to stop retry\n raise\n except RateLimitError as e:\n logger.warning(f\"[get_json_completion] Rate limit hit: {str(e)}\")\n \n # Extract retry_delay from error metadata if available\n retry_delay = 5.0 # Default fallback\n try:\n # Based on logs, the raw response is in e.body or e.response.json()\n # The logs show 'metadata': {'raw': '...'} which suggests a proxy or specific client wrapper\n # Let's try to find the 'retryDelay' in the error message or response\n import re\n \n # Try to find \"retryDelay\": \"XXs\" in the string representation of the error\n error_str = str(e)\n match = re.search(r'\"retryDelay\":\\s*\"(\\d+)s\"', error_str)\n if match:\n retry_delay = float(match.group(1))\n else:\n # Try to parse from response if it's a standard OpenAI-like error with body\n if hasattr(e, 'body') and isinstance(e.body, dict):\n # Some providers put it in details\n details = e.body.get('error', {}).get('details', [])\n for detail in details:\n if detail.get('@type') == 'type.googleapis.com/google.rpc.RetryInfo':\n delay_str = detail.get('retryDelay', '5s')\n retry_delay = float(delay_str.rstrip('s'))\n break\n except Exception as parse_e:\n logger.debug(f\"[get_json_completion] Failed to parse retry delay: {parse_e}\")\n \n # Add a small safety margin (0.5s) as requested\n wait_time = retry_delay + 0.5\n logger.info(f\"[get_json_completion] Waiting for {wait_time}s before retry...\")\n await asyncio.sleep(wait_time)\n raise\n except Exception as e:\n logger.error(f\"[get_json_completion] LLM call failed: {str(e)}\")\n raise\n\n if not response or not hasattr(response, 'choices') or not response.choices:\n raise RuntimeError(f\"Invalid LLM response: {response}\")\n\n content = response.choices[0].message.content\n logger.debug(f\"[get_json_completion] Raw content to parse: {content}\")\n \n try:\n return json.loads(content)\n except json.JSONDecodeError:\n logger.warning(\"[get_json_completion] Failed to parse JSON directly, attempting to extract from code blocks\")\n if \"```json\" in content:\n json_str = content.split(\"```json\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n elif \"```\" in content:\n json_str = content.split(\"```\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n else:\n raise\n # [/DEF:LLMClient.get_json_completion:Function]\n\n # [DEF:LLMClient.test_runtime_connection:Function]\n # @PURPOSE: Validate provider credentials using the same chat completions transport as runtime analysis.\n # @PRE: Client is initialized with provider credentials and default_model.\n # @POST: Returns lightweight JSON payload when runtime auth/model path is valid.\n # @SIDE_EFFECT: Calls external LLM API.\n async def test_runtime_connection(self) -> Dict[str, Any]:\n with belief_scope(\"test_runtime_connection\"):\n messages = [\n {\n \"role\": \"user\",\n \"content\": 'Return exactly this JSON object and nothing else: {\"ok\": true}',\n }\n ]\n return await self.get_json_completion(messages)\n # [/DEF:LLMClient.test_runtime_connection:Function]\n\n # [DEF:LLMClient.analyze_dashboard:Function]\n # @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.\n # @PRE: screenshot_path exists, logs is a list of strings.\n # @POST: Returns a structured analysis dictionary (status, summary, issues).\n # @SIDE_EFFECT: Reads screenshot file and calls external LLM API.\n async def analyze_dashboard(\n self,\n screenshot_path: str,\n logs: List[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n ) -> Dict[str, Any]:\n with belief_scope(\"analyze_dashboard\"):\n # Optimize image to reduce token count (US1 / T023)\n # Gemini/Gemma models have limits on input tokens, and large images contribute significantly.\n try:\n with Image.open(screenshot_path) as img:\n # Convert to RGB if necessary\n if img.mode in (\"RGBA\", \"P\"):\n img = img.convert(\"RGB\")\n \n # Resize if too large (max 1024px width while maintaining aspect ratio)\n # We reduce width further to 1024px to stay within token limits for long dashboards\n max_width = 1024\n if img.width > max_width or img.height > 2048:\n # Calculate scaling factor to fit within 1024x2048\n scale = min(max_width / img.width, 2048 / img.height)\n if scale < 1.0:\n new_width = int(img.width * scale)\n new_height = int(img.height * scale)\n img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)\n logger.info(f\"[analyze_dashboard] Resized image from {img.width}x{img.height} to {new_width}x{new_height}\")\n \n # Compress and convert to base64\n buffer = io.BytesIO()\n # Lower quality to 60% to further reduce payload size\n img.save(buffer, format=\"JPEG\", quality=60, optimize=True)\n base_64_image = base64.b64encode(buffer.getvalue()).decode('utf-8')\n logger.info(f\"[analyze_dashboard] Optimized image size: {len(buffer.getvalue()) / 1024:.2f} KB\")\n except Exception as img_e:\n logger.warning(f\"[analyze_dashboard] Image optimization failed: {img_e}. Using raw image.\")\n with open(screenshot_path, \"rb\") as image_file:\n base_64_image = base64.b64encode(image_file.read()).decode('utf-8')\n\n log_text = \"\\n\".join(logs)\n prompt = render_prompt(prompt_template, {\"logs\": log_text})\n \n messages = [\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": prompt},\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": f\"data:image/jpeg;base64,{base_64_image}\"\n }\n }\n ]\n }\n ]\n \n try:\n return await self.get_json_completion(messages)\n except Exception as e:\n logger.error(f\"[analyze_dashboard] Failed to get analysis: {str(e)}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {str(e)}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # [/DEF:LLMClient.analyze_dashboard:Function]\n# [/DEF:LLMClient:Class]\n" - }, - { - "contract_id": "LLMClient.__init__", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 680, - "end_line": 720, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PRE": "api_key, base_url, and default_model are non-empty strings.", - "PURPOSE": "Initializes the LLMClient with provider settings." - }, - "relations": [], - "schema_warnings": [ - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:LLMClient.__init__:Function]\n # @PURPOSE: Initializes the LLMClient with provider settings.\n # @PRE: api_key, base_url, and default_model are non-empty strings.\n def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):\n self.provider_type = provider_type\n normalized_key = (api_key or \"\").strip()\n if normalized_key.lower().startswith(\"bearer \"):\n normalized_key = normalized_key[7:].strip()\n self.api_key = normalized_key\n self.base_url = base_url\n self.default_model = default_model\n \n # DEBUG: Log initialization parameters (without exposing full API key)\n logger.info(\"[LLMClient.__init__] Initializing LLM client:\")\n logger.info(f\"[LLMClient.__init__] Provider Type: {provider_type}\")\n logger.info(f\"[LLMClient.__init__] Base URL: {base_url}\")\n logger.info(f\"[LLMClient.__init__] Default Model: {default_model}\")\n logger.info(f\"[LLMClient.__init__] API Key (first 8 chars): {self.api_key[:8] if self.api_key and len(self.api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n logger.info(f\"[LLMClient.__init__] API Key Length: {len(self.api_key) if self.api_key else 0}\")\n\n # Some OpenAI-compatible gateways are strict about auth header naming.\n default_headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n if self.provider_type == LLMProviderType.OPENROUTER:\n default_headers[\"HTTP-Referer\"] = (\n os.getenv(\"OPENROUTER_SITE_URL\", \"\").strip()\n or os.getenv(\"APP_BASE_URL\", \"\").strip()\n or \"http://localhost:8000\"\n )\n default_headers[\"X-Title\"] = os.getenv(\"OPENROUTER_APP_NAME\", \"\").strip() or \"ss-tools\"\n if self.provider_type == LLMProviderType.KILO:\n default_headers[\"Authentication\"] = f\"Bearer {self.api_key}\"\n default_headers[\"X-API-Key\"] = self.api_key\n\n http_client = httpx.AsyncClient(headers=default_headers, timeout=120.0)\n self.client = AsyncOpenAI(\n api_key=self.api_key,\n base_url=base_url,\n default_headers=default_headers,\n http_client=http_client,\n )\n # [/DEF:LLMClient.__init__:Function]\n" - }, - { - "contract_id": "LLMClient._supports_json_response_format", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 722, - "end_line": 740, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns False for known-incompatible combinations to avoid avoidable 400 errors.", - "PRE": "Client initialized with base_url and default_model.", - "PURPOSE": "Detect whether provider/model is likely compatible with response_format=json_object." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:LLMClient._supports_json_response_format:Function]\n # @PURPOSE: Detect whether provider/model is likely compatible with response_format=json_object.\n # @PRE: Client initialized with base_url and default_model.\n # @POST: Returns False for known-incompatible combinations to avoid avoidable 400 errors.\n def _supports_json_response_format(self) -> bool:\n base = (self.base_url or \"\").lower()\n model = (self.default_model or \"\").lower()\n\n # OpenRouter routes to many upstream providers; some models reject json_object mode.\n if \"openrouter.ai\" in base:\n incompatible_tokens = (\n \"stepfun/\",\n \"step-\",\n \":free\",\n )\n if any(token in model for token in incompatible_tokens):\n return False\n return True\n # [/DEF:LLMClient._supports_json_response_format:Function]\n" - }, - { - "contract_id": "LLMClient.get_json_completion", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 742, - "end_line": 862, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns a parsed JSON dictionary.", - "PRE": "messages is a list of valid message dictionaries.", - "PURPOSE": "Helper to handle LLM calls with JSON mode and fallback parsing.", - "SIDE_EFFECT": "Calls external LLM 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" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:LLMClient.get_json_completion:Function]\n # @PURPOSE: Helper to handle LLM calls with JSON mode and fallback parsing.\n # @PRE: messages is a list of valid message dictionaries.\n # @POST: Returns a parsed JSON dictionary.\n # @SIDE_EFFECT: Calls external LLM API.\n def _should_retry(exception: Exception) -> bool:\n \"\"\"Custom retry predicate that excludes authentication errors.\"\"\"\n # Don't retry on authentication errors\n if isinstance(exception, OpenAIAuthenticationError):\n return False\n # Retry on rate limit errors and other exceptions\n return isinstance(exception, (RateLimitError, Exception))\n \n @retry(\n stop=stop_after_attempt(5),\n wait=wait_exponential(multiplier=2, min=5, max=60),\n retry=retry_if_exception(_should_retry),\n reraise=True\n )\n async def get_json_completion(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:\n with belief_scope(\"get_json_completion\"):\n response = None\n try:\n use_json_mode = self._supports_json_response_format()\n try:\n logger.info(\n f\"[get_json_completion] Attempting LLM call for model: {self.default_model} \"\n f\"(json_mode={'on' if use_json_mode else 'off'})\"\n )\n logger.info(f\"[get_json_completion] Base URL being used: {self.base_url}\")\n logger.info(f\"[get_json_completion] Number of messages: {len(messages)}\")\n logger.info(f\"[get_json_completion] API Key present: {bool(self.api_key and len(self.api_key) > 0)}\")\n\n if use_json_mode:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages,\n response_format={\"type\": \"json_object\"}\n )\n else:\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n except Exception as e:\n if use_json_mode and (\n \"JSON mode is not enabled\" in str(e)\n or \"json_object is not supported\" in str(e).lower()\n or \"response_format\" in str(e).lower()\n or \"400\" in str(e)\n ):\n logger.warning(f\"[get_json_completion] JSON mode failed or not supported: {str(e)}. Falling back to plain text response.\")\n response = await self.client.chat.completions.create(\n model=self.default_model,\n messages=messages\n )\n else:\n raise e\n \n logger.debug(f\"[get_json_completion] LLM Response: {response}\")\n except OpenAIAuthenticationError as e:\n logger.error(f\"[get_json_completion] Authentication error: {str(e)}\")\n # Do not retry on auth errors - re-raise to stop retry\n raise\n except RateLimitError as e:\n logger.warning(f\"[get_json_completion] Rate limit hit: {str(e)}\")\n \n # Extract retry_delay from error metadata if available\n retry_delay = 5.0 # Default fallback\n try:\n # Based on logs, the raw response is in e.body or e.response.json()\n # The logs show 'metadata': {'raw': '...'} which suggests a proxy or specific client wrapper\n # Let's try to find the 'retryDelay' in the error message or response\n import re\n \n # Try to find \"retryDelay\": \"XXs\" in the string representation of the error\n error_str = str(e)\n match = re.search(r'\"retryDelay\":\\s*\"(\\d+)s\"', error_str)\n if match:\n retry_delay = float(match.group(1))\n else:\n # Try to parse from response if it's a standard OpenAI-like error with body\n if hasattr(e, 'body') and isinstance(e.body, dict):\n # Some providers put it in details\n details = e.body.get('error', {}).get('details', [])\n for detail in details:\n if detail.get('@type') == 'type.googleapis.com/google.rpc.RetryInfo':\n delay_str = detail.get('retryDelay', '5s')\n retry_delay = float(delay_str.rstrip('s'))\n break\n except Exception as parse_e:\n logger.debug(f\"[get_json_completion] Failed to parse retry delay: {parse_e}\")\n \n # Add a small safety margin (0.5s) as requested\n wait_time = retry_delay + 0.5\n logger.info(f\"[get_json_completion] Waiting for {wait_time}s before retry...\")\n await asyncio.sleep(wait_time)\n raise\n except Exception as e:\n logger.error(f\"[get_json_completion] LLM call failed: {str(e)}\")\n raise\n\n if not response or not hasattr(response, 'choices') or not response.choices:\n raise RuntimeError(f\"Invalid LLM response: {response}\")\n\n content = response.choices[0].message.content\n logger.debug(f\"[get_json_completion] Raw content to parse: {content}\")\n \n try:\n return json.loads(content)\n except json.JSONDecodeError:\n logger.warning(\"[get_json_completion] Failed to parse JSON directly, attempting to extract from code blocks\")\n if \"```json\" in content:\n json_str = content.split(\"```json\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n elif \"```\" in content:\n json_str = content.split(\"```\")[1].split(\"```\")[0].strip()\n return json.loads(json_str)\n else:\n raise\n # [/DEF:LLMClient.get_json_completion:Function]\n" - }, - { - "contract_id": "LLMClient.test_runtime_connection", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 864, - "end_line": 878, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns lightweight JSON payload when runtime auth/model path is valid.", - "PRE": "Client is initialized with provider credentials and default_model.", - "PURPOSE": "Validate provider credentials using the same chat completions transport as runtime analysis.", - "SIDE_EFFECT": "Calls external LLM 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" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:LLMClient.test_runtime_connection:Function]\n # @PURPOSE: Validate provider credentials using the same chat completions transport as runtime analysis.\n # @PRE: Client is initialized with provider credentials and default_model.\n # @POST: Returns lightweight JSON payload when runtime auth/model path is valid.\n # @SIDE_EFFECT: Calls external LLM API.\n async def test_runtime_connection(self) -> Dict[str, Any]:\n with belief_scope(\"test_runtime_connection\"):\n messages = [\n {\n \"role\": \"user\",\n \"content\": 'Return exactly this JSON object and nothing else: {\"ok\": true}',\n }\n ]\n return await self.get_json_completion(messages)\n # [/DEF:LLMClient.test_runtime_connection:Function]\n" - }, - { - "contract_id": "LLMClient.analyze_dashboard", - "contract_type": "Function", - "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 880, - "end_line": 950, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns a structured analysis dictionary (status, summary, issues).", - "PRE": "screenshot_path exists, logs is a list of strings.", - "PURPOSE": "Sends dashboard data (screenshot + logs) to LLM for health analysis.", - "SIDE_EFFECT": "Reads screenshot file and calls external LLM 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" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:LLMClient.analyze_dashboard:Function]\n # @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.\n # @PRE: screenshot_path exists, logs is a list of strings.\n # @POST: Returns a structured analysis dictionary (status, summary, issues).\n # @SIDE_EFFECT: Reads screenshot file and calls external LLM API.\n async def analyze_dashboard(\n self,\n screenshot_path: str,\n logs: List[str],\n prompt_template: str = DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n ) -> Dict[str, Any]:\n with belief_scope(\"analyze_dashboard\"):\n # Optimize image to reduce token count (US1 / T023)\n # Gemini/Gemma models have limits on input tokens, and large images contribute significantly.\n try:\n with Image.open(screenshot_path) as img:\n # Convert to RGB if necessary\n if img.mode in (\"RGBA\", \"P\"):\n img = img.convert(\"RGB\")\n \n # Resize if too large (max 1024px width while maintaining aspect ratio)\n # We reduce width further to 1024px to stay within token limits for long dashboards\n max_width = 1024\n if img.width > max_width or img.height > 2048:\n # Calculate scaling factor to fit within 1024x2048\n scale = min(max_width / img.width, 2048 / img.height)\n if scale < 1.0:\n new_width = int(img.width * scale)\n new_height = int(img.height * scale)\n img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)\n logger.info(f\"[analyze_dashboard] Resized image from {img.width}x{img.height} to {new_width}x{new_height}\")\n \n # Compress and convert to base64\n buffer = io.BytesIO()\n # Lower quality to 60% to further reduce payload size\n img.save(buffer, format=\"JPEG\", quality=60, optimize=True)\n base_64_image = base64.b64encode(buffer.getvalue()).decode('utf-8')\n logger.info(f\"[analyze_dashboard] Optimized image size: {len(buffer.getvalue()) / 1024:.2f} KB\")\n except Exception as img_e:\n logger.warning(f\"[analyze_dashboard] Image optimization failed: {img_e}. Using raw image.\")\n with open(screenshot_path, \"rb\") as image_file:\n base_64_image = base64.b64encode(image_file.read()).decode('utf-8')\n\n log_text = \"\\n\".join(logs)\n prompt = render_prompt(prompt_template, {\"logs\": log_text})\n \n messages = [\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": prompt},\n {\n \"type\": \"image_url\",\n \"image_url\": {\n \"url\": f\"data:image/jpeg;base64,{base_64_image}\"\n }\n }\n ]\n }\n ]\n \n try:\n return await self.get_json_completion(messages)\n except Exception as e:\n logger.error(f\"[analyze_dashboard] Failed to get analysis: {str(e)}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {str(e)}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # [/DEF:LLMClient.analyze_dashboard:Function]\n" + "anchor_syntax": "region", + "body": "# #region LLMClient [TYPE Class]\n# @BRIEF Wrapper for LLM provider APIs.\nclass LLMClient:\n # region LLMClient.__init__ [TYPE Function]\n # @PURPOSE: Initializes the LLMClient with provider settings.\n # @PRE: api_key, base_url, and default_model are non-empty strings.\n def __init__(self, provider_type: LLMProviderType, api_key: str, base_url: str, default_model: str):\n self.provider_type = provider_type\n normalized_key = (api_key or \"\").strip()\n if normalized_key.lower().startswith(\"bearer \"):\n normalized_key = normalized_key[7:].strip()\n self.api_key = normalized_key\n self.base_url = base_url\n self.default_model = default_model\n\n # DEBUG: Log initialization parameters (without exposing full API key)\n logger.info(\"[LLMClient.__init__] Initializing LLM client:\")\n logger.info(f\"[LLMClient.__init__] Provider Type: {provider_type}\")\n logger.info(f\"[LLMClient.__init__] Base URL: {base_url}\")\n logger.info(f\"[LLMClient.__init__] Default Model: {default_model}\")\n logger.info(f\"[LLMClient.__init__] API Key (first 8 chars): {self.api_key[:8] if self.api_key and len(self.api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n logger.info(f\"[LLMClient.__init__] API Key Length: {len(self.api_key) if self.api_key else 0}\")\n\n # Some OpenAI-compatible gateways are strict about auth header naming.\n default_headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n if self.provider_type == LLMProviderType.OPENROUTER:\n default_headers[\"HTTP-Referer\"] = (\n os.getenv(\"OPENROUTER_SITE_URL\", \"\").strip()\n or os.getenv(\"APP_BASE_URL\", \"\").strip()\n or \"http://localhost:8000\"\n )\n default_headers[\"X-Title\"] = os.getenv(\"OPENROUTER_APP_NAME\", \"\").strip() or \"ss-tools\"\n if self.provider_type == LLMProviderType.KILO:\n default_headers[\"Authentication\"] = f\"Bearer {self.api_key}\"\n default_headers[\"X-API-Key\"] = self.api_key\n\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" }, { "contract_id": "MapperPluginModule", "contract_type": "Module", "file_path": "backend/src/plugins/mapper.py", "start_line": 1, - "end_line": 214, + "end_line": 213, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "Plugins", - "PURPOSE": "Implements a plugin for mapping dataset columns using external database connections or Excel files.", - "SEMANTICS": [ - "plugin", - "mapper", - "datasets", - "postgresql", - "excel" - ] + "PURPOSE": "Implements a plugin for mapping dataset columns using external database connections or Excel files." }, "relations": [ { @@ -65561,15 +51522,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MapperPluginModule:Module]\n# @SEMANTICS: plugin, mapper, datasets, postgresql, excel\n# @PURPOSE: Implements a plugin for mapping dataset columns using external database connections or Excel files.\n# @LAYER: Plugins\n# @RELATION: Inherits from PluginBase. Uses DatasetMapper from superset_tool.\n# @RELATION: USES -> TaskContext\n\n# [SECTION: IMPORTS]\nfrom typing import Dict, Any, Optional\nfrom ..core.plugin_base import PluginBase\nfrom ..core.superset_client import SupersetClient\nfrom ..core.logger import logger, belief_scope\nfrom ..core.database import SessionLocal\nfrom ..models.connection import ConnectionConfig\nfrom ..core.utils.dataset_mapper import DatasetMapper\nfrom ..core.task_manager.context import TaskContext\n# [/SECTION]\n\n# [DEF:MapperPlugin:Class]\n# @PURPOSE: Plugin for mapping dataset columns verbose names.\nclass MapperPlugin(PluginBase):\n \"\"\"\n Plugin for mapping dataset columns verbose names.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"dataset-mapper\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"dataset-mapper\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Dataset Mapper\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Map dataset column verbose names using PostgreSQL comments or Excel files.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the mapper plugin.\n # @RETURN: str - \"/tools/mapper\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/mapper\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the mapper plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (e.g., 'dev').\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset to update.\"\n },\n \"source\": {\n \"type\": \"string\",\n \"title\": \"Mapping Source\",\n \"enum\": [\"postgres\", \"excel\"],\n \"default\": \"postgres\"\n },\n \"connection_id\": {\n \"type\": \"string\",\n \"title\": \"Saved Connection\",\n \"description\": \"The ID of a saved database connection (for postgres source).\"\n },\n \"table_name\": {\n \"type\": \"string\",\n \"title\": \"Table Name\",\n \"description\": \"Target table name in PostgreSQL.\"\n },\n \"table_schema\": {\n \"type\": \"string\",\n \"title\": \"Table Schema\",\n \"description\": \"Target table schema in PostgreSQL.\",\n \"default\": \"public\"\n },\n \"excel_path\": {\n \"type\": \"string\",\n \"title\": \"Excel Path\",\n \"description\": \"Path to the Excel file (for excel source).\"\n }\n },\n \"required\": [\"env\", \"dataset_id\", \"source\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the dataset mapping logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Mapping parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env', 'dataset_id', and 'source'. params must be a dictionary.\n # @POST: Updates the dataset in Superset.\n # @RETURN: Dict[str, Any] - Execution status.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"execute\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n source = params.get(\"source\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n \n # Create sub-loggers for different components\n superset_log = log.with_source(\"superset_api\") if context else log\n db_log = log.with_source(\"postgres\") if context else log\n \n if not env_name or dataset_id is None or not source:\n log.error(\"Missing required parameters: env, dataset_id, source\")\n raise ValueError(\"Missing required parameters: env, dataset_id, source\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n postgres_config = None\n if source == \"postgres\":\n connection_id = params.get(\"connection_id\")\n if not connection_id:\n log.error(\"connection_id is required for postgres source.\")\n raise ValueError(\"connection_id is required for postgres source.\")\n \n # Load connection from DB\n db = SessionLocal()\n try:\n conn_config = db.query(ConnectionConfig).filter(ConnectionConfig.id == connection_id).first()\n if not conn_config:\n db_log.error(f\"Connection {connection_id} not found.\")\n raise ValueError(f\"Connection {connection_id} not found.\")\n \n postgres_config = {\n 'dbname': conn_config.database,\n 'user': conn_config.username,\n 'password': conn_config.password,\n 'host': conn_config.host,\n 'port': str(conn_config.port) if conn_config.port else '5432'\n }\n db_log.debug(f\"Loaded connection config for {conn_config.host}:{conn_config.port}/{conn_config.database}\")\n finally:\n db.close()\n\n log.info(f\"Starting mapping for dataset {dataset_id} in {env_name}\")\n \n mapper = DatasetMapper()\n \n try:\n mapper.run_mapping(\n superset_client=client,\n dataset_id=dataset_id,\n source=source,\n postgres_config=postgres_config,\n excel_path=params.get(\"excel_path\"),\n table_name=params.get(\"table_name\"),\n table_schema=params.get(\"table_schema\") or \"public\"\n )\n superset_log.info(f\"Mapping completed for dataset {dataset_id}\")\n return {\"status\": \"success\", \"dataset_id\": dataset_id}\n except Exception as e:\n log.error(f\"Mapping failed: {e}\")\n raise\n # [/DEF:execute:Function]\n\n# [/DEF:MapperPlugin:Class]\n# [/DEF:MapperPluginModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region MapperPluginModule [TYPE Module] [SEMANTICS mapper, dataset, column, excel, mapping]\n# @BRIEF Implements a plugin for mapping dataset columns using external database connections or Excel files.\n# @LAYER: Plugins\n# @RELATION Inherits from PluginBase. Uses DatasetMapper from superset_tool.\n# @RELATION USES -> TaskContext\n\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 ..core.utils.dataset_mapper import DatasetMapper\nfrom ..models.connection import ConnectionConfig\n\n\n# #region MapperPlugin [TYPE Class]\n# @BRIEF Plugin for mapping dataset columns verbose names.\nclass MapperPlugin(PluginBase):\n \"\"\"\n Plugin for mapping dataset columns verbose names.\n \"\"\"\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"dataset-mapper\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"dataset-mapper\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Dataset Mapper\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns a description of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Map dataset column verbose names using PostgreSQL comments or Excel files.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the version of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the mapper plugin.\n # @RETURN: str - \"/tools/mapper\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/mapper\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for the mapper plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (e.g., 'dev').\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset to update.\"\n },\n \"source\": {\n \"type\": \"string\",\n \"title\": \"Mapping Source\",\n \"enum\": [\"postgres\", \"excel\"],\n \"default\": \"postgres\"\n },\n \"connection_id\": {\n \"type\": \"string\",\n \"title\": \"Saved Connection\",\n \"description\": \"The ID of a saved database connection (for postgres source).\"\n },\n \"table_name\": {\n \"type\": \"string\",\n \"title\": \"Table Name\",\n \"description\": \"Target table name in PostgreSQL.\"\n },\n \"table_schema\": {\n \"type\": \"string\",\n \"title\": \"Table Schema\",\n \"description\": \"Target table schema in PostgreSQL.\",\n \"default\": \"public\"\n },\n \"excel_path\": {\n \"type\": \"string\",\n \"title\": \"Excel Path\",\n \"description\": \"Path to the Excel file (for excel source).\"\n }\n },\n \"required\": [\"env\", \"dataset_id\", \"source\"]\n }\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Executes the dataset mapping logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Mapping parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env', 'dataset_id', and 'source'. params must be a dictionary.\n # @POST: Updates the dataset in Superset.\n # @RETURN: Dict[str, Any] - Execution status.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None) -> dict[str, Any]:\n with belief_scope(\"execute\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n source = params.get(\"source\")\n\n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n\n # Create sub-loggers for different components\n superset_log = log.with_source(\"superset_api\") if context else log\n db_log = log.with_source(\"postgres\") if context else log\n\n if not env_name or dataset_id is None or not source:\n log.error(\"Missing required parameters: env, dataset_id, source\")\n raise ValueError(\"Missing required parameters: env, dataset_id, source\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n postgres_config = None\n if source == \"postgres\":\n connection_id = params.get(\"connection_id\")\n if not connection_id:\n log.error(\"connection_id is required for postgres source.\")\n raise ValueError(\"connection_id is required for postgres source.\")\n\n # Load connection from DB\n db = SessionLocal()\n try:\n conn_config = db.query(ConnectionConfig).filter(ConnectionConfig.id == connection_id).first()\n if not conn_config:\n db_log.error(f\"Connection {connection_id} not found.\")\n raise ValueError(f\"Connection {connection_id} not found.\")\n\n postgres_config = {\n 'dbname': conn_config.database,\n 'user': conn_config.username,\n 'password': conn_config.password,\n 'host': conn_config.host,\n 'port': str(conn_config.port) if conn_config.port else '5432'\n }\n db_log.debug(f\"Loaded connection config for {conn_config.host}:{conn_config.port}/{conn_config.database}\")\n finally:\n db.close()\n\n log.info(f\"Starting mapping for dataset {dataset_id} in {env_name}\")\n\n mapper = DatasetMapper()\n\n try:\n mapper.run_mapping(\n superset_client=client,\n dataset_id=dataset_id,\n source=source,\n postgres_config=postgres_config,\n excel_path=params.get(\"excel_path\"),\n table_name=params.get(\"table_name\"),\n table_schema=params.get(\"table_schema\") or \"public\"\n )\n superset_log.info(f\"Mapping completed for dataset {dataset_id}\")\n return {\"status\": \"success\", \"dataset_id\": dataset_id}\n except Exception as e:\n log.error(f\"Mapping failed: {e}\")\n raise\n # endregion execute\n\n# #endregion MapperPlugin\n# #endregion MapperPluginModule\n" }, { "contract_id": "MapperPlugin", "contract_type": "Class", "file_path": "backend/src/plugins/mapper.py", - "start_line": 19, - "end_line": 213, + "start_line": 18, + "end_line": 212, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -65587,33 +51548,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MapperPlugin:Class]\n# @PURPOSE: Plugin for mapping dataset columns verbose names.\nclass MapperPlugin(PluginBase):\n \"\"\"\n Plugin for mapping dataset columns verbose names.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"dataset-mapper\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"dataset-mapper\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Dataset Mapper\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Map dataset column verbose names using PostgreSQL comments or Excel files.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the mapper plugin.\n # @RETURN: str - \"/tools/mapper\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/mapper\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the mapper plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (e.g., 'dev').\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset to update.\"\n },\n \"source\": {\n \"type\": \"string\",\n \"title\": \"Mapping Source\",\n \"enum\": [\"postgres\", \"excel\"],\n \"default\": \"postgres\"\n },\n \"connection_id\": {\n \"type\": \"string\",\n \"title\": \"Saved Connection\",\n \"description\": \"The ID of a saved database connection (for postgres source).\"\n },\n \"table_name\": {\n \"type\": \"string\",\n \"title\": \"Table Name\",\n \"description\": \"Target table name in PostgreSQL.\"\n },\n \"table_schema\": {\n \"type\": \"string\",\n \"title\": \"Table Schema\",\n \"description\": \"Target table schema in PostgreSQL.\",\n \"default\": \"public\"\n },\n \"excel_path\": {\n \"type\": \"string\",\n \"title\": \"Excel Path\",\n \"description\": \"Path to the Excel file (for excel source).\"\n }\n },\n \"required\": [\"env\", \"dataset_id\", \"source\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the dataset mapping logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Mapping parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env', 'dataset_id', and 'source'. params must be a dictionary.\n # @POST: Updates the dataset in Superset.\n # @RETURN: Dict[str, Any] - Execution status.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"execute\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n source = params.get(\"source\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n \n # Create sub-loggers for different components\n superset_log = log.with_source(\"superset_api\") if context else log\n db_log = log.with_source(\"postgres\") if context else log\n \n if not env_name or dataset_id is None or not source:\n log.error(\"Missing required parameters: env, dataset_id, source\")\n raise ValueError(\"Missing required parameters: env, dataset_id, source\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n postgres_config = None\n if source == \"postgres\":\n connection_id = params.get(\"connection_id\")\n if not connection_id:\n log.error(\"connection_id is required for postgres source.\")\n raise ValueError(\"connection_id is required for postgres source.\")\n \n # Load connection from DB\n db = SessionLocal()\n try:\n conn_config = db.query(ConnectionConfig).filter(ConnectionConfig.id == connection_id).first()\n if not conn_config:\n db_log.error(f\"Connection {connection_id} not found.\")\n raise ValueError(f\"Connection {connection_id} not found.\")\n \n postgres_config = {\n 'dbname': conn_config.database,\n 'user': conn_config.username,\n 'password': conn_config.password,\n 'host': conn_config.host,\n 'port': str(conn_config.port) if conn_config.port else '5432'\n }\n db_log.debug(f\"Loaded connection config for {conn_config.host}:{conn_config.port}/{conn_config.database}\")\n finally:\n db.close()\n\n log.info(f\"Starting mapping for dataset {dataset_id} in {env_name}\")\n \n mapper = DatasetMapper()\n \n try:\n mapper.run_mapping(\n superset_client=client,\n dataset_id=dataset_id,\n source=source,\n postgres_config=postgres_config,\n excel_path=params.get(\"excel_path\"),\n table_name=params.get(\"table_name\"),\n table_schema=params.get(\"table_schema\") or \"public\"\n )\n superset_log.info(f\"Mapping completed for dataset {dataset_id}\")\n return {\"status\": \"success\", \"dataset_id\": dataset_id}\n except Exception as e:\n log.error(f\"Mapping failed: {e}\")\n raise\n # [/DEF:execute:Function]\n\n# [/DEF:MapperPlugin:Class]\n" + "anchor_syntax": "region", + "body": "# #region MapperPlugin [TYPE Class]\n# @BRIEF Plugin for mapping dataset columns verbose names.\nclass MapperPlugin(PluginBase):\n \"\"\"\n Plugin for mapping dataset columns verbose names.\n \"\"\"\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"dataset-mapper\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"dataset-mapper\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Dataset Mapper\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns a description of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Map dataset column verbose names using PostgreSQL comments or Excel files.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the version of the mapper plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the mapper plugin.\n # @RETURN: str - \"/tools/mapper\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/mapper\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for the mapper plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment (e.g., 'dev').\"\n },\n \"dataset_id\": {\n \"type\": \"integer\",\n \"title\": \"Dataset ID\",\n \"description\": \"The ID of the dataset to update.\"\n },\n \"source\": {\n \"type\": \"string\",\n \"title\": \"Mapping Source\",\n \"enum\": [\"postgres\", \"excel\"],\n \"default\": \"postgres\"\n },\n \"connection_id\": {\n \"type\": \"string\",\n \"title\": \"Saved Connection\",\n \"description\": \"The ID of a saved database connection (for postgres source).\"\n },\n \"table_name\": {\n \"type\": \"string\",\n \"title\": \"Table Name\",\n \"description\": \"Target table name in PostgreSQL.\"\n },\n \"table_schema\": {\n \"type\": \"string\",\n \"title\": \"Table Schema\",\n \"description\": \"Target table schema in PostgreSQL.\",\n \"default\": \"public\"\n },\n \"excel_path\": {\n \"type\": \"string\",\n \"title\": \"Excel Path\",\n \"description\": \"Path to the Excel file (for excel source).\"\n }\n },\n \"required\": [\"env\", \"dataset_id\", \"source\"]\n }\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Executes the dataset mapping logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Mapping parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env', 'dataset_id', and 'source'. params must be a dictionary.\n # @POST: Updates the dataset in Superset.\n # @RETURN: Dict[str, Any] - Execution status.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None) -> dict[str, Any]:\n with belief_scope(\"execute\"):\n env_name = params.get(\"env\")\n dataset_id = params.get(\"dataset_id\")\n source = params.get(\"source\")\n\n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n\n # Create sub-loggers for different components\n superset_log = log.with_source(\"superset_api\") if context else log\n db_log = log.with_source(\"postgres\") if context else log\n\n if not env_name or dataset_id is None or not source:\n log.error(\"Missing required parameters: env, dataset_id, source\")\n raise ValueError(\"Missing required parameters: env, dataset_id, source\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n postgres_config = None\n if source == \"postgres\":\n connection_id = params.get(\"connection_id\")\n if not connection_id:\n log.error(\"connection_id is required for postgres source.\")\n raise ValueError(\"connection_id is required for postgres source.\")\n\n # Load connection from DB\n db = SessionLocal()\n try:\n conn_config = db.query(ConnectionConfig).filter(ConnectionConfig.id == connection_id).first()\n if not conn_config:\n db_log.error(f\"Connection {connection_id} not found.\")\n raise ValueError(f\"Connection {connection_id} not found.\")\n\n postgres_config = {\n 'dbname': conn_config.database,\n 'user': conn_config.username,\n 'password': conn_config.password,\n 'host': conn_config.host,\n 'port': str(conn_config.port) if conn_config.port else '5432'\n }\n db_log.debug(f\"Loaded connection config for {conn_config.host}:{conn_config.port}/{conn_config.database}\")\n finally:\n db.close()\n\n log.info(f\"Starting mapping for dataset {dataset_id} in {env_name}\")\n\n mapper = DatasetMapper()\n\n try:\n mapper.run_mapping(\n superset_client=client,\n dataset_id=dataset_id,\n source=source,\n postgres_config=postgres_config,\n excel_path=params.get(\"excel_path\"),\n table_name=params.get(\"table_name\"),\n table_schema=params.get(\"table_schema\") or \"public\"\n )\n superset_log.info(f\"Mapping completed for dataset {dataset_id}\")\n return {\"status\": \"success\", \"dataset_id\": dataset_id}\n except Exception as e:\n log.error(f\"Mapping failed: {e}\")\n raise\n # endregion execute\n\n# #endregion MapperPlugin\n" }, { "contract_id": "MigrationPlugin", "contract_type": "Module", "file_path": "backend/src/plugins/migration.py", "start_line": 1, - "end_line": 393, + "end_line": 389, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[TaskContext{from_env,to_env,dashboard_regex,replace_db_config,from_db_id,to_db_id,passwords?}] -> Output[MigrationResult|artifact set]", "INVARIANT": "Dashboards must never be imported with unmapped/source DB connections to prevent data leaks or cross-environment pollution.", "LAYER": "App", "POST": "Plugin metadata remains stable and migration execution preserves mapped-environment import guarantees.", "PRE": "Plugin loader can resolve infrastructure dependencies and execution requests provide validated migration context.", "PURPOSE": "Orchestrates export, DB-mapping transformation, and import of Superset dashboards across environments.", - "SEMANTICS": [ - "migration", - "superset", - "automation", - "dashboard", - "plugin", - "transformation" - ], "SIDE_EFFECT": "Reads config, opens database sessions, creates temporary artifacts, and triggers Superset export/import workflows." }, "relations": [ @@ -65686,27 +51639,20 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrationPlugin:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: migration, superset, automation, dashboard, plugin, transformation\n# @PURPOSE: Orchestrates export, DB-mapping transformation, and import of Superset dashboards across environments.\n# @LAYER: App\n# @RELATION: IMPLEMENTS -> PluginBase\n# @RELATION: DEPENDS_ON -> SupersetClient\n# @RELATION: DEPENDS_ON -> MigrationEngine\n# @RELATION: DEPENDS_ON -> IdMappingService\n# @RELATION: USES -> TaskContext\n# @PRE: Plugin loader can resolve infrastructure dependencies and execution requests provide validated migration context.\n# @POST: Plugin metadata remains stable and migration execution preserves mapped-environment import guarantees.\n# @SIDE_EFFECT: Reads config, opens database sessions, creates temporary artifacts, and triggers Superset export/import workflows.\n# @DATA_CONTRACT: Input[TaskContext{from_env,to_env,dashboard_regex,replace_db_config,from_db_id,to_db_id,passwords?}] -> Output[MigrationResult|artifact set]\n# @INVARIANT: Dashboards must never be imported with unmapped/source DB connections to prevent data leaks or cross-environment pollution.\n\nfrom typing import Dict, Any, Optional\nimport re\n\nfrom ..core.plugin_base import PluginBase\nfrom ..core.logger import belief_scope\nfrom ..core.cot_logger import MarkerLogger\nfrom ..core.superset_client import SupersetClient\n\nlog = MarkerLogger(\"MigrationPlugin\")\nfrom ..core.utils.fileio import create_temp_file\nfrom ..dependencies import get_config_manager\nfrom ..core.migration_engine import MigrationEngine\nfrom ..core.database import SessionLocal\nfrom ..models.mapping import DatabaseMapping, Environment\nfrom ..core.mapping_service import IdMappingService\nfrom ..core.task_manager.context import TaskContext\n\n# [DEF:MigrationPlugin:Class]\n# @PURPOSE: Implementation of the migration plugin workflow and transformation orchestration.\n# @PRE: SupersetClient authenticated, database session active\n# @POST: Returns MigrationResult with success/failure status and artifact list\n# @TEST_FIXTURE: superset_export_zip -> file:backend/tests/fixtures/migration/dashboard_export.zip\n# @TEST_FIXTURE: db_mapping_payload -> INLINE_JSON: {\"db_mappings\": {\"source_uuid_1\": \"target_uuid_2\"}}\n# @TEST_FIXTURE: password_inject_payload -> INLINE_JSON: {\"passwords\": {\"PostgreSQL\": \"secret123\"}}\n# @TEST_INVARIANT: strict_db_isolation -> VERIFIED_BY: [successful_dashboard_transfer, missing_mapping_resolution]\n# @SIDE_EFFECT: Writes migration artifacts to database, triggers dashboard imports\n# @DATA_CONTRACT: MigrationPlan AST, DryRunResult, RiskAssessment\nclass MigrationPlugin(PluginBase):\n \"\"\"\n A plugin to migrate Superset dashboards between environments.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the migration plugin.\n # @PRE: None.\n # @POST: Returns stable string \"superset-migration\".\n # @RETURN: str\n def id(self) -> str:\n with belief_scope(\"MigrationPlugin.id\"):\n return \"superset-migration\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the plugin.\n # @PRE: None.\n # @POST: Returns \"Superset Dashboard Migration\".\n # @RETURN: str\n def name(self) -> str:\n with belief_scope(\"MigrationPlugin.name\"):\n return \"Superset Dashboard Migration\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns the semantic description of the plugin.\n # @PRE: None.\n # @POST: Returns description string.\n # @RETURN: str\n def description(self) -> str:\n with belief_scope(\"MigrationPlugin.description\"):\n return \"Migrates dashboards between Superset environments.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the semantic version of the migration plugin.\n # @PRE: None.\n # @POST: Returns \"1.0.0\".\n # @RETURN: str\n def version(self) -> str:\n with belief_scope(\"MigrationPlugin.version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend routing anchor for the plugin.\n # @PRE: None.\n # @POST: Returns \"/migration\".\n # @RETURN: str\n def ui_route(self) -> str:\n with belief_scope(\"MigrationPlugin.ui_route\"):\n return \"/migration\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Generates the JSON Schema for the plugin execution form dynamically.\n # @PRE: ConfigManager is accessible and environments are defined.\n # @POST: Returns a JSON Schema dict matching current system environments.\n # @RETURN: Dict[str, Any]\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"MigrationPlugin.get_schema\"):\n log.reason(\"Generating migration UI schema\")\n config_manager = get_config_manager()\n envs = [e.name for e in config_manager.get_environments()]\n \n schema = {\n \"type\": \"object\",\n \"properties\": {\n \"from_env\": {\n \"type\": \"string\",\n \"title\": \"Source Environment\",\n \"description\": \"The environment to migrate from.\",\n \"enum\": envs if envs else [\"dev\", \"prod\"],\n },\n \"to_env\": {\n \"type\": \"string\",\n \"title\": \"Target Environment\",\n \"description\": \"The environment to migrate to.\",\n \"enum\": envs if envs else [\"dev\", \"prod\"],\n },\n \"dashboard_regex\": {\n \"type\": \"string\",\n \"title\": \"Dashboard Regex\",\n \"description\": \"A regular expression to filter dashboards to migrate.\",\n },\n \"replace_db_config\": {\n \"type\": \"boolean\",\n \"title\": \"Replace DB Config\",\n \"description\": \"Whether to replace the database configuration.\",\n \"default\": False,\n },\n \"from_db_id\": {\n \"type\": \"integer\",\n \"title\": \"Source DB ID\",\n \"description\": \"The ID of the source database to replace (if replacing).\",\n },\n \"to_db_id\": {\n \"type\": \"integer\",\n \"title\": \"Target DB ID\",\n \"description\": \"The ID of the target database to replace with (if replacing).\",\n },\n },\n \"required\": [\"from_env\", \"to_env\", \"dashboard_regex\"],\n }\n log.reflect(\"Schema generated successfully\", payload={\"environments_count\": len(envs)})\n return schema\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Orchestrates the dashboard migration pipeline including extraction, AST mutation, and ingestion.\n # @PARAM: params (Dict[str, Any]) - Extracted parameters from UI/API execution request.\n # @PARAM: context (Optional[TaskContext]) - Dependency injected TaskContext for IO tracing.\n # @PRE: Source and target environments must resolve. Matching dashboards must exist.\n # @POST: Dashboard ZIP bundles are transformed and imported. ID mappings are synchronized.\n # @SIDE_EFFECT: Creates temp files, mutates target Superset state, blocks on user input (passwords/mappings).\n # @TEST_CONTRACT: Dict[str, Any] -> Dict[str, Any]\n # @TEST_SCENARIO: successful_dashboard_transfer -> ZIP is downloaded, DB mappings applied via AST, target import succeeds.\n # @TEST_SCENARIO: missing_password_injection -> Target import fails on auth, TaskManager pauses for user input, retries with password successfully.\n # @TEST_SCENARIO: empty_selection -> Returns NO_MATCHES gracefully when regex finds zero dashboards.\n # @TEST_EDGE: missing_env_field -> [ValueError: Could not resolve source or target environment]\n # @TEST_EDGE: invalid_regex_pattern -> [Regex compilation exception is thrown or caught gracefully]\n # @TEST_EDGE: target_api_timeout -> [Dashboard added to failed_dashboards, task concludes with PARTIAL_SUCCESS]\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None):\n with belief_scope(\"MigrationPlugin.execute\"):\n log.reason(\"Evaluating migration task parameters\", payload={\"params\": params})\n \n source_env_id = params.get(\"source_env_id\")\n target_env_id = params.get(\"target_env_id\")\n selected_ids = params.get(\"selected_ids\")\n \n from_env_name = params.get(\"from_env\")\n to_env_name = params.get(\"to_env\")\n dashboard_regex = params.get(\"dashboard_regex\")\n replace_db_config = params.get(\"replace_db_config\", False)\n fix_cross_filters = params.get(\"fix_cross_filters\", True)\n \n task_id = params.get(\"_task_id\")\n from ..dependencies import get_task_manager\n tm = get_task_manager()\n\n exec_log = context.logger if context else log\n superset_log = exec_log.with_source(\"superset_api\") if context else exec_log\n migration_log = exec_log.with_source(\"migration\") if context else exec_log\n \n exec_log.info(\"Starting migration task.\")\n\n try:\n config_manager = get_config_manager()\n environments = config_manager.get_environments()\n \n # Resolve environments\n src_env = next((e for e in environments if e.id == source_env_id), None) if source_env_id else next((e for e in environments if e.name == from_env_name), None)\n tgt_env = next((e for e in environments if e.id == target_env_id), None) if target_env_id else next((e for e in environments if e.name == to_env_name), None)\n \n if not src_env or not tgt_env:\n log.explore(\"Environment resolution failed\", error=\"Could not resolve source or target environment\", payload={\"src\": source_env_id or from_env_name, \"tgt\": target_env_id or to_env_name})\n raise ValueError(f\"Could not resolve source or target environment. Source: {source_env_id or from_env_name}, Target: {target_env_id or to_env_name}\")\n\n from_env_name = src_env.name\n to_env_name = tgt_env.name\n\n log.reason(\"Environments resolved successfully\", payload={\"from\": from_env_name, \"to\": to_env_name})\n \n migration_result = {\n \"status\": \"SUCCESS\",\n \"source_environment\": from_env_name,\n \"target_environment\": to_env_name,\n \"selected_dashboards\": 0,\n \"migrated_dashboards\": [],\n \"failed_dashboards\": [],\n \"mapping_count\": 0\n }\n\n from_c = SupersetClient(src_env)\n to_c = SupersetClient(tgt_env)\n \n if not from_c or not to_c:\n raise ValueError(f\"Clients not initialized for environments: {from_env_name}, {to_env_name}\")\n\n _, all_dashboards = from_c.get_dashboards()\n \n # Selection Logic\n if selected_ids:\n dashboards_to_migrate = [d for d in all_dashboards if d[\"id\"] in selected_ids]\n elif dashboard_regex:\n regex_pattern = re.compile(str(dashboard_regex), re.IGNORECASE)\n dashboards_to_migrate = [d for d in all_dashboards if regex_pattern.search(d.get(\"dashboard_title\", \"\"))]\n else:\n log.explore(\"No deterministic selection criteria provided\", error=\"No dashboard selection criteria (selected_ids or dashboard_regex)\")\n migration_result[\"status\"] = \"NO_SELECTION\"\n return migration_result\n\n if not dashboards_to_migrate:\n log.explore(\"Zero dashboards match selection criteria\", error=\"No dashboards matched the given selection criteria\")\n migration_result[\"status\"] = \"NO_MATCHES\"\n return migration_result\n\n migration_result[\"selected_dashboards\"] = len(dashboards_to_migrate)\n\n # Database Mapping Resolution\n db_mapping = params.get(\"db_mappings\", {})\n if not isinstance(db_mapping, dict):\n db_mapping = {}\n \n if replace_db_config:\n log.reason(\"Fetching environment DB mappings from catalog\")\n db = SessionLocal()\n try:\n src_env_db = db.query(Environment).filter(Environment.name == from_env_name).first()\n tgt_env_db = db.query(Environment).filter(Environment.name == to_env_name).first()\n \n if src_env_db and tgt_env_db:\n stored_mappings = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == src_env_db.id,\n DatabaseMapping.target_env_id == tgt_env_db.id\n ).all()\n stored_map_dict = {m.source_db_uuid: m.target_db_uuid for m in stored_mappings}\n stored_map_dict.update(db_mapping)\n db_mapping = stored_map_dict\n exec_log.info(f\"Loaded {len(stored_mappings)} database mappings from database.\")\n finally:\n db.close()\n\n migration_result[\"mapping_count\"] = len(db_mapping)\n engine = MigrationEngine()\n\n # Migration Loop\n for dash in dashboards_to_migrate:\n dash_id, dash_slug, title = dash[\"id\"], dash.get(\"slug\"), dash[\"dashboard_title\"]\n log.reason(f\"Starting pipeline for dashboard '{title}'\", payload={\"dash_id\": dash_id})\n \n try:\n exported_content, _ = from_c.export_dashboard(dash_id)\n with create_temp_file(content=exported_content, dry_run=True, suffix=\".zip\") as tmp_zip_path:\n with create_temp_file(suffix=\".zip\", dry_run=True) as tmp_new_zip:\n \n success = engine.transform_zip(\n str(tmp_zip_path), \n str(tmp_new_zip), \n db_mapping, \n strip_databases=False, \n target_env_id=tgt_env.id if tgt_env else None, \n fix_cross_filters=fix_cross_filters\n )\n \n if not success and replace_db_config:\n if task_id:\n log.explore(\"Missing mapping blocks AST transform. Pausing task for user intervention.\", error=\"AST transform blocked by missing mappings\", payload={\"task_id\": task_id})\n await tm.wait_for_resolution(task_id)\n \n log.reason(\"Task resumed, re-evaluating mapping states\")\n db = SessionLocal()\n try:\n src_env_rt = db.query(Environment).filter(Environment.name == from_env_name).first()\n tgt_env_rt = db.query(Environment).filter(Environment.name == to_env_name).first()\n mappings = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == src_env_rt.id,\n DatabaseMapping.target_env_id == tgt_env_rt.id\n ).all()\n db_mapping = {m.source_db_uuid: m.target_db_uuid for m in mappings}\n finally:\n db.close()\n \n success = engine.transform_zip(\n str(tmp_zip_path), \n str(tmp_new_zip), \n db_mapping, \n strip_databases=False,\n target_env_id=tgt_env.id if tgt_env else None,\n fix_cross_filters=fix_cross_filters\n )\n\n if success:\n log.reason(\"Pushing transformed ZIP to target Superset\")\n to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug)\n migration_result[\"migrated_dashboards\"].append({\"id\": dash_id, \"title\": title})\n log.reflect(\"Import successful\", payload={\"title\": title})\n else:\n log.explore(\"Transformation strictly failed, bypassing ingestion\", error=\"Dashboard ZIP transformation failed\")\n migration_log.error(f\"Failed to transform ZIP for dashboard {title}\")\n migration_result[\"failed_dashboards\"].append({\n \"id\": dash_id, \"title\": title, \"error\": \"Failed to transform ZIP\"\n })\n \n except Exception as exc:\n error_msg = str(exc)\n if \"Must provide a password for the database\" in error_msg:\n db_name = \"unknown\"\n match = re.search(r\"databases/([^.]+)\\.yaml\", error_msg)\n if match:\n db_name = match.group(1)\n else:\n match_alt = re.search(r\"database '([^']+)'\", error_msg)\n if match_alt:\n db_name = match_alt.group(1)\n\n log.explore(\"Missing DB password detected during ingestion. Escalating to UI.\", error=\"Missing DB password for import\", payload={\"db_name\": db_name})\n \n if task_id:\n tm.await_input(task_id, {\n \"type\": \"database_password\",\n \"databases\": [db_name],\n \"error_message\": error_msg\n })\n \n await tm.wait_for_input(task_id)\n task = tm.get_task(task_id)\n passwords = task.params.get(\"passwords\", {})\n \n if passwords:\n log.reason(f\"Retrying import for {title} with injected credentials\")\n to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug, passwords=passwords)\n migration_result[\"migrated_dashboards\"].append({\"id\": dash_id, \"title\": title})\n log.reflect(\"Password injection unblocked import\")\n if \"passwords\" in task.params:\n del task.params[\"passwords\"]\n continue\n\n log.explore(\"Catastrophic dashboard ingestion failure\", error=f\"Dashboard ingestion failed: {exc}\")\n migration_result[\"failed_dashboards\"].append({\"id\": dash_id, \"title\": title, \"error\": str(exc)})\n\n if migration_result[\"failed_dashboards\"]:\n migration_result[\"status\"] = \"PARTIAL_SUCCESS\"\n\n # Post-Migration ID Mapping Synchronization\n try:\n log.reason(\"Executing incremental ID catalog sync on target\")\n db_session = SessionLocal()\n mapping_service = IdMappingService(db_session)\n mapping_service.sync_environment(tgt_env.id, to_c, incremental=True)\n db_session.close()\n log.reflect(\"Incremental catalog sync closed out cleanly\")\n except Exception as sync_exc:\n log.explore(\"ID Mapping sync failed, mapping state might be degraded\", error=f\"ID Mapping sync error: {sync_exc}\")\n\n log.reflect(\"Migration cycle fully resolved\", payload={\"result\": migration_result})\n return migration_result\n\n except Exception as e:\n log.explore(\"Fatal plugin failure\", error=f\"Plugin execution failed: {e}\", exc_info=True)\n raise e\n # [/DEF:execute:Function]\n# [/DEF:MigrationPlugin:Class]\n# [/DEF:MigrationPlugin:Module]\n" + "anchor_syntax": "region", + "body": "# #region MigrationPlugin [C:5] [TYPE Module] [SEMANTICS migration, export, import, mapping, superset]\n# @BRIEF Orchestrates export, DB-mapping transformation, and import of Superset dashboards across environments.\n# @LAYER: App\n# @RELATION IMPLEMENTS -> PluginBase\n# @RELATION DEPENDS_ON -> SupersetClient\n# @RELATION DEPENDS_ON -> MigrationEngine\n# @RELATION DEPENDS_ON -> IdMappingService\n# @RELATION USES -> TaskContext\n# @PRE: Plugin loader can resolve infrastructure dependencies and execution requests provide validated migration context.\n# @POST: Plugin metadata remains stable and migration execution preserves mapped-environment import guarantees.\n# @SIDE_EFFECT: Reads config, opens database sessions, creates temporary artifacts, and triggers Superset export/import workflows.\n# @DATA_CONTRACT: Input[TaskContext{from_env,to_env,dashboard_regex,replace_db_config,from_db_id,to_db_id,passwords?}] -> Output[MigrationResult|artifact set]\n# @INVARIANT: Dashboards must never be imported with unmapped/source DB connections to prevent data leaks or cross-environment pollution.\n\nimport re\nfrom typing import Any\n\nfrom ..core.database import SessionLocal\nfrom ..core.logger import belief_scope\nfrom ..core.logger import logger as app_logger\nfrom ..core.mapping_service import IdMappingService\nfrom ..core.migration_engine import MigrationEngine\nfrom ..core.plugin_base import PluginBase\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager.context import TaskContext\nfrom ..core.utils.fileio import create_temp_file\nfrom ..dependencies import get_config_manager\nfrom ..models.mapping import DatabaseMapping, Environment\n\n\n# #region MigrationPlugin [TYPE Class]\n# @BRIEF Implementation of the migration plugin workflow and transformation orchestration.\n# @PRE: SupersetClient authenticated, database session active\n# @POST: Returns MigrationResult with success/failure status and artifact list\n# @TEST_FIXTURE: superset_export_zip -> file:backend/tests/fixtures/migration/dashboard_export.zip\n# @TEST_FIXTURE: db_mapping_payload -> INLINE_JSON: {\"db_mappings\": {\"source_uuid_1\": \"target_uuid_2\"}}\n# @TEST_FIXTURE: password_inject_payload -> INLINE_JSON: {\"passwords\": {\"PostgreSQL\": \"secret123\"}}\n# @TEST_INVARIANT: strict_db_isolation -> VERIFIED_BY: [successful_dashboard_transfer, missing_mapping_resolution]\n# @SIDE_EFFECT: Writes migration artifacts to database, triggers dashboard imports\n# @DATA_CONTRACT: MigrationPlan AST, DryRunResult, RiskAssessment\nclass MigrationPlugin(PluginBase):\n \"\"\"\n A plugin to migrate Superset dashboards between environments.\n \"\"\"\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the migration plugin.\n # @PRE: None.\n # @POST: Returns stable string \"superset-migration\".\n # @RETURN: str\n def id(self) -> str:\n with belief_scope(\"MigrationPlugin.id\"):\n return \"superset-migration\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the plugin.\n # @PRE: None.\n # @POST: Returns \"Superset Dashboard Migration\".\n # @RETURN: str\n def name(self) -> str:\n with belief_scope(\"MigrationPlugin.name\"):\n return \"Superset Dashboard Migration\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns the semantic description of the plugin.\n # @PRE: None.\n # @POST: Returns description string.\n # @RETURN: str\n def description(self) -> str:\n with belief_scope(\"MigrationPlugin.description\"):\n return \"Migrates dashboards between Superset environments.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the semantic version of the migration plugin.\n # @PRE: None.\n # @POST: Returns \"1.0.0\".\n # @RETURN: str\n def version(self) -> str:\n with belief_scope(\"MigrationPlugin.version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend routing anchor for the plugin.\n # @PRE: None.\n # @POST: Returns \"/migration\".\n # @RETURN: str\n def ui_route(self) -> str:\n with belief_scope(\"MigrationPlugin.ui_route\"):\n return \"/migration\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Generates the JSON Schema for the plugin execution form dynamically.\n # @PRE: ConfigManager is accessible and environments are defined.\n # @POST: Returns a JSON Schema dict matching current system environments.\n # @RETURN: Dict[str, Any]\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"MigrationPlugin.get_schema\"):\n app_logger.reason(\"Generating migration UI schema\")\n config_manager = get_config_manager()\n envs = [e.name for e in config_manager.get_environments()]\n\n schema = {\n \"type\": \"object\",\n \"properties\": {\n \"from_env\": {\n \"type\": \"string\",\n \"title\": \"Source Environment\",\n \"description\": \"The environment to migrate from.\",\n \"enum\": envs if envs else [\"dev\", \"prod\"],\n },\n \"to_env\": {\n \"type\": \"string\",\n \"title\": \"Target Environment\",\n \"description\": \"The environment to migrate to.\",\n \"enum\": envs if envs else [\"dev\", \"prod\"],\n },\n \"dashboard_regex\": {\n \"type\": \"string\",\n \"title\": \"Dashboard Regex\",\n \"description\": \"A regular expression to filter dashboards to migrate.\",\n },\n \"replace_db_config\": {\n \"type\": \"boolean\",\n \"title\": \"Replace DB Config\",\n \"description\": \"Whether to replace the database configuration.\",\n \"default\": False,\n },\n \"from_db_id\": {\n \"type\": \"integer\",\n \"title\": \"Source DB ID\",\n \"description\": \"The ID of the source database to replace (if replacing).\",\n },\n \"to_db_id\": {\n \"type\": \"integer\",\n \"title\": \"Target DB ID\",\n \"description\": \"The ID of the target database to replace with (if replacing).\",\n },\n },\n \"required\": [\"from_env\", \"to_env\", \"dashboard_regex\"],\n }\n app_logger.reflect(\"Schema generated successfully\", extra={\"environments_count\": len(envs)})\n return schema\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Orchestrates the dashboard migration pipeline including extraction, AST mutation, and ingestion.\n # @PARAM: params (Dict[str, Any]) - Extracted parameters from UI/API execution request.\n # @PARAM: context (Optional[TaskContext]) - Dependency injected TaskContext for IO tracing.\n # @PRE: Source and target environments must resolve. Matching dashboards must exist.\n # @POST: Dashboard ZIP bundles are transformed and imported. ID mappings are synchronized.\n # @SIDE_EFFECT: Creates temp files, mutates target Superset state, blocks on user input (passwords/mappings).\n # @TEST_CONTRACT: Dict[str, Any] -> Dict[str, Any]\n # @TEST_SCENARIO: successful_dashboard_transfer -> ZIP is downloaded, DB mappings applied via AST, target import succeeds.\n # @TEST_SCENARIO: missing_password_injection -> Target import fails on auth, TaskManager pauses for user input, retries with password successfully.\n # @TEST_SCENARIO: empty_selection -> Returns NO_MATCHES gracefully when regex finds zero dashboards.\n # @TEST_EDGE: missing_env_field -> [ValueError: Could not resolve source or target environment]\n # @TEST_EDGE: invalid_regex_pattern -> [Regex compilation exception is thrown or caught gracefully]\n # @TEST_EDGE: target_api_timeout -> [Dashboard added to failed_dashboards, task concludes with PARTIAL_SUCCESS]\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"MigrationPlugin.execute\"):\n app_logger.reason(\"Evaluating migration task parameters\", extra={\"params\": params})\n\n source_env_id = params.get(\"source_env_id\")\n target_env_id = params.get(\"target_env_id\")\n selected_ids = params.get(\"selected_ids\")\n\n from_env_name = params.get(\"from_env\")\n to_env_name = params.get(\"to_env\")\n dashboard_regex = params.get(\"dashboard_regex\")\n replace_db_config = params.get(\"replace_db_config\", False)\n fix_cross_filters = params.get(\"fix_cross_filters\", True)\n\n task_id = params.get(\"_task_id\")\n from ..dependencies import get_task_manager\n tm = get_task_manager()\n\n log = context.logger if context else app_logger\n superset_log = log.with_source(\"superset_api\") if context else log\n migration_log = log.with_source(\"migration\") if context else log\n\n log.info(\"Starting migration task.\")\n\n try:\n config_manager = get_config_manager()\n environments = config_manager.get_environments()\n\n # Resolve environments\n src_env = next((e for e in environments if e.id == source_env_id), None) if source_env_id else next((e for e in environments if e.name == from_env_name), None)\n tgt_env = next((e for e in environments if e.id == target_env_id), None) if target_env_id else next((e for e in environments if e.name == to_env_name), None)\n\n if not src_env or not tgt_env:\n app_logger.explore(\"Environment resolution failed\", extra={\"src\": source_env_id or from_env_name, \"tgt\": target_env_id or to_env_name})\n raise ValueError(f\"Could not resolve source or target environment. Source: {source_env_id or from_env_name}, Target: {target_env_id or to_env_name}\")\n\n from_env_name = src_env.name\n to_env_name = tgt_env.name\n\n app_logger.reason(\"Environments resolved successfully\", extra={\"from\": from_env_name, \"to\": to_env_name})\n\n migration_result = {\n \"status\": \"SUCCESS\",\n \"source_environment\": from_env_name,\n \"target_environment\": to_env_name,\n \"selected_dashboards\": 0,\n \"migrated_dashboards\": [],\n \"failed_dashboards\": [],\n \"mapping_count\": 0\n }\n\n from_c = SupersetClient(src_env)\n to_c = SupersetClient(tgt_env)\n\n if not from_c or not to_c:\n raise ValueError(f\"Clients not initialized for environments: {from_env_name}, {to_env_name}\")\n\n _, all_dashboards = from_c.get_dashboards()\n\n # Selection Logic\n if selected_ids:\n dashboards_to_migrate = [d for d in all_dashboards if d[\"id\"] in selected_ids]\n elif dashboard_regex:\n regex_pattern = re.compile(str(dashboard_regex), re.IGNORECASE)\n dashboards_to_migrate = [d for d in all_dashboards if regex_pattern.search(d.get(\"dashboard_title\", \"\"))]\n else:\n app_logger.explore(\"No deterministic selection criteria provided\")\n migration_result[\"status\"] = \"NO_SELECTION\"\n return migration_result\n\n if not dashboards_to_migrate:\n app_logger.explore(\"Zero dashboards match selection criteria\")\n migration_result[\"status\"] = \"NO_MATCHES\"\n return migration_result\n\n migration_result[\"selected_dashboards\"] = len(dashboards_to_migrate)\n\n # Database Mapping Resolution\n db_mapping = params.get(\"db_mappings\", {})\n if not isinstance(db_mapping, dict):\n db_mapping = {}\n\n if replace_db_config:\n app_logger.reason(\"Fetching environment DB mappings from catalog\")\n db = SessionLocal()\n try:\n src_env_db = db.query(Environment).filter(Environment.name == from_env_name).first()\n tgt_env_db = db.query(Environment).filter(Environment.name == to_env_name).first()\n\n if src_env_db and tgt_env_db:\n stored_mappings = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == src_env_db.id,\n DatabaseMapping.target_env_id == tgt_env_db.id\n ).all()\n stored_map_dict = {m.source_db_uuid: m.target_db_uuid for m in stored_mappings}\n stored_map_dict.update(db_mapping)\n db_mapping = stored_map_dict\n log.info(f\"Loaded {len(stored_mappings)} database mappings from database.\")\n finally:\n db.close()\n\n migration_result[\"mapping_count\"] = len(db_mapping)\n engine = MigrationEngine()\n\n # Migration Loop\n for dash in dashboards_to_migrate:\n dash_id, dash_slug, title = dash[\"id\"], dash.get(\"slug\"), dash[\"dashboard_title\"]\n app_logger.reason(f\"Starting pipeline for dashboard '{title}'\", extra={\"dash_id\": dash_id})\n\n try:\n exported_content, _ = from_c.export_dashboard(dash_id)\n with create_temp_file(content=exported_content, dry_run=True, suffix=\".zip\") as tmp_zip_path, create_temp_file(suffix=\".zip\", dry_run=True) as tmp_new_zip:\n\n success = engine.transform_zip(\n str(tmp_zip_path),\n str(tmp_new_zip),\n db_mapping,\n strip_databases=False,\n target_env_id=tgt_env.id if tgt_env else None,\n fix_cross_filters=fix_cross_filters\n )\n\n if not success and replace_db_config:\n if task_id:\n app_logger.explore(\"Missing mapping blocks AST transform. Pausing task for user intervention.\", extra={\"task_id\": task_id})\n await tm.wait_for_resolution(task_id)\n\n app_logger.reason(\"Task resumed, re-evaluating mapping states\")\n db = SessionLocal()\n try:\n src_env_rt = db.query(Environment).filter(Environment.name == from_env_name).first()\n tgt_env_rt = db.query(Environment).filter(Environment.name == to_env_name).first()\n mappings = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == src_env_rt.id,\n DatabaseMapping.target_env_id == tgt_env_rt.id\n ).all()\n db_mapping = {m.source_db_uuid: m.target_db_uuid for m in mappings}\n finally:\n db.close()\n\n success = engine.transform_zip(\n str(tmp_zip_path),\n str(tmp_new_zip),\n db_mapping,\n strip_databases=False,\n target_env_id=tgt_env.id if tgt_env else None,\n fix_cross_filters=fix_cross_filters\n )\n\n if success:\n app_logger.reason(\"Pushing transformed ZIP to target Superset\")\n to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug)\n migration_result[\"migrated_dashboards\"].append({\"id\": dash_id, \"title\": title})\n app_logger.reflect(\"Import successful\", extra={\"title\": title})\n else:\n app_logger.explore(\"Transformation strictly failed, bypassing ingestion\")\n migration_log.error(f\"Failed to transform ZIP for dashboard {title}\")\n migration_result[\"failed_dashboards\"].append({\n \"id\": dash_id, \"title\": title, \"error\": \"Failed to transform ZIP\"\n })\n\n except Exception as exc:\n error_msg = str(exc)\n if \"Must provide a password for the database\" in error_msg:\n db_name = \"unknown\"\n match = re.search(r\"databases/([^.]+)\\.yaml\", error_msg)\n if match:\n db_name = match.group(1)\n else:\n match_alt = re.search(r\"database '([^']+)'\", error_msg)\n if match_alt:\n db_name = match_alt.group(1)\n\n app_logger.explore(\"Missing DB password detected during ingestion. Escalating to UI.\", extra={\"db_name\": db_name})\n\n if task_id:\n tm.await_input(task_id, {\n \"type\": \"database_password\",\n \"databases\": [db_name],\n \"error_message\": error_msg\n })\n\n await tm.wait_for_input(task_id)\n task = tm.get_task(task_id)\n passwords = task.params.get(\"passwords\", {})\n\n if passwords:\n app_logger.reason(f\"Retrying import for {title} with injected credentials\")\n to_c.import_dashboard(file_name=tmp_new_zip, dash_id=dash_id, dash_slug=dash_slug, passwords=passwords)\n migration_result[\"migrated_dashboards\"].append({\"id\": dash_id, \"title\": title})\n app_logger.reflect(\"Password injection unblocked import\")\n if \"passwords\" in task.params:\n del task.params[\"passwords\"]\n continue\n\n app_logger.explore(f\"Catastrophic dashboard ingestion failure: {exc}\")\n migration_result[\"failed_dashboards\"].append({\"id\": dash_id, \"title\": title, \"error\": str(exc)})\n\n if migration_result[\"failed_dashboards\"]:\n migration_result[\"status\"] = \"PARTIAL_SUCCESS\"\n\n # Post-Migration ID Mapping Synchronization\n try:\n app_logger.reason(\"Executing incremental ID catalog sync on target\")\n db_session = SessionLocal()\n mapping_service = IdMappingService(db_session)\n mapping_service.sync_environment(tgt_env.id, to_c, incremental=True)\n db_session.close()\n app_logger.reflect(\"Incremental catalog sync closed out cleanly\")\n except Exception as sync_exc:\n app_logger.explore(f\"ID Mapping sync failed, mapping state might be degraded: {sync_exc}\")\n\n app_logger.reflect(\"Migration cycle fully resolved\", extra={\"result\": migration_result})\n return migration_result\n\n except Exception as e:\n app_logger.explore(f\"Fatal plugin failure: {e}\", exc_info=True)\n raise e\n # endregion execute\n# #endregion MigrationPlugin\n# #endregion MigrationPlugin\n" }, { "contract_id": "SearchPluginModule", "contract_type": "Module", "file_path": "backend/src/plugins/search.py", "start_line": 1, - "end_line": 220, + "end_line": 219, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "Plugins", - "PURPOSE": "Implements a plugin for searching text patterns across all datasets in a specific Superset environment.", - "SEMANTICS": [ - "plugin", - "search", - "datasets", - "regex", - "superset" - ] + "PURPOSE": "Implements a plugin for searching text patterns across all datasets in a specific Superset environment." }, "relations": [ { @@ -65760,15 +51706,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SearchPluginModule:Module]\n# @SEMANTICS: plugin, search, datasets, regex, superset\n# @PURPOSE: Implements a plugin for searching text patterns across all datasets in a specific Superset environment.\n# @LAYER: Plugins\n# @RELATION: Inherits from PluginBase. Uses SupersetClient from core.\n# @RELATION: USES -> TaskContext\n\n# [SECTION: IMPORTS]\nimport re\nfrom typing import Dict, Any, Optional\nfrom ..core.plugin_base import PluginBase\nfrom ..core.superset_client import SupersetClient\nfrom ..core.logger import logger, belief_scope\nfrom ..core.task_manager.context import TaskContext\n# [/SECTION]\n\n# [DEF:SearchPlugin:Class]\n# @PURPOSE: Plugin for searching text patterns in Superset datasets.\nclass SearchPlugin(PluginBase):\n \"\"\"\n Plugin for searching text patterns in Superset datasets.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"search-datasets\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"search-datasets\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Search Datasets\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Search for text patterns across all datasets in a specific environment.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the search plugin.\n # @RETURN: str - \"/tools/search\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/search\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the search plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment to search in (e.g., 'dev', 'prod').\"\n },\n \"query\": {\n \"type\": \"string\",\n \"title\": \"Search Query (Regex)\",\n \"description\": \"The regex pattern to search for.\"\n }\n },\n \"required\": [\"env\", \"query\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the dataset search logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Search parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env' and 'query'.\n # @POST: Returns a dictionary with count and results list.\n # @RETURN: Dict[str, Any] - Search results.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"SearchPlugin.execute\", f\"params={params}\"):\n env_name = params.get(\"env\")\n search_query = params.get(\"query\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n \n # Create sub-loggers for different components\n log.with_source(\"superset_api\") if context else log\n search_log = log.with_source(\"search\") if context else log\n \n if not env_name or not search_query:\n log.error(\"Missing required parameters: env, query\")\n raise ValueError(\"Missing required parameters: env, query\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n log.info(f\"Searching for pattern: '{search_query}' in environment: {env_name}\")\n \n try:\n # Ported logic from search_script.py\n _, datasets = client.get_datasets(query={\"columns\": [\"id\", \"table_name\", \"sql\", \"database\", \"columns\"]})\n\n if not datasets:\n search_log.warning(\"No datasets found.\")\n return {\"count\": 0, \"results\": []}\n\n pattern = re.compile(search_query, re.IGNORECASE)\n results = []\n \n for dataset in datasets:\n dataset_id = dataset.get('id')\n dataset_name = dataset.get('table_name', 'Unknown')\n if not dataset_id:\n continue\n\n for field, value in dataset.items():\n value_str = str(value)\n if pattern.search(value_str):\n match_obj = pattern.search(value_str)\n results.append({\n \"dataset_id\": dataset_id,\n \"dataset_name\": dataset_name,\n \"field\": field,\n \"match_context\": self._get_context(value_str, match_obj.group() if match_obj else \"\"),\n \"full_value\": value_str\n })\n\n search_log.info(f\"Found matches in {len(results)} locations.\")\n return {\n \"count\": len(results),\n \"results\": results\n }\n\n except re.error as e:\n search_log.error(f\"Invalid regex pattern: {e}\")\n raise ValueError(f\"Invalid regex pattern: {e}\")\n except Exception as e:\n log.error(f\"Error during search: {e}\")\n raise\n # [/DEF:execute:Function]\n\n # [DEF:_get_context:Function]\n # @PURPOSE: Extracts a small context around the match for display.\n # @PARAM: text (str) - The full text to extract context from.\n # @PARAM: match_text (str) - The matched text pattern.\n # @PARAM: context_lines (int) - Number of lines of context to include.\n # @PRE: text and match_text must be strings.\n # @POST: Returns context string.\n # @RETURN: str - Extracted context.\n def _get_context(self, text: str, match_text: str, context_lines: int = 1) -> str:\n \"\"\"\n Extracts a small context around the match for display.\n \"\"\"\n with belief_scope(\"_get_context\"):\n if not match_text:\n return text[:100] + \"...\" if len(text) > 100 else text\n \n lines = text.splitlines()\n match_line_index = -1\n for i, line in enumerate(lines):\n if match_text in line:\n match_line_index = i\n break\n \n if match_line_index != -1:\n start = max(0, match_line_index - context_lines)\n end = min(len(lines), match_line_index + context_lines + 1)\n context = []\n for i in range(start, end):\n line_content = lines[i]\n if i == match_line_index:\n context.append(f\"==> {line_content}\")\n else:\n context.append(f\" {line_content}\")\n return \"\\n\".join(context)\n \n return text[:100] + \"...\" if len(text) > 100 else text\n # [/DEF:_get_context:Function]\n\n# [/DEF:SearchPlugin:Class]\n# [/DEF:SearchPluginModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region SearchPluginModule [TYPE Module] [SEMANTICS search, dataset, text, pattern, superset]\n# @BRIEF Implements a plugin for searching text patterns across all datasets in a specific Superset environment.\n# @LAYER: Plugins\n# @RELATION Inherits from PluginBase. Uses SupersetClient from core.\n# @RELATION USES -> TaskContext\n\nimport re\nfrom typing import Any\n\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\n\n\n# #region SearchPlugin [TYPE Class]\n# @BRIEF Plugin for searching text patterns in Superset datasets.\nclass SearchPlugin(PluginBase):\n \"\"\"\n Plugin for searching text patterns in Superset datasets.\n \"\"\"\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"search-datasets\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"search-datasets\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Search Datasets\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns a description of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Search for text patterns across all datasets in a specific environment.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the version of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the search plugin.\n # @RETURN: str - \"/tools/search\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/search\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for the search plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment to search in (e.g., 'dev', 'prod').\"\n },\n \"query\": {\n \"type\": \"string\",\n \"title\": \"Search Query (Regex)\",\n \"description\": \"The regex pattern to search for.\"\n }\n },\n \"required\": [\"env\", \"query\"]\n }\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Executes the dataset search logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Search parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env' and 'query'.\n # @POST: Returns a dictionary with count and results list.\n # @RETURN: Dict[str, Any] - Search results.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None) -> dict[str, Any]:\n with belief_scope(\"SearchPlugin.execute\", f\"params={params}\"):\n env_name = params.get(\"env\")\n search_query = params.get(\"query\")\n\n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n\n # Create sub-loggers for different components\n log.with_source(\"superset_api\") if context else log\n search_log = log.with_source(\"search\") if context else log\n\n if not env_name or not search_query:\n log.error(\"Missing required parameters: env, query\")\n raise ValueError(\"Missing required parameters: env, query\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n log.info(f\"Searching for pattern: '{search_query}' in environment: {env_name}\")\n\n try:\n # Ported logic from search_script.py\n _, datasets = client.get_datasets(query={\"columns\": [\"id\", \"table_name\", \"sql\", \"database\", \"columns\"]})\n\n if not datasets:\n search_log.warning(\"No datasets found.\")\n return {\"count\": 0, \"results\": []}\n\n pattern = re.compile(search_query, re.IGNORECASE)\n results = []\n\n for dataset in datasets:\n dataset_id = dataset.get('id')\n dataset_name = dataset.get('table_name', 'Unknown')\n if not dataset_id:\n continue\n\n for field, value in dataset.items():\n value_str = str(value)\n if pattern.search(value_str):\n match_obj = pattern.search(value_str)\n results.append({\n \"dataset_id\": dataset_id,\n \"dataset_name\": dataset_name,\n \"field\": field,\n \"match_context\": self._get_context(value_str, match_obj.group() if match_obj else \"\"),\n \"full_value\": value_str\n })\n\n search_log.info(f\"Found matches in {len(results)} locations.\")\n return {\n \"count\": len(results),\n \"results\": results\n }\n\n except re.error as e:\n search_log.error(f\"Invalid regex pattern: {e}\")\n raise ValueError(f\"Invalid regex pattern: {e}\")\n except Exception as e:\n log.error(f\"Error during search: {e}\")\n raise\n # endregion execute\n\n # region _get_context [TYPE Function]\n # @PURPOSE: Extracts a small context around the match for display.\n # @PARAM: text (str) - The full text to extract context from.\n # @PARAM: match_text (str) - The matched text pattern.\n # @PARAM: context_lines (int) - Number of lines of context to include.\n # @PRE: text and match_text must be strings.\n # @POST: Returns context string.\n # @RETURN: str - Extracted context.\n def _get_context(self, text: str, match_text: str, context_lines: int = 1) -> str:\n \"\"\"\n Extracts a small context around the match for display.\n \"\"\"\n with belief_scope(\"_get_context\"):\n if not match_text:\n return text[:100] + \"...\" if len(text) > 100 else text\n\n lines = text.splitlines()\n match_line_index = -1\n for i, line in enumerate(lines):\n if match_text in line:\n match_line_index = i\n break\n\n if match_line_index != -1:\n start = max(0, match_line_index - context_lines)\n end = min(len(lines), match_line_index + context_lines + 1)\n context = []\n for i in range(start, end):\n line_content = lines[i]\n if i == match_line_index:\n context.append(f\"==> {line_content}\")\n else:\n context.append(f\" {line_content}\")\n return \"\\n\".join(context)\n\n return text[:100] + \"...\" if len(text) > 100 else text\n # endregion _get_context\n\n# #endregion SearchPlugin\n# #endregion SearchPluginModule\n" }, { "contract_id": "SearchPlugin", "contract_type": "Class", "file_path": "backend/src/plugins/search.py", - "start_line": 17, - "end_line": 219, + "start_line": 16, + "end_line": 218, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -65786,87 +51732,35 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SearchPlugin:Class]\n# @PURPOSE: Plugin for searching text patterns in Superset datasets.\nclass SearchPlugin(PluginBase):\n \"\"\"\n Plugin for searching text patterns in Superset datasets.\n \"\"\"\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"search-datasets\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"search-datasets\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Search Datasets\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Search for text patterns across all datasets in a specific environment.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the search plugin.\n # @RETURN: str - \"/tools/search\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/search\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for the search plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment to search in (e.g., 'dev', 'prod').\"\n },\n \"query\": {\n \"type\": \"string\",\n \"title\": \"Search Query (Regex)\",\n \"description\": \"The regex pattern to search for.\"\n }\n },\n \"required\": [\"env\", \"query\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes the dataset search logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Search parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env' and 'query'.\n # @POST: Returns a dictionary with count and results list.\n # @RETURN: Dict[str, Any] - Search results.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None) -> Dict[str, Any]:\n with belief_scope(\"SearchPlugin.execute\", f\"params={params}\"):\n env_name = params.get(\"env\")\n search_query = params.get(\"query\")\n \n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n \n # Create sub-loggers for different components\n log.with_source(\"superset_api\") if context else log\n search_log = log.with_source(\"search\") if context else log\n \n if not env_name or not search_query:\n log.error(\"Missing required parameters: env, query\")\n raise ValueError(\"Missing required parameters: env, query\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n log.info(f\"Searching for pattern: '{search_query}' in environment: {env_name}\")\n \n try:\n # Ported logic from search_script.py\n _, datasets = client.get_datasets(query={\"columns\": [\"id\", \"table_name\", \"sql\", \"database\", \"columns\"]})\n\n if not datasets:\n search_log.warning(\"No datasets found.\")\n return {\"count\": 0, \"results\": []}\n\n pattern = re.compile(search_query, re.IGNORECASE)\n results = []\n \n for dataset in datasets:\n dataset_id = dataset.get('id')\n dataset_name = dataset.get('table_name', 'Unknown')\n if not dataset_id:\n continue\n\n for field, value in dataset.items():\n value_str = str(value)\n if pattern.search(value_str):\n match_obj = pattern.search(value_str)\n results.append({\n \"dataset_id\": dataset_id,\n \"dataset_name\": dataset_name,\n \"field\": field,\n \"match_context\": self._get_context(value_str, match_obj.group() if match_obj else \"\"),\n \"full_value\": value_str\n })\n\n search_log.info(f\"Found matches in {len(results)} locations.\")\n return {\n \"count\": len(results),\n \"results\": results\n }\n\n except re.error as e:\n search_log.error(f\"Invalid regex pattern: {e}\")\n raise ValueError(f\"Invalid regex pattern: {e}\")\n except Exception as e:\n log.error(f\"Error during search: {e}\")\n raise\n # [/DEF:execute:Function]\n\n # [DEF:_get_context:Function]\n # @PURPOSE: Extracts a small context around the match for display.\n # @PARAM: text (str) - The full text to extract context from.\n # @PARAM: match_text (str) - The matched text pattern.\n # @PARAM: context_lines (int) - Number of lines of context to include.\n # @PRE: text and match_text must be strings.\n # @POST: Returns context string.\n # @RETURN: str - Extracted context.\n def _get_context(self, text: str, match_text: str, context_lines: int = 1) -> str:\n \"\"\"\n Extracts a small context around the match for display.\n \"\"\"\n with belief_scope(\"_get_context\"):\n if not match_text:\n return text[:100] + \"...\" if len(text) > 100 else text\n \n lines = text.splitlines()\n match_line_index = -1\n for i, line in enumerate(lines):\n if match_text in line:\n match_line_index = i\n break\n \n if match_line_index != -1:\n start = max(0, match_line_index - context_lines)\n end = min(len(lines), match_line_index + context_lines + 1)\n context = []\n for i in range(start, end):\n line_content = lines[i]\n if i == match_line_index:\n context.append(f\"==> {line_content}\")\n else:\n context.append(f\" {line_content}\")\n return \"\\n\".join(context)\n \n return text[:100] + \"...\" if len(text) > 100 else text\n # [/DEF:_get_context:Function]\n\n# [/DEF:SearchPlugin:Class]\n" + "anchor_syntax": "region", + "body": "# #region SearchPlugin [TYPE Class]\n# @BRIEF Plugin for searching text patterns in Superset datasets.\nclass SearchPlugin(PluginBase):\n \"\"\"\n Plugin for searching text patterns in Superset datasets.\n \"\"\"\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string ID.\n # @RETURN: str - \"search-datasets\"\n def id(self) -> str:\n with belief_scope(\"id\"):\n return \"search-datasets\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string name.\n # @RETURN: str - Plugin name.\n def name(self) -> str:\n with belief_scope(\"name\"):\n return \"Search Datasets\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns a description of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string description.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"description\"):\n return \"Search for text patterns across all datasets in a specific environment.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the version of the search plugin.\n # @PRE: Plugin instance exists.\n # @POST: Returns string version.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the search plugin.\n # @RETURN: str - \"/tools/search\"\n def ui_route(self) -> str:\n with belief_scope(\"ui_route\"):\n return \"/tools/search\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for the search plugin parameters.\n # @PRE: Plugin instance exists.\n # @POST: Returns dictionary schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"env\": {\n \"type\": \"string\",\n \"title\": \"Environment\",\n \"description\": \"The Superset environment to search in (e.g., 'dev', 'prod').\"\n },\n \"query\": {\n \"type\": \"string\",\n \"title\": \"Search Query (Regex)\",\n \"description\": \"The regex pattern to search for.\"\n }\n },\n \"required\": [\"env\", \"query\"]\n }\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Executes the dataset search logic with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Search parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: Params contain valid 'env' and 'query'.\n # @POST: Returns a dictionary with count and results list.\n # @RETURN: Dict[str, Any] - Search results.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None) -> dict[str, Any]:\n with belief_scope(\"SearchPlugin.execute\", f\"params={params}\"):\n env_name = params.get(\"env\")\n search_query = params.get(\"query\")\n\n # Use TaskContext logger if available, otherwise fall back to app logger\n log = context.logger if context else logger\n\n # Create sub-loggers for different components\n log.with_source(\"superset_api\") if context else log\n search_log = log.with_source(\"search\") if context else log\n\n if not env_name or not search_query:\n log.error(\"Missing required parameters: env, query\")\n raise ValueError(\"Missing required parameters: env, query\")\n\n # Get config and initialize client\n from ..dependencies import get_config_manager\n config_manager = get_config_manager()\n env_config = config_manager.get_environment(env_name)\n if not env_config:\n log.error(f\"Environment '{env_name}' not found in configuration.\")\n raise ValueError(f\"Environment '{env_name}' not found in configuration.\")\n\n client = SupersetClient(env_config)\n client.authenticate()\n\n log.info(f\"Searching for pattern: '{search_query}' in environment: {env_name}\")\n\n try:\n # Ported logic from search_script.py\n _, datasets = client.get_datasets(query={\"columns\": [\"id\", \"table_name\", \"sql\", \"database\", \"columns\"]})\n\n if not datasets:\n search_log.warning(\"No datasets found.\")\n return {\"count\": 0, \"results\": []}\n\n pattern = re.compile(search_query, re.IGNORECASE)\n results = []\n\n for dataset in datasets:\n dataset_id = dataset.get('id')\n dataset_name = dataset.get('table_name', 'Unknown')\n if not dataset_id:\n continue\n\n for field, value in dataset.items():\n value_str = str(value)\n if pattern.search(value_str):\n match_obj = pattern.search(value_str)\n results.append({\n \"dataset_id\": dataset_id,\n \"dataset_name\": dataset_name,\n \"field\": field,\n \"match_context\": self._get_context(value_str, match_obj.group() if match_obj else \"\"),\n \"full_value\": value_str\n })\n\n search_log.info(f\"Found matches in {len(results)} locations.\")\n return {\n \"count\": len(results),\n \"results\": results\n }\n\n except re.error as e:\n search_log.error(f\"Invalid regex pattern: {e}\")\n raise ValueError(f\"Invalid regex pattern: {e}\")\n except Exception as e:\n log.error(f\"Error during search: {e}\")\n raise\n # endregion execute\n\n # region _get_context [TYPE Function]\n # @PURPOSE: Extracts a small context around the match for display.\n # @PARAM: text (str) - The full text to extract context from.\n # @PARAM: match_text (str) - The matched text pattern.\n # @PARAM: context_lines (int) - Number of lines of context to include.\n # @PRE: text and match_text must be strings.\n # @POST: Returns context string.\n # @RETURN: str - Extracted context.\n def _get_context(self, text: str, match_text: str, context_lines: int = 1) -> str:\n \"\"\"\n Extracts a small context around the match for display.\n \"\"\"\n with belief_scope(\"_get_context\"):\n if not match_text:\n return text[:100] + \"...\" if len(text) > 100 else text\n\n lines = text.splitlines()\n match_line_index = -1\n for i, line in enumerate(lines):\n if match_text in line:\n match_line_index = i\n break\n\n if match_line_index != -1:\n start = max(0, match_line_index - context_lines)\n end = min(len(lines), match_line_index + context_lines + 1)\n context = []\n for i in range(start, end):\n line_content = lines[i]\n if i == match_line_index:\n context.append(f\"==> {line_content}\")\n else:\n context.append(f\" {line_content}\")\n return \"\\n\".join(context)\n\n return text[:100] + \"...\" if len(text) > 100 else text\n # endregion _get_context\n\n# #endregion SearchPlugin\n" }, { - "contract_id": "_get_context", - "contract_type": "Function", - "file_path": "backend/src/plugins/search.py", - "start_line": 181, - "end_line": 217, + "contract_id": "StoragePluginPackage", + "contract_type": "Package", + "file_path": "backend/src/plugins/storage/__init__.py", + "start_line": 1, + "end_line": 5, "tier": "TIER_1", "complexity": 1, - "metadata": { - "PARAM": "context_lines (int) - Number of lines of context to include.", - "POST": "Returns context string.", - "PRE": "text and match_text must be strings.", - "PURPOSE": "Extracts a small context around the match for display.", - "RETURN": "str - Extracted context." - }, + "metadata": {}, "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_context:Function]\n # @PURPOSE: Extracts a small context around the match for display.\n # @PARAM: text (str) - The full text to extract context from.\n # @PARAM: match_text (str) - The matched text pattern.\n # @PARAM: context_lines (int) - Number of lines of context to include.\n # @PRE: text and match_text must be strings.\n # @POST: Returns context string.\n # @RETURN: str - Extracted context.\n def _get_context(self, text: str, match_text: str, context_lines: int = 1) -> str:\n \"\"\"\n Extracts a small context around the match for display.\n \"\"\"\n with belief_scope(\"_get_context\"):\n if not match_text:\n return text[:100] + \"...\" if len(text) > 100 else text\n \n lines = text.splitlines()\n match_line_index = -1\n for i, line in enumerate(lines):\n if match_text in line:\n match_line_index = i\n break\n \n if match_line_index != -1:\n start = max(0, match_line_index - context_lines)\n end = min(len(lines), match_line_index + context_lines + 1)\n context = []\n for i in range(start, end):\n line_content = lines[i]\n if i == match_line_index:\n context.append(f\"==> {line_content}\")\n else:\n context.append(f\" {line_content}\")\n return \"\\n\".join(context)\n \n return text[:100] + \"...\" if len(text) > 100 else text\n # [/DEF:_get_context:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region StoragePluginPackage [TYPE Package] [SEMANTICS storage, plugin, package, export]\nfrom .plugin import StoragePlugin\n\n__all__ = [\"StoragePlugin\"]\n# #endregion StoragePluginPackage\n" }, { "contract_id": "StoragePlugin", "contract_type": "Module", "file_path": "backend/src/plugins/storage/plugin.py", "start_line": 1, - "end_line": 387, + "end_line": 391, "tier": "TIER_1", "complexity": 1, "metadata": { "INVARIANT": "All file operations must be restricted to the configured storage root.", "LAYER": "App", - "PURPOSE": "Provides core filesystem operations for managing backups and repositories.", - "SEMANTICS": [ - "storage", - "files", - "filesystem", - "plugin" - ] + "PURPOSE": "Provides core filesystem operations for managing backups and repositories." }, "relations": [ { @@ -65935,354 +51829,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:StoragePlugin:Module]\n#\n# @SEMANTICS: storage, files, filesystem, plugin\n# @PURPOSE: Provides core filesystem operations for managing backups and repositories.\n# @LAYER: App\n# @RELATION: IMPLEMENTS -> PluginBase\n# @RELATION: DEPENDS_ON -> backend.src.models.storage\n# @RELATION: USES -> TaskContext\n#\n# @INVARIANT: All file operations must be restricted to the configured storage root.\n\n# [SECTION: IMPORTS]\nimport os\nimport shutil\nfrom pathlib import Path\nfrom datetime import datetime\nfrom typing import Dict, Any, List, Optional\nfrom fastapi import UploadFile\n\nfrom ...core.plugin_base import PluginBase\nfrom ...core.logger import belief_scope\nfrom ...core.cot_logger import MarkerLogger\nfrom ...models.storage import StoredFile, FileCategory\n\nlog = MarkerLogger(\"StoragePlugin\")\nfrom ...dependencies import get_config_manager\nfrom ...core.task_manager.context import TaskContext\n# [/SECTION]\n\n# [DEF:StoragePlugin:Class]\n# @PURPOSE: Implementation of the storage management plugin.\nclass StoragePlugin(PluginBase):\n \"\"\"\n Plugin for managing local file storage for backups and repositories.\n \"\"\"\n\n # [DEF:__init__:Function]\n # @PURPOSE: Initializes the StoragePlugin and ensures required directories exist.\n # @PRE: Configuration manager must be accessible.\n # @POST: Storage root and category directories are created on disk.\n def __init__(self):\n with belief_scope(\"StoragePlugin:init\"):\n self.ensure_directories()\n # [/DEF:__init__:Function]\n\n @property\n # [DEF:id:Function]\n # @PURPOSE: Returns the unique identifier for the storage plugin.\n # @PRE: None.\n # @POST: Returns the plugin ID string.\n # @RETURN: str - \"storage-manager\"\n def id(self) -> str:\n with belief_scope(\"StoragePlugin:id\"):\n return \"storage-manager\"\n # [/DEF:id:Function]\n\n @property\n # [DEF:name:Function]\n # @PURPOSE: Returns the human-readable name of the storage plugin.\n # @PRE: None.\n # @POST: Returns the plugin name string.\n # @RETURN: str - \"Storage Manager\"\n def name(self) -> str:\n with belief_scope(\"StoragePlugin:name\"):\n return \"Storage Manager\"\n # [/DEF:name:Function]\n\n @property\n # [DEF:description:Function]\n # @PURPOSE: Returns a description of the storage plugin.\n # @PRE: None.\n # @POST: Returns the plugin description string.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"StoragePlugin:description\"):\n return \"Manages local file storage for backups and repositories.\"\n # [/DEF:description:Function]\n\n @property\n # [DEF:version:Function]\n # @PURPOSE: Returns the version of the storage plugin.\n # @PRE: None.\n # @POST: Returns the version string.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"StoragePlugin:version\"):\n return \"1.0.0\"\n # [/DEF:version:Function]\n\n @property\n # [DEF:ui_route:Function]\n # @PURPOSE: Returns the frontend route for the storage plugin.\n # @RETURN: str - \"/tools/storage\"\n def ui_route(self) -> str:\n with belief_scope(\"StoragePlugin:ui_route\"):\n return \"/tools/storage\"\n # [/DEF:ui_route:Function]\n\n # [DEF:get_schema:Function]\n # @PURPOSE: Returns the JSON schema for storage plugin parameters.\n # @PRE: None.\n # @POST: Returns a dictionary representing the JSON schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> Dict[str, Any]:\n with belief_scope(\"StoragePlugin:get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"category\": {\n \"type\": \"string\",\n \"enum\": [c.value for c in FileCategory],\n \"title\": \"Category\"\n }\n },\n \"required\": [\"category\"]\n }\n # [/DEF:get_schema:Function]\n\n # [DEF:execute:Function]\n # @PURPOSE: Executes storage-related tasks with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Storage parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: params must match the plugin schema.\n # @POST: Task is executed and logged.\n async def execute(self, params: Dict[str, Any], context: Optional[TaskContext] = None):\n with belief_scope(\"StoragePlugin:execute\"):\n log.reason(f\"Executing with params: {params}\")\n # [/DEF:execute:Function]\n\n # [DEF:get_storage_root:Function]\n # @PURPOSE: Resolves the absolute path to the storage root.\n # @PRE: Settings must define a storage root path.\n # @POST: Returns a Path object representing the storage root.\n def get_storage_root(self) -> Path:\n with belief_scope(\"StoragePlugin:get_storage_root\"):\n config_manager = get_config_manager()\n global_settings = config_manager.get_config().settings\n \n # Use storage.root_path as the source of truth for storage UI\n root = Path(global_settings.storage.root_path)\n \n if not root.is_absolute():\n # Resolve relative to the backend directory\n # Path(__file__) is backend/src/plugins/storage/plugin.py\n # parents[3] is the project root (ss-tools)\n # We need to ensure it's relative to where backend/ is\n project_root = Path(__file__).parents[3]\n root = (project_root / root).resolve()\n return root\n # [/DEF:get_storage_root:Function]\n\n # [DEF:resolve_path:Function]\n # @PURPOSE: Resolves a dynamic path pattern using provided variables.\n # @PARAM: pattern (str) - The path pattern to resolve.\n # @PARAM: variables (Dict[str, str]) - Variables to substitute in the pattern.\n # @PRE: pattern must be a valid format string.\n # @POST: Returns the resolved path string.\n # @RETURN: str - The resolved path.\n def resolve_path(self, pattern: str, variables: Dict[str, str]) -> str:\n with belief_scope(\"StoragePlugin:resolve_path\"):\n # Add common variables\n vars_with_defaults = {\n \"timestamp\": datetime.now().strftime(\"%Y%m%dT%H%M%S\"),\n **variables\n }\n try:\n resolved = pattern.format(**vars_with_defaults)\n # Clean up any double slashes or leading/trailing slashes for relative path\n return os.path.normpath(resolved).strip(\"/\")\n except KeyError as e:\n log.explore(\"Missing variable for path resolution\", error=str(e))\n # Fallback to literal pattern if formatting fails partially (or handle as needed)\n return pattern.replace(\"{\", \"\").replace(\"}\", \"\")\n # [/DEF:resolve_path:Function]\n\n # [DEF:ensure_directories:Function]\n # @PURPOSE: Creates the storage root and category subdirectories if they don't exist.\n # @PRE: Storage root must be resolvable.\n # @POST: Directories are created on the filesystem.\n # @SIDE_EFFECT: Creates directories on the filesystem.\n def ensure_directories(self):\n with belief_scope(\"StoragePlugin:ensure_directories\"):\n root = self.get_storage_root()\n for category in FileCategory:\n # Use singular name for consistency with BackupPlugin and GitService\n path = root / category.value\n path.mkdir(parents=True, exist_ok=True)\n log.reason(f\"Ensured directory: {path}\")\n # [/DEF:ensure_directories:Function]\n\n # [DEF:validate_path:Function]\n # @PURPOSE: Prevents path traversal attacks by ensuring the path is within the storage root.\n # @PRE: path must be a Path object.\n # @POST: Returns the resolved absolute path if valid, otherwise raises ValueError.\n def validate_path(self, path: Path) -> Path:\n with belief_scope(\"StoragePlugin:validate_path\"):\n root = self.get_storage_root().resolve()\n resolved = path.resolve()\n try:\n resolved.relative_to(root)\n except ValueError:\n log.explore(f\"Path traversal detected: {resolved} is not under {root}\", error=\"Path outside storage root\")\n raise ValueError(\"Access denied: Path is outside of storage root.\")\n return resolved\n # [/DEF:validate_path:Function]\n\n # [DEF:list_files:Function]\n # @PURPOSE: Lists all files and directories in a specific category and subpath.\n # @PARAM: category (Optional[FileCategory]) - The category to list.\n # @PARAM: subpath (Optional[str]) - Nested path within the category.\n # @PARAM: recursive (bool) - Whether to scan nested subdirectories recursively.\n # @PRE: Storage root must exist.\n # @POST: Returns a list of StoredFile objects.\n # @RETURN: List[StoredFile] - List of file and directory metadata objects.\n def list_files(\n self,\n category: Optional[FileCategory] = None,\n subpath: Optional[str] = None,\n recursive: bool = False,\n ) -> List[StoredFile]:\n with belief_scope(\"StoragePlugin:list_files\"):\n root = self.get_storage_root()\n log.reason(\n f\"Listing files in root: {root}, category: {category}, subpath: {subpath}, recursive: {recursive}\"\n )\n files = []\n\n # Root view contract: show category directories only.\n if category is None and not subpath:\n for cat in FileCategory:\n base_dir = root / cat.value\n if not base_dir.exists():\n continue\n stat = base_dir.stat()\n files.append(\n StoredFile(\n name=cat.value,\n path=cat.value,\n size=0,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=cat,\n mime_type=\"directory\",\n )\n )\n return sorted(files, key=lambda x: x.name)\n \n categories = [category] if category else list(FileCategory)\n \n for cat in categories:\n # Scan the category subfolder + optional subpath\n base_dir = root / cat.value\n if subpath:\n target_dir = self.validate_path(base_dir / subpath)\n else:\n target_dir = base_dir\n\n if not target_dir.exists():\n continue\n \n log.reason(f\"Scanning directory: {target_dir}\")\n\n if recursive:\n for current_root, dirs, filenames in os.walk(target_dir):\n dirs[:] = [d for d in dirs if \"Logs\" not in d]\n for filename in filenames:\n file_path = Path(current_root) / filename\n if \"Logs\" in str(file_path):\n continue\n stat = file_path.stat()\n files.append(\n StoredFile(\n name=filename,\n path=str(file_path.relative_to(root)),\n size=stat.st_size,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=cat,\n mime_type=None,\n )\n )\n continue\n\n # Use os.scandir for better performance and to distinguish files vs dirs\n with os.scandir(target_dir) as it:\n for entry in it:\n # Skip logs\n if \"Logs\" in entry.path:\n continue\n\n stat = entry.stat()\n is_dir = entry.is_dir()\n\n files.append(StoredFile(\n name=entry.name,\n path=str(Path(entry.path).relative_to(root)),\n size=stat.st_size if not is_dir else 0,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=cat,\n mime_type=\"directory\" if is_dir else None\n ))\n \n # Sort: directories first, then by name\n return sorted(files, key=lambda x: (x.mime_type != \"directory\", x.name))\n # [/DEF:list_files:Function]\n\n # [DEF:save_file:Function]\n # @PURPOSE: Saves an uploaded file to the specified category and optional subpath.\n # @PARAM: file (UploadFile) - The uploaded file.\n # @PARAM: category (FileCategory) - The target category.\n # @PARAM: subpath (Optional[str]) - The target subpath.\n # @PRE: file must be a valid UploadFile; category must be valid.\n # @POST: File is written to disk and metadata is returned.\n # @RETURN: StoredFile - Metadata of the saved file.\n # @SIDE_EFFECT: Writes file to disk.\n async def save_file(self, file: UploadFile, category: FileCategory, subpath: Optional[str] = None) -> StoredFile:\n with belief_scope(\"StoragePlugin:save_file\"):\n root = self.get_storage_root()\n dest_dir = root / category.value\n if subpath:\n dest_dir = dest_dir / subpath\n \n dest_dir.mkdir(parents=True, exist_ok=True)\n \n dest_path = self.validate_path(dest_dir / file.filename)\n \n with dest_path.open(\"wb\") as buffer:\n shutil.copyfileobj(file.file, buffer)\n \n stat = dest_path.stat()\n return StoredFile(\n name=dest_path.name,\n path=str(dest_path.relative_to(root)),\n size=stat.st_size,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=category,\n mime_type=file.content_type\n )\n # [/DEF:save_file:Function]\n\n # [DEF:delete_file:Function]\n # @PURPOSE: Deletes a file or directory from the specified category and path.\n # @PARAM: category (FileCategory) - The category.\n # @PARAM: path (str) - The relative path of the file or directory.\n # @PRE: path must belong to the specified category and exist on disk.\n # @POST: The file or directory is removed from disk.\n # @SIDE_EFFECT: Removes item from disk.\n def delete_file(self, category: FileCategory, path: str):\n with belief_scope(\"StoragePlugin:delete_file\"):\n root = self.get_storage_root()\n # path is relative to root, but we ensure it starts with category\n full_path = self.validate_path(root / path)\n \n if not str(Path(path)).startswith(category.value):\n raise ValueError(f\"Path {path} does not belong to category {category}\")\n\n if full_path.exists():\n if full_path.is_dir():\n shutil.rmtree(full_path)\n else:\n full_path.unlink()\n log.reflect(f\"Deleted: {full_path}\")\n else:\n raise FileNotFoundError(f\"Item {path} not found\")\n # [/DEF:delete_file:Function]\n\n # [DEF:get_file_path:Function]\n # @PURPOSE: Returns the absolute path of a file for download.\n # @PARAM: category (FileCategory) - The category.\n # @PARAM: path (str) - The relative path of the file.\n # @PRE: path must belong to the specified category and be a file.\n # @POST: Returns the absolute Path to the file.\n # @RETURN: Path - Absolute path to the file.\n def get_file_path(self, category: FileCategory, path: str) -> Path:\n with belief_scope(\"StoragePlugin:get_file_path\"):\n root = self.get_storage_root()\n file_path = self.validate_path(root / path)\n \n if not str(Path(path)).startswith(category.value):\n raise ValueError(f\"Path {path} does not belong to category {category}\")\n\n if not file_path.exists() or file_path.is_dir():\n raise FileNotFoundError(f\"File {path} not found\")\n \n return file_path\n # [/DEF:get_file_path:Function]\n\n# [/DEF:StoragePlugin:Class]\n# [/DEF:StoragePlugin:Module]\n" - }, - { - "contract_id": "get_storage_root", - "contract_type": "Function", - "file_path": "backend/src/plugins/storage/plugin.py", - "start_line": 130, - "end_line": 150, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns a Path object representing the storage root.", - "PRE": "Settings must define a storage root path.", - "PURPOSE": "Resolves the absolute path to the storage root." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_storage_root:Function]\n # @PURPOSE: Resolves the absolute path to the storage root.\n # @PRE: Settings must define a storage root path.\n # @POST: Returns a Path object representing the storage root.\n def get_storage_root(self) -> Path:\n with belief_scope(\"StoragePlugin:get_storage_root\"):\n config_manager = get_config_manager()\n global_settings = config_manager.get_config().settings\n \n # Use storage.root_path as the source of truth for storage UI\n root = Path(global_settings.storage.root_path)\n \n if not root.is_absolute():\n # Resolve relative to the backend directory\n # Path(__file__) is backend/src/plugins/storage/plugin.py\n # parents[3] is the project root (ss-tools)\n # We need to ensure it's relative to where backend/ is\n project_root = Path(__file__).parents[3]\n root = (project_root / root).resolve()\n return root\n # [/DEF:get_storage_root:Function]\n" - }, - { - "contract_id": "resolve_path", - "contract_type": "Function", - "file_path": "backend/src/plugins/storage/plugin.py", - "start_line": 152, - "end_line": 174, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "variables (Dict[str, str]) - Variables to substitute in the pattern.", - "POST": "Returns the resolved path string.", - "PRE": "pattern must be a valid format string.", - "PURPOSE": "Resolves a dynamic path pattern using provided variables.", - "RETURN": "str - The resolved path." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:resolve_path:Function]\n # @PURPOSE: Resolves a dynamic path pattern using provided variables.\n # @PARAM: pattern (str) - The path pattern to resolve.\n # @PARAM: variables (Dict[str, str]) - Variables to substitute in the pattern.\n # @PRE: pattern must be a valid format string.\n # @POST: Returns the resolved path string.\n # @RETURN: str - The resolved path.\n def resolve_path(self, pattern: str, variables: Dict[str, str]) -> str:\n with belief_scope(\"StoragePlugin:resolve_path\"):\n # Add common variables\n vars_with_defaults = {\n \"timestamp\": datetime.now().strftime(\"%Y%m%dT%H%M%S\"),\n **variables\n }\n try:\n resolved = pattern.format(**vars_with_defaults)\n # Clean up any double slashes or leading/trailing slashes for relative path\n return os.path.normpath(resolved).strip(\"/\")\n except KeyError as e:\n log.explore(\"Missing variable for path resolution\", error=str(e))\n # Fallback to literal pattern if formatting fails partially (or handle as needed)\n return pattern.replace(\"{\", \"\").replace(\"}\", \"\")\n # [/DEF:resolve_path:Function]\n" - }, - { - "contract_id": "ensure_directories", - "contract_type": "Function", - "file_path": "backend/src/plugins/storage/plugin.py", - "start_line": 176, - "end_line": 189, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Directories are created on the filesystem.", - "PRE": "Storage root must be resolvable.", - "PURPOSE": "Creates the storage root and category subdirectories if they don't exist.", - "SIDE_EFFECT": "Creates directories on the filesystem." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ensure_directories:Function]\n # @PURPOSE: Creates the storage root and category subdirectories if they don't exist.\n # @PRE: Storage root must be resolvable.\n # @POST: Directories are created on the filesystem.\n # @SIDE_EFFECT: Creates directories on the filesystem.\n def ensure_directories(self):\n with belief_scope(\"StoragePlugin:ensure_directories\"):\n root = self.get_storage_root()\n for category in FileCategory:\n # Use singular name for consistency with BackupPlugin and GitService\n path = root / category.value\n path.mkdir(parents=True, exist_ok=True)\n log.reason(f\"Ensured directory: {path}\")\n # [/DEF:ensure_directories:Function]\n" - }, - { - "contract_id": "save_file", - "contract_type": "Function", - "file_path": "backend/src/plugins/storage/plugin.py", - "start_line": 305, - "end_line": 337, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "subpath (Optional[str]) - The target subpath.", - "POST": "File is written to disk and metadata is returned.", - "PRE": "file must be a valid UploadFile; category must be valid.", - "PURPOSE": "Saves an uploaded file to the specified category and optional subpath.", - "RETURN": "StoredFile - Metadata of the saved file.", - "SIDE_EFFECT": "Writes file to disk." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:save_file:Function]\n # @PURPOSE: Saves an uploaded file to the specified category and optional subpath.\n # @PARAM: file (UploadFile) - The uploaded file.\n # @PARAM: category (FileCategory) - The target category.\n # @PARAM: subpath (Optional[str]) - The target subpath.\n # @PRE: file must be a valid UploadFile; category must be valid.\n # @POST: File is written to disk and metadata is returned.\n # @RETURN: StoredFile - Metadata of the saved file.\n # @SIDE_EFFECT: Writes file to disk.\n async def save_file(self, file: UploadFile, category: FileCategory, subpath: Optional[str] = None) -> StoredFile:\n with belief_scope(\"StoragePlugin:save_file\"):\n root = self.get_storage_root()\n dest_dir = root / category.value\n if subpath:\n dest_dir = dest_dir / subpath\n \n dest_dir.mkdir(parents=True, exist_ok=True)\n \n dest_path = self.validate_path(dest_dir / file.filename)\n \n with dest_path.open(\"wb\") as buffer:\n shutil.copyfileobj(file.file, buffer)\n \n stat = dest_path.stat()\n return StoredFile(\n name=dest_path.name,\n path=str(dest_path.relative_to(root)),\n size=stat.st_size,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=category,\n mime_type=file.content_type\n )\n # [/DEF:save_file:Function]\n" - }, - { - "contract_id": "get_file_path", - "contract_type": "Function", - "file_path": "backend/src/plugins/storage/plugin.py", - "start_line": 365, - "end_line": 384, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "path (str) - The relative path of the file.", - "POST": "Returns the absolute Path to the file.", - "PRE": "path must belong to the specified category and be a file.", - "PURPOSE": "Returns the absolute path of a file for download.", - "RETURN": "Path - Absolute path to the file." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_file_path:Function]\n # @PURPOSE: Returns the absolute path of a file for download.\n # @PARAM: category (FileCategory) - The category.\n # @PARAM: path (str) - The relative path of the file.\n # @PRE: path must belong to the specified category and be a file.\n # @POST: Returns the absolute Path to the file.\n # @RETURN: Path - Absolute path to the file.\n def get_file_path(self, category: FileCategory, path: str) -> Path:\n with belief_scope(\"StoragePlugin:get_file_path\"):\n root = self.get_storage_root()\n file_path = self.validate_path(root / path)\n \n if not str(Path(path)).startswith(category.value):\n raise ValueError(f\"Path {path} does not belong to category {category}\")\n\n if not file_path.exists() or file_path.is_dir():\n raise FileNotFoundError(f\"File {path} not found\")\n \n return file_path\n # [/DEF:get_file_path:Function]\n" + "anchor_syntax": "region", + "body": "# #region StoragePlugin [TYPE Module] [SEMANTICS fastapi, storage, filesystem, backup, archive]\n#\n# @BRIEF Provides core filesystem operations for managing backups and repositories.\n# @LAYER: App\n# @RELATION IMPLEMENTS -> PluginBase\n# @RELATION DEPENDS_ON -> backend.src.models.storage\n# @RELATION USES -> TaskContext\n#\n# @INVARIANT: All file operations must be restricted to the configured storage root.\n\nimport os\nimport shutil\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Any\n\nfrom fastapi import UploadFile\n\nfrom ...core.logger import belief_scope, logger\nfrom ...core.plugin_base import PluginBase\nfrom ...core.task_manager.context import TaskContext\nfrom ...dependencies import get_config_manager\nfrom ...models.storage import FileCategory, StoredFile\n\n\n# #region StoragePlugin [TYPE Class]\n# @BRIEF Implementation of the storage management plugin.\nclass StoragePlugin(PluginBase):\n \"\"\"\n Plugin for managing local file storage for backups and repositories.\n \"\"\"\n\n # region __init__ [TYPE Function]\n # @PURPOSE: Initializes the StoragePlugin and ensures required directories exist.\n # @PRE: Configuration manager must be accessible.\n # @POST: Storage root and category directories are created on disk.\n def __init__(self):\n with belief_scope(\"StoragePlugin:init\"):\n self.ensure_directories()\n # endregion __init__\n\n @property\n # region id [TYPE Function]\n # @PURPOSE: Returns the unique identifier for the storage plugin.\n # @PRE: None.\n # @POST: Returns the plugin ID string.\n # @RETURN: str - \"storage-manager\"\n def id(self) -> str:\n with belief_scope(\"StoragePlugin:id\"):\n return \"storage-manager\"\n # endregion id\n\n @property\n # region name [TYPE Function]\n # @PURPOSE: Returns the human-readable name of the storage plugin.\n # @PRE: None.\n # @POST: Returns the plugin name string.\n # @RETURN: str - \"Storage Manager\"\n def name(self) -> str:\n with belief_scope(\"StoragePlugin:name\"):\n return \"Storage Manager\"\n # endregion name\n\n @property\n # region description [TYPE Function]\n # @PURPOSE: Returns a description of the storage plugin.\n # @PRE: None.\n # @POST: Returns the plugin description string.\n # @RETURN: str - Plugin description.\n def description(self) -> str:\n with belief_scope(\"StoragePlugin:description\"):\n return \"Manages local file storage for backups and repositories.\"\n # endregion description\n\n @property\n # region version [TYPE Function]\n # @PURPOSE: Returns the version of the storage plugin.\n # @PRE: None.\n # @POST: Returns the version string.\n # @RETURN: str - \"1.0.0\"\n def version(self) -> str:\n with belief_scope(\"StoragePlugin:version\"):\n return \"1.0.0\"\n # endregion version\n\n @property\n # region ui_route [TYPE Function]\n # @PURPOSE: Returns the frontend route for the storage plugin.\n # @RETURN: str - \"/tools/storage\"\n def ui_route(self) -> str:\n with belief_scope(\"StoragePlugin:ui_route\"):\n return \"/tools/storage\"\n # endregion ui_route\n\n # region get_schema [TYPE Function]\n # @PURPOSE: Returns the JSON schema for storage plugin parameters.\n # @PRE: None.\n # @POST: Returns a dictionary representing the JSON schema.\n # @RETURN: Dict[str, Any] - JSON schema.\n def get_schema(self) -> dict[str, Any]:\n with belief_scope(\"StoragePlugin:get_schema\"):\n return {\n \"type\": \"object\",\n \"properties\": {\n \"category\": {\n \"type\": \"string\",\n \"enum\": [c.value for c in FileCategory],\n \"title\": \"Category\"\n }\n },\n \"required\": [\"category\"]\n }\n # endregion get_schema\n\n # region execute [TYPE Function]\n # @PURPOSE: Executes storage-related tasks with TaskContext support.\n # @PARAM: params (Dict[str, Any]) - Storage parameters.\n # @PARAM: context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE: params must match the plugin schema.\n # @POST: Task is executed and logged.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"StoragePlugin:execute\"):\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 storage_log = log.with_source(\"storage\") if context else log\n log.with_source(\"filesystem\") if context else log\n\n storage_log.info(f\"Executing with params: {params}\")\n # endregion execute\n\n # region get_storage_root [TYPE Function]\n # @PURPOSE: Resolves the absolute path to the storage root.\n # @PRE: Settings must define a storage root path.\n # @POST: Returns a Path object representing the storage root.\n def get_storage_root(self) -> Path:\n with belief_scope(\"StoragePlugin:get_storage_root\"):\n config_manager = get_config_manager()\n global_settings = config_manager.get_config().settings\n\n # Use storage.root_path as the source of truth for storage UI\n root = Path(global_settings.storage.root_path)\n\n if not root.is_absolute():\n # Resolve relative to the backend directory\n # Path(__file__) is backend/src/plugins/storage/plugin.py\n # parents[3] is the project root (ss-tools)\n # We need to ensure it's relative to where backend/ is\n project_root = Path(__file__).parents[3]\n root = (project_root / root).resolve()\n return root\n # endregion get_storage_root\n\n # region resolve_path [TYPE Function]\n # @PURPOSE: Resolves a dynamic path pattern using provided variables.\n # @PARAM: pattern (str) - The path pattern to resolve.\n # @PARAM: variables (Dict[str, str]) - Variables to substitute in the pattern.\n # @PRE: pattern must be a valid format string.\n # @POST: Returns the resolved path string.\n # @RETURN: str - The resolved path.\n def resolve_path(self, pattern: str, variables: dict[str, str]) -> str:\n with belief_scope(\"StoragePlugin:resolve_path\"):\n # Add common variables\n vars_with_defaults = {\n \"timestamp\": datetime.now().strftime(\"%Y%m%dT%H%M%S\"),\n **variables\n }\n try:\n resolved = pattern.format(**vars_with_defaults)\n # Clean up any double slashes or leading/trailing slashes for relative path\n return os.path.normpath(resolved).strip(\"/\")\n except KeyError as e:\n logger.warning(f\"[StoragePlugin][Coherence:Failed] Missing variable for path resolution: {e}\")\n # Fallback to literal pattern if formatting fails partially (or handle as needed)\n return pattern.replace(\"{\", \"\").replace(\"}\", \"\")\n # endregion resolve_path\n\n # region ensure_directories [TYPE Function]\n # @PURPOSE: Creates the storage root and category subdirectories if they don't exist.\n # @PRE: Storage root must be resolvable.\n # @POST: Directories are created on the filesystem.\n # @SIDE_EFFECT: Creates directories on the filesystem.\n def ensure_directories(self):\n with belief_scope(\"StoragePlugin:ensure_directories\"):\n root = self.get_storage_root()\n for category in FileCategory:\n # Use singular name for consistency with BackupPlugin and GitService\n path = root / category.value\n path.mkdir(parents=True, exist_ok=True)\n logger.reason(f\"Ensured directory: {path}\", extra={\"src\": \"StoragePlugin.ensure_directories\"})\n # endregion ensure_directories\n\n # region validate_path [TYPE Function]\n # @PURPOSE: Prevents path traversal attacks by ensuring the path is within the storage root.\n # @PRE: path must be a Path object.\n # @POST: Returns the resolved absolute path if valid, otherwise raises ValueError.\n def validate_path(self, path: Path) -> Path:\n with belief_scope(\"StoragePlugin:validate_path\"):\n root = self.get_storage_root().resolve()\n resolved = path.resolve()\n try:\n resolved.relative_to(root)\n except ValueError:\n logger.error(f\"[StoragePlugin][Coherence:Failed] Path traversal detected: {resolved} is not under {root}\")\n raise ValueError(\"Access denied: Path is outside of storage root.\")\n return resolved\n # endregion validate_path\n\n # region list_files [TYPE Function]\n # @PURPOSE: Lists all files and directories in a specific category and subpath.\n # @PARAM: category (Optional[FileCategory]) - The category to list.\n # @PARAM: subpath (Optional[str]) - Nested path within the category.\n # @PARAM: recursive (bool) - Whether to scan nested subdirectories recursively.\n # @PRE: Storage root must exist.\n # @POST: Returns a list of StoredFile objects.\n # @RETURN: List[StoredFile] - List of file and directory metadata objects.\n def list_files(\n self,\n category: FileCategory | None = None,\n subpath: str | None = None,\n recursive: bool = False,\n ) -> list[StoredFile]:\n with belief_scope(\"StoragePlugin:list_files\"):\n root = self.get_storage_root()\n logger.reason(\n f\"Listing files in root: {root}\",\n extra={\"src\": \"StoragePlugin.list_files\", \"payload\": {\"root\": str(root), \"category\": category.value if category else None, \"subpath\": subpath, \"recursive\": recursive}},\n )\n files = []\n\n # Root view contract: show category directories only.\n if category is None and not subpath:\n for cat in FileCategory:\n base_dir = root / cat.value\n if not base_dir.exists():\n continue\n stat = base_dir.stat()\n files.append(\n StoredFile(\n name=cat.value,\n path=cat.value,\n size=0,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=cat,\n mime_type=\"directory\",\n )\n )\n return sorted(files, key=lambda x: x.name)\n\n categories = [category] if category else list(FileCategory)\n\n for cat in categories:\n # Scan the category subfolder + optional subpath\n base_dir = root / cat.value\n if subpath:\n target_dir = self.validate_path(base_dir / subpath)\n else:\n target_dir = base_dir\n\n if not target_dir.exists():\n continue\n\n logger.reason(f\"Scanning directory: {target_dir}\", extra={\"src\": \"StoragePlugin.list_files\"})\n\n if recursive:\n for current_root, dirs, filenames in os.walk(target_dir):\n dirs[:] = [d for d in dirs if \"Logs\" not in d]\n for filename in filenames:\n file_path = Path(current_root) / filename\n if \"Logs\" in str(file_path):\n continue\n stat = file_path.stat()\n files.append(\n StoredFile(\n name=filename,\n path=str(file_path.relative_to(root)),\n size=stat.st_size,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=cat,\n mime_type=None,\n )\n )\n continue\n\n # Use os.scandir for better performance and to distinguish files vs dirs\n with os.scandir(target_dir) as it:\n for entry in it:\n # Skip logs\n if \"Logs\" in entry.path:\n continue\n\n stat = entry.stat()\n is_dir = entry.is_dir()\n\n files.append(StoredFile(\n name=entry.name,\n path=str(Path(entry.path).relative_to(root)),\n size=stat.st_size if not is_dir else 0,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=cat,\n mime_type=\"directory\" if is_dir else None\n ))\n\n # Sort: directories first, then by name\n return sorted(files, key=lambda x: (x.mime_type != \"directory\", x.name))\n # endregion list_files\n\n # region save_file [TYPE Function]\n # @PURPOSE: Saves an uploaded file to the specified category and optional subpath.\n # @PARAM: file (UploadFile) - The uploaded file.\n # @PARAM: category (FileCategory) - The target category.\n # @PARAM: subpath (Optional[str]) - The target subpath.\n # @PRE: file must be a valid UploadFile; category must be valid.\n # @POST: File is written to disk and metadata is returned.\n # @RETURN: StoredFile - Metadata of the saved file.\n # @SIDE_EFFECT: Writes file to disk.\n async def save_file(self, file: UploadFile, category: FileCategory, subpath: str | None = None) -> StoredFile:\n with belief_scope(\"StoragePlugin:save_file\"):\n root = self.get_storage_root()\n dest_dir = root / category.value\n if subpath:\n dest_dir = dest_dir / subpath\n\n dest_dir.mkdir(parents=True, exist_ok=True)\n\n dest_path = self.validate_path(dest_dir / file.filename)\n\n with dest_path.open(\"wb\") as buffer:\n shutil.copyfileobj(file.file, buffer)\n\n stat = dest_path.stat()\n return StoredFile(\n name=dest_path.name,\n path=str(dest_path.relative_to(root)),\n size=stat.st_size,\n created_at=datetime.fromtimestamp(stat.st_ctime),\n category=category,\n mime_type=file.content_type\n )\n # endregion save_file\n\n # region delete_file [TYPE Function]\n # @PURPOSE: Deletes a file or directory from the specified category and path.\n # @PARAM: category (FileCategory) - The category.\n # @PARAM: path (str) - The relative path of the file or directory.\n # @PRE: path must belong to the specified category and exist on disk.\n # @POST: The file or directory is removed from disk.\n # @SIDE_EFFECT: Removes item from disk.\n def delete_file(self, category: FileCategory, path: str):\n with belief_scope(\"StoragePlugin:delete_file\"):\n root = self.get_storage_root()\n # path is relative to root, but we ensure it starts with category\n full_path = self.validate_path(root / path)\n\n if not str(Path(path)).startswith(category.value):\n raise ValueError(f\"Path {path} does not belong to category {category}\")\n\n if full_path.exists():\n if full_path.is_dir():\n shutil.rmtree(full_path)\n else:\n full_path.unlink()\n logger.reason(f\"Deleted: {full_path}\", extra={\"src\": \"StoragePlugin.delete_file\"})\n else:\n raise FileNotFoundError(f\"Item {path} not found\")\n # endregion delete_file\n\n # region get_file_path [TYPE Function]\n # @PURPOSE: Returns the absolute path of a file for download.\n # @PARAM: category (FileCategory) - The category.\n # @PARAM: path (str) - The relative path of the file.\n # @PRE: path must belong to the specified category and be a file.\n # @POST: Returns the absolute Path to the file.\n # @RETURN: Path - Absolute path to the file.\n def get_file_path(self, category: FileCategory, path: str) -> Path:\n with belief_scope(\"StoragePlugin:get_file_path\"):\n root = self.get_storage_root()\n file_path = self.validate_path(root / path)\n\n if not str(Path(path)).startswith(category.value):\n raise ValueError(f\"Path {path} does not belong to category {category}\")\n\n if not file_path.exists() or file_path.is_dir():\n raise FileNotFoundError(f\"File {path} not found\")\n\n return file_path\n # endregion get_file_path\n\n# #endregion StoragePlugin\n# #endregion StoragePlugin\n" }, { "contract_id": "TranslatePluginPackage", "contract_type": "Package", "file_path": "backend/src/plugins/translate/__init__.py", "start_line": 1, - "end_line": 2, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Package'", - "detail": { - "actual_type": "Package", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Package' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Package" - } - } - ], - "anchor_syntax": "region", - "body": "# #region TranslatePluginPackage [C:1] [TYPE Package]\n# #endregion TranslatePluginPackage\n" - }, - { - "contract_id": "TranslatePluginTestsPackage", - "contract_type": "Package", - "file_path": "backend/src/plugins/translate/__tests__/__init__.py", - "start_line": 1, "end_line": 3, "tier": "TIER_1", "complexity": 1, "metadata": { - "PURPOSE": "Tests for the translate plugin package." + "PURPOSE": "Translation plugin package for LLM-based cross-Superset SQL/dashboard translation." }, "relations": [], "schema_warnings": [ @@ -66314,74 +51873,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TranslatePluginTestsPackage:Package]\n# @PURPOSE: Tests for the translate plugin package.\n# [/DEF:TranslatePluginTestsPackage:Package]\n" + "anchor_syntax": "region", + "body": "# #region TranslatePluginPackage [TYPE Package] [SEMANTICS translate, plugin, package, sql, dashboard]\n# @BRIEF Translation plugin package for LLM-based cross-Superset SQL/dashboard translation.\n# #endregion TranslatePluginPackage\n" }, { - "contract_id": "DictionaryTests", - "contract_type": "Module", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 1, - "end_line": 644, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Validate DictionaryManager CRUD, import, deletion guards, and batch filtering.", - "SEMANTICS": [ - "tests", - "dictionary", - "crud", - "import", - "filter" - ], - "TEST_CONTRACT": "[DictionaryManager] -> {" - }, - "relations": [ - { - "source_id": "DictionaryTests", - "relation_type": "BINDS_TO", - "target_id": "DictionaryManager:Class", - "target_ref": "[DictionaryManager:Class]" - } - ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:DictionaryTests:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, dictionary, crud, import, filter\n# @PURPOSE: Validate DictionaryManager CRUD, import, deletion guards, and batch filtering.\n# @RELATION: BINDS_TO -> [DictionaryManager:Class]\n#\n# @TEST_CONTRACT: [DictionaryManager] -> {\n# invariants: [\n# \"Create/Read/Update/Delete dictionaries works\",\n# \"Entry CRUD enforces unique source_term_normalized per dictionary\",\n# \"Import CSV/TSV handles overwrite/keep_existing/cancel conflict modes\",\n# \"Delete blocked when dictionary attached to active/scheduled jobs\",\n# \"filter_for_batch returns matched entries with word-boundary awareness\"\n# ]\n# }\n# @TEST_EDGE: duplicate_entry -> 409-style ValueError on repeated source_term\n# @TEST_EDGE: delete_active_job -> ValueError with active/scheduled message\n# @TEST_EDGE: import_invalid_format -> ValueError for missing columns\n# @TEST_INVARIANT: unique_normalized -> verifies: [duplicate_entry]\n\nimport pytest\nimport csv\nimport io\nfrom datetime import datetime, timezone\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import sessionmaker, Session\n\nfrom src.models.translate import (\n Base,\n TerminologyDictionary,\n DictionaryEntry,\n TranslationJob,\n TranslationJobDictionary,\n TranslationRun,\n)\nfrom src.plugins.translate.dictionary import DictionaryManager\nfrom src.plugins.translate._utils import _normalize_term, _detect_delimiter\n\n\n# [DEF:_FakeJob:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Helper to create inline TranslationJob records.\nclass _FakeJob:\n pass\n# [/DEF:_FakeJob:Class]\n\n\n# [DEF:db_session:Fixture]\n# @PURPOSE: Provide an in-memory SQLite session for each test, with tables created and torn down.\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n\n # Enable WAL and foreign keys for SQLite\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# [/DEF:db_session:Fixture]\n\n\n# [DEF:test_create_dictionary:Function]\n# @PURPOSE: Verify dictionary creation and read-back.\ndef test_create_dictionary(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session,\n name=\"Finance Terms\",\n source_dialect=\"postgresql\",\n target_dialect=\"clickhouse\",\n created_by=\"test_user\",\n 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 # Read back\n fetched = DictionaryManager.get_dictionary(db_session, d.id)\n assert fetched.id == d.id\n assert fetched.name == \"Finance Terms\"\n# [/DEF:test_create_dictionary:Function]\n\n\n# [DEF:test_update_dictionary:Function]\n# @PURPOSE: Verify dictionary metadata update.\ndef test_update_dictionary(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Old Name\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n updated = DictionaryManager.update_dictionary(\n db_session, d.id,\n name=\"New Name\",\n description=\"Updated desc\",\n is_active=False,\n )\n assert updated.name == \"New Name\"\n assert updated.description == \"Updated desc\"\n assert updated.is_active is False\n# [/DEF:test_update_dictionary:Function]\n\n\n# [DEF:test_delete_dictionary:Function]\n# @PURPOSE: Verify dictionary deletion.\ndef test_delete_dictionary(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"To Delete\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n # Add an entry\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n assert entry.id is not None\n\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# [/DEF:test_delete_dictionary:Function]\n\n\n# [DEF:test_list_dictionaries:Function]\n# @PURPOSE: Verify paginated dictionary listing.\ndef test_list_dictionaries(db_session: Session):\n for i in range(5):\n DictionaryManager.create_dictionary(\n db_session, name=f\"Dict {i}\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n\n dicts, total = DictionaryManager.list_dictionaries(db_session, page=1, page_size=2)\n assert total == 5\n assert len(dicts) == 2\n# [/DEF:test_list_dictionaries:Function]\n\n\n# [DEF:test_add_entry_duplicate:Function]\n# @PURPOSE: Verify duplicate entry raises ValueError and unique constraint is enforced.\ndef test_add_entry_duplicate(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"Hello\", \"Hola\")\n\n # Same normalized term should raise\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"Bonjour\")\n\n # Different case, same normalized should also raise\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"HELLO\", \"Ciao\")\n# [/DEF:test_add_entry_duplicate:Function]\n\n\n# [DEF:test_add_entry_duplicate_per_dictionary:Function]\n# @PURPOSE: Verify duplicate is per-dictionary (same term in different dictionaries is OK).\ndef test_add_entry_duplicate_per_dictionary(db_session: Session):\n d1 = DictionaryManager.create_dictionary(\n db_session, name=\"Dict1\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n d2 = DictionaryManager.create_dictionary(\n db_session, name=\"Dict2\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d1.id, \"hello\", \"hola\")\n # Same term in different dictionary should work\n entry = DictionaryManager.add_entry(db_session, d2.id, \"hello\", \"bonjour\")\n assert entry.id is not None\n# [/DEF:test_add_entry_duplicate_per_dictionary:Function]\n\n\n# [DEF:test_edit_entry:Function]\n# @PURPOSE: Verify entry edit updates fields and enforces uniqueness.\ndef test_edit_entry(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n updated = DictionaryManager.edit_entry(db_session, entry.id, target_term=\"HOLA!\")\n assert updated.target_term == \"HOLA!\"\n\n # Edit source term to a value that's already taken should fail\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.edit_entry(db_session, entry.id, source_term=\"WORLD\")\n# [/DEF:test_edit_entry:Function]\n\n\n# [DEF:test_delete_entry:Function]\n# @PURPOSE: Verify entry deletion.\ndef test_delete_entry(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n DictionaryManager.delete_entry(db_session, entry.id)\n\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n# [/DEF:test_delete_entry:Function]\n\n\n# [DEF:test_import_csv_overwrite:Function]\n# @PURPOSE: Verify CSV import with overwrite conflict mode.\ndef test_import_csv_overwrite(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n # Pre-add an entry\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n csv_content = \"source_term,target_term,context_notes\\nhello,HELLO,\\nworld,mundo,\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"overwrite\",\n )\n assert result[\"total\"] == 2\n assert result[\"created\"] == 1 # world\n assert result[\"updated\"] == 1 # hello (overwritten)\n assert result[\"skipped\"] == 0\n\n # Verify update\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# [/DEF:test_import_csv_overwrite:Function]\n\n\n# [DEF:test_import_csv_keep_existing:Function]\n# @PURPOSE: Verify CSV import with keep_existing conflict mode.\ndef test_import_csv_keep_existing(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n csv_content = \"source_term,target_term\\nhello,HELLO\\nworld,mundo\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"keep_existing\",\n )\n assert result[\"created\"] == 1 # world\n assert result[\"updated\"] == 0\n assert result[\"skipped\"] == 1 # hello kept\n\n # Verify hello was NOT overwritten\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# [/DEF:test_import_csv_keep_existing:Function]\n\n\n# [DEF:test_import_csv_cancel_on_conflict:Function]\n# @PURPOSE: Verify CSV import with cancel conflict mode raises errors.\ndef test_import_csv_cancel_on_conflict(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n csv_content = \"source_term,target_term\\nhello,HELLO\\nworld,mundo\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"cancel\",\n )\n assert result[\"total\"] == 2\n assert result[\"created\"] == 1 # world\n assert result[\"updated\"] == 0\n assert len(result[\"errors\"]) == 1 # hello conflict error\n assert \"Conflict\" in result[\"errors\"][0][\"error\"]\n# [/DEF:test_import_csv_cancel_on_conflict:Function]\n\n\n# [DEF:test_import_tsv:Function]\n# @PURPOSE: Verify TSV import.\ndef test_import_tsv(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n tsv_content = \"source_term\\ttarget_term\\nhello\\thola\\nworld\\tmundo\"\n result = DictionaryManager.import_entries(\n db_session, d.id, tsv_content,\n delimiter=\"\\t\", on_conflict=\"overwrite\",\n )\n assert result[\"created\"] == 2\n assert result[\"total\"] == 2\n# [/DEF:test_import_tsv:Function]\n\n\n# [DEF:test_import_invalid_format:Function]\n# @PURPOSE: Verify import raises ValueError for missing required columns.\ndef test_import_invalid_format(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n bad_content = \"name,value\\nhello,hola\"\n with pytest.raises(ValueError, match=\"source_term\"):\n DictionaryManager.import_entries(\n db_session, d.id, bad_content,\n delimiter=\",\", on_conflict=\"overwrite\",\n )\n# [/DEF:test_import_invalid_format:Function]\n\n\n# [DEF:test_import_empty_rows:Function]\n# @PURPOSE: Verify import handles rows with missing fields gracefully.\ndef test_import_empty_rows(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n csv_content = \"source_term,target_term\\nhello,hola\\n,world\\nfoo,\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"overwrite\",\n )\n assert result[\"created\"] == 1 # hello\n assert len(result[\"errors\"]) == 2 # empty source or target\n# [/DEF:test_import_empty_rows:Function]\n\n\n# [DEF:test_import_preview:Function]\n# @PURPOSE: Verify import preview returns conflicts without mutating.\ndef test_import_preview(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n csv_content = \"source_term,target_term\\nhello,HELLO\\nworld,mundo\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"overwrite\",\n preview_only=True,\n )\n assert len(result[\"preview\"]) == 2\n # First row should be conflict\n assert result[\"preview\"][0][\"is_conflict\"] is True\n assert result[\"preview\"][0][\"existing_target_term\"] == \"hola\"\n # Second row should be new\n assert result[\"preview\"][1][\"is_conflict\"] is False\n\n # Verify no mutation happened\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 1 # still only the original entry\n# [/DEF:test_import_preview:Function]\n\n\n# [DEF:test_delete_dictionary_blocked_by_active_job:Function]\n# @PURPOSE: Verify deletion is blocked when dictionary is attached to active/scheduled jobs.\ndef test_delete_dictionary_blocked_by_active_job(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n # Create an active job\n job = TranslationJob(\n name=\"Active Job\",\n source_dialect=\"a\",\n target_dialect=\"b\",\n status=\"ACTIVE\",\n created_by=\"test_user\",\n )\n db_session.add(job)\n db_session.flush()\n\n # Attach dictionary to job\n link = TranslationJobDictionary(\n job_id=job.id,\n dictionary_id=d.id,\n )\n db_session.add(link)\n db_session.commit()\n\n with pytest.raises(ValueError, match=\"active/scheduled\"):\n DictionaryManager.delete_dictionary(db_session, d.id)\n# [/DEF:test_delete_dictionary_blocked_by_active_job:Function]\n\n\n# [DEF:test_delete_dictionary_allowed_with_completed_job:Function]\n# @PURPOSE: Verify deletion is allowed when only completed/failed jobs reference the dictionary.\ndef test_delete_dictionary_allowed_with_completed_job(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n # Create a completed job\n job = TranslationJob(\n name=\"Completed Job\",\n source_dialect=\"a\",\n target_dialect=\"b\",\n status=\"COMPLETED\",\n created_by=\"test_user\",\n )\n db_session.add(job)\n db_session.flush()\n\n link = TranslationJobDictionary(\n job_id=job.id,\n dictionary_id=d.id,\n )\n db_session.add(link)\n db_session.commit()\n\n # Should not raise\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# [/DEF:test_delete_dictionary_allowed_with_completed_job:Function]\n\n\n# [DEF:test_filter_for_batch_no_dictionaries:Function]\n# @PURPOSE: Verify filter_for_batch returns empty when job has no dictionaries.\ndef test_filter_for_batch_no_dictionaries(db_session: Session):\n job = TranslationJob(\n name=\"No Dict Job\",\n source_dialect=\"a\", target_dialect=\"b\",\n status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.commit()\n\n result = DictionaryManager.filter_for_batch(db_session, [\"hello world\"], job.id)\n assert result == []\n# [/DEF:test_filter_for_batch_no_dictionaries:Function]\n\n\n# [DEF:test_filter_for_batch_matches:Function]\n# @PURPOSE: Verify filter_for_batch returns correct matched entries with word-boundary awareness.\ndef test_filter_for_batch_matches(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test Dict\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\")\n DictionaryManager.add_entry(db_session, d.id, \"foo\", \"bar\")\n\n job = TranslationJob(\n name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.flush()\n\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(\n db_session,\n [\"hello beautiful world\", \"nothing here\", \"foo bar\"],\n job.id,\n )\n\n assert len(result) >= 3 # hello, world, foo\n\n # Check that 'hello' matched in text 0\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 # Check that 'world' matched in text 0\n world_matches = [m for m in result if m[\"source_term\"] == \"world\"]\n assert len(world_matches) == 1\n assert world_matches[0][\"text_index\"] == 0\n\n # Check that 'foo' matched in text 2\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# [/DEF:test_filter_for_batch_matches:Function]\n\n\n# [DEF:test_filter_for_batch_case_insensitive:Function]\n# @PURPOSE: Verify filter_for_batch matching is case-insensitive.\ndef test_filter_for_batch_case_insensitive(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test Dict\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"Hello World\", \"Hola Mundo\")\n\n job = TranslationJob(\n name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.flush()\n\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n # Different case should still match\n result = DictionaryManager.filter_for_batch(\n db_session,\n [\"hello world is great\"],\n job.id,\n )\n assert len(result) == 1\n assert result[0][\"source_term\"] == \"Hello World\"\n assert result[0][\"target_term\"] == \"Hola Mundo\"\n# [/DEF:test_filter_for_batch_case_insensitive:Function]\n\n\n# [DEF:test_filter_for_batch_word_boundary:Function]\n# @PURPOSE: Verify filter_for_batch respects word boundaries (no substring matching within words).\ndef test_filter_for_batch_word_boundary(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test Dict\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"cat\", \"gato\")\n\n job = TranslationJob(\n name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.flush()\n\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n # \"cat\" in \"catalog\" should NOT match (word boundary)\n result = DictionaryManager.filter_for_batch(\n db_session,\n [\"catalog\"],\n job.id,\n )\n assert len(result) == 0\n\n # \"cat\" as standalone word should match\n result = DictionaryManager.filter_for_batch(\n db_session,\n [\"the cat sat\"],\n job.id,\n )\n assert len(result) == 1\n# [/DEF:test_filter_for_batch_word_boundary:Function]\n\n\n# [DEF:test_filter_for_batch_multi_dictionary_priority:Function]\n# @PURPOSE: Verify filter_for_batch respects dictionary link order priority.\ndef test_filter_for_batch_multi_dictionary_priority(db_session: Session):\n d1 = DictionaryManager.create_dictionary(\n db_session, name=\"Priority1\", source_dialect=\"a\", target_dialect=\"b\",\n )\n d2 = DictionaryManager.create_dictionary(\n db_session, name=\"Priority2\", source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d1.id, \"hello\", \"hola\")\n DictionaryManager.add_entry(db_session, d2.id, \"hello\", \"bonjour\")\n\n job = TranslationJob(\n name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.flush()\n\n # Add d1 first (higher priority), then d2\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(\n db_session,\n [\"hello\"],\n job.id,\n )\n assert len(result) == 2\n # d1 match should come first (higher priority by link order)\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# [/DEF:test_filter_for_batch_multi_dictionary_priority:Function]\n\n\n# [DEF:test_normalize_term:Function]\n# @PURPOSE: Verify _normalize_term produces lowercase NFC-normalized strings.\ndef test_normalize_term():\n assert _normalize_term(\"Hello\") == \"hello\"\n assert _normalize_term(\"HELLO\") == \"hello\"\n assert _normalize_term(\" hello \") == \"hello\"\n # NFC normalization test\n composed = \"\\u00C9\" # É precomposed\n decomposed = \"\\u0045\\u0301\" # E + combining acute\n assert _normalize_term(composed) == _normalize_term(decomposed)\n# [/DEF:test_normalize_term:Function]\n\n\n# [DEF:test_detect_delimiter:Function]\n# @PURPOSE: Verify _detect_delimiter correctly identifies CSV vs TSV.\ndef test_detect_delimiter():\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) == \",\" # default fallback\n# [/DEF:test_detect_delimiter:Function]\n\n\n# [DEF:test_clear_entries:Function]\n# @PURPOSE: Verify clearing all entries for a dictionary.\ndef test_clear_entries(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\")\n\n deleted = DictionaryManager.clear_entries(db_session, d.id)\n assert deleted == 2\n\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n# [/DEF:test_clear_entries:Function]\n# [/DEF:DictionaryTests:Module]\n" - }, - { - "contract_id": "_FakeJob", + "contract_id": "TestClickHouseTimestampNormalization", "contract_type": "Class", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 40, - "end_line": 45, + "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", + "start_line": 76, + "end_line": 136, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Helper to create inline TranslationJob records." + "PURPOSE": "Verify Unix timestamp detection and conversion for ClickHouse Date columns." }, "relations": [], "schema_warnings": [ @@ -66395,502 +51899,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_FakeJob:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Helper to create inline TranslationJob records.\nclass _FakeJob:\n pass\n# [/DEF:_FakeJob:Class]\n" + "anchor_syntax": "region", + "body": "# #region TestClickHouseTimestampNormalization [TYPE Class]\n# @BRIEF Verify Unix timestamp detection and conversion for ClickHouse Date columns.\nclass TestClickHouseTimestampNormalization:\n \"\"\"Verify _normalize_timestamp_value handles real-world ClickHouse Date scenarios.\"\"\"\n\n # region test_financial_arrears_date_millis [TYPE Function]\n # @BRIEF Real-world: report_date as Unix millis from Superset chart data API.\n def test_financial_arrears_date_millis(self) -> None:\n # 1726358400000 millis = 2024-09-15 00:00:00 UTC\n result = _normalize_timestamp_value(\"1726358400000.0\")\n assert result == \"2024-09-15\"\n\n # region test_financial_arrears_date_seconds [TYPE Function]\n # @BRIEF Real-world: report_date as Unix seconds.\n def test_financial_arrears_date_seconds(self) -> None:\n result = _normalize_timestamp_value(\"1726358400\")\n assert result == \"2024-09-15\"\n\n # region test_document_number_not_affected [TYPE Function]\n # @BRIEF document_number string must pass through unchanged.\n def test_document_number_not_affected(self) -> None:\n result = _normalize_timestamp_value(\"DOC-001\")\n assert result is None\n\n # region test_empty_string [TYPE Function]\n # @BRIEF Empty string returns None.\n def test_empty_string(self) -> None:\n assert _normalize_timestamp_value(\"\") is None\n\n # region test_none_value [TYPE Function]\n # @BRIEF None returns None.\n def test_none_value(self) -> None:\n assert _normalize_timestamp_value(None) is None\n\n # region test_clickhouse_encode_with_timestamp [TYPE Function]\n # @BRIEF ClickHouse dialect converts timestamp string to 'YYYY-MM-DD'.\n def test_clickhouse_encode_with_timestamp(self) -> None:\n result = _encode_sql_value(\"1726358400000.0\", dialect=\"clickhouse\")\n assert result == \"'2024-09-15'\"\n\n # region test_clickhouse_encode_with_document_number [TYPE Function]\n # @BRIEF ClickHouse dialect passes non-timestamp strings unchanged.\n def test_clickhouse_encode_with_document_number(self) -> None:\n result = _encode_sql_value(\"DOC-001\", dialect=\"clickhouse\")\n assert result == \"'DOC-001'\"\n\n # region test_postgresql_encode_not_affected [TYPE Function]\n # @BRIEF PostgreSQL dialect does NOT normalize timestamps.\n def test_postgresql_encode_not_affected(self) -> None:\n result = _encode_sql_value(\"1726358400000.0\", dialect=\"postgresql\")\n assert result == \"'1726358400000.0'\"\n\n # endregion test_financial_arrears_date_millis\n # endregion test_financial_arrears_date_seconds\n # endregion test_document_number_not_affected\n # endregion test_empty_string\n # endregion test_none_value\n # endregion test_clickhouse_encode_with_timestamp\n # endregion test_clickhouse_encode_with_document_number\n # endregion test_postgresql_encode_not_affected\n# #endregion TestClickHouseTimestampNormalization\n" }, { - "contract_id": "test_create_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 71, - "end_line": 93, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify dictionary creation and read-back." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_create_dictionary:Function]\n# @PURPOSE: Verify dictionary creation and read-back.\ndef test_create_dictionary(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session,\n name=\"Finance Terms\",\n source_dialect=\"postgresql\",\n target_dialect=\"clickhouse\",\n created_by=\"test_user\",\n 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 # Read back\n fetched = DictionaryManager.get_dictionary(db_session, d.id)\n assert fetched.id == d.id\n assert fetched.name == \"Finance Terms\"\n# [/DEF:test_create_dictionary:Function]\n" - }, - { - "contract_id": "test_update_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 96, - "end_line": 112, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify dictionary metadata update." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_update_dictionary:Function]\n# @PURPOSE: Verify dictionary metadata update.\ndef test_update_dictionary(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Old Name\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n updated = DictionaryManager.update_dictionary(\n db_session, d.id,\n name=\"New Name\",\n description=\"Updated desc\",\n is_active=False,\n )\n assert updated.name == \"New Name\"\n assert updated.description == \"Updated desc\"\n assert updated.is_active is False\n# [/DEF:test_update_dictionary:Function]\n" - }, - { - "contract_id": "test_delete_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 115, - "end_line": 129, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify dictionary deletion." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_delete_dictionary:Function]\n# @PURPOSE: Verify dictionary deletion.\ndef test_delete_dictionary(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"To Delete\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n # Add an entry\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n assert entry.id is not None\n\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# [/DEF:test_delete_dictionary:Function]\n" - }, - { - "contract_id": "test_list_dictionaries", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 132, - "end_line": 144, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify paginated dictionary listing." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_list_dictionaries:Function]\n# @PURPOSE: Verify paginated dictionary listing.\ndef test_list_dictionaries(db_session: Session):\n for i in range(5):\n DictionaryManager.create_dictionary(\n db_session, name=f\"Dict {i}\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n\n dicts, total = DictionaryManager.list_dictionaries(db_session, page=1, page_size=2)\n assert total == 5\n assert len(dicts) == 2\n# [/DEF:test_list_dictionaries:Function]\n" - }, - { - "contract_id": "test_add_entry_duplicate", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 147, - "end_line": 163, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify duplicate entry raises ValueError and unique constraint is enforced." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_add_entry_duplicate:Function]\n# @PURPOSE: Verify duplicate entry raises ValueError and unique constraint is enforced.\ndef test_add_entry_duplicate(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"Hello\", \"Hola\")\n\n # Same normalized term should raise\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"Bonjour\")\n\n # Different case, same normalized should also raise\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"HELLO\", \"Ciao\")\n# [/DEF:test_add_entry_duplicate:Function]\n" - }, - { - "contract_id": "test_add_entry_duplicate_per_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 166, - "end_line": 181, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify duplicate is per-dictionary (same term in different dictionaries is OK)." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_add_entry_duplicate_per_dictionary:Function]\n# @PURPOSE: Verify duplicate is per-dictionary (same term in different dictionaries is OK).\ndef test_add_entry_duplicate_per_dictionary(db_session: Session):\n d1 = DictionaryManager.create_dictionary(\n db_session, name=\"Dict1\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n d2 = DictionaryManager.create_dictionary(\n db_session, name=\"Dict2\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d1.id, \"hello\", \"hola\")\n # Same term in different dictionary should work\n entry = DictionaryManager.add_entry(db_session, d2.id, \"hello\", \"bonjour\")\n assert entry.id is not None\n# [/DEF:test_add_entry_duplicate_per_dictionary:Function]\n" - }, - { - "contract_id": "test_edit_entry", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 184, - "end_line": 200, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify entry edit updates fields and enforces uniqueness." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_edit_entry:Function]\n# @PURPOSE: Verify entry edit updates fields and enforces uniqueness.\ndef test_edit_entry(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n updated = DictionaryManager.edit_entry(db_session, entry.id, target_term=\"HOLA!\")\n assert updated.target_term == \"HOLA!\"\n\n # Edit source term to a value that's already taken should fail\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.edit_entry(db_session, entry.id, source_term=\"WORLD\")\n# [/DEF:test_edit_entry:Function]\n" - }, - { - "contract_id": "test_delete_entry", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 203, - "end_line": 215, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify entry deletion." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_delete_entry:Function]\n# @PURPOSE: Verify entry deletion.\ndef test_delete_entry(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n DictionaryManager.delete_entry(db_session, entry.id)\n\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n# [/DEF:test_delete_entry:Function]\n" - }, - { - "contract_id": "test_import_csv_overwrite", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 218, - "end_line": 242, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify CSV import with overwrite conflict mode." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_import_csv_overwrite:Function]\n# @PURPOSE: Verify CSV import with overwrite conflict mode.\ndef test_import_csv_overwrite(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n # Pre-add an entry\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n csv_content = \"source_term,target_term,context_notes\\nhello,HELLO,\\nworld,mundo,\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"overwrite\",\n )\n assert result[\"total\"] == 2\n assert result[\"created\"] == 1 # world\n assert result[\"updated\"] == 1 # hello (overwritten)\n assert result[\"skipped\"] == 0\n\n # Verify update\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# [/DEF:test_import_csv_overwrite:Function]\n" - }, - { - "contract_id": "test_import_csv_keep_existing", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 245, - "end_line": 267, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify CSV import with keep_existing conflict mode." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_import_csv_keep_existing:Function]\n# @PURPOSE: Verify CSV import with keep_existing conflict mode.\ndef test_import_csv_keep_existing(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n csv_content = \"source_term,target_term\\nhello,HELLO\\nworld,mundo\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"keep_existing\",\n )\n assert result[\"created\"] == 1 # world\n assert result[\"updated\"] == 0\n assert result[\"skipped\"] == 1 # hello kept\n\n # Verify hello was NOT overwritten\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# [/DEF:test_import_csv_keep_existing:Function]\n" - }, - { - "contract_id": "test_import_csv_cancel_on_conflict", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 270, - "end_line": 289, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify CSV import with cancel conflict mode raises errors." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_import_csv_cancel_on_conflict:Function]\n# @PURPOSE: Verify CSV import with cancel conflict mode raises errors.\ndef test_import_csv_cancel_on_conflict(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n csv_content = \"source_term,target_term\\nhello,HELLO\\nworld,mundo\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"cancel\",\n )\n assert result[\"total\"] == 2\n assert result[\"created\"] == 1 # world\n assert result[\"updated\"] == 0\n assert len(result[\"errors\"]) == 1 # hello conflict error\n assert \"Conflict\" in result[\"errors\"][0][\"error\"]\n# [/DEF:test_import_csv_cancel_on_conflict:Function]\n" - }, - { - "contract_id": "test_import_tsv", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 292, - "end_line": 306, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify TSV import." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_import_tsv:Function]\n# @PURPOSE: Verify TSV import.\ndef test_import_tsv(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n tsv_content = \"source_term\\ttarget_term\\nhello\\thola\\nworld\\tmundo\"\n result = DictionaryManager.import_entries(\n db_session, d.id, tsv_content,\n delimiter=\"\\t\", on_conflict=\"overwrite\",\n )\n assert result[\"created\"] == 2\n assert result[\"total\"] == 2\n# [/DEF:test_import_tsv:Function]\n" - }, - { - "contract_id": "test_import_invalid_format", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 309, - "end_line": 322, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify import raises ValueError for missing required columns." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_import_invalid_format:Function]\n# @PURPOSE: Verify import raises ValueError for missing required columns.\ndef test_import_invalid_format(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n bad_content = \"name,value\\nhello,hola\"\n with pytest.raises(ValueError, match=\"source_term\"):\n DictionaryManager.import_entries(\n db_session, d.id, bad_content,\n delimiter=\",\", on_conflict=\"overwrite\",\n )\n# [/DEF:test_import_invalid_format:Function]\n" - }, - { - "contract_id": "test_import_empty_rows", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 325, - "end_line": 339, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify import handles rows with missing fields gracefully." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_import_empty_rows:Function]\n# @PURPOSE: Verify import handles rows with missing fields gracefully.\ndef test_import_empty_rows(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n csv_content = \"source_term,target_term\\nhello,hola\\n,world\\nfoo,\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"overwrite\",\n )\n assert result[\"created\"] == 1 # hello\n assert len(result[\"errors\"]) == 2 # empty source or target\n# [/DEF:test_import_empty_rows:Function]\n" - }, - { - "contract_id": "test_import_preview", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 342, - "end_line": 367, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify import preview returns conflicts without mutating." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_import_preview:Function]\n# @PURPOSE: Verify import preview returns conflicts without mutating.\ndef test_import_preview(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n\n csv_content = \"source_term,target_term\\nhello,HELLO\\nworld,mundo\"\n result = DictionaryManager.import_entries(\n db_session, d.id, csv_content,\n delimiter=\",\", on_conflict=\"overwrite\",\n preview_only=True,\n )\n assert len(result[\"preview\"]) == 2\n # First row should be conflict\n assert result[\"preview\"][0][\"is_conflict\"] is True\n assert result[\"preview\"][0][\"existing_target_term\"] == \"hola\"\n # Second row should be new\n assert result[\"preview\"][1][\"is_conflict\"] is False\n\n # Verify no mutation happened\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 1 # still only the original entry\n# [/DEF:test_import_preview:Function]\n" - }, - { - "contract_id": "test_delete_dictionary_blocked_by_active_job", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 370, - "end_line": 398, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify deletion is blocked when dictionary is attached to active/scheduled jobs." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_delete_dictionary_blocked_by_active_job:Function]\n# @PURPOSE: Verify deletion is blocked when dictionary is attached to active/scheduled jobs.\ndef test_delete_dictionary_blocked_by_active_job(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n # Create an active job\n job = TranslationJob(\n name=\"Active Job\",\n source_dialect=\"a\",\n target_dialect=\"b\",\n status=\"ACTIVE\",\n created_by=\"test_user\",\n )\n db_session.add(job)\n db_session.flush()\n\n # Attach dictionary to job\n link = TranslationJobDictionary(\n job_id=job.id,\n dictionary_id=d.id,\n )\n db_session.add(link)\n db_session.commit()\n\n with pytest.raises(ValueError, match=\"active/scheduled\"):\n DictionaryManager.delete_dictionary(db_session, d.id)\n# [/DEF:test_delete_dictionary_blocked_by_active_job:Function]\n" - }, - { - "contract_id": "test_delete_dictionary_allowed_with_completed_job", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 401, - "end_line": 430, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify deletion is allowed when only completed/failed jobs reference the dictionary." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_delete_dictionary_allowed_with_completed_job:Function]\n# @PURPOSE: Verify deletion is allowed when only completed/failed jobs reference the dictionary.\ndef test_delete_dictionary_allowed_with_completed_job(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n # Create a completed job\n job = TranslationJob(\n name=\"Completed Job\",\n source_dialect=\"a\",\n target_dialect=\"b\",\n status=\"COMPLETED\",\n created_by=\"test_user\",\n )\n db_session.add(job)\n db_session.flush()\n\n link = TranslationJobDictionary(\n job_id=job.id,\n dictionary_id=d.id,\n )\n db_session.add(link)\n db_session.commit()\n\n # Should not raise\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# [/DEF:test_delete_dictionary_allowed_with_completed_job:Function]\n" - }, - { - "contract_id": "test_filter_for_batch_no_dictionaries", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 433, - "end_line": 446, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify filter_for_batch returns empty when job has no dictionaries." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_filter_for_batch_no_dictionaries:Function]\n# @PURPOSE: Verify filter_for_batch returns empty when job has no dictionaries.\ndef test_filter_for_batch_no_dictionaries(db_session: Session):\n job = TranslationJob(\n name=\"No Dict Job\",\n source_dialect=\"a\", target_dialect=\"b\",\n status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.commit()\n\n result = DictionaryManager.filter_for_batch(db_session, [\"hello world\"], job.id)\n assert result == []\n# [/DEF:test_filter_for_batch_no_dictionaries:Function]\n" - }, - { - "contract_id": "test_filter_for_batch_matches", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 449, - "end_line": 492, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify filter_for_batch returns correct matched entries with word-boundary awareness." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_filter_for_batch_matches:Function]\n# @PURPOSE: Verify filter_for_batch returns correct matched entries with word-boundary awareness.\ndef test_filter_for_batch_matches(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test Dict\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\")\n DictionaryManager.add_entry(db_session, d.id, \"foo\", \"bar\")\n\n job = TranslationJob(\n name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.flush()\n\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(\n db_session,\n [\"hello beautiful world\", \"nothing here\", \"foo bar\"],\n job.id,\n )\n\n assert len(result) >= 3 # hello, world, foo\n\n # Check that 'hello' matched in text 0\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 # Check that 'world' matched in text 0\n world_matches = [m for m in result if m[\"source_term\"] == \"world\"]\n assert len(world_matches) == 1\n assert world_matches[0][\"text_index\"] == 0\n\n # Check that 'foo' matched in text 2\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# [/DEF:test_filter_for_batch_matches:Function]\n" - }, - { - "contract_id": "test_filter_for_batch_case_insensitive", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 495, - "end_line": 523, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify filter_for_batch matching is case-insensitive." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_filter_for_batch_case_insensitive:Function]\n# @PURPOSE: Verify filter_for_batch matching is case-insensitive.\ndef test_filter_for_batch_case_insensitive(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test Dict\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"Hello World\", \"Hola Mundo\")\n\n job = TranslationJob(\n name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.flush()\n\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n # Different case should still match\n result = DictionaryManager.filter_for_batch(\n db_session,\n [\"hello world is great\"],\n job.id,\n )\n assert len(result) == 1\n assert result[0][\"source_term\"] == \"Hello World\"\n assert result[0][\"target_term\"] == \"Hola Mundo\"\n# [/DEF:test_filter_for_batch_case_insensitive:Function]\n" - }, - { - "contract_id": "test_filter_for_batch_word_boundary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 526, - "end_line": 560, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify filter_for_batch respects word boundaries (no substring matching within words)." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_filter_for_batch_word_boundary:Function]\n# @PURPOSE: Verify filter_for_batch respects word boundaries (no substring matching within words).\ndef test_filter_for_batch_word_boundary(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test Dict\",\n source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"cat\", \"gato\")\n\n job = TranslationJob(\n name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.flush()\n\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n\n # \"cat\" in \"catalog\" should NOT match (word boundary)\n result = DictionaryManager.filter_for_batch(\n db_session,\n [\"catalog\"],\n job.id,\n )\n assert len(result) == 0\n\n # \"cat\" as standalone word should match\n result = DictionaryManager.filter_for_batch(\n db_session,\n [\"the cat sat\"],\n job.id,\n )\n assert len(result) == 1\n# [/DEF:test_filter_for_batch_word_boundary:Function]\n" - }, - { - "contract_id": "test_filter_for_batch_multi_dictionary_priority", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 563, - "end_line": 599, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify filter_for_batch respects dictionary link order priority." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_filter_for_batch_multi_dictionary_priority:Function]\n# @PURPOSE: Verify filter_for_batch respects dictionary link order priority.\ndef test_filter_for_batch_multi_dictionary_priority(db_session: Session):\n d1 = DictionaryManager.create_dictionary(\n db_session, name=\"Priority1\", source_dialect=\"a\", target_dialect=\"b\",\n )\n d2 = DictionaryManager.create_dictionary(\n db_session, name=\"Priority2\", source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d1.id, \"hello\", \"hola\")\n DictionaryManager.add_entry(db_session, d2.id, \"hello\", \"bonjour\")\n\n job = TranslationJob(\n name=\"Test Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\",\n )\n db_session.add(job)\n db_session.flush()\n\n # Add d1 first (higher priority), then d2\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(\n db_session,\n [\"hello\"],\n job.id,\n )\n assert len(result) == 2\n # d1 match should come first (higher priority by link order)\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# [/DEF:test_filter_for_batch_multi_dictionary_priority:Function]\n" - }, - { - "contract_id": "test_normalize_term", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 602, - "end_line": 612, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify _normalize_term produces lowercase NFC-normalized strings." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_normalize_term:Function]\n# @PURPOSE: Verify _normalize_term produces lowercase NFC-normalized strings.\ndef test_normalize_term():\n assert _normalize_term(\"Hello\") == \"hello\"\n assert _normalize_term(\"HELLO\") == \"hello\"\n assert _normalize_term(\" hello \") == \"hello\"\n # NFC normalization test\n composed = \"\\u00C9\" # É precomposed\n decomposed = \"\\u0045\\u0301\" # E + combining acute\n assert _normalize_term(composed) == _normalize_term(decomposed)\n# [/DEF:test_normalize_term:Function]\n" - }, - { - "contract_id": "test_detect_delimiter", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 615, - "end_line": 626, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify _detect_delimiter correctly identifies CSV vs TSV." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_detect_delimiter:Function]\n# @PURPOSE: Verify _detect_delimiter correctly identifies CSV vs TSV.\ndef test_detect_delimiter():\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) == \",\" # default fallback\n# [/DEF:test_detect_delimiter:Function]\n" - }, - { - "contract_id": "test_clear_entries", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_dictionary.py", - "start_line": 629, - "end_line": 643, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify clearing all entries for a dictionary." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_clear_entries:Function]\n# @PURPOSE: Verify clearing all entries for a dictionary.\ndef test_clear_entries(db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\",\n )\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\")\n\n deleted = DictionaryManager.clear_entries(db_session, d.id)\n assert deleted == 2\n\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n# [/DEF:test_clear_entries:Function]\n" - }, - { - "contract_id": "TranslationPreviewTests", - "contract_type": "Module", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 1, - "end_line": 500, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Test", - "PURPOSE": "Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate.", - "SEMANTICS": [ - "test", - "translate", - "preview", - "session" - ] - }, - "relations": [ - { - "source_id": "TranslationPreviewTests", - "relation_type": "BINDS_TO", - "target_id": "TranslationPreview:Module", - "target_ref": "[TranslationPreview:Module]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:TranslationPreviewTests:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: test, translate, preview, session\n# @PURPOSE: Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [TranslationPreview:Module]\n\nfrom unittest.mock import MagicMock, patch, PropertyMock\nfrom datetime import datetime, timezone, timedelta\nimport json\nimport pytest\n\nfrom src.models.translate import (\n TranslationJob,\n TranslationPreviewSession,\n TranslationPreviewRecord,\n TranslationJobDictionary,\n)\nfrom src.plugins.translate.preview import TranslationPreview, TokenEstimator\n\n\n# [DEF:_make_mock_job:Function]\n# @PURPOSE: Create a mock TranslationJob with test config.\ndef _make_mock_job(**overrides):\n job = MagicMock(spec=TranslationJob)\n job.id = overrides.get(\"id\", \"job-123\")\n job.name = overrides.get(\"name\", \"Test Job\")\n job.source_dialect = overrides.get(\"source_dialect\", \"postgresql\")\n job.target_dialect = overrides.get(\"target_dialect\", \"clickhouse\")\n job.source_datasource_id = overrides.get(\"source_datasource_id\", \"42\")\n job.source_table = overrides.get(\"source_table\", \"source_table\")\n job.translation_column = overrides.get(\"translation_column\", \"title\")\n job.context_columns = overrides.get(\"context_columns\", [\"category\", \"description\"])\n job.target_language = overrides.get(\"target_language\", \"ru\")\n job.provider_id = overrides.get(\"provider_id\", \"provider-1\")\n job.batch_size = overrides.get(\"batch_size\", 50)\n job.upsert_strategy = overrides.get(\"upsert_strategy\", \"MERGE\")\n job.status = overrides.get(\"status\", \"DRAFT\")\n return job\n# [/DEF:_make_mock_job:Function]\n\n\n# [DEF:_make_mock_provider:Function]\n# @PURPOSE: Create a mock LLMProvider.\ndef _make_mock_provider(**overrides):\n provider = MagicMock()\n provider.id = overrides.get(\"id\", \"provider-1\")\n provider.provider_type = overrides.get(\"provider_type\", \"openai\")\n provider.base_url = overrides.get(\"base_url\", \"https://api.openai.com/v1\")\n provider.default_model = overrides.get(\"default_model\", \"gpt-4o-mini\")\n provider.api_key = \"encrypted-key-123\"\n return provider\n# [/DEF:_make_mock_provider:Function]\n\n\n# [DEF:TestTranslationPreview:Class]\n# @PURPOSE: Test suite for TranslationPreview service.\nclass TestTranslationPreview:\n\n # [DEF:test_preview_valid_job:Function]\n # @PURPOSE: Verify preview creates session and records successfully.\n def test_preview_valid_job(self):\n \"\"\"Preview with valid job should create session and return records.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n # Mock job query\n job = _make_mock_job()\n db.query.return_value.filter.return_value.first.return_value = job\n\n # Mock environments\n env = MagicMock()\n env.id = \"postgresql\"\n env.url = \"https://superset.example.com\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = True\n env.timeout = 30\n config_manager.get_environments.return_value = [env]\n\n # Mock SupersetClient\n with patch(\"src.plugins.translate.preview.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n MockClient.return_value = mock_client\n\n # Mock dataset_detail\n mock_client.get_dataset_detail.return_value = {\n \"table_name\": \"test_table\",\n \"columns\": [\n {\"column_name\": \"title\", \"type\": \"VARCHAR\"},\n {\"column_name\": \"category\", \"type\": \"VARCHAR\"},\n {\"column_name\": \"description\", \"type\": \"TEXT\"},\n ],\n }\n\n # Mock build_dataset_preview_query_context\n mock_client.build_dataset_preview_query_context.return_value = {\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"queries\": [{\n \"columns\": [],\n \"metrics\": [\"count\"],\n \"row_limit\": 10,\n }],\n \"form_data\": {},\n \"result_format\": \"json\",\n \"result_type\": \"query\",\n \"force\": True,\n }\n\n # Mock network request to Superset\n mock_client.network.request.return_value = {\n \"result\": [{\n \"status\": \"success\",\n \"data\": [\n {\"title\": \"Hello World\", \"category\": \"greeting\", \"description\": \"A friendly message\"},\n {\"title\": \"Goodbye\", \"category\": \"farewell\", \"description\": \"A parting message\"},\n ],\n }]\n }\n\n # Mock LLM provider\n with patch(\"src.plugins.translate.preview.LLMProviderService\") as MockProviderService:\n mock_provider_svc = MagicMock()\n MockProviderService.return_value = mock_provider_svc\n mock_provider = _make_mock_provider()\n mock_provider_svc.get_provider.return_value = mock_provider\n mock_provider_svc.get_decrypted_api_key.return_value = \"sk-test-key\"\n\n # Mock _call_openai_compatible to return structured JSON\n with patch.object(TranslationPreview, \"_call_openai_compatible\") as mock_llm:\n mock_llm.return_value = json.dumps({\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Привет мир\"},\n {\"row_id\": \"1\", \"translation\": \"Прощай\"},\n ]\n })\n\n # Mock DictionaryManager.filter_for_batch\n with patch(\"src.plugins.translate.preview.DictionaryManager.filter_for_batch\") as mock_filter:\n mock_filter.return_value = []\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.preview_rows(job_id=\"job-123\", sample_size=10)\n\n # Verify result structure\n assert result[\"job_id\"] == \"job-123\"\n assert result[\"status\"] == \"ACTIVE\"\n assert len(result[\"records\"]) == 2\n assert result[\"records\"][0][\"target_sql\"] == \"Привет мир\"\n assert result[\"records\"][1][\"target_sql\"] == \"Прощай\"\n\n # Verify cost estimate\n assert result[\"cost_estimate\"][\"sample_size\"] == 2\n assert result[\"cost_estimate\"][\"sample_total_tokens\"] > 0\n assert result[\"cost_estimate\"][\"sample_cost\"] >= 0\n\n # Verify config and dict hashes\n assert result[\"config_hash\"] is not None\n assert result[\"dict_snapshot_hash\"] is not None\n\n # Verify session was added to DB\n assert db.add.called\n assert db.commit.called\n # [/DEF:test_preview_valid_job:Function]\n\n # [DEF:test_preview_with_dictionary:Function]\n # @PURPOSE: Verify glossary entries from dictionary are included in LLM prompt.\n def test_preview_with_dictionary(self):\n \"\"\"Preview should include dictionary glossary in the prompt sent to LLM.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job()\n db.query.return_value.filter.return_value.first.return_value = job\n\n env = MagicMock()\n env.id = \"postgresql\"\n config_manager.get_environments.return_value = [env]\n\n with patch(\"src.plugins.translate.preview.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n MockClient.return_value = mock_client\n mock_client.get_dataset_detail.return_value = {\"table_name\": \"test\", \"columns\": []}\n mock_client.build_dataset_preview_query_context.return_value = {\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"queries\": [{\"columns\": [], \"metrics\": [\"count\"], \"row_limit\": 10}],\n \"form_data\": {},\n \"result_format\": \"json\",\n \"result_type\": \"query\",\n \"force\": True,\n }\n mock_client.network.request.return_value = {\n \"result\": [{\"status\": \"success\", \"data\": [{\"title\": \"Hello World\", \"category\": \"greeting\"}]}]\n }\n\n with patch(\"src.plugins.translate.preview.LLMProviderService\") as MockProviderService:\n mock_provider_svc = MagicMock()\n MockProviderService.return_value = mock_provider_svc\n mock_provider_svc.get_provider.return_value = _make_mock_provider()\n mock_provider_svc.get_decrypted_api_key.return_value = \"sk-test-key\"\n\n with patch.object(TranslationPreview, \"_call_openai_compatible\") as mock_llm:\n mock_llm.return_value = json.dumps({\"rows\": [{\"row_id\": \"0\", \"translation\": \"Привет мир\"}]})\n\n # Mock dictionary filter to return glossary entries\n with patch(\"src.plugins.translate.preview.DictionaryManager.filter_for_batch\") as mock_filter:\n mock_filter.return_value = [\n {\n \"source_term\": \"Hello\",\n \"target_term\": \"Здравствуйте\",\n \"dictionary_id\": \"dict-1\",\n \"dictionary_name\": \"Test Dict\",\n \"context_notes\": \"Formal greeting\",\n }\n ]\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.preview_rows(job_id=\"job-123\", sample_size=5)\n\n # Verify LLM was called\n mock_llm.assert_called_once()\n assert len(result[\"records\"]) == 1\n # [/DEF:test_preview_with_dictionary:Function]\n\n # [DEF:test_preview_row_approve:Function]\n # @PURPOSE: Verify approving a preview row changes its status.\n def test_preview_row_approve(self):\n \"\"\"Approve action should set record status to APPROVED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n # Mock active session\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n session.created_at = datetime.now(timezone.utc)\n\n # Mock record\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"test translation\"\n record.feedback = None\n\n # Setup: db.query().filter().order_by().first() -> session (session lookup)\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n # Setup: db.query().filter().first() -> record (record lookup)\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"approve\",\n )\n\n assert record.status == \"APPROVED\"\n assert result[\"status\"] == \"APPROVED\"\n assert db.commit.called\n # [/DEF:test_preview_row_approve:Function]\n\n # [DEF:test_preview_row_reject:Function]\n # @PURPOSE: Verify rejecting a preview row changes its status.\n def test_preview_row_reject(self):\n \"\"\"Reject action should set record status to REJECTED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"test\"\n record.feedback = None\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"reject\",\n )\n\n assert record.status == \"REJECTED\"\n assert result[\"status\"] == \"REJECTED\"\n # [/DEF:test_preview_row_reject:Function]\n\n # [DEF:test_preview_row_edit:Function]\n # @PURPOSE: Verify editing a preview row updates its translation and status.\n def test_preview_row_edit(self):\n \"\"\"Edit action should update translation and set status to APPROVED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"old translation\"\n record.feedback = None\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"edit\",\n translation=\"edited translation\",\n )\n\n assert record.target_sql == \"edited translation\"\n assert record.status == \"APPROVED\"\n assert result[\"status\"] == \"APPROVED\"\n assert result[\"target_sql\"] == \"edited translation\"\n # [/DEF:test_preview_row_edit:Function]\n\n # [DEF:test_preview_row_invalid_action:Function]\n # @PURPOSE: Verify invalid action raises ValueError.\n def test_preview_row_invalid_action(self):\n \"\"\"Invalid action should raise ValueError.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"Invalid action\"):\n preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"invalid_action\",\n )\n # [/DEF:test_preview_row_invalid_action:Function]\n\n # [DEF:test_preview_accept_session:Function]\n # @PURPOSE: Verify accepting a preview session gates execution.\n def test_preview_accept_session(self):\n \"\"\"Accept should set session status to APPLIED and return records.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n session.created_by = \"test-user\"\n session.created_at = datetime.now(timezone.utc)\n session.expires_at = datetime.now(timezone.utc) + timedelta(hours=24)\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.source_sql = \"Hello\"\n record.target_sql = \"Привет\"\n record.status = \"APPROVED\"\n record.feedback = None\n\n # Session lookup\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n # Records lookup\n db.query.return_value.filter.return_value.all.return_value = [record]\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.accept_preview_session(job_id=\"job-123\")\n\n assert session.status == \"APPLIED\"\n assert result[\"status\"] == \"APPLIED\"\n assert len(result[\"records\"]) == 1\n assert result[\"records\"][0][\"status\"] == \"APPROVED\"\n assert db.commit.called\n # [/DEF:test_preview_accept_session:Function]\n\n # [DEF:test_preview_no_active_session:Function]\n # @PURPOSE: Verify accept raises error when no active session.\n def test_preview_no_active_session(self):\n \"\"\"Accept without active session should raise ValueError.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"No active preview session\"):\n preview_service.accept_preview_session(job_id=\"job-123\")\n # [/DEF:test_preview_no_active_session:Function]\n\n # [DEF:test_cost_estimation:Function]\n # @PURPOSE: Verify token and cost estimation methods.\n def test_cost_estimation(self):\n \"\"\"Token and cost estimation should return reasonable values.\"\"\"\n prompt = \"Translate this text from English to Russian. \" * 100\n tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n assert tokens > 0\n assert tokens < len(prompt) # tokens typically fewer than chars\n\n output_tokens = TokenEstimator.estimate_output_tokens(10)\n assert output_tokens == 500\n\n cost = TokenEstimator.estimate_cost(1000, 0.002)\n assert cost == 0.002\n\n cost_default = TokenEstimator.estimate_cost(1000)\n assert cost_default == 0.002\n # [/DEF:test_cost_estimation:Function]\n\n # [DEF:test_preview_parse_llm_response:Function]\n # @PURPOSE: Verify LLM JSON response parsing.\n def test_preview_parse_llm_response(self):\n \"\"\"Parse LLM response should extract translations keyed by row_id.\"\"\"\n response = json.dumps({\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Привет\"},\n {\"row_id\": \"1\", \"translation\": \"Мир\"},\n ]\n })\n result = TranslationPreview._parse_llm_response(response, 2)\n assert result == {\"0\": \"Привет\", \"1\": \"Мир\"}\n # [/DEF:test_preview_parse_llm_response:Function]\n\n # [DEF:test_preview_parse_llm_response_with_code_block:Function]\n # @PURPOSE: Verify LLM response parsing handles markdown code blocks.\n def test_preview_parse_llm_response_with_code_block(self):\n \"\"\"Parse LLM response should handle markdown code block wrapping.\"\"\"\n response = \"```json\\n{\\n \\\"rows\\\": [\\n {\\\"row_id\\\": \\\"0\\\", \\\"translation\\\": \\\"Test\\\"}\\n ]\\n}\\n```\"\n result = TranslationPreview._parse_llm_response(response, 1)\n assert result == {\"0\": \"Test\"}\n # [/DEF:test_preview_parse_llm_response_with_code_block:Function]\n\n # [DEF:test_preview_compute_config_hash:Function]\n # @PURPOSE: Verify config hash computation is deterministic.\n def test_preview_compute_config_hash(self):\n \"\"\"Config hash should be deterministic and non-empty.\"\"\"\n job = _make_mock_job()\n hash1 = TranslationPreview._compute_config_hash(job)\n hash2 = TranslationPreview._compute_config_hash(job)\n assert hash1 == hash2\n assert len(hash1) == 16\n # [/DEF:test_preview_compute_config_hash:Function]\n\n # [DEF:test_preview_missing_datasource:Function]\n # @PURPOSE: Verify error when job has no datasource configured.\n def test_preview_missing_datasource(self):\n \"\"\"Preview should fail if job has no datasource.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job(source_datasource_id=None)\n db.query.return_value.filter.return_value.first.return_value = job\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"source datasource\"):\n preview_service.preview_rows(job_id=\"job-123\")\n # [/DEF:test_preview_missing_datasource:Function]\n\n # [DEF:test_preview_missing_translation_column:Function]\n # @PURPOSE: Verify error when job has no translation column.\n def test_preview_missing_translation_column(self):\n \"\"\"Preview should fail if job has no translation column.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job(translation_column=None)\n db.query.return_value.filter.return_value.first.return_value = job\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"translation column\"):\n preview_service.preview_rows(job_id=\"job-123\")\n # [/DEF:test_preview_missing_translation_column:Function]\n\n\n# [/DEF:TestTranslationPreview:Class]\n\n# [/DEF:TranslationPreviewTests:Module]\n" - }, - { - "contract_id": "_make_mock_job", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 22, - "end_line": 40, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Create a mock TranslationJob with test config." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_make_mock_job:Function]\n# @PURPOSE: Create a mock TranslationJob with test config.\ndef _make_mock_job(**overrides):\n job = MagicMock(spec=TranslationJob)\n job.id = overrides.get(\"id\", \"job-123\")\n job.name = overrides.get(\"name\", \"Test Job\")\n job.source_dialect = overrides.get(\"source_dialect\", \"postgresql\")\n job.target_dialect = overrides.get(\"target_dialect\", \"clickhouse\")\n job.source_datasource_id = overrides.get(\"source_datasource_id\", \"42\")\n job.source_table = overrides.get(\"source_table\", \"source_table\")\n job.translation_column = overrides.get(\"translation_column\", \"title\")\n job.context_columns = overrides.get(\"context_columns\", [\"category\", \"description\"])\n job.target_language = overrides.get(\"target_language\", \"ru\")\n job.provider_id = overrides.get(\"provider_id\", \"provider-1\")\n job.batch_size = overrides.get(\"batch_size\", 50)\n job.upsert_strategy = overrides.get(\"upsert_strategy\", \"MERGE\")\n job.status = overrides.get(\"status\", \"DRAFT\")\n return job\n# [/DEF:_make_mock_job:Function]\n" - }, - { - "contract_id": "_make_mock_provider", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 43, - "end_line": 53, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Create a mock LLMProvider." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_make_mock_provider:Function]\n# @PURPOSE: Create a mock LLMProvider.\ndef _make_mock_provider(**overrides):\n provider = MagicMock()\n provider.id = overrides.get(\"id\", \"provider-1\")\n provider.provider_type = overrides.get(\"provider_type\", \"openai\")\n provider.base_url = overrides.get(\"base_url\", \"https://api.openai.com/v1\")\n provider.default_model = overrides.get(\"default_model\", \"gpt-4o-mini\")\n provider.api_key = \"encrypted-key-123\"\n return provider\n# [/DEF:_make_mock_provider:Function]\n" - }, - { - "contract_id": "TestTranslationPreview", + "contract_id": "TestClickHouseInsertSqlGeneration", "contract_type": "Class", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 56, - "end_line": 498, + "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", + "start_line": 139, + "end_line": 248, "tier": "TIER_1", "complexity": 1, "metadata": { - "PURPOSE": "Test suite for TranslationPreview service." + "PURPOSE": "Verify SQLGenerator produces valid ClickHouse INSERT SQL with normalized dates." }, "relations": [], "schema_warnings": [ @@ -66904,744 +51925,304 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TestTranslationPreview:Class]\n# @PURPOSE: Test suite for TranslationPreview service.\nclass TestTranslationPreview:\n\n # [DEF:test_preview_valid_job:Function]\n # @PURPOSE: Verify preview creates session and records successfully.\n def test_preview_valid_job(self):\n \"\"\"Preview with valid job should create session and return records.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n # Mock job query\n job = _make_mock_job()\n db.query.return_value.filter.return_value.first.return_value = job\n\n # Mock environments\n env = MagicMock()\n env.id = \"postgresql\"\n env.url = \"https://superset.example.com\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = True\n env.timeout = 30\n config_manager.get_environments.return_value = [env]\n\n # Mock SupersetClient\n with patch(\"src.plugins.translate.preview.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n MockClient.return_value = mock_client\n\n # Mock dataset_detail\n mock_client.get_dataset_detail.return_value = {\n \"table_name\": \"test_table\",\n \"columns\": [\n {\"column_name\": \"title\", \"type\": \"VARCHAR\"},\n {\"column_name\": \"category\", \"type\": \"VARCHAR\"},\n {\"column_name\": \"description\", \"type\": \"TEXT\"},\n ],\n }\n\n # Mock build_dataset_preview_query_context\n mock_client.build_dataset_preview_query_context.return_value = {\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"queries\": [{\n \"columns\": [],\n \"metrics\": [\"count\"],\n \"row_limit\": 10,\n }],\n \"form_data\": {},\n \"result_format\": \"json\",\n \"result_type\": \"query\",\n \"force\": True,\n }\n\n # Mock network request to Superset\n mock_client.network.request.return_value = {\n \"result\": [{\n \"status\": \"success\",\n \"data\": [\n {\"title\": \"Hello World\", \"category\": \"greeting\", \"description\": \"A friendly message\"},\n {\"title\": \"Goodbye\", \"category\": \"farewell\", \"description\": \"A parting message\"},\n ],\n }]\n }\n\n # Mock LLM provider\n with patch(\"src.plugins.translate.preview.LLMProviderService\") as MockProviderService:\n mock_provider_svc = MagicMock()\n MockProviderService.return_value = mock_provider_svc\n mock_provider = _make_mock_provider()\n mock_provider_svc.get_provider.return_value = mock_provider\n mock_provider_svc.get_decrypted_api_key.return_value = \"sk-test-key\"\n\n # Mock _call_openai_compatible to return structured JSON\n with patch.object(TranslationPreview, \"_call_openai_compatible\") as mock_llm:\n mock_llm.return_value = json.dumps({\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Привет мир\"},\n {\"row_id\": \"1\", \"translation\": \"Прощай\"},\n ]\n })\n\n # Mock DictionaryManager.filter_for_batch\n with patch(\"src.plugins.translate.preview.DictionaryManager.filter_for_batch\") as mock_filter:\n mock_filter.return_value = []\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.preview_rows(job_id=\"job-123\", sample_size=10)\n\n # Verify result structure\n assert result[\"job_id\"] == \"job-123\"\n assert result[\"status\"] == \"ACTIVE\"\n assert len(result[\"records\"]) == 2\n assert result[\"records\"][0][\"target_sql\"] == \"Привет мир\"\n assert result[\"records\"][1][\"target_sql\"] == \"Прощай\"\n\n # Verify cost estimate\n assert result[\"cost_estimate\"][\"sample_size\"] == 2\n assert result[\"cost_estimate\"][\"sample_total_tokens\"] > 0\n assert result[\"cost_estimate\"][\"sample_cost\"] >= 0\n\n # Verify config and dict hashes\n assert result[\"config_hash\"] is not None\n assert result[\"dict_snapshot_hash\"] is not None\n\n # Verify session was added to DB\n assert db.add.called\n assert db.commit.called\n # [/DEF:test_preview_valid_job:Function]\n\n # [DEF:test_preview_with_dictionary:Function]\n # @PURPOSE: Verify glossary entries from dictionary are included in LLM prompt.\n def test_preview_with_dictionary(self):\n \"\"\"Preview should include dictionary glossary in the prompt sent to LLM.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job()\n db.query.return_value.filter.return_value.first.return_value = job\n\n env = MagicMock()\n env.id = \"postgresql\"\n config_manager.get_environments.return_value = [env]\n\n with patch(\"src.plugins.translate.preview.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n MockClient.return_value = mock_client\n mock_client.get_dataset_detail.return_value = {\"table_name\": \"test\", \"columns\": []}\n mock_client.build_dataset_preview_query_context.return_value = {\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"queries\": [{\"columns\": [], \"metrics\": [\"count\"], \"row_limit\": 10}],\n \"form_data\": {},\n \"result_format\": \"json\",\n \"result_type\": \"query\",\n \"force\": True,\n }\n mock_client.network.request.return_value = {\n \"result\": [{\"status\": \"success\", \"data\": [{\"title\": \"Hello World\", \"category\": \"greeting\"}]}]\n }\n\n with patch(\"src.plugins.translate.preview.LLMProviderService\") as MockProviderService:\n mock_provider_svc = MagicMock()\n MockProviderService.return_value = mock_provider_svc\n mock_provider_svc.get_provider.return_value = _make_mock_provider()\n mock_provider_svc.get_decrypted_api_key.return_value = \"sk-test-key\"\n\n with patch.object(TranslationPreview, \"_call_openai_compatible\") as mock_llm:\n mock_llm.return_value = json.dumps({\"rows\": [{\"row_id\": \"0\", \"translation\": \"Привет мир\"}]})\n\n # Mock dictionary filter to return glossary entries\n with patch(\"src.plugins.translate.preview.DictionaryManager.filter_for_batch\") as mock_filter:\n mock_filter.return_value = [\n {\n \"source_term\": \"Hello\",\n \"target_term\": \"Здравствуйте\",\n \"dictionary_id\": \"dict-1\",\n \"dictionary_name\": \"Test Dict\",\n \"context_notes\": \"Formal greeting\",\n }\n ]\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.preview_rows(job_id=\"job-123\", sample_size=5)\n\n # Verify LLM was called\n mock_llm.assert_called_once()\n assert len(result[\"records\"]) == 1\n # [/DEF:test_preview_with_dictionary:Function]\n\n # [DEF:test_preview_row_approve:Function]\n # @PURPOSE: Verify approving a preview row changes its status.\n def test_preview_row_approve(self):\n \"\"\"Approve action should set record status to APPROVED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n # Mock active session\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n session.created_at = datetime.now(timezone.utc)\n\n # Mock record\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"test translation\"\n record.feedback = None\n\n # Setup: db.query().filter().order_by().first() -> session (session lookup)\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n # Setup: db.query().filter().first() -> record (record lookup)\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"approve\",\n )\n\n assert record.status == \"APPROVED\"\n assert result[\"status\"] == \"APPROVED\"\n assert db.commit.called\n # [/DEF:test_preview_row_approve:Function]\n\n # [DEF:test_preview_row_reject:Function]\n # @PURPOSE: Verify rejecting a preview row changes its status.\n def test_preview_row_reject(self):\n \"\"\"Reject action should set record status to REJECTED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"test\"\n record.feedback = None\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"reject\",\n )\n\n assert record.status == \"REJECTED\"\n assert result[\"status\"] == \"REJECTED\"\n # [/DEF:test_preview_row_reject:Function]\n\n # [DEF:test_preview_row_edit:Function]\n # @PURPOSE: Verify editing a preview row updates its translation and status.\n def test_preview_row_edit(self):\n \"\"\"Edit action should update translation and set status to APPROVED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"old translation\"\n record.feedback = None\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"edit\",\n translation=\"edited translation\",\n )\n\n assert record.target_sql == \"edited translation\"\n assert record.status == \"APPROVED\"\n assert result[\"status\"] == \"APPROVED\"\n assert result[\"target_sql\"] == \"edited translation\"\n # [/DEF:test_preview_row_edit:Function]\n\n # [DEF:test_preview_row_invalid_action:Function]\n # @PURPOSE: Verify invalid action raises ValueError.\n def test_preview_row_invalid_action(self):\n \"\"\"Invalid action should raise ValueError.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"Invalid action\"):\n preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"invalid_action\",\n )\n # [/DEF:test_preview_row_invalid_action:Function]\n\n # [DEF:test_preview_accept_session:Function]\n # @PURPOSE: Verify accepting a preview session gates execution.\n def test_preview_accept_session(self):\n \"\"\"Accept should set session status to APPLIED and return records.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n session.created_by = \"test-user\"\n session.created_at = datetime.now(timezone.utc)\n session.expires_at = datetime.now(timezone.utc) + timedelta(hours=24)\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.source_sql = \"Hello\"\n record.target_sql = \"Привет\"\n record.status = \"APPROVED\"\n record.feedback = None\n\n # Session lookup\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n # Records lookup\n db.query.return_value.filter.return_value.all.return_value = [record]\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.accept_preview_session(job_id=\"job-123\")\n\n assert session.status == \"APPLIED\"\n assert result[\"status\"] == \"APPLIED\"\n assert len(result[\"records\"]) == 1\n assert result[\"records\"][0][\"status\"] == \"APPROVED\"\n assert db.commit.called\n # [/DEF:test_preview_accept_session:Function]\n\n # [DEF:test_preview_no_active_session:Function]\n # @PURPOSE: Verify accept raises error when no active session.\n def test_preview_no_active_session(self):\n \"\"\"Accept without active session should raise ValueError.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"No active preview session\"):\n preview_service.accept_preview_session(job_id=\"job-123\")\n # [/DEF:test_preview_no_active_session:Function]\n\n # [DEF:test_cost_estimation:Function]\n # @PURPOSE: Verify token and cost estimation methods.\n def test_cost_estimation(self):\n \"\"\"Token and cost estimation should return reasonable values.\"\"\"\n prompt = \"Translate this text from English to Russian. \" * 100\n tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n assert tokens > 0\n assert tokens < len(prompt) # tokens typically fewer than chars\n\n output_tokens = TokenEstimator.estimate_output_tokens(10)\n assert output_tokens == 500\n\n cost = TokenEstimator.estimate_cost(1000, 0.002)\n assert cost == 0.002\n\n cost_default = TokenEstimator.estimate_cost(1000)\n assert cost_default == 0.002\n # [/DEF:test_cost_estimation:Function]\n\n # [DEF:test_preview_parse_llm_response:Function]\n # @PURPOSE: Verify LLM JSON response parsing.\n def test_preview_parse_llm_response(self):\n \"\"\"Parse LLM response should extract translations keyed by row_id.\"\"\"\n response = json.dumps({\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Привет\"},\n {\"row_id\": \"1\", \"translation\": \"Мир\"},\n ]\n })\n result = TranslationPreview._parse_llm_response(response, 2)\n assert result == {\"0\": \"Привет\", \"1\": \"Мир\"}\n # [/DEF:test_preview_parse_llm_response:Function]\n\n # [DEF:test_preview_parse_llm_response_with_code_block:Function]\n # @PURPOSE: Verify LLM response parsing handles markdown code blocks.\n def test_preview_parse_llm_response_with_code_block(self):\n \"\"\"Parse LLM response should handle markdown code block wrapping.\"\"\"\n response = \"```json\\n{\\n \\\"rows\\\": [\\n {\\\"row_id\\\": \\\"0\\\", \\\"translation\\\": \\\"Test\\\"}\\n ]\\n}\\n```\"\n result = TranslationPreview._parse_llm_response(response, 1)\n assert result == {\"0\": \"Test\"}\n # [/DEF:test_preview_parse_llm_response_with_code_block:Function]\n\n # [DEF:test_preview_compute_config_hash:Function]\n # @PURPOSE: Verify config hash computation is deterministic.\n def test_preview_compute_config_hash(self):\n \"\"\"Config hash should be deterministic and non-empty.\"\"\"\n job = _make_mock_job()\n hash1 = TranslationPreview._compute_config_hash(job)\n hash2 = TranslationPreview._compute_config_hash(job)\n assert hash1 == hash2\n assert len(hash1) == 16\n # [/DEF:test_preview_compute_config_hash:Function]\n\n # [DEF:test_preview_missing_datasource:Function]\n # @PURPOSE: Verify error when job has no datasource configured.\n def test_preview_missing_datasource(self):\n \"\"\"Preview should fail if job has no datasource.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job(source_datasource_id=None)\n db.query.return_value.filter.return_value.first.return_value = job\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"source datasource\"):\n preview_service.preview_rows(job_id=\"job-123\")\n # [/DEF:test_preview_missing_datasource:Function]\n\n # [DEF:test_preview_missing_translation_column:Function]\n # @PURPOSE: Verify error when job has no translation column.\n def test_preview_missing_translation_column(self):\n \"\"\"Preview should fail if job has no translation column.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job(translation_column=None)\n db.query.return_value.filter.return_value.first.return_value = job\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"translation column\"):\n preview_service.preview_rows(job_id=\"job-123\")\n # [/DEF:test_preview_missing_translation_column:Function]\n\n\n# [/DEF:TestTranslationPreview:Class]\n" + "anchor_syntax": "region", + "body": "# #region TestClickHouseInsertSqlGeneration [TYPE Class]\n# @BRIEF Verify SQLGenerator produces valid ClickHouse INSERT SQL with normalized dates.\nclass TestClickHouseInsertSqlGeneration:\n \"\"\"Verify SQLGenerator.generate for ClickHouse with timestamp key columns.\"\"\"\n\n # region test_insert_with_timestamp_key_cols [TYPE Function]\n # @BRIEF INSERT SQL for financial_comments_translated with report_date + document_number keys.\n def test_insert_with_timestamp_key_cols(self) -> None:\n rows = [\n {\n \"comment_text_en\": \"Translated comment text\",\n \"report_date\": \"1726358400000.0\", # Should become '2024-09-15'\n \"document_number\": \"DOC-001\",\n }\n ]\n columns = [\"comment_text_en\", \"report_date\", \"document_number\"]\n\n sql, count = SQLGenerator.generate(\n dialect=\"clickhouse\",\n target_schema=\"dm_view\",\n target_table=\"financial_comments_translated\",\n columns=columns,\n rows=rows,\n key_columns=[\"report_date\", \"document_number\"],\n upsert_strategy=\"INSERT\",\n )\n\n # Verify structure\n assert count == 1\n assert \"INSERT INTO\" in sql\n assert \"dm_view\" in sql\n assert \"financial_comments_translated\" in sql\n\n # Verify timestamp is normalized to YYYY-MM-DD\n assert \"'2024-09-15'\" in sql\n assert \"1726358400000\" not in sql\n\n # Verify document_number is preserved\n assert \"'DOC-001'\" in sql\n\n # Verify translated text is present\n assert \"'Translated comment text'\" in sql\n\n # Verify backtick quoting for ClickHouse\n assert \"`comment_text_en`\" in sql\n assert \"`report_date`\" in sql\n assert \"`document_number`\" in sql\n\n # region test_insert_multiple_rows_mixed_timestamps [TYPE Function]\n # @BRIEF Multiple rows with different timestamp formats.\n def test_insert_multiple_rows_mixed_timestamps(self) -> None:\n rows = [\n {\n \"comment_text_en\": \"Comment 1\",\n \"report_date\": \"1726358400000.0\", # 2024-09-15\n \"document_number\": \"DOC-001\",\n },\n {\n \"comment_text_en\": \"Comment 2\",\n \"report_date\": \"1726444800000.0\", # 2024-09-16\n \"document_number\": \"DOC-002\",\n },\n ]\n columns = [\"comment_text_en\", \"report_date\", \"document_number\"]\n\n sql, count = SQLGenerator.generate(\n dialect=\"clickhouse\",\n target_schema=\"dm_view\",\n target_table=\"financial_comments_translated\",\n columns=columns,\n rows=rows,\n key_columns=[\"report_date\", \"document_number\"],\n upsert_strategy=\"INSERT\",\n )\n\n assert count == 2\n assert \"'2024-09-15'\" in sql\n assert \"'2024-09-16'\" in sql\n assert \"'DOC-001'\" in sql\n assert \"'DOC-002'\" in sql\n\n # region test_insert_with_seconds_timestamp [TYPE Function]\n # @BRIEF Unix seconds timestamp (10 digits) also converts correctly.\n def test_insert_with_seconds_timestamp(self) -> None:\n rows = [\n {\n \"comment_text_en\": \"Test\",\n \"report_date\": \"1726358400\", # seconds\n \"document_number\": \"DOC-100\",\n }\n ]\n columns = [\"comment_text_en\", \"report_date\", \"document_number\"]\n\n sql, count = SQLGenerator.generate(\n dialect=\"clickhouse\",\n target_schema=\"dm_view\",\n target_table=\"financial_comments_translated\",\n columns=columns,\n rows=rows,\n key_columns=[\"report_date\", \"document_number\"],\n upsert_strategy=\"INSERT\",\n )\n\n assert \"'2024-09-15'\" in sql\n assert \"1726358400\" not in sql or \"'2024-09-15'\" in sql\n\n # endregion test_insert_with_timestamp_key_cols\n # endregion test_insert_multiple_rows_mixed_timestamps\n # endregion test_insert_with_seconds_timestamp\n# #endregion TestClickHouseInsertSqlGeneration\n" }, { - "contract_id": "test_preview_valid_job", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 60, - "end_line": 164, + "contract_id": "TestClickHouseJoinVerification", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", + "start_line": 251, + "end_line": 319, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "PURPOSE": "Verify preview creates session and records successfully." + "PURPOSE": "Verify the JOIN query SQL is syntactically correct for ClickHouse." }, "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_valid_job:Function]\n # @PURPOSE: Verify preview creates session and records successfully.\n def test_preview_valid_job(self):\n \"\"\"Preview with valid job should create session and return records.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n # Mock job query\n job = _make_mock_job()\n db.query.return_value.filter.return_value.first.return_value = job\n\n # Mock environments\n env = MagicMock()\n env.id = \"postgresql\"\n env.url = \"https://superset.example.com\"\n env.username = \"admin\"\n env.password = \"admin\"\n env.verify_ssl = True\n env.timeout = 30\n config_manager.get_environments.return_value = [env]\n\n # Mock SupersetClient\n with patch(\"src.plugins.translate.preview.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n MockClient.return_value = mock_client\n\n # Mock dataset_detail\n mock_client.get_dataset_detail.return_value = {\n \"table_name\": \"test_table\",\n \"columns\": [\n {\"column_name\": \"title\", \"type\": \"VARCHAR\"},\n {\"column_name\": \"category\", \"type\": \"VARCHAR\"},\n {\"column_name\": \"description\", \"type\": \"TEXT\"},\n ],\n }\n\n # Mock build_dataset_preview_query_context\n mock_client.build_dataset_preview_query_context.return_value = {\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"queries\": [{\n \"columns\": [],\n \"metrics\": [\"count\"],\n \"row_limit\": 10,\n }],\n \"form_data\": {},\n \"result_format\": \"json\",\n \"result_type\": \"query\",\n \"force\": True,\n }\n\n # Mock network request to Superset\n mock_client.network.request.return_value = {\n \"result\": [{\n \"status\": \"success\",\n \"data\": [\n {\"title\": \"Hello World\", \"category\": \"greeting\", \"description\": \"A friendly message\"},\n {\"title\": \"Goodbye\", \"category\": \"farewell\", \"description\": \"A parting message\"},\n ],\n }]\n }\n\n # Mock LLM provider\n with patch(\"src.plugins.translate.preview.LLMProviderService\") as MockProviderService:\n mock_provider_svc = MagicMock()\n MockProviderService.return_value = mock_provider_svc\n mock_provider = _make_mock_provider()\n mock_provider_svc.get_provider.return_value = mock_provider\n mock_provider_svc.get_decrypted_api_key.return_value = \"sk-test-key\"\n\n # Mock _call_openai_compatible to return structured JSON\n with patch.object(TranslationPreview, \"_call_openai_compatible\") as mock_llm:\n mock_llm.return_value = json.dumps({\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Привет мир\"},\n {\"row_id\": \"1\", \"translation\": \"Прощай\"},\n ]\n })\n\n # Mock DictionaryManager.filter_for_batch\n with patch(\"src.plugins.translate.preview.DictionaryManager.filter_for_batch\") as mock_filter:\n mock_filter.return_value = []\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.preview_rows(job_id=\"job-123\", sample_size=10)\n\n # Verify result structure\n assert result[\"job_id\"] == \"job-123\"\n assert result[\"status\"] == \"ACTIVE\"\n assert len(result[\"records\"]) == 2\n assert result[\"records\"][0][\"target_sql\"] == \"Привет мир\"\n assert result[\"records\"][1][\"target_sql\"] == \"Прощай\"\n\n # Verify cost estimate\n assert result[\"cost_estimate\"][\"sample_size\"] == 2\n assert result[\"cost_estimate\"][\"sample_total_tokens\"] > 0\n assert result[\"cost_estimate\"][\"sample_cost\"] >= 0\n\n # Verify config and dict hashes\n assert result[\"config_hash\"] is not None\n assert result[\"dict_snapshot_hash\"] is not None\n\n # Verify session was added to DB\n assert db.add.called\n assert db.commit.called\n # [/DEF:test_preview_valid_job:Function]\n" + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "body": "# #region TestClickHouseJoinVerification [TYPE Class]\n# @BRIEF Verify the JOIN query SQL is syntactically correct for ClickHouse.\nclass TestClickHouseJoinVerification:\n \"\"\"Verify the verification JOIN query structure for ClickHouse.\"\"\"\n\n # region test_join_query_structure [TYPE Function]\n # @BRIEF The verification JOIN query between financial_arrears and financial_comments_translated.\n def test_join_query_structure(self) -> None:\n # This is the exact query the user wants to verify\n verification_sql = \"\"\"\n SELECT\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\n FROM dm_view.financial_arrears f_a\n LEFT 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\n \"\"\"\n\n # Verify the query is valid SQL structure\n assert \"SELECT\" in verification_sql\n assert \"FROM dm_view.financial_arrears\" in verification_sql\n assert \"LEFT JOIN dm_view.financial_comments_translated\" in verification_sql\n assert \"f_a.report_date::date = f_c.report_date::date\" in verification_sql\n assert \"f_a.document_number = f_c.document_number\" in verification_sql\n\n # region test_insert_then_join_consistency [TYPE Function]\n # @BRIEF INSERT SQL and JOIN query use consistent column names and date formats.\n def test_insert_then_join_consistency(self) -> None:\n # Generate INSERT SQL\n rows = [\n {\n \"comment_text_en\": \"Translated\",\n \"report_date\": \"1726358400000.0\",\n \"document_number\": \"DOC-001\",\n }\n ]\n columns = [\"comment_text_en\", \"report_date\", \"document_number\"]\n\n insert_sql, _ = SQLGenerator.generate(\n dialect=\"clickhouse\",\n target_schema=\"dm_view\",\n target_table=\"financial_comments_translated\",\n columns=columns,\n rows=rows,\n key_columns=[\"report_date\", \"document_number\"],\n upsert_strategy=\"INSERT\",\n )\n\n # The INSERT uses '2024-09-15' for report_date\n assert \"'2024-09-15'\" in insert_sql\n\n # The JOIN compares f_a.report_date::date = f_c.report_date::date\n # ClickHouse ::date cast on a Date column is a no-op (already Date)\n # ClickHouse ::date cast on a DateTime column truncates to Date\n # So the INSERT value '2024-09-15' will match correctly in the JOIN\n\n # Verify column names match between INSERT and expected JOIN\n assert \"`report_date`\" in insert_sql\n assert \"`document_number`\" in insert_sql\n assert \"`comment_text_en`\" in insert_sql\n\n # endregion test_join_query_structure\n # endregion test_insert_then_join_consistency\n# #endregion TestClickHouseJoinVerification\n" }, { - "contract_id": "test_preview_with_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 166, - "end_line": 223, + "contract_id": "TestOrchestratorInsertFlow", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", + "start_line": 322, + "end_line": 410, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "PURPOSE": "Verify glossary entries from dictionary are included in LLM prompt." + "PURPOSE": "Verify the orchestrator's _generate_and_insert_sql produces correct SQL for ClickHouse." }, "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_with_dictionary:Function]\n # @PURPOSE: Verify glossary entries from dictionary are included in LLM prompt.\n def test_preview_with_dictionary(self):\n \"\"\"Preview should include dictionary glossary in the prompt sent to LLM.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job()\n db.query.return_value.filter.return_value.first.return_value = job\n\n env = MagicMock()\n env.id = \"postgresql\"\n config_manager.get_environments.return_value = [env]\n\n with patch(\"src.plugins.translate.preview.SupersetClient\") as MockClient:\n mock_client = MagicMock()\n MockClient.return_value = mock_client\n mock_client.get_dataset_detail.return_value = {\"table_name\": \"test\", \"columns\": []}\n mock_client.build_dataset_preview_query_context.return_value = {\n \"datasource\": {\"id\": 42, \"type\": \"table\"},\n \"queries\": [{\"columns\": [], \"metrics\": [\"count\"], \"row_limit\": 10}],\n \"form_data\": {},\n \"result_format\": \"json\",\n \"result_type\": \"query\",\n \"force\": True,\n }\n mock_client.network.request.return_value = {\n \"result\": [{\"status\": \"success\", \"data\": [{\"title\": \"Hello World\", \"category\": \"greeting\"}]}]\n }\n\n with patch(\"src.plugins.translate.preview.LLMProviderService\") as MockProviderService:\n mock_provider_svc = MagicMock()\n MockProviderService.return_value = mock_provider_svc\n mock_provider_svc.get_provider.return_value = _make_mock_provider()\n mock_provider_svc.get_decrypted_api_key.return_value = \"sk-test-key\"\n\n with patch.object(TranslationPreview, \"_call_openai_compatible\") as mock_llm:\n mock_llm.return_value = json.dumps({\"rows\": [{\"row_id\": \"0\", \"translation\": \"Привет мир\"}]})\n\n # Mock dictionary filter to return glossary entries\n with patch(\"src.plugins.translate.preview.DictionaryManager.filter_for_batch\") as mock_filter:\n mock_filter.return_value = [\n {\n \"source_term\": \"Hello\",\n \"target_term\": \"Здравствуйте\",\n \"dictionary_id\": \"dict-1\",\n \"dictionary_name\": \"Test Dict\",\n \"context_notes\": \"Formal greeting\",\n }\n ]\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.preview_rows(job_id=\"job-123\", sample_size=5)\n\n # Verify LLM was called\n mock_llm.assert_called_once()\n assert len(result[\"records\"]) == 1\n # [/DEF:test_preview_with_dictionary:Function]\n" + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "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 mock_query = MagicMock()\n mock_query.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" }, { - "contract_id": "test_preview_row_approve", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 225, - "end_line": 262, + "contract_id": "TestClickHouseEndToEnd", + "contract_type": "Class", + "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", + "start_line": 413, + "end_line": 539, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "PURPOSE": "Verify approving a preview row changes its status." + "PURPOSE": "End-to-end test: generate SQL, verify structure, simulate execution." }, "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_row_approve:Function]\n # @PURPOSE: Verify approving a preview row changes its status.\n def test_preview_row_approve(self):\n \"\"\"Approve action should set record status to APPROVED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n # Mock active session\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n session.created_at = datetime.now(timezone.utc)\n\n # Mock record\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"test translation\"\n record.feedback = None\n\n # Setup: db.query().filter().order_by().first() -> session (session lookup)\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n # Setup: db.query().filter().first() -> record (record lookup)\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"approve\",\n )\n\n assert record.status == \"APPROVED\"\n assert result[\"status\"] == \"APPROVED\"\n assert db.commit.called\n # [/DEF:test_preview_row_approve:Function]\n" - }, - { - "contract_id": "test_preview_row_reject", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 264, - "end_line": 295, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify rejecting a preview row changes its status." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_row_reject:Function]\n # @PURPOSE: Verify rejecting a preview row changes its status.\n def test_preview_row_reject(self):\n \"\"\"Reject action should set record status to REJECTED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"test\"\n record.feedback = None\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"reject\",\n )\n\n assert record.status == \"REJECTED\"\n assert result[\"status\"] == \"REJECTED\"\n # [/DEF:test_preview_row_reject:Function]\n" - }, - { - "contract_id": "test_preview_row_edit", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 297, - "end_line": 331, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify editing a preview row updates its translation and status." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_row_edit:Function]\n # @PURPOSE: Verify editing a preview row updates its translation and status.\n def test_preview_row_edit(self):\n \"\"\"Edit action should update translation and set status to APPROVED.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n record.target_sql = \"old translation\"\n record.feedback = None\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"edit\",\n translation=\"edited translation\",\n )\n\n assert record.target_sql == \"edited translation\"\n assert record.status == \"APPROVED\"\n assert result[\"status\"] == \"APPROVED\"\n assert result[\"target_sql\"] == \"edited translation\"\n # [/DEF:test_preview_row_edit:Function]\n" - }, - { - "contract_id": "test_preview_row_invalid_action", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 333, - "end_line": 360, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify invalid action raises ValueError." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_row_invalid_action:Function]\n # @PURPOSE: Verify invalid action raises ValueError.\n def test_preview_row_invalid_action(self):\n \"\"\"Invalid action should raise ValueError.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.status = \"PENDING\"\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n db.query.return_value.filter.return_value.first.return_value = record\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"Invalid action\"):\n preview_service.update_preview_row(\n job_id=\"job-123\",\n row_id=\"record-1\",\n action=\"invalid_action\",\n )\n # [/DEF:test_preview_row_invalid_action:Function]\n" - }, - { - "contract_id": "test_preview_accept_session", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 362, - "end_line": 398, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify accepting a preview session gates execution." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_accept_session:Function]\n # @PURPOSE: Verify accepting a preview session gates execution.\n def test_preview_accept_session(self):\n \"\"\"Accept should set session status to APPLIED and return records.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n session = MagicMock(spec=TranslationPreviewSession)\n session.id = \"session-1\"\n session.job_id = \"job-123\"\n session.status = \"ACTIVE\"\n session.created_by = \"test-user\"\n session.created_at = datetime.now(timezone.utc)\n session.expires_at = datetime.now(timezone.utc) + timedelta(hours=24)\n\n record = MagicMock(spec=TranslationPreviewRecord)\n record.id = \"record-1\"\n record.session_id = \"session-1\"\n record.source_sql = \"Hello\"\n record.target_sql = \"Привет\"\n record.status = \"APPROVED\"\n record.feedback = None\n\n # Session lookup\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = session\n # Records lookup\n db.query.return_value.filter.return_value.all.return_value = [record]\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n result = preview_service.accept_preview_session(job_id=\"job-123\")\n\n assert session.status == \"APPLIED\"\n assert result[\"status\"] == \"APPLIED\"\n assert len(result[\"records\"]) == 1\n assert result[\"records\"][0][\"status\"] == \"APPROVED\"\n assert db.commit.called\n # [/DEF:test_preview_accept_session:Function]\n" - }, - { - "contract_id": "test_preview_no_active_session", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 400, - "end_line": 412, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify accept raises error when no active session." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_no_active_session:Function]\n # @PURPOSE: Verify accept raises error when no active session.\n def test_preview_no_active_session(self):\n \"\"\"Accept without active session should raise ValueError.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"No active preview session\"):\n preview_service.accept_preview_session(job_id=\"job-123\")\n # [/DEF:test_preview_no_active_session:Function]\n" - }, - { - "contract_id": "test_cost_estimation", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 414, - "end_line": 431, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify token and cost estimation methods." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_cost_estimation:Function]\n # @PURPOSE: Verify token and cost estimation methods.\n def test_cost_estimation(self):\n \"\"\"Token and cost estimation should return reasonable values.\"\"\"\n prompt = \"Translate this text from English to Russian. \" * 100\n tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n assert tokens > 0\n assert tokens < len(prompt) # tokens typically fewer than chars\n\n output_tokens = TokenEstimator.estimate_output_tokens(10)\n assert output_tokens == 500\n\n cost = TokenEstimator.estimate_cost(1000, 0.002)\n assert cost == 0.002\n\n cost_default = TokenEstimator.estimate_cost(1000)\n assert cost_default == 0.002\n # [/DEF:test_cost_estimation:Function]\n" - }, - { - "contract_id": "test_preview_parse_llm_response", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 433, - "end_line": 445, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify LLM JSON response parsing." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_parse_llm_response:Function]\n # @PURPOSE: Verify LLM JSON response parsing.\n def test_preview_parse_llm_response(self):\n \"\"\"Parse LLM response should extract translations keyed by row_id.\"\"\"\n response = json.dumps({\n \"rows\": [\n {\"row_id\": \"0\", \"translation\": \"Привет\"},\n {\"row_id\": \"1\", \"translation\": \"Мир\"},\n ]\n })\n result = TranslationPreview._parse_llm_response(response, 2)\n assert result == {\"0\": \"Привет\", \"1\": \"Мир\"}\n # [/DEF:test_preview_parse_llm_response:Function]\n" - }, - { - "contract_id": "test_preview_parse_llm_response_with_code_block", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 447, - "end_line": 454, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify LLM response parsing handles markdown code blocks." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_parse_llm_response_with_code_block:Function]\n # @PURPOSE: Verify LLM response parsing handles markdown code blocks.\n def test_preview_parse_llm_response_with_code_block(self):\n \"\"\"Parse LLM response should handle markdown code block wrapping.\"\"\"\n response = \"```json\\n{\\n \\\"rows\\\": [\\n {\\\"row_id\\\": \\\"0\\\", \\\"translation\\\": \\\"Test\\\"}\\n ]\\n}\\n```\"\n result = TranslationPreview._parse_llm_response(response, 1)\n assert result == {\"0\": \"Test\"}\n # [/DEF:test_preview_parse_llm_response_with_code_block:Function]\n" - }, - { - "contract_id": "test_preview_compute_config_hash", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 456, - "end_line": 465, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify config hash computation is deterministic." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_compute_config_hash:Function]\n # @PURPOSE: Verify config hash computation is deterministic.\n def test_preview_compute_config_hash(self):\n \"\"\"Config hash should be deterministic and non-empty.\"\"\"\n job = _make_mock_job()\n hash1 = TranslationPreview._compute_config_hash(job)\n hash2 = TranslationPreview._compute_config_hash(job)\n assert hash1 == hash2\n assert len(hash1) == 16\n # [/DEF:test_preview_compute_config_hash:Function]\n" - }, - { - "contract_id": "test_preview_missing_datasource", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 467, - "end_line": 480, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify error when job has no datasource configured." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_missing_datasource:Function]\n # @PURPOSE: Verify error when job has no datasource configured.\n def test_preview_missing_datasource(self):\n \"\"\"Preview should fail if job has no datasource.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job(source_datasource_id=None)\n db.query.return_value.filter.return_value.first.return_value = job\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"source datasource\"):\n preview_service.preview_rows(job_id=\"job-123\")\n # [/DEF:test_preview_missing_datasource:Function]\n" - }, - { - "contract_id": "test_preview_missing_translation_column", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/__tests__/test_preview.py", - "start_line": 482, - "end_line": 495, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify error when job has no translation column." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:test_preview_missing_translation_column:Function]\n # @PURPOSE: Verify error when job has no translation column.\n def test_preview_missing_translation_column(self):\n \"\"\"Preview should fail if job has no translation column.\"\"\"\n db = MagicMock()\n config_manager = MagicMock()\n\n job = _make_mock_job(translation_column=None)\n db.query.return_value.filter.return_value.first.return_value = job\n\n preview_service = TranslationPreview(db, config_manager, \"test-user\")\n with pytest.raises(ValueError, match=\"translation column\"):\n preview_service.preview_rows(job_id=\"job-123\")\n # [/DEF:test_preview_missing_translation_column:Function]\n" + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "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": "DictionaryUtils", "contract_type": "Module", "file_path": "backend/src/plugins/translate/_utils.py", "start_line": 1, - "end_line": 2, + "end_line": 33, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "COMPLEXITY": 1, + "LAYER": "Domain", + "PURPOSE": "Utility helpers for dictionary management (term normalization and delimiter detection)." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "region", - "body": "# #region DictionaryUtils [C:1] [TYPE Module]\n# #endregion DictionaryUtils\n" + "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" }, { "contract_id": "_normalize_term", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_utils.py", "start_line": 8, - "end_line": 16, + "end_line": 20, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Normalize a term for case-insensitive unique constraint lookup using NFC normalization." + "PURPOSE": "Normalize a term for case-insensitive unique constraint lookup.", + "RATIONALE": "NFC normalization is applied before lowercasing to ensure consistent" }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "region", - "body": "# #region _normalize_term [C:2] [TYPE Function] [SEMANTICS dictionary,normalize]\n# @BRIEF Normalize a term for case-insensitive unique constraint lookup using NFC normalization.\ndef _normalize_term(term: str) -> str:\n \"\"\"Normalize a term by NFC, lowercasing, and removing extra whitespace.\"\"\"\n if not term:\n return \"\"\n term = unicodedata.normalize('NFC', term)\n return \" \".join(term.lower().split())\n# #endregion _normalize_term\n" + "body": "# #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" }, { "contract_id": "_detect_delimiter", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_utils.py", - "start_line": 19, - "end_line": 27, + "start_line": 23, + "end_line": 32, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Detect the delimiter used in a CSV/TSV header line." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "region", - "body": "# #region _detect_delimiter [C:1] [TYPE Function] [SEMANTICS dictionary,delimiter]\ndef _detect_delimiter(header_line: str) -> str:\n \"\"\"Detect delimiter by counting tabs vs commas in the first line.\"\"\"\n if not header_line:\n return \",\"\n tab_count = header_line.count(\"\\t\")\n comma_count = header_line.count(\",\")\n return \"\\t\" if tab_count > comma_count else \",\"\n# #endregion _detect_delimiter\n" + "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": "DictionaryManagerModule", "contract_type": "Module", "file_path": "backend/src/plugins/translate/dictionary.py", "start_line": 1, - "end_line": 757, - "tier": "TIER_3", - "complexity": 5, + "end_line": 766, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[name, source_dialect, target_dialect] -> Output[TerminologyDictionary]", - "INVARIANT": "UniqueConstraint(dictionary_id, source_term_normalized) prohibits duplicate entries within a dictionary.", + "COMPLEXITY": 4, "LAYER": "Domain", "PURPOSE": "Business logic for terminology dictionary management, entry CRUD, CSV/TSV import with conflict detection, and per-batch filtering.", - "RATIONALE": "C5 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion and unique constraint on normalized source term.", + "RATIONALE": "C4 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion and unique constraint on normalized source term.", "REJECTED": "\"Keep both\" as conflict option — UniqueConstraint(dictionary_id, source_term_normalized) prohibits variants; only overwrite/keep existing." }, "relations": [ { "source_id": "DictionaryManagerModule", "relation_type": "DEPENDS_ON", - "target_id": "TerminologyDictionary", - "target_ref": "[TerminologyDictionary]" + "target_id": "TerminologyDictionary:Class", + "target_ref": "[TerminologyDictionary:Class]" }, { "source_id": "DictionaryManagerModule", "relation_type": "DEPENDS_ON", - "target_id": "DictionaryEntry", - "target_ref": "[DictionaryEntry]" + "target_id": "DictionaryEntry:Class", + "target_ref": "[DictionaryEntry:Class]" }, { "source_id": "DictionaryManagerModule", "relation_type": "DEPENDS_ON", - "target_id": "TranslationJobDictionary", - "target_ref": "[TranslationJobDictionary]" + "target_id": "TranslationJobDictionary:Class", + "target_ref": "[TranslationJobDictionary:Class]" }, { "source_id": "DictionaryManagerModule", "relation_type": "DEPENDS_ON", - "target_id": "TranslationJob", - "target_ref": "[TranslationJob]" + "target_id": "TranslationJob:Class", + "target_ref": "[TranslationJob:Class]" } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, { "code": "missing_required_tag", "tag": "POST", - "message": "@POST is required for contract type 'Module' at C5", + "message": "@POST is required for contract type 'Module' at C4", "detail": { - "actual_complexity": 5, + "actual_complexity": 4, "contract_type": "Module" } }, { "code": "missing_required_tag", "tag": "PRE", - "message": "@PRE is required for contract type 'Module' at C5", + "message": "@PRE is required for contract type 'Module' at C4", "detail": { - "actual_complexity": 5, + "actual_complexity": 4, "contract_type": "Module" } }, { "code": "missing_required_tag", "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", + "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", "detail": { - "actual_complexity": 5, + "actual_complexity": 4, "contract_type": "Module" } } ], "anchor_syntax": "region", - "body": "# #region DictionaryManagerModule [C:5] [TYPE Module] [SEMANTICS dictionary,manager,terminology,crud,import,filter]\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 -> [TerminologyDictionary]\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @DATA_CONTRACT Input[name, source_dialect, target_dialect] -> Output[TerminologyDictionary]\n# @INVARIANT UniqueConstraint(dictionary_id, source_term_normalized) prohibits duplicate entries within a dictionary.\n# @RATIONALE C5 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion and unique constraint on normalized source term.\n# @REJECTED Pure C3 CRUD without state guards would allow orphaned job-dictionary links.\n# @REJECTED \"Keep both\" as conflict option — UniqueConstraint(dictionary_id, source_term_normalized) prohibits variants; only overwrite/keep existing.\n\nimport csv\nimport io\nimport re\nfrom typing import Any, Dict, List, Optional, Tuple\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import func\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.logger import belief_scope\nfrom ...models.translate import (\n TerminologyDictionary,\n DictionaryEntry,\n TranslationJobDictionary,\n TranslationJob,\n)\nfrom ._utils import _normalize_term, _detect_delimiter\n\nlog = MarkerLogger(\"DictionaryManager\")\n\n\n# #region DictionaryManager [C:5] [TYPE Class] [SEMANTICS dictionary,manager,crud,import]\n# @BRIEF Manages terminology dictionaries and their entries with referential integrity, import, and batch filtering.\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# @DATA_CONTRACT Input[name, source_dialect, target_dialect] -> Output[TerminologyDictionary]\n# @INVARIANT UniqueConstraint(dictionary_id, source_term_normalized) enforces no duplicates within a dictionary.\nclass DictionaryManager:\n # #region DictionaryManager.create_dictionary [C:3] [TYPE Function] [SEMANTICS dictionary,create]\n # @BRIEF Create a new terminology dictionary with name, source_dialect, and target_dialect.\n # @RELATION DEPENDS_ON -> [TerminologyDictionary]\n @staticmethod\n def create_dictionary(\n db: Session, name: str, source_dialect: str, target_dialect: str,\n created_by: Optional[str] = None, description: Optional[str] = None,\n is_active: bool = True,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.create_dictionary\"):\n log.reason(\"Creating dictionary\", payload={\"name\": name, \"source\": source_dialect, \"target\": target_dialect})\n dictionary = TerminologyDictionary(\n name=name,\n description=description,\n source_dialect=source_dialect,\n target_dialect=target_dialect,\n is_active=is_active,\n created_by=created_by,\n )\n db.add(dictionary)\n db.commit()\n db.refresh(dictionary)\n log.reflect(\"Dictionary created\", payload={\"id\": dictionary.id})\n return dictionary\n # #endregion DictionaryManager.create_dictionary\n\n # #region DictionaryManager.update_dictionary [C:3] [TYPE Function] [SEMANTICS dictionary,update]\n # @BRIEF Update an existing terminology dictionary's metadata fields.\n # @RELATION DEPENDS_ON -> [TerminologyDictionary]\n @staticmethod\n def update_dictionary(\n db: Session, dict_id: str, name: Optional[str] = None,\n description: Optional[str] = None, source_dialect: Optional[str] = None,\n target_dialect: Optional[str] = None, is_active: Optional[bool] = None,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.update_dictionary\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n log.reason(\"Updating dictionary\", payload={\"id\": dict_id})\n if name is not None:\n dictionary.name = name\n if description is not None:\n dictionary.description = description\n if source_dialect is not None:\n dictionary.source_dialect = source_dialect\n if target_dialect is not None:\n dictionary.target_dialect = target_dialect\n if is_active is not None:\n dictionary.is_active = is_active\n db.commit()\n db.refresh(dictionary)\n log.reflect(\"Dictionary updated\", payload={\"id\": dictionary.id})\n return dictionary\n # #endregion DictionaryManager.update_dictionary\n\n # #region DictionaryManager.delete_dictionary [C:4] [TYPE Function] [SEMANTICS dictionary,delete]\n # @BRIEF Delete a dictionary, blocked if attached to active/scheduled jobs (referential integrity guard).\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; checks job attachments first.\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 log.explore(\"Delete blocked: dictionary attached to active jobs\", payload={\"dict_id\": dict_id, \"jobs\": attached_jobs}, error=f\"Dictionary attached to {attached_jobs} active job(s)\")\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 log.reason(\"Deleting dictionary\", payload={\"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 log.reflect(\"Dictionary deleted\", payload={\"id\": dict_id})\n # #endregion DictionaryManager.delete_dictionary\n\n # #region DictionaryManager.get_dictionary [C:2] [TYPE Function] [SEMANTICS dictionary,get]\n # @BRIEF Get a single dictionary by ID with entry count.\n @staticmethod\n def get_dictionary(db: Session, dict_id: str) -> TerminologyDictionary:\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n return dictionary\n # #endregion DictionaryManager.get_dictionary\n\n # #region DictionaryManager.list_dictionaries [C:2] [TYPE Function] [SEMANTICS dictionary,list]\n # @BRIEF List dictionaries with pagination and total count.\n @staticmethod\n def list_dictionaries(\n db: Session, page: int = 1, page_size: int = 20,\n ) -> Tuple[List[TerminologyDictionary], int]:\n total = db.query(func.count(TerminologyDictionary.id)).scalar() or 0\n dictionaries = (\n db.query(TerminologyDictionary)\n .order_by(TerminologyDictionary.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return dictionaries, total\n # #endregion DictionaryManager.list_dictionaries\n\n # #region DictionaryManager.add_entry [C:3] [TYPE Function] [SEMANTICS dictionary,entry,add]\n # @BRIEF Add an entry to a dictionary, enforcing unique source_term_normalized per dictionary.\n # @RELATION DEPENDS_ON -> [DictionaryEntry]\n @staticmethod\n def add_entry(\n db: Session, dict_id: str, source_term: str, target_term: str,\n context_notes: Optional[str] = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.add_entry\"):\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}). Use overwrite or keep_existing conflict mode.\"\n )\n\n log.reason(\"Adding dictionary entry\", payload={\"dict_id\": dict_id, \"term\": source_term})\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=target_term.strip(),\n context_notes=context_notes.strip() if context_notes else None,\n )\n db.add(entry)\n db.commit()\n db.refresh(entry)\n log.reflect(\"Entry added\", payload={\"entry_id\": entry.id})\n return entry\n # #endregion DictionaryManager.add_entry\n\n # #region DictionaryManager.edit_entry [C:3] [TYPE Function] [SEMANTICS dictionary,entry,edit]\n # @BRIEF Edit an existing dictionary entry with duplicate-aware normalization check.\n # @RELATION DEPENDS_ON -> [DictionaryEntry]\n @staticmethod\n def edit_entry(\n db: Session, entry_id: str, source_term: Optional[str] = None,\n target_term: Optional[str] = None, context_notes: Optional[str] = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.edit_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n\n log.reason(\"Editing dictionary entry\", payload={\"entry_id\": entry_id})\n if source_term is not None:\n normalized = _normalize_term(source_term)\n # Check uniqueness within the same dictionary\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == entry.dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.id != entry_id,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}).\"\n )\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n log.reflect(\"Entry updated\", payload={\"entry_id\": entry.id})\n return entry\n # #endregion DictionaryManager.edit_entry\n\n # #region DictionaryManager.delete_entry [C:2] [TYPE Function] [SEMANTICS dictionary,entry,delete]\n # @BRIEF Delete a single dictionary entry.\n @staticmethod\n def delete_entry(db: Session, entry_id: str) -> None:\n with belief_scope(\"DictionaryManager.delete_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n log.reason(\"Deleting dictionary entry\", payload={\"entry_id\": entry_id})\n db.delete(entry)\n db.commit()\n log.reflect(\"Entry deleted\", payload={\"entry_id\": entry_id})\n # #endregion DictionaryManager.delete_entry\n\n # #region DictionaryManager.clear_entries [C:2] [TYPE Function] [SEMANTICS dictionary,entry,clear]\n # @BRIEF Delete all entries for a dictionary.\n @staticmethod\n def clear_entries(db: Session, dict_id: str) -> int:\n with belief_scope(\"DictionaryManager.clear_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n log.reason(\"Clearing all entries\", payload={\"dict_id\": dict_id})\n deleted = db.query(DictionaryEntry).filter(DictionaryEntry.dictionary_id == dict_id).delete()\n db.commit()\n log.reflect(\"Entries cleared\", payload={\"dict_id\": dict_id, \"count\": deleted})\n return deleted\n # #endregion DictionaryManager.clear_entries\n\n # #region DictionaryManager.list_entries [C:2] [TYPE Function] [SEMANTICS dictionary,entry,list]\n # @BRIEF List entries for a dictionary with pagination.\n @staticmethod\n def list_entries(\n db: Session, dict_id: str, page: int = 1, page_size: int = 50,\n ) -> Tuple[List[DictionaryEntry], int]:\n total = (\n db.query(func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .scalar()\n or 0\n )\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return entries, total\n # #endregion DictionaryManager.list_entries\n\n # #region DictionaryManager.import_entries [C:4] [TYPE Function] [SEMANTICS dictionary,import,csv]\n # @BRIEF Import entries from CSV/TSV content with duplicate detection and conflict resolution (overwrite/keep_existing/cancel).\n # @PRE content is valid CSV or TSV. dict_id exists.\n # @POST Entries are created/updated/skipped per conflict mode. Returns result summary with preview support.\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: Optional[str] = None,\n on_conflict: str = \"overwrite\",\n preview_only: bool = False,\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 log.reason(\"Detected delimiter\", payload={\"delimiter\": repr(delimiter)})\n\n if delimiter not in (\",\", \"\\t\"):\n raise ValueError(f\"Unsupported delimiter: {repr(delimiter)}. 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\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 )\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 \"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 )\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 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 )\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 log.reflect(\"Import complete\", payload={\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.filter_for_batch [C:4] [TYPE Function] [SEMANTICS dictionary,filter,batch]\n # @BRIEF Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.\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 dictionary link priority.\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 ) -> 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 log.reason(\"No dictionaries attached to job\", payload={\"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 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 # 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 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 })\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 log.reflect(\"Batch filter match complete\", payload={\n \"job_id\": job_id,\n \"source_texts\": len(source_texts),\n \"matches\": len(matched),\n \"dictionaries_used\": len(dictionaries),\n })\n return matched\n # #endregion DictionaryManager.filter_for_batch\n\n\n # #region DictionaryManager.submit_correction [C:4] [TYPE Function] [SEMANTICS dictionary,correction,submit]\n # @BRIEF Submit a term correction from a run result with conflict detection and origin tracking.\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: Optional[str] = None,\n origin_row_key: Optional[str] = None,\n origin_user_id: Optional[str] = None,\n on_conflict: str = \"overwrite\",\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 normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n\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 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\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 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 log.reflect(\"Correction processed\", payload=result)\n return result\n # #endregion DictionaryManager.submit_correction\n\n # #region DictionaryManager.submit_bulk_corrections [C:4] [TYPE Function] [SEMANTICS dictionary,correction,bulk]\n # @BRIEF Submit multiple term corrections atomically — all succeed or all fail on conflict.\n # @PRE corrections list is non-empty. dict_id exists.\n # @POST All corrections applied or none applied with conflict list (atomic).\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: Optional[str] = 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 existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\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 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# #endregion DictionaryManagerModule\n" + "body": "# #region DictionaryManagerModule [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, dictionary, batch, term]\n# @BRIEF Business logic for terminology dictionary management, entry CRUD, CSV/TSV import with conflict detection, and per-batch filtering.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TerminologyDictionary:Class]\n# @RELATION DEPENDS_ON -> [DictionaryEntry:Class]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary:Class]\n# @RELATION DEPENDS_ON -> [TranslationJob:Class]\n# @RATIONALE: C4 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion and unique constraint on normalized source term.\n# @REJECTED: Pure C3 CRUD without state guards would allow orphaned job-dictionary links.\n# @REJECTED: \"Keep both\" as conflict option — UniqueConstraint(dictionary_id, source_term_normalized) prohibits variants; only overwrite/keep existing.\n\nimport csv\nimport io\nimport re\nfrom typing import Any\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)\nfrom ._utils import _detect_delimiter, _normalize_term\n\n\n# #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.\nclass DictionaryManager:\n # region DictionaryManager.create_dictionary [TYPE Function]\n # @PURPOSE: Create a new terminology dictionary.\n # @PRE: payload contains name, source_dialect, target_dialect.\n # @POST: New TerminologyDictionary row is created and returned.\n @staticmethod\n def create_dictionary(\n db: Session, name: str, 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(\"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,\n target_dialect=target_dialect,\n is_active=is_active,\n created_by=created_by,\n )\n db.add(dictionary)\n db.commit()\n db.refresh(dictionary)\n 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, enforcing unique source_term_normalized.\n # @PRE: dict_id exists. source_term and target_term are non-empty.\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 ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.add_entry\"):\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}). Use overwrite or keep_existing conflict mode.\"\n )\n\n 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 )\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 DictionaryManager.add_entry\n\n # region DictionaryManager.edit_entry [TYPE Function]\n # @PURPOSE: Edit an existing dictionary entry with duplicate-aware normalization.\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 ) -> 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 if source_term is not None:\n normalized = _normalize_term(source_term)\n # Check uniqueness within the same dictionary\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == entry.dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.id != entry_id,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}).\"\n )\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n 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 ) -> 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\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 )\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 \"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 )\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 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 )\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.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 # @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 # @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 ) -> 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 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 # 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 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 })\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 })\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 ) -> 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 normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n\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 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\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 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 existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\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 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# #endregion DictionaryManagerModule\n" }, { "contract_id": "DictionaryManager", "contract_type": "Class", "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 33, - "end_line": 756, - "tier": "TIER_3", - "complexity": 5, + "start_line": 30, + "end_line": 765, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[name, source_dialect, target_dialect] -> Output[TerminologyDictionary]", - "INVARIANT": "UniqueConstraint(dictionary_id, source_term_normalized) enforces no duplicates within a dictionary.", + "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, import, and batch filtering.", + "PURPOSE": "Manages terminology dictionaries and their entries with referential integrity.", "SIDE_EFFECT": "Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards." }, "relations": [], "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, { "code": "missing_required_tag", "tag": "RELATION", - "message": "@RELATION is required for contract type 'Class' at C5", + "message": "@RELATION is required for contract type 'Class' at C4", "detail": { - "actual_complexity": 5, + "actual_complexity": 4, "contract_type": "Class" } } ], "anchor_syntax": "region", - "body": "# #region DictionaryManager [C:5] [TYPE Class] [SEMANTICS dictionary,manager,crud,import]\n# @BRIEF Manages terminology dictionaries and their entries with referential integrity, import, and batch filtering.\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# @DATA_CONTRACT Input[name, source_dialect, target_dialect] -> Output[TerminologyDictionary]\n# @INVARIANT UniqueConstraint(dictionary_id, source_term_normalized) enforces no duplicates within a dictionary.\nclass DictionaryManager:\n # #region DictionaryManager.create_dictionary [C:3] [TYPE Function] [SEMANTICS dictionary,create]\n # @BRIEF Create a new terminology dictionary with name, source_dialect, and target_dialect.\n # @RELATION DEPENDS_ON -> [TerminologyDictionary]\n @staticmethod\n def create_dictionary(\n db: Session, name: str, source_dialect: str, target_dialect: str,\n created_by: Optional[str] = None, description: Optional[str] = None,\n is_active: bool = True,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.create_dictionary\"):\n log.reason(\"Creating dictionary\", payload={\"name\": name, \"source\": source_dialect, \"target\": target_dialect})\n dictionary = TerminologyDictionary(\n name=name,\n description=description,\n source_dialect=source_dialect,\n target_dialect=target_dialect,\n is_active=is_active,\n created_by=created_by,\n )\n db.add(dictionary)\n db.commit()\n db.refresh(dictionary)\n log.reflect(\"Dictionary created\", payload={\"id\": dictionary.id})\n return dictionary\n # #endregion DictionaryManager.create_dictionary\n\n # #region DictionaryManager.update_dictionary [C:3] [TYPE Function] [SEMANTICS dictionary,update]\n # @BRIEF Update an existing terminology dictionary's metadata fields.\n # @RELATION DEPENDS_ON -> [TerminologyDictionary]\n @staticmethod\n def update_dictionary(\n db: Session, dict_id: str, name: Optional[str] = None,\n description: Optional[str] = None, source_dialect: Optional[str] = None,\n target_dialect: Optional[str] = None, is_active: Optional[bool] = None,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.update_dictionary\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n log.reason(\"Updating dictionary\", payload={\"id\": dict_id})\n if name is not None:\n dictionary.name = name\n if description is not None:\n dictionary.description = description\n if source_dialect is not None:\n dictionary.source_dialect = source_dialect\n if target_dialect is not None:\n dictionary.target_dialect = target_dialect\n if is_active is not None:\n dictionary.is_active = is_active\n db.commit()\n db.refresh(dictionary)\n log.reflect(\"Dictionary updated\", payload={\"id\": dictionary.id})\n return dictionary\n # #endregion DictionaryManager.update_dictionary\n\n # #region DictionaryManager.delete_dictionary [C:4] [TYPE Function] [SEMANTICS dictionary,delete]\n # @BRIEF Delete a dictionary, blocked if attached to active/scheduled jobs (referential integrity guard).\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; checks job attachments first.\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 log.explore(\"Delete blocked: dictionary attached to active jobs\", payload={\"dict_id\": dict_id, \"jobs\": attached_jobs}, error=f\"Dictionary attached to {attached_jobs} active job(s)\")\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 log.reason(\"Deleting dictionary\", payload={\"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 log.reflect(\"Dictionary deleted\", payload={\"id\": dict_id})\n # #endregion DictionaryManager.delete_dictionary\n\n # #region DictionaryManager.get_dictionary [C:2] [TYPE Function] [SEMANTICS dictionary,get]\n # @BRIEF Get a single dictionary by ID with entry count.\n @staticmethod\n def get_dictionary(db: Session, dict_id: str) -> TerminologyDictionary:\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n return dictionary\n # #endregion DictionaryManager.get_dictionary\n\n # #region DictionaryManager.list_dictionaries [C:2] [TYPE Function] [SEMANTICS dictionary,list]\n # @BRIEF List dictionaries with pagination and total count.\n @staticmethod\n def list_dictionaries(\n db: Session, page: int = 1, page_size: int = 20,\n ) -> Tuple[List[TerminologyDictionary], int]:\n total = db.query(func.count(TerminologyDictionary.id)).scalar() or 0\n dictionaries = (\n db.query(TerminologyDictionary)\n .order_by(TerminologyDictionary.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return dictionaries, total\n # #endregion DictionaryManager.list_dictionaries\n\n # #region DictionaryManager.add_entry [C:3] [TYPE Function] [SEMANTICS dictionary,entry,add]\n # @BRIEF Add an entry to a dictionary, enforcing unique source_term_normalized per dictionary.\n # @RELATION DEPENDS_ON -> [DictionaryEntry]\n @staticmethod\n def add_entry(\n db: Session, dict_id: str, source_term: str, target_term: str,\n context_notes: Optional[str] = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.add_entry\"):\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}). Use overwrite or keep_existing conflict mode.\"\n )\n\n log.reason(\"Adding dictionary entry\", payload={\"dict_id\": dict_id, \"term\": source_term})\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=target_term.strip(),\n context_notes=context_notes.strip() if context_notes else None,\n )\n db.add(entry)\n db.commit()\n db.refresh(entry)\n log.reflect(\"Entry added\", payload={\"entry_id\": entry.id})\n return entry\n # #endregion DictionaryManager.add_entry\n\n # #region DictionaryManager.edit_entry [C:3] [TYPE Function] [SEMANTICS dictionary,entry,edit]\n # @BRIEF Edit an existing dictionary entry with duplicate-aware normalization check.\n # @RELATION DEPENDS_ON -> [DictionaryEntry]\n @staticmethod\n def edit_entry(\n db: Session, entry_id: str, source_term: Optional[str] = None,\n target_term: Optional[str] = None, context_notes: Optional[str] = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.edit_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n\n log.reason(\"Editing dictionary entry\", payload={\"entry_id\": entry_id})\n if source_term is not None:\n normalized = _normalize_term(source_term)\n # Check uniqueness within the same dictionary\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == entry.dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.id != entry_id,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}).\"\n )\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n log.reflect(\"Entry updated\", payload={\"entry_id\": entry.id})\n return entry\n # #endregion DictionaryManager.edit_entry\n\n # #region DictionaryManager.delete_entry [C:2] [TYPE Function] [SEMANTICS dictionary,entry,delete]\n # @BRIEF Delete a single dictionary entry.\n @staticmethod\n def delete_entry(db: Session, entry_id: str) -> None:\n with belief_scope(\"DictionaryManager.delete_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n log.reason(\"Deleting dictionary entry\", payload={\"entry_id\": entry_id})\n db.delete(entry)\n db.commit()\n log.reflect(\"Entry deleted\", payload={\"entry_id\": entry_id})\n # #endregion DictionaryManager.delete_entry\n\n # #region DictionaryManager.clear_entries [C:2] [TYPE Function] [SEMANTICS dictionary,entry,clear]\n # @BRIEF Delete all entries for a dictionary.\n @staticmethod\n def clear_entries(db: Session, dict_id: str) -> int:\n with belief_scope(\"DictionaryManager.clear_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n log.reason(\"Clearing all entries\", payload={\"dict_id\": dict_id})\n deleted = db.query(DictionaryEntry).filter(DictionaryEntry.dictionary_id == dict_id).delete()\n db.commit()\n log.reflect(\"Entries cleared\", payload={\"dict_id\": dict_id, \"count\": deleted})\n return deleted\n # #endregion DictionaryManager.clear_entries\n\n # #region DictionaryManager.list_entries [C:2] [TYPE Function] [SEMANTICS dictionary,entry,list]\n # @BRIEF List entries for a dictionary with pagination.\n @staticmethod\n def list_entries(\n db: Session, dict_id: str, page: int = 1, page_size: int = 50,\n ) -> Tuple[List[DictionaryEntry], int]:\n total = (\n db.query(func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .scalar()\n or 0\n )\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return entries, total\n # #endregion DictionaryManager.list_entries\n\n # #region DictionaryManager.import_entries [C:4] [TYPE Function] [SEMANTICS dictionary,import,csv]\n # @BRIEF Import entries from CSV/TSV content with duplicate detection and conflict resolution (overwrite/keep_existing/cancel).\n # @PRE content is valid CSV or TSV. dict_id exists.\n # @POST Entries are created/updated/skipped per conflict mode. Returns result summary with preview support.\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: Optional[str] = None,\n on_conflict: str = \"overwrite\",\n preview_only: bool = False,\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 log.reason(\"Detected delimiter\", payload={\"delimiter\": repr(delimiter)})\n\n if delimiter not in (\",\", \"\\t\"):\n raise ValueError(f\"Unsupported delimiter: {repr(delimiter)}. 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\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 )\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 \"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 )\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 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 )\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 log.reflect(\"Import complete\", payload={\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.filter_for_batch [C:4] [TYPE Function] [SEMANTICS dictionary,filter,batch]\n # @BRIEF Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.\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 dictionary link priority.\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 ) -> 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 log.reason(\"No dictionaries attached to job\", payload={\"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 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 # 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 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 })\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 log.reflect(\"Batch filter match complete\", payload={\n \"job_id\": job_id,\n \"source_texts\": len(source_texts),\n \"matches\": len(matched),\n \"dictionaries_used\": len(dictionaries),\n })\n return matched\n # #endregion DictionaryManager.filter_for_batch\n\n\n # #region DictionaryManager.submit_correction [C:4] [TYPE Function] [SEMANTICS dictionary,correction,submit]\n # @BRIEF Submit a term correction from a run result with conflict detection and origin tracking.\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: Optional[str] = None,\n origin_row_key: Optional[str] = None,\n origin_user_id: Optional[str] = None,\n on_conflict: str = \"overwrite\",\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 normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n\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 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\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 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 log.reflect(\"Correction processed\", payload=result)\n return result\n # #endregion DictionaryManager.submit_correction\n\n # #region DictionaryManager.submit_bulk_corrections [C:4] [TYPE Function] [SEMANTICS dictionary,correction,bulk]\n # @BRIEF Submit multiple term corrections atomically — all succeed or all fail on conflict.\n # @PRE corrections list is non-empty. dict_id exists.\n # @POST All corrections applied or none applied with conflict list (atomic).\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: Optional[str] = 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 existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\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 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" - }, - { - "contract_id": "DictionaryManager.create_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 41, - "end_line": 65, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Create a new terminology dictionary with name, source_dialect, and target_dialect." - }, - "relations": [ - { - "source_id": "DictionaryManager.create_dictionary", - "relation_type": "DEPENDS_ON", - "target_id": "TerminologyDictionary", - "target_ref": "[TerminologyDictionary]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.create_dictionary [C:3] [TYPE Function] [SEMANTICS dictionary,create]\n # @BRIEF Create a new terminology dictionary with name, source_dialect, and target_dialect.\n # @RELATION DEPENDS_ON -> [TerminologyDictionary]\n @staticmethod\n def create_dictionary(\n db: Session, name: str, source_dialect: str, target_dialect: str,\n created_by: Optional[str] = None, description: Optional[str] = None,\n is_active: bool = True,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.create_dictionary\"):\n log.reason(\"Creating dictionary\", payload={\"name\": name, \"source\": source_dialect, \"target\": target_dialect})\n dictionary = TerminologyDictionary(\n name=name,\n description=description,\n source_dialect=source_dialect,\n target_dialect=target_dialect,\n is_active=is_active,\n created_by=created_by,\n )\n db.add(dictionary)\n db.commit()\n db.refresh(dictionary)\n log.reflect(\"Dictionary created\", payload={\"id\": dictionary.id})\n return dictionary\n # #endregion DictionaryManager.create_dictionary\n" - }, - { - "contract_id": "DictionaryManager.update_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 67, - "end_line": 95, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Update an existing terminology dictionary's metadata fields." - }, - "relations": [ - { - "source_id": "DictionaryManager.update_dictionary", - "relation_type": "DEPENDS_ON", - "target_id": "TerminologyDictionary", - "target_ref": "[TerminologyDictionary]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.update_dictionary [C:3] [TYPE Function] [SEMANTICS dictionary,update]\n # @BRIEF Update an existing terminology dictionary's metadata fields.\n # @RELATION DEPENDS_ON -> [TerminologyDictionary]\n @staticmethod\n def update_dictionary(\n db: Session, dict_id: str, name: Optional[str] = None,\n description: Optional[str] = None, source_dialect: Optional[str] = None,\n target_dialect: Optional[str] = None, is_active: Optional[bool] = None,\n ) -> TerminologyDictionary:\n with belief_scope(\"DictionaryManager.update_dictionary\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n log.reason(\"Updating dictionary\", payload={\"id\": dict_id})\n if name is not None:\n dictionary.name = name\n if description is not None:\n dictionary.description = description\n if source_dialect is not None:\n dictionary.source_dialect = source_dialect\n if target_dialect is not None:\n dictionary.target_dialect = target_dialect\n if is_active is not None:\n dictionary.is_active = is_active\n db.commit()\n db.refresh(dictionary)\n log.reflect(\"Dictionary updated\", payload={\"id\": dictionary.id})\n return dictionary\n # #endregion DictionaryManager.update_dictionary\n" - }, - { - "contract_id": "DictionaryManager.delete_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 97, - "end_line": 138, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Dictionary and its entries are deleted, unless attached to ACTIVE/READY/RUNNING/SCHEDULED jobs.", - "PRE": "dict_id exists.", - "PURPOSE": "Delete a dictionary, blocked if attached to active/scheduled jobs (referential integrity guard).", - "SIDE_EFFECT": "Deletes TerminologyDictionary row and all DictionaryEntry rows; checks job attachments first." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region DictionaryManager.delete_dictionary [C:4] [TYPE Function] [SEMANTICS dictionary,delete]\n # @BRIEF Delete a dictionary, blocked if attached to active/scheduled jobs (referential integrity guard).\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; checks job attachments first.\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 log.explore(\"Delete blocked: dictionary attached to active jobs\", payload={\"dict_id\": dict_id, \"jobs\": attached_jobs}, error=f\"Dictionary attached to {attached_jobs} active job(s)\")\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 log.reason(\"Deleting dictionary\", payload={\"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 log.reflect(\"Dictionary deleted\", payload={\"id\": dict_id})\n # #endregion DictionaryManager.delete_dictionary\n" - }, - { - "contract_id": "DictionaryManager.get_dictionary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 140, - "end_line": 148, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Get a single dictionary by ID with entry count." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.get_dictionary [C:2] [TYPE Function] [SEMANTICS dictionary,get]\n # @BRIEF Get a single dictionary by ID with entry count.\n @staticmethod\n def get_dictionary(db: Session, dict_id: str) -> TerminologyDictionary:\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n return dictionary\n # #endregion DictionaryManager.get_dictionary\n" - }, - { - "contract_id": "DictionaryManager.list_dictionaries", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 150, - "end_line": 165, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "List dictionaries with pagination and total count." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.list_dictionaries [C:2] [TYPE Function] [SEMANTICS dictionary,list]\n # @BRIEF List dictionaries with pagination and total count.\n @staticmethod\n def list_dictionaries(\n db: Session, page: int = 1, page_size: int = 20,\n ) -> Tuple[List[TerminologyDictionary], int]:\n total = db.query(func.count(TerminologyDictionary.id)).scalar() or 0\n dictionaries = (\n db.query(TerminologyDictionary)\n .order_by(TerminologyDictionary.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return dictionaries, total\n # #endregion DictionaryManager.list_dictionaries\n" - }, - { - "contract_id": "DictionaryManager.add_entry", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 167, - "end_line": 204, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Add an entry to a dictionary, enforcing unique source_term_normalized per dictionary." - }, - "relations": [ - { - "source_id": "DictionaryManager.add_entry", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryEntry", - "target_ref": "[DictionaryEntry]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.add_entry [C:3] [TYPE Function] [SEMANTICS dictionary,entry,add]\n # @BRIEF Add an entry to a dictionary, enforcing unique source_term_normalized per dictionary.\n # @RELATION DEPENDS_ON -> [DictionaryEntry]\n @staticmethod\n def add_entry(\n db: Session, dict_id: str, source_term: str, target_term: str,\n context_notes: Optional[str] = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.add_entry\"):\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}). Use overwrite or keep_existing conflict mode.\"\n )\n\n log.reason(\"Adding dictionary entry\", payload={\"dict_id\": dict_id, \"term\": source_term})\n entry = DictionaryEntry(\n dictionary_id=dict_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=target_term.strip(),\n context_notes=context_notes.strip() if context_notes else None,\n )\n db.add(entry)\n db.commit()\n db.refresh(entry)\n log.reflect(\"Entry added\", payload={\"entry_id\": entry.id})\n return entry\n # #endregion DictionaryManager.add_entry\n" - }, - { - "contract_id": "DictionaryManager.edit_entry", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 206, - "end_line": 247, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Edit an existing dictionary entry with duplicate-aware normalization check." - }, - "relations": [ - { - "source_id": "DictionaryManager.edit_entry", - "relation_type": "DEPENDS_ON", - "target_id": "DictionaryEntry", - "target_ref": "[DictionaryEntry]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.edit_entry [C:3] [TYPE Function] [SEMANTICS dictionary,entry,edit]\n # @BRIEF Edit an existing dictionary entry with duplicate-aware normalization check.\n # @RELATION DEPENDS_ON -> [DictionaryEntry]\n @staticmethod\n def edit_entry(\n db: Session, entry_id: str, source_term: Optional[str] = None,\n target_term: Optional[str] = None, context_notes: Optional[str] = None,\n ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.edit_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n\n log.reason(\"Editing dictionary entry\", payload={\"entry_id\": entry_id})\n if source_term is not None:\n normalized = _normalize_term(source_term)\n # Check uniqueness within the same dictionary\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == entry.dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.id != entry_id,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}).\"\n )\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n log.reflect(\"Entry updated\", payload={\"entry_id\": entry.id})\n return entry\n # #endregion DictionaryManager.edit_entry\n" - }, - { - "contract_id": "DictionaryManager.delete_entry", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 249, - "end_line": 261, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Delete a single dictionary entry." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.delete_entry [C:2] [TYPE Function] [SEMANTICS dictionary,entry,delete]\n # @BRIEF Delete a single dictionary entry.\n @staticmethod\n def delete_entry(db: Session, entry_id: str) -> None:\n with belief_scope(\"DictionaryManager.delete_entry\"):\n entry = db.query(DictionaryEntry).filter(DictionaryEntry.id == entry_id).first()\n if not entry:\n raise ValueError(f\"Entry not found: {entry_id}\")\n log.reason(\"Deleting dictionary entry\", payload={\"entry_id\": entry_id})\n db.delete(entry)\n db.commit()\n log.reflect(\"Entry deleted\", payload={\"entry_id\": entry_id})\n # #endregion DictionaryManager.delete_entry\n" - }, - { - "contract_id": "DictionaryManager.clear_entries", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 263, - "end_line": 276, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Delete all entries for a dictionary." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.clear_entries [C:2] [TYPE Function] [SEMANTICS dictionary,entry,clear]\n # @BRIEF Delete all entries for a dictionary.\n @staticmethod\n def clear_entries(db: Session, dict_id: str) -> int:\n with belief_scope(\"DictionaryManager.clear_entries\"):\n dictionary = db.query(TerminologyDictionary).filter(TerminologyDictionary.id == dict_id).first()\n if not dictionary:\n raise ValueError(f\"Dictionary not found: {dict_id}\")\n log.reason(\"Clearing all entries\", payload={\"dict_id\": dict_id})\n deleted = db.query(DictionaryEntry).filter(DictionaryEntry.dictionary_id == dict_id).delete()\n db.commit()\n log.reflect(\"Entries cleared\", payload={\"dict_id\": dict_id, \"count\": deleted})\n return deleted\n # #endregion DictionaryManager.clear_entries\n" - }, - { - "contract_id": "DictionaryManager.list_entries", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 278, - "end_line": 299, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "List entries for a dictionary with pagination." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region DictionaryManager.list_entries [C:2] [TYPE Function] [SEMANTICS dictionary,entry,list]\n # @BRIEF List entries for a dictionary with pagination.\n @staticmethod\n def list_entries(\n db: Session, dict_id: str, page: int = 1, page_size: int = 50,\n ) -> Tuple[List[DictionaryEntry], int]:\n total = (\n db.query(func.count(DictionaryEntry.id))\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .scalar()\n or 0\n )\n entries = (\n db.query(DictionaryEntry)\n .filter(DictionaryEntry.dictionary_id == dict_id)\n .order_by(DictionaryEntry.source_term.asc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return entries, total\n # #endregion DictionaryManager.list_entries\n" - }, - { - "contract_id": "DictionaryManager.import_entries", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 301, - "end_line": 438, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Entries are created/updated/skipped per conflict mode. Returns result summary with preview support.", - "PRE": "content is valid CSV or TSV. dict_id exists.", - "PURPOSE": "Import entries from CSV/TSV content with duplicate detection and conflict resolution (overwrite/keep_existing/cancel).", - "SIDE_EFFECT": "Batch-inserts or updates DictionaryEntry rows." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region DictionaryManager.import_entries [C:4] [TYPE Function] [SEMANTICS dictionary,import,csv]\n # @BRIEF Import entries from CSV/TSV content with duplicate detection and conflict resolution (overwrite/keep_existing/cancel).\n # @PRE content is valid CSV or TSV. dict_id exists.\n # @POST Entries are created/updated/skipped per conflict mode. Returns result summary with preview support.\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: Optional[str] = None,\n on_conflict: str = \"overwrite\",\n preview_only: bool = False,\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 log.reason(\"Detected delimiter\", payload={\"delimiter\": repr(delimiter)})\n\n if delimiter not in (\",\", \"\\t\"):\n raise ValueError(f\"Unsupported delimiter: {repr(delimiter)}. 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\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 )\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 \"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 )\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 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 )\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 log.reflect(\"Import complete\", payload={\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" - }, - { - "contract_id": "DictionaryManager.filter_for_batch", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 440, - "end_line": 535, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns list of matched entries with match info, sorted by dictionary link priority.", - "PRE": "job_id exists and source_texts is a list of strings.", - "PURPOSE": "Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.", - "SIDE_EFFECT": "Queries TranslationJobDictionary, TerminologyDictionary, and DictionaryEntry tables." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region DictionaryManager.filter_for_batch [C:4] [TYPE Function] [SEMANTICS dictionary,filter,batch]\n # @BRIEF Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.\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 dictionary link priority.\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 ) -> 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 log.reason(\"No dictionaries attached to job\", payload={\"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 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 # 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 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 })\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 log.reflect(\"Batch filter match complete\", payload={\n \"job_id\": job_id,\n \"source_texts\": len(source_texts),\n \"matches\": len(matched),\n \"dictionaries_used\": len(dictionaries),\n })\n return matched\n # #endregion DictionaryManager.filter_for_batch\n" - }, - { - "contract_id": "DictionaryManager.submit_correction", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 538, - "end_line": 633, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Entry created or updated; origin tracking populated. Returns action + conflict info.", - "PRE": "source_term, incorrect_target_term, corrected_target_term are non-empty. dict_id exists.", - "PURPOSE": "Submit a term correction from a run result with conflict detection and origin tracking.", - "SIDE_EFFECT": "Creates/updates DictionaryEntry row." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region DictionaryManager.submit_correction [C:4] [TYPE Function] [SEMANTICS dictionary,correction,submit]\n # @BRIEF Submit a term correction from a run result with conflict detection and origin tracking.\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: Optional[str] = None,\n origin_row_key: Optional[str] = None,\n origin_user_id: Optional[str] = None,\n on_conflict: str = \"overwrite\",\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 normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n\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 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\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 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 log.reflect(\"Correction processed\", payload=result)\n return result\n # #endregion DictionaryManager.submit_correction\n" - }, - { - "contract_id": "DictionaryManager.submit_bulk_corrections", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 635, - "end_line": 753, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "All corrections applied or none applied with conflict list (atomic).", - "PRE": "corrections list is non-empty. dict_id exists.", - "PURPOSE": "Submit multiple term corrections atomically — all succeed or all fail on conflict.", - "SIDE_EFFECT": "Creates/updates DictionaryEntry rows; commits once." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region DictionaryManager.submit_bulk_corrections [C:4] [TYPE Function] [SEMANTICS dictionary,correction,bulk]\n # @BRIEF Submit multiple term corrections atomically — all succeed or all fail on conflict.\n # @PRE corrections list is non-empty. dict_id exists.\n # @POST All corrections applied or none applied with conflict list (atomic).\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: Optional[str] = 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 existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\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 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" + "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.\nclass DictionaryManager:\n # region DictionaryManager.create_dictionary [TYPE Function]\n # @PURPOSE: Create a new terminology dictionary.\n # @PRE: payload contains name, source_dialect, target_dialect.\n # @POST: New TerminologyDictionary row is created and returned.\n @staticmethod\n def create_dictionary(\n db: Session, name: str, 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(\"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,\n target_dialect=target_dialect,\n is_active=is_active,\n created_by=created_by,\n )\n db.add(dictionary)\n db.commit()\n db.refresh(dictionary)\n 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, enforcing unique source_term_normalized.\n # @PRE: dict_id exists. source_term and target_term are non-empty.\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 ) -> DictionaryEntry:\n with belief_scope(\"DictionaryManager.add_entry\"):\n normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}). Use overwrite or keep_existing conflict mode.\"\n )\n\n 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 )\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 DictionaryManager.add_entry\n\n # region DictionaryManager.edit_entry [TYPE Function]\n # @PURPOSE: Edit an existing dictionary entry with duplicate-aware normalization.\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 ) -> 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 if source_term is not None:\n normalized = _normalize_term(source_term)\n # Check uniqueness within the same dictionary\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == entry.dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.id != entry_id,\n )\n .first()\n )\n if existing:\n raise ValueError(\n f\"Duplicate entry: '{source_term}' already exists in this dictionary \"\n f\"(id={existing.id}).\"\n )\n entry.source_term = source_term.strip()\n entry.source_term_normalized = normalized\n if target_term is not None:\n entry.target_term = target_term.strip()\n if context_notes is not None:\n entry.context_notes = context_notes.strip() if context_notes else None\n db.commit()\n db.refresh(entry)\n 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 ) -> 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\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 )\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 \"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 )\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 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 )\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.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 # @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 # @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 ) -> 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 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 # 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 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 })\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 })\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 ) -> 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 normalized = _normalize_term(source_term)\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\n )\n .first()\n )\n\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 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\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 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 existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dict_id,\n DictionaryEntry.source_term_normalized == normalized,\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 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" }, { "contract_id": "TranslationEventLog", "contract_type": "Module", "file_path": "backend/src/plugins/translate/events.py", "start_line": 1, - "end_line": 307, + "end_line": 259, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -67672,139 +52253,18 @@ ], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslationEventLog [C:5] [TYPE Module] [SEMANTICS translate,events,audit,logging]\n# @BRIEF Structured event logging for translation operations with terminal event invariant enforcement.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationEvent]\n# @RELATION DEPENDS_ON -> [MetricSnapshot]\n# @PRE Database session is open and valid.\n# @POST Events are persisted immutably; terminal events enforce exactly-one invariant per run.\n# @SIDE_EFFECT Writes TranslationEvent rows; prunes expired events with MetricSnapshot before deletion.\n# @DATA_CONTRACT Input[run_id:Optional[str], job_id:str, event_type:str, payload:dict] -> Output[TranslationEvent]\n# @INVARIANT Exactly one run_started + exactly one terminal event per non-null run_id.\n# @RATIONALE Immutable event log with nullable run_id allows job-level events (no run context) and run-level events.\n# @REJECTED stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.\n# @REJECTED Separate event table per entity — single TranslationEvent table with nullable run_id is simpler for audit.\nfrom typing import Any, Dict, List, Optional\nfrom sqlalchemy.orm import Session\nfrom datetime import datetime, timezone, timedelta\nimport uuid\n\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.logger import belief_scope\nfrom ...models.translate import TranslationEvent, MetricSnapshot\n\nlog = MarkerLogger(\"EventLog\")\n\n# Terminal events per run — exactly one of these must exist for a completed run.\nTERMINAL_EVENT_TYPES = {\"RUN_COMPLETED\", \"RUN_FAILED\", \"RUN_CANCELLED\"}\n# All valid event types for validation.\nVALID_EVENT_TYPES = TERMINAL_EVENT_TYPES | {\n \"JOB_CREATED\", \"JOB_UPDATED\", \"JOB_DELETED\",\n \"RUN_STARTED\", \"RUN_RETRYING\", \"RUN_RETRY_INSERT\",\n \"BATCH_STARTED\", \"BATCH_COMPLETED\", \"BATCH_FAILED\", \"BATCH_RETRYING\",\n \"TRANSLATION_PHASE_STARTED\", \"TRANSLATION_PHASE_COMPLETED\",\n \"INSERT_PHASE_STARTED\", \"INSERT_PHASE_COMPLETED\",\n \"PREVIEW_CREATED\", \"PREVIEW_ACCEPTED\", \"PREVIEW_DISCARDED\",\n \"SCHEDULE_CREATED\", \"SCHEDULE_UPDATED\", \"SCHEDULE_DELETED\",\n \"METRICS_SNAPSHOT_CREATED\",\n}\n\nDEFAULT_RETENTION_DAYS = 90\n\n\n# #region TranslationEventLog [C:5] [TYPE Class] [SEMANTICS translate,events,log,invariants]\n# @BRIEF Structured event logging for translation operations with terminal event invariant enforcement and pruning.\n# @PRE Database session is available.\n# @POST Events are written immutably; terminal events enforced per run; expired events pruned with MetricSnapshot.\n# @SIDE_EFFECT Writes TranslationEvent rows; prunes expired events.\n# @DATA_CONTRACT Input[run_id:Optional[str], job_id:str, event_type:str, payload:dict] -> Output[TranslationEvent]\n# @INVARIANT Exactly one run_started + exactly one terminal event per non-null run_id.\nclass TranslationEventLog:\n\n def __init__(self, db: Session):\n self.db = db\n\n # #region _create_event_raw [C:3] [TYPE Function] [SEMANTICS translate,events,create]\n # @BRIEF Create a TranslationEvent row without invariant checks. Internal helper for callers that manage invariants themselves.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def _create_event_raw(\n self,\n job_id: str,\n event_type: str,\n payload: Optional[Dict[str, Any]] = None,\n run_id: Optional[str] = None,\n created_by: Optional[str] = None,\n ) -> TranslationEvent:\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job_id,\n run_id=run_id,\n event_type=event_type,\n event_data=payload or {},\n created_by=created_by,\n created_at=datetime.now(timezone.utc),\n )\n self.db.add(event)\n self.db.flush()\n return event\n # #endregion _create_event_raw\n\n # #region log_event [C:4] [TYPE Function] [SEMANTICS translate,events,log]\n # @BRIEF Write an immutable event. Enforces terminal event invariant and run_started ordering for non-null run_id.\n # @PRE event_type must be a known type. If run_id is not None, enforce terminal + start invariants.\n # @POST TranslationEvent row is created; invariants checked before write.\n # @SIDE_EFFECT DB write.\n def log_event(\n self,\n job_id: str,\n event_type: str,\n payload: Optional[Dict[str, Any]] = None,\n run_id: Optional[str] = None,\n created_by: Optional[str] = None,\n ) -> TranslationEvent:\n with belief_scope(\"TranslationEventLog.log_event\"):\n if event_type not in VALID_EVENT_TYPES:\n raise ValueError(\n f\"Invalid event_type '{event_type}'. \"\n f\"Must be one of: {', '.join(sorted(VALID_EVENT_TYPES))}\"\n )\n\n # Enforce terminal event invariant for non-null run_id\n if run_id is not None and event_type in TERMINAL_EVENT_TYPES:\n existing_terminal = (\n self.db.query(TranslationEvent.id)\n .filter(\n TranslationEvent.run_id == run_id,\n TranslationEvent.event_type.in_(TERMINAL_EVENT_TYPES),\n )\n .first()\n )\n if existing_terminal:\n existing_id = existing_terminal[0] if isinstance(existing_terminal, (tuple, list)) else str(existing_terminal.id)\n raise ValueError(\n f\"Run '{run_id}' already has a terminal event \"\n f\"({existing_id}). Cannot add another terminal event.\"\n )\n\n # Enforce run_started invariant — must exist before other run events\n if run_id is not None and event_type not in {\"RUN_STARTED\"} | TERMINAL_EVENT_TYPES:\n has_started = (\n self.db.query(TranslationEvent.id)\n .filter(\n TranslationEvent.run_id == run_id,\n TranslationEvent.event_type == \"RUN_STARTED\",\n )\n .first()\n )\n if not has_started:\n raise ValueError(\n f\"Cannot log '{event_type}' for run '{run_id}': \"\n f\"RUN_STARTED event must precede other run events.\"\n )\n\n event = self._create_event_raw(\n job_id=job_id,\n event_type=event_type,\n payload=payload,\n run_id=run_id,\n created_by=created_by,\n )\n log.reason(\"Event logged\", payload={\n \"event_id\": event.id,\n \"job_id\": job_id,\n \"run_id\": run_id,\n \"event_type\": event_type,\n })\n return event\n # #endregion log_event\n\n # #region query_events [C:2] [TYPE Function] [SEMANTICS translate,events,query]\n # @BRIEF Query events with optional filters (job_id, run_id, event_type) and pagination.\n def query_events(\n self,\n job_id: Optional[str] = None,\n run_id: Optional[str] = None,\n event_type: Optional[str] = None,\n limit: int = 100,\n offset: int = 0,\n ) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationEventLog.query_events\"):\n query = self.db.query(TranslationEvent)\n\n if job_id:\n query = query.filter(TranslationEvent.job_id == job_id)\n if run_id:\n query = query.filter(TranslationEvent.run_id == run_id)\n if event_type:\n query = query.filter(TranslationEvent.event_type == event_type)\n\n events = (\n query.order_by(TranslationEvent.created_at.desc())\n .offset(offset)\n .limit(limit)\n .all()\n )\n\n return [\n {\n \"id\": e.id,\n \"job_id\": e.job_id,\n \"run_id\": e.run_id,\n \"event_type\": e.event_type,\n \"event_data\": e.event_data or {},\n \"created_by\": e.created_by,\n \"created_at\": e.created_at.isoformat() if e.created_at else None,\n }\n for e in events\n ]\n # #endregion query_events\n\n # #region prune_expired [C:4] [TYPE Function] [SEMANTICS translate,events,prune]\n # @BRIEF Delete events older than retention_days. Creates MetricSnapshot before pruning for audit trail.\n # @PRE None.\n # @POST Expired events are deleted; MetricSnapshot is created before deletion in batch.\n # @SIDE_EFFECT Creates MetricSnapshot row; deletes TranslationEvent rows in batches.\n def prune_expired(\n self,\n retention_days: int = DEFAULT_RETENTION_DAYS,\n batch_size: int = 1000,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.prune_expired\"):\n cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)\n log.reason(\"Pruning expired events\", payload={\n \"cutoff\": cutoff.isoformat(),\n \"retention_days\": retention_days,\n })\n\n # Find events to prune\n expired_query = self.db.query(TranslationEvent).filter(\n TranslationEvent.created_at < cutoff\n )\n total_expired = expired_query.count()\n\n if total_expired == 0:\n log.reflect(\"No expired events to prune\", payload={})\n return {\"pruned\": 0, \"snapshot_id\": None}\n\n # Create MetricSnapshot before pruning\n snapshot = MetricSnapshot(\n id=str(uuid.uuid4()),\n job_id=\"_prune_aggregate_\",\n key_hash=f\"prune_{cutoff.timestamp():.0f}\",\n covers_events_before=cutoff,\n total_records=total_expired,\n snapshot_date=datetime.now(timezone.utc),\n )\n self.db.add(snapshot)\n self.db.flush()\n snapshot_id = snapshot.id\n\n # Delete in batches\n pruned = 0\n while True:\n batch_ids = (\n self.db.query(TranslationEvent.id)\n .filter(TranslationEvent.created_at < cutoff)\n .limit(batch_size)\n .all()\n )\n if not batch_ids:\n break\n ids = [row[0] for row in batch_ids]\n self.db.query(TranslationEvent).filter(\n TranslationEvent.id.in_(ids)\n ).delete(synchronize_session=False)\n self.db.flush()\n pruned += len(ids)\n log.reason(\"Pruned batch\", payload={\"batch_size\": len(ids), \"total_pruned\": pruned})\n\n self.db.commit()\n\n log.reflect(\"Pruning complete\", payload={\n \"pruned\": pruned,\n \"snapshot_id\": snapshot_id,\n })\n return {\"pruned\": pruned, \"snapshot_id\": snapshot_id}\n # #endregion prune_expired\n\n # #region get_run_event_summary [C:3] [TYPE Function] [SEMANTICS translate,events,summary]\n # @BRIEF Get a summary of events for a run, including invariant validity check.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def get_run_event_summary(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_summary\"):\n events = self.query_events(run_id=run_id)\n\n has_started = any(e[\"event_type\"] == \"RUN_STARTED\" for e in events)\n terminal_events = [e for e in events if e[\"event_type\"] in TERMINAL_EVENT_TYPES]\n\n return {\n \"run_id\": run_id,\n \"event_count\": len(events),\n \"has_run_started\": has_started,\n \"terminal_event_count\": len(terminal_events),\n \"terminal_events\": terminal_events,\n \"invariant_valid\": has_started and len(terminal_events) <= 1,\n \"events\": events,\n }\n # #endregion get_run_event_summary\n\n # #region get_run_event_invariants_lightweight [C:3] [TYPE Function] [SEMANTICS translate,events,invariants]\n # @BRIEF Lightweight invariant check for a run — fetches only event_type column, avoids event_data for performance.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def get_run_event_invariants_lightweight(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_invariants_lightweight\"):\n # Lightweight query: only fetch event_type column, not event_data\n event_types = [\n row[0]\n for row in (\n self.db.query(TranslationEvent.event_type)\n .filter(TranslationEvent.run_id == run_id)\n .order_by(TranslationEvent.created_at.desc())\n .limit(100)\n .all()\n )\n ]\n\n has_run_started = \"RUN_STARTED\" in event_types\n terminal_count = sum(1 for et in event_types if et in TERMINAL_EVENT_TYPES)\n\n return {\n \"has_run_started\": has_run_started,\n \"terminal_event_count\": terminal_count,\n \"invariant_valid\": has_run_started and terminal_count <= 1,\n }\n # #endregion get_run_event_invariants_lightweight\n\n\n# #endregion TranslationEventLog\n# #endregion TranslationEventLog\n" - }, - { - "contract_id": "_create_event_raw", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/events.py", - "start_line": 54, - "end_line": 77, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Create a TranslationEvent row without invariant checks. Internal helper for callers that manage invariants themselves." - }, - "relations": [ - { - "source_id": "_create_event_raw", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationEvent", - "target_ref": "[TranslationEvent]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region _create_event_raw [C:3] [TYPE Function] [SEMANTICS translate,events,create]\n # @BRIEF Create a TranslationEvent row without invariant checks. Internal helper for callers that manage invariants themselves.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def _create_event_raw(\n self,\n job_id: str,\n event_type: str,\n payload: Optional[Dict[str, Any]] = None,\n run_id: Optional[str] = None,\n created_by: Optional[str] = None,\n ) -> TranslationEvent:\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job_id,\n run_id=run_id,\n event_type=event_type,\n event_data=payload or {},\n created_by=created_by,\n created_at=datetime.now(timezone.utc),\n )\n self.db.add(event)\n self.db.flush()\n return event\n # #endregion _create_event_raw\n" - }, - { - "contract_id": "query_events", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/events.py", - "start_line": 148, - "end_line": 187, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Query events with optional filters (job_id, run_id, event_type) and pagination." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region query_events [C:2] [TYPE Function] [SEMANTICS translate,events,query]\n # @BRIEF Query events with optional filters (job_id, run_id, event_type) and pagination.\n def query_events(\n self,\n job_id: Optional[str] = None,\n run_id: Optional[str] = None,\n event_type: Optional[str] = None,\n limit: int = 100,\n offset: int = 0,\n ) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationEventLog.query_events\"):\n query = self.db.query(TranslationEvent)\n\n if job_id:\n query = query.filter(TranslationEvent.job_id == job_id)\n if run_id:\n query = query.filter(TranslationEvent.run_id == run_id)\n if event_type:\n query = query.filter(TranslationEvent.event_type == event_type)\n\n events = (\n query.order_by(TranslationEvent.created_at.desc())\n .offset(offset)\n .limit(limit)\n .all()\n )\n\n return [\n {\n \"id\": e.id,\n \"job_id\": e.job_id,\n \"run_id\": e.run_id,\n \"event_type\": e.event_type,\n \"event_data\": e.event_data or {},\n \"created_by\": e.created_by,\n \"created_at\": e.created_at.isoformat() if e.created_at else None,\n }\n for e in events\n ]\n # #endregion query_events\n" - }, - { - "contract_id": "prune_expired", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/events.py", - "start_line": 189, - "end_line": 255, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Expired events are deleted; MetricSnapshot is created before deletion in batch.", - "PRE": "None.", - "PURPOSE": "Delete events older than retention_days. Creates MetricSnapshot before pruning for audit trail.", - "SIDE_EFFECT": "Creates MetricSnapshot row; deletes TranslationEvent rows in batches." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region prune_expired [C:4] [TYPE Function] [SEMANTICS translate,events,prune]\n # @BRIEF Delete events older than retention_days. Creates MetricSnapshot before pruning for audit trail.\n # @PRE None.\n # @POST Expired events are deleted; MetricSnapshot is created before deletion in batch.\n # @SIDE_EFFECT Creates MetricSnapshot row; deletes TranslationEvent rows in batches.\n def prune_expired(\n self,\n retention_days: int = DEFAULT_RETENTION_DAYS,\n batch_size: int = 1000,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.prune_expired\"):\n cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)\n log.reason(\"Pruning expired events\", payload={\n \"cutoff\": cutoff.isoformat(),\n \"retention_days\": retention_days,\n })\n\n # Find events to prune\n expired_query = self.db.query(TranslationEvent).filter(\n TranslationEvent.created_at < cutoff\n )\n total_expired = expired_query.count()\n\n if total_expired == 0:\n log.reflect(\"No expired events to prune\", payload={})\n return {\"pruned\": 0, \"snapshot_id\": None}\n\n # Create MetricSnapshot before pruning\n snapshot = MetricSnapshot(\n id=str(uuid.uuid4()),\n job_id=\"_prune_aggregate_\",\n key_hash=f\"prune_{cutoff.timestamp():.0f}\",\n covers_events_before=cutoff,\n total_records=total_expired,\n snapshot_date=datetime.now(timezone.utc),\n )\n self.db.add(snapshot)\n self.db.flush()\n snapshot_id = snapshot.id\n\n # Delete in batches\n pruned = 0\n while True:\n batch_ids = (\n self.db.query(TranslationEvent.id)\n .filter(TranslationEvent.created_at < cutoff)\n .limit(batch_size)\n .all()\n )\n if not batch_ids:\n break\n ids = [row[0] for row in batch_ids]\n self.db.query(TranslationEvent).filter(\n TranslationEvent.id.in_(ids)\n ).delete(synchronize_session=False)\n self.db.flush()\n pruned += len(ids)\n log.reason(\"Pruned batch\", payload={\"batch_size\": len(ids), \"total_pruned\": pruned})\n\n self.db.commit()\n\n log.reflect(\"Pruning complete\", payload={\n \"pruned\": pruned,\n \"snapshot_id\": snapshot_id,\n })\n return {\"pruned\": pruned, \"snapshot_id\": snapshot_id}\n # #endregion prune_expired\n" - }, - { - "contract_id": "get_run_event_summary", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/events.py", - "start_line": 257, - "end_line": 276, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Get a summary of events for a run, including invariant validity check." - }, - "relations": [ - { - "source_id": "get_run_event_summary", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationEvent", - "target_ref": "[TranslationEvent]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region get_run_event_summary [C:3] [TYPE Function] [SEMANTICS translate,events,summary]\n # @BRIEF Get a summary of events for a run, including invariant validity check.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def get_run_event_summary(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_summary\"):\n events = self.query_events(run_id=run_id)\n\n has_started = any(e[\"event_type\"] == \"RUN_STARTED\" for e in events)\n terminal_events = [e for e in events if e[\"event_type\"] in TERMINAL_EVENT_TYPES]\n\n return {\n \"run_id\": run_id,\n \"event_count\": len(events),\n \"has_run_started\": has_started,\n \"terminal_event_count\": len(terminal_events),\n \"terminal_events\": terminal_events,\n \"invariant_valid\": has_started and len(terminal_events) <= 1,\n \"events\": events,\n }\n # #endregion get_run_event_summary\n" - }, - { - "contract_id": "get_run_event_invariants_lightweight", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/events.py", - "start_line": 278, - "end_line": 303, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Lightweight invariant check for a run — fetches only event_type column, avoids event_data for performance." - }, - "relations": [ - { - "source_id": "get_run_event_invariants_lightweight", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationEvent", - "target_ref": "[TranslationEvent]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region get_run_event_invariants_lightweight [C:3] [TYPE Function] [SEMANTICS translate,events,invariants]\n # @BRIEF Lightweight invariant check for a run — fetches only event_type column, avoids event_data for performance.\n # @RELATION DEPENDS_ON -> [TranslationEvent]\n def get_run_event_invariants_lightweight(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_invariants_lightweight\"):\n # Lightweight query: only fetch event_type column, not event_data\n event_types = [\n row[0]\n for row in (\n self.db.query(TranslationEvent.event_type)\n .filter(TranslationEvent.run_id == run_id)\n .order_by(TranslationEvent.created_at.desc())\n .limit(100)\n .all()\n )\n ]\n\n has_run_started = \"RUN_STARTED\" in event_types\n terminal_count = sum(1 for et in event_types if et in TERMINAL_EVENT_TYPES)\n\n return {\n \"has_run_started\": has_run_started,\n \"terminal_event_count\": terminal_count,\n \"invariant_valid\": has_run_started and terminal_count <= 1,\n }\n # #endregion get_run_event_invariants_lightweight\n" + "body": "# #region TranslationEventLog [C:5] [TYPE Module] [SEMANTICS sqlalchemy, translate, event, log, audit]\n# @BRIEF Structured event logging for translation operations with terminal event invariant enforcement.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TranslationEvent]\n# @RELATION DEPENDS_ON -> [MetricSnapshot]\n# @PRE: Database session is open and valid.\n# @POST: Events are persisted immutably; terminal events enforce exactly-one invariant per run.\n# @SIDE_EFFECT: Writes TranslationEvent rows; prunes expired events with MetricSnapshot before deletion.\n# @DATA_CONTRACT: Input[run_id:Optional[str], job_id:str, event_type:str, payload:dict] -> Output[TranslationEvent]\n# @INVARIANT: Exactly one run_started + exactly one terminal event per non-null run_id.\n# @RATIONALE: Immutable event log with nullable run_id allows job-level events (no run context) and run-level events.\n# @REJECTED: stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.\n# @REJECTED: Separate event table per entity — single TranslationEvent table with nullable run_id is simpler for audit.\nimport uuid\nfrom datetime import UTC, datetime, timedelta\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import MetricSnapshot, TranslationEvent\n\n# Terminal events per run — exactly one of these must exist for a completed run.\nTERMINAL_EVENT_TYPES = {\"RUN_COMPLETED\", \"RUN_FAILED\", \"RUN_CANCELLED\"}\n# All valid event types for validation.\nVALID_EVENT_TYPES = TERMINAL_EVENT_TYPES | {\n \"JOB_CREATED\", \"JOB_UPDATED\", \"JOB_DELETED\",\n \"RUN_STARTED\", \"RUN_RETRYING\", \"RUN_RETRY_INSERT\",\n \"BATCH_STARTED\", \"BATCH_COMPLETED\", \"BATCH_FAILED\", \"BATCH_RETRYING\",\n \"TRANSLATION_PHASE_STARTED\", \"TRANSLATION_PHASE_COMPLETED\",\n \"INSERT_PHASE_STARTED\", \"INSERT_PHASE_COMPLETED\",\n \"PREVIEW_CREATED\", \"PREVIEW_ACCEPTED\", \"PREVIEW_DISCARDED\",\n \"SCHEDULE_CREATED\", \"SCHEDULE_UPDATED\", \"SCHEDULE_DELETED\",\n \"METRICS_SNAPSHOT_CREATED\",\n}\n\nDEFAULT_RETENTION_DAYS = 90\n\n\n# #region TranslationEventLog [C:5] [TYPE Class]\n# @BRIEF Structured event logging for translation operations with terminal event invariant enforcement.\n# @PRE: Database session is available.\n# @POST: Events are written immutably; terminal events enforced per run.\n# @SIDE_EFFECT: Writes TranslationEvent rows; prunes expired events.\n# @INVARIANT: Exactly one run_started + exactly one terminal event per non-null run_id.\nclass TranslationEventLog:\n\n def __init__(self, db: Session):\n self.db = db\n\n # region log_event [TYPE Function]\n # @PURPOSE: Write an immutable event. Enforces terminal event invariant for non-null run_id.\n # @PRE: event_type must be a known type. If run_id is not None, enforce terminal invariant.\n # @POST: TranslationEvent row is created.\n # @SIDE_EFFECT: DB write.\n def log_event(\n self,\n job_id: str,\n event_type: str,\n payload: dict[str, Any] | None = None,\n run_id: str | None = None,\n created_by: str | None = None,\n ) -> TranslationEvent:\n with belief_scope(\"TranslationEventLog.log_event\"):\n if event_type not in VALID_EVENT_TYPES:\n raise ValueError(\n f\"Invalid event_type '{event_type}'. \"\n f\"Must be one of: {', '.join(sorted(VALID_EVENT_TYPES))}\"\n )\n\n # Enforce terminal event invariant for non-null run_id\n if run_id is not None and event_type in TERMINAL_EVENT_TYPES:\n existing_terminal = (\n self.db.query(TranslationEvent.id)\n .filter(\n TranslationEvent.run_id == run_id,\n TranslationEvent.event_type.in_(TERMINAL_EVENT_TYPES),\n )\n .first()\n )\n if existing_terminal:\n raise ValueError(\n f\"Run '{run_id}' already has a terminal event \"\n f\"({existing_terminal[0]}). Cannot add another terminal event.\"\n )\n\n # Enforce run_started invariant — must exist before other run events\n if run_id is not None and event_type not in {\"RUN_STARTED\"} | TERMINAL_EVENT_TYPES:\n has_started = (\n self.db.query(TranslationEvent.id)\n .filter(\n TranslationEvent.run_id == run_id,\n TranslationEvent.event_type == \"RUN_STARTED\",\n )\n .first()\n )\n if not has_started:\n raise ValueError(\n f\"Cannot log '{event_type}' for run '{run_id}': \"\n f\"RUN_STARTED event must precede other run events.\"\n )\n\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job_id,\n run_id=run_id,\n event_type=event_type,\n event_data=payload or {},\n created_by=created_by,\n created_at=datetime.now(UTC),\n )\n self.db.add(event)\n self.db.flush()\n\n logger.reason(f\"Event logged: {event_type}\", {\n \"event_id\": event.id,\n \"job_id\": job_id,\n \"run_id\": run_id,\n \"event_type\": event_type,\n })\n return event\n # endregion log_event\n\n # region query_events [TYPE Function]\n # @PURPOSE: Query events with optional filters.\n # @PRE: None.\n # @POST: Returns list of TranslationEvent dicts matching filters.\n def query_events(\n self,\n job_id: str | None = None,\n run_id: str | None = None,\n event_type: str | None = None,\n limit: int = 100,\n offset: int = 0,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"TranslationEventLog.query_events\"):\n query = self.db.query(TranslationEvent)\n\n if job_id:\n query = query.filter(TranslationEvent.job_id == job_id)\n if run_id:\n query = query.filter(TranslationEvent.run_id == run_id)\n if event_type:\n query = query.filter(TranslationEvent.event_type == event_type)\n\n events = (\n query.order_by(TranslationEvent.created_at.desc())\n .offset(offset)\n .limit(limit)\n .all()\n )\n\n return [\n {\n \"id\": e.id,\n \"job_id\": e.job_id,\n \"run_id\": e.run_id,\n \"event_type\": e.event_type,\n \"event_data\": e.event_data or {},\n \"created_by\": e.created_by,\n \"created_at\": e.created_at.isoformat() if e.created_at else None,\n }\n for e in events\n ]\n # endregion query_events\n\n # region prune_expired [TYPE Function]\n # @PURPOSE: Delete events older than retention_days. Persists MetricSnapshot before pruning.\n # @PRE: None.\n # @POST: Expired events are deleted; MetricSnapshot is created before deletion.\n # @SIDE_EFFECT: Creates MetricSnapshot row; deletes TranslationEvent rows.\n def prune_expired(\n self,\n retention_days: int = DEFAULT_RETENTION_DAYS,\n batch_size: int = 1000,\n ) -> dict[str, Any]:\n with belief_scope(\"TranslationEventLog.prune_expired\"):\n cutoff = datetime.now(UTC) - timedelta(days=retention_days)\n logger.reason(\"Pruning expired events\", {\n \"cutoff\": cutoff.isoformat(),\n \"retention_days\": retention_days,\n })\n\n # Find events to prune\n expired_query = self.db.query(TranslationEvent).filter(\n TranslationEvent.created_at < cutoff\n )\n total_expired = expired_query.count()\n\n if total_expired == 0:\n logger.reflect(\"No expired events to prune\", {})\n return {\"pruned\": 0, \"snapshot_id\": None}\n\n # Create MetricSnapshot before pruning\n snapshot = MetricSnapshot(\n id=str(uuid.uuid4()),\n job_id=\"_prune_aggregate_\",\n key_hash=f\"prune_{cutoff.timestamp():.0f}\",\n covers_events_before=cutoff,\n total_records=total_expired,\n snapshot_date=datetime.now(UTC),\n )\n self.db.add(snapshot)\n self.db.flush()\n snapshot_id = snapshot.id\n\n # Delete in batches\n pruned = 0\n while True:\n batch_ids = (\n self.db.query(TranslationEvent.id)\n .filter(TranslationEvent.created_at < cutoff)\n .limit(batch_size)\n .all()\n )\n if not batch_ids:\n break\n ids = [row[0] for row in batch_ids]\n self.db.query(TranslationEvent).filter(\n TranslationEvent.id.in_(ids)\n ).delete(synchronize_session=False)\n self.db.flush()\n pruned += len(ids)\n logger.reason(\"Pruned batch\", {\"batch_size\": len(ids), \"total_pruned\": pruned})\n\n self.db.commit()\n\n logger.reflect(\"Pruning complete\", {\n \"pruned\": pruned,\n \"snapshot_id\": snapshot_id,\n })\n return {\"pruned\": pruned, \"snapshot_id\": snapshot_id}\n # endregion prune_expired\n\n # region get_run_event_summary [TYPE Function]\n # @PURPOSE: Get a summary of events for a run, including invariant check.\n # @PRE: run_id is not None.\n # @POST: Returns dict with event list and invariant validity.\n def get_run_event_summary(self, run_id: str) -> dict[str, Any]:\n with belief_scope(\"TranslationEventLog.get_run_event_summary\"):\n events = self.query_events(run_id=run_id)\n\n has_started = any(e[\"event_type\"] == \"RUN_STARTED\" for e in events)\n terminal_events = [e for e in events if e[\"event_type\"] in TERMINAL_EVENT_TYPES]\n\n return {\n \"run_id\": run_id,\n \"event_count\": len(events),\n \"has_run_started\": has_started,\n \"terminal_event_count\": len(terminal_events),\n \"terminal_events\": terminal_events,\n \"invariant_valid\": has_started and len(terminal_events) <= 1,\n \"events\": events,\n }\n # endregion get_run_event_summary\n\n\n# #endregion TranslationEventLog\n# #endregion TranslationEventLog\n" }, { "contract_id": "TranslationExecutor", "contract_type": "Module", "file_path": "backend/src/plugins/translate/executor.py", "start_line": 1, - "end_line": 781, - "tier": "TIER_3", - "complexity": 5, + "end_line": 848, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[TranslationRun, job] -> Output[TranslationRun with batches and records]", - "INVARIANT": "Batch processing with retry — independent batches allow partial recovery; MAX_RETRIES_PER_BATCH = 3.", + "COMPLEXITY": 4, "LAYER": "Domain", "POST": "TranslationBatch and TranslationRecord rows are created. Run status is updated.", "PRE": "Valid TranslationRun with job configuration. DB session is available.", @@ -67851,27 +52311,46 @@ "target_ref": "[TranslationPreview]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationExecutor [C:5] [TYPE Module] [SEMANTICS translate,executor,batch,llm]\n# @BRIEF Process translation in batches: fetch source rows, call LLM, persist TranslationBatch and TranslationRecord rows.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationBatch]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [DictionaryManager]\n# @RELATION DEPENDS_ON -> [TranslationPreview]\n# @PRE Valid TranslationRun with job configuration. DB session is available.\n# @POST TranslationBatch and TranslationRecord rows are created. Run status is updated.\n# @SIDE_EFFECT Calls LLM provider; creates DB rows; updates run statistics.\n# @DATA_CONTRACT Input[TranslationRun, job] -> Output[TranslationRun with batches and records]\n# @INVARIANT Batch processing with retry — independent batches allow partial recovery; MAX_RETRIES_PER_BATCH = 3.\n# @RATIONALE Batch processing with retry — independent batches allow partial recovery.\n# @REJECTED Single monolithic LLM call — would lose all progress on any failure.\n\nimport json\nimport time\nimport uuid\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, List, Optional, Set, Tuple, Callable\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger, belief_scope\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.config_manager import ConfigManager\nfrom ...models.translate import (\n TranslationJob,\n TranslationRun,\n TranslationBatch,\n TranslationRecord,\n TranslationPreviewSession,\n TranslationPreviewRecord,\n)\nfrom ...services.llm_provider import LLMProviderService\nfrom ...services.llm_prompt_templates import render_prompt\nfrom ...core.superset_client import SupersetClient\nfrom .dictionary import DictionaryManager\nfrom .preview import DEFAULT_EXECUTION_PROMPT_TEMPLATE\n\nlog = MarkerLogger(\"TranslationExecutor\")\n\n# #region MAX_RETRIES_PER_BATCH [C:1] [TYPE Constant] [SEMANTICS translate,executor,retry]\nMAX_RETRIES_PER_BATCH = 3\n# #endregion MAX_RETRIES_PER_BATCH\n\n\n# #region TranslationExecutor [C:5] [TYPE Class] [SEMANTICS translate,executor,batch]\n# @BRIEF Process translation batches: fetch source rows, filter dict, call LLM, persist results.\n# @PRE DB session and config manager available.\n# @POST Batches and records created with status tracking; run statistics updated.\n# @SIDE_EFFECT LLM API calls; DB writes.\n# @DATA_CONTRACT Input[TranslationRun] -> Output[TranslationRun with batches, records, stats]\n# @INVARIANT MAX_RETRIES_PER_BATCH limit enforced; partial batch success tracked via COMPLETED_WITH_ERRORS.\nclass TranslationExecutor:\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n current_user: Optional[str] = None,\n on_batch_progress: Optional[Callable[[str, int, int, int, int], None]] = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.on_batch_progress = on_batch_progress\n self._current_run_id: Optional[str] = None\n\n # #region execute_run [C:4] [TYPE Function] [SEMANTICS translate,run,execute]\n # @BRIEF Run full translation execution for a TranslationRun: fetch rows, batch, call LLM, persist.\n # @PRE run is in PENDING or RUNNING status with valid job config.\n # @POST Run is populated with batches and records; run status updated to COMPLETED/FAILED.\n # @SIDE_EFFECT LLM API calls; DB batch writes.\n def execute_run(\n self,\n run: TranslationRun,\n llm_progress_callback: Optional[Callable[[str, int, int, int], None]] = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationExecutor.execute_run\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found for run '{run.id}'\")\n\n log.reason(\"Starting translation execution\", payload={\n \"run_id\": run.id,\n \"job_id\": job.id,\n \"batch_size\": job.batch_size,\n \"full_translation\": full_translation,\n })\n\n # Mark run as RUNNING\n run.status = \"RUNNING\"\n run.started_at = datetime.now(timezone.utc)\n self.db.flush()\n\n # Fetch source rows\n if full_translation:\n # Full translation: fetch ALL rows from Superset dataset\n source_rows = self._fetch_all_rows_from_superset(job)\n else:\n # Preview-based: fetch rows from the accepted preview session\n source_rows = self._fetch_source_rows(job.id, run.id)\n if not source_rows:\n log.explore(\"No source rows to translate\", payload={\"run_id\": run.id}, error=\"Preview produced 0 source rows for translation\")\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n return run\n\n total_rows = len(source_rows)\n run.total_records = total_rows\n\n # Split into batches\n batch_size = job.batch_size or 50\n batches = [\n source_rows[i:i + batch_size]\n for i in range(0, total_rows, batch_size)\n ]\n\n log.reason(f\"Processing {len(batches)} batches\", payload={\n \"run_id\": run.id,\n \"total_rows\": total_rows,\n \"batch_size\": batch_size,\n })\n\n successful_records = 0\n failed_records = 0\n skipped_records = 0\n\n for batch_idx, batch_rows in enumerate(batches):\n batch_result = self._process_batch(\n job=job,\n run_id=run.id,\n batch_index=batch_idx,\n batch_rows=batch_rows,\n )\n successful_records += batch_result[\"successful\"]\n failed_records += batch_result[\"failed\"]\n skipped_records += batch_result[\"skipped\"]\n\n # Update run stats incrementally\n run.successful_records = successful_records\n run.failed_records = failed_records\n run.skipped_records = skipped_records\n self.db.flush()\n\n if self.on_batch_progress:\n self.on_batch_progress(\n run.id, batch_idx + 1, len(batches),\n successful_records, total_rows,\n )\n\n # Update final run status\n if failed_records == 0 and skipped_records == 0:\n run.status = \"COMPLETED\"\n elif successful_records == 0:\n run.status = \"FAILED\"\n else:\n run.status = \"COMPLETED\" # Partial success\n\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n log.reflect(\"Translation execution complete\", payload={\n \"run_id\": run.id,\n \"status\": run.status,\n \"total\": total_rows,\n \"successful\": successful_records,\n \"failed\": failed_records,\n \"skipped\": skipped_records,\n })\n\n return run\n # #endregion execute_run\n\n # #region _fetch_source_rows [C:4] [TYPE Function] [SEMANTICS translate,source,rows]\n # @BRIEF Fetch source rows from the accepted preview session for this job.\n # @PRE job_id exists.\n # @POST Returns list of dicts with source data (row_index, source_text, approved_translation, etc).\n # @SIDE_EFFECT Queries preview session and records from DB.\n def _fetch_source_rows(self, job_id: str, run_id: str) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationExecutor._fetch_source_rows\"):\n # Get the latest APPLIED preview session\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job_id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n log.explore(\"No accepted preview session found\", error=\"Preview session has no accepted rows\", payload={\"job_id\": job_id})\n return []\n\n # Fetch APPROVED or all records from the session\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(\n TranslationPreviewRecord.session_id == session.id,\n TranslationPreviewRecord.status.in_([\"APPROVED\", \"PENDING\"]),\n )\n .all()\n )\n\n source_rows = []\n for rec in records:\n source_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": rec.target_sql if rec.status == \"APPROVED\" else None,\n \"source_object_name\": rec.source_object_name or \"\",\n \"source_data\": rec.source_data or {},\n })\n\n log.reason(f\"Fetched {len(source_rows)} source rows from preview\", payload={\n \"run_id\": run_id,\n \"session_id\": session.id,\n })\n return source_rows\n # #endregion _fetch_source_rows\n\n # #region _fetch_all_rows_from_superset [C:4] [TYPE Function] [SEMANTICS translate,superset,rows]\n # @BRIEF Fetch ALL rows from the Superset dataset for full translation (paginated).\n # @PRE job has source_datasource_id and environment_id configured.\n # @POST Returns list of row dicts with row_index, source_text, source_data.\n # @SIDE_EFFECT Calls Superset chart data endpoint (paginated).\n def _fetch_all_rows_from_superset(self, job: TranslationJob) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationExecutor._fetch_all_rows_from_superset\"):\n env_id = job.environment_id or job.source_dialect or \"\"\n environments = self.config_manager.get_environments()\n env_config = next(\n (e for e in environments if e.id == env_id),\n None,\n )\n if not env_config:\n raise ValueError(f\"Superset environment '{env_id}' not found\")\n\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))\n\n # Build query context (same as preview, but with large row_limit)\n query_context = client.build_dataset_preview_query_context(\n dataset_id=int(job.source_datasource_id),\n dataset_record=dataset_detail,\n template_params={},\n effective_filters=[],\n )\n\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0][\"row_limit\"] = 100000 # Superset max default\n queries[0].pop(\"result_type\", None)\n queries[0].pop(\"columns\", None)\n queries[0][\"metrics\"] = []\n queries[0][\"row_offset\"] = 0\n query_context[\"result_type\"] = \"samples\"\n form_data = query_context.get(\"form_data\", {})\n form_data.pop(\"query_mode\", None)\n\n all_rows: List[Dict[str, Any]] = []\n batch_size_fetch = 10000 # Fetch 10K rows per pagination request\n\n while True:\n # Set pagination for this batch\n if queries:\n queries[0][\"row_limit\"] = min(batch_size_fetch, 100000)\n queries[0][\"row_offset\"] = len(all_rows)\n\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n log.explore(\"Chart data API failed during full fetch\", error=\"Chart data API error\",\n payload={\n \"offset\": len(all_rows),\n \"error\": str(e),\n })\n if not all_rows:\n raise ValueError(f\"Failed to fetch data from Superset: {e}\")\n break # Return what we have\n\n from .preview import TranslationPreview\n rows = TranslationPreview._extract_data_rows(response)\n if not rows:\n break # No more data\n\n all_rows.extend(rows)\n log.reason(f\"Fetched {len(rows)} rows (total: {len(all_rows)})\", payload={\n \"offset\": len(all_rows) - len(rows),\n })\n\n if len(rows) < batch_size_fetch:\n break # Last page\n\n # Convert to the format expected by _process_batch\n source_rows = []\n for idx, row in enumerate(all_rows):\n translation_value = str(row.get(job.translation_column, \"\") or \"\")\n context_values = {}\n if job.context_columns:\n for col in job.context_columns:\n context_values[col] = str(row.get(col, \"\") or \"\")\n # Also include target_key_cols values in context_data\n if job.target_key_cols:\n for col in job.target_key_cols:\n context_values[col] = str(row.get(col, \"\") or \"\")\n source_rows.append({\n \"row_index\": str(idx),\n \"source_text\": translation_value,\n \"source_data\": context_values,\n \"source_object_name\": f\"Row {idx + 1}\",\n \"approved_translation\": None,\n })\n\n log.reason(f\"Prepared {len(source_rows)} source rows for full translation\", payload={\n \"job_id\": job.id,\n })\n return source_rows\n # #endregion _fetch_all_rows_from_superset\n\n # #region _process_batch [C:4] [TYPE Function] [SEMANTICS translate,batch,process]\n # @BRIEF Process a single batch: filter dictionary, build prompt, call LLM, persist records.\n # @PRE job and batch_rows are valid.\n # @POST TranslationBatch and TranslationRecord rows are created.\n # @SIDE_EFFECT LLM API call; DB writes.\n def _process_batch(\n self,\n job: TranslationJob,\n run_id: str,\n batch_index: int,\n batch_rows: List[Dict[str, Any]],\n ) -> Dict[str, int]:\n with belief_scope(\"TranslationExecutor._process_batch\"):\n batch_start = time.monotonic()\n\n # Create batch record\n batch = TranslationBatch(\n id=str(uuid.uuid4()),\n run_id=run_id,\n batch_index=batch_index,\n status=\"RUNNING\",\n total_records=len(batch_rows),\n started_at=datetime.now(timezone.utc),\n )\n self.db.add(batch)\n self.db.flush()\n batch_id = batch.id\n\n result = {\"successful\": 0, \"failed\": 0, \"skipped\": 0, \"retries\": 0}\n\n # Extract source texts for dict filtering\n source_texts = [\n row.get(\"source_text\", \"\")\n for row in batch_rows\n if row.get(\"source_text\")\n ]\n\n # Filter dictionary entries\n dict_matches = DictionaryManager.filter_for_batch(\n self.db, source_texts, job.id\n )\n\n # For each row, determine if we need LLM translation or can use approved translation\n rows_for_llm = []\n pre_translated = []\n\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n pre_translated.append(row)\n else:\n rows_for_llm.append(row)\n\n # Handle pre-translated (approved) rows\n for row in pre_translated:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=row.get(\"approved_translation\"),\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n result[\"successful\"] += 1\n\n # Process rows needing LLM translation\n if rows_for_llm:\n llm_result = self._call_llm_for_batch(\n job=job,\n run_id=run_id,\n batch_rows=rows_for_llm,\n dict_matches=dict_matches,\n batch_id=batch_id,\n )\n result[\"successful\"] += llm_result[\"successful\"]\n result[\"failed\"] += llm_result[\"failed\"]\n result[\"skipped\"] += llm_result[\"skipped\"]\n result[\"retries\"] += llm_result.get(\"retries\", 0)\n\n # Update batch status\n batch.successful_records = result[\"successful\"]\n batch.failed_records = result[\"failed\"]\n batch.completed_at = datetime.now(timezone.utc)\n batch.status = \"COMPLETED\" if result[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n self.db.flush()\n\n batch_latency = int((time.monotonic() - batch_start) * 1000)\n log.reason(f\"Batch {batch_index} complete\", payload={\n \"batch_id\": batch_id,\n \"latency_ms\": batch_latency,\n **result,\n })\n\n return result\n # #endregion _process_batch\n\n # #region _call_llm_for_batch [C:4] [TYPE Function] [SEMANTICS translate,llm,batch]\n # @BRIEF Call LLM for a batch of rows requiring translation. Parse structured JSON response.\n # @PRE job has valid provider_id. batch_rows is non-empty.\n # @POST Returns dict with successful/failed/skipped counts. Creates TranslationRecord rows.\n # @SIDE_EFFECT HTTP call to LLM provider.\n def _call_llm_for_batch(\n self,\n job: TranslationJob,\n run_id: str,\n batch_rows: List[Dict[str, Any]],\n dict_matches: List[Dict[str, Any]],\n batch_id: str,\n ) -> Dict[str, int]:\n with belief_scope(\"TranslationExecutor._call_llm_for_batch\"):\n # Build dictionary section\n dictionary_section = \"\"\n if dict_matches:\n glossary_lines = []\n for m in dict_matches:\n glossary_lines.append(\n f\"- '{m['source_term']}' -> '{m['target_term']}'\"\n f\"{' (' + m['context_notes'] + ')' if m.get('context_notes') else ''}\"\n )\n dictionary_section = (\n \"Terminology dictionary (use these translations when applicable):\\n\"\n + \"\\n\".join(glossary_lines)\n + \"\\n\\n\"\n )\n\n # Build rows JSON for LLM\n rows_json = json.dumps([\n {\n \"row_id\": str(row.get(\"row_index\", idx)),\n \"text\": row.get(\"source_text\", \"\"),\n }\n for idx, row in enumerate(batch_rows)\n ], indent=2)\n\n # Build prompt\n prompt = render_prompt(\n DEFAULT_EXECUTION_PROMPT_TEMPLATE,\n {\n \"source_language\": job.source_dialect or \"SQL\",\n \"target_language\": job.target_language or job.target_dialect or \"en\",\n \"source_dialect\": job.source_dialect or \"\",\n \"target_dialect\": job.target_dialect or \"\",\n \"translation_column\": job.translation_column or \"\",\n \"dictionary_section\": dictionary_section,\n \"rows_json\": rows_json,\n \"row_count\": str(len(batch_rows)),\n },\n )\n\n # Call LLM with retry\n llm_response = None\n last_error = None\n retries = 0\n\n for attempt in range(1, MAX_RETRIES_PER_BATCH + 1):\n try:\n llm_response = self._call_llm(job, prompt)\n break\n except Exception as e:\n last_error = str(e)\n retries += 1\n log.explore(\"LLM call failed\", error=\"LLM call failed, retrying\",\n payload={\n \"batch_id\": batch_id,\n \"error\": last_error,\n \"attempt\": attempt,\n })\n if attempt < MAX_RETRIES_PER_BATCH:\n time.sleep(2 ** attempt) # Exponential backoff\n else:\n log.explore(\"LLM call exhausted retries\", error=\"LLM retries exhausted\",\n payload={\n \"batch_id\": batch_id,\n \"last_error\": last_error,\n })\n\n if llm_response is None:\n # All retries failed — mark all rows as failed\n for row in batch_rows:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=None,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"FAILED\",\n error_message=f\"LLM call failed after {retries} retries: {last_error}\",\n )\n self.db.add(record)\n return {\"successful\": 0, \"failed\": len(batch_rows), \"skipped\": 0, \"retries\": retries}\n\n # Parse LLM response\n try:\n translations = self._parse_llm_response(llm_response, len(batch_rows))\n except ValueError as e:\n # Parse failure — mark all rows as SKIPPED\n log.explore(\"LLM response parse failed\", error=\"Failed to parse LLM JSON response\",\n payload={\n \"batch_id\": batch_id,\n \"error\": str(e),\n \"response_preview\": llm_response[:500] if llm_response else \"\",\n })\n skipped = len(batch_rows)\n for row in batch_rows:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=None,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=f\"LLM parse failure: {e}\",\n )\n self.db.add(record)\n return {\n \"successful\": 0,\n \"failed\": 0,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n\n successful = 0\n failed = 0\n skipped = 0\n\n for row in batch_rows:\n row_id = str(row.get(\"row_index\", \"\"))\n translation = translations.get(row_id)\n\n if translation is None:\n # NULL translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=\"NULL translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n if translation.strip() == \"\":\n # Empty translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=\"Empty translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n successful += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=translation,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n\n return {\n \"successful\": successful,\n \"failed\": failed,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n # #endregion _call_llm_for_batch\n\n # #region _call_llm [C:4] [TYPE Function] [SEMANTICS translate,llm,call]\n # @BRIEF Call the configured LLM provider with the batch prompt, routing by provider type.\n # @PRE job has valid provider_id.\n # @POST Returns raw LLM response string.\n # @SIDE_EFFECT HTTP call to LLM provider.\n def _call_llm(self, job: TranslationJob, prompt: str) -> str:\n with belief_scope(\"TranslationExecutor._call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\n\n provider_svc = LLMProviderService(self.db)\n provider = provider_svc.get_provider(job.provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{job.provider_id}' not found\")\n\n api_key = provider_svc.get_decrypted_api_key(job.provider_id)\n if not api_key:\n raise ValueError(f\"Could not decrypt API key for provider '{job.provider_id}'\")\n\n model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n\n if provider_type in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\"):\n return self._call_openai_compatible(\n base_url=provider.base_url,\n api_key=api_key,\n model=model,\n prompt=prompt,\n provider_type=provider_type,\n )\n else:\n raise ValueError(f\"Unsupported provider type '{provider_type}'\")\n # #endregion _call_llm\n\n # #region _call_openai_compatible [C:4] [TYPE Function] [SEMANTICS translate,llm,openai]\n # @BRIEF Call OpenAI-compatible API for batch translation with retry and structured output support.\n # @PRE Valid API endpoint, key, model, and prompt.\n # @POST Returns response text.\n # @SIDE_EFFECT HTTP POST to LLM API.\n @staticmethod\n def _call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n ) -> str:\n with belief_scope(\"TranslationExecutor._call_openai_compatible\"):\n import requests as http_requests\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n payload = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": \"You are a database content translation assistant. Translate the provided text accurately, preserving data semantics.\"},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": 4096,\n }\n # Structured output (response_format) only for native OpenAI — upstream providers routed via\n # Kilo/OpenRouter may not support it (e.g. StepFun returns \"structured_outputs is not supported\")\n if provider_type in (\"openai\", \"openai_compatible\"):\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n log.reason(\n f\"LLM request model={payload.get('model')} \"\n f\"provider_type={provider_type} \"\n f\"response_format={'yes' if 'response_format' in payload else 'no'} \"\n f\"prompt_len={len(prompt)}\"\n )\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n if not response.ok:\n log.explore(\"LLM API error\", error=f\"LLM API returned status {response.status_code}\", payload={\n \"status_code\": response.status_code,\n \"model\": payload.get('model'),\n \"body\": response.text[:2000],\n })\n response.raise_for_status()\n data = response.json()\n\n choices = data.get(\"choices\", [])\n if not choices:\n raise ValueError(\"LLM returned no choices\")\n\n content = choices[0].get(\"message\", {}).get(\"content\", \"\")\n if not content:\n # Log full response for diagnostics\n finish_reason = choices[0].get(\"finish_reason\", \"unknown\")\n log.explore(\"LLM returned empty content\", error=\"Empty response from LLM\",\n payload={\n \"finish_reason\": finish_reason,\n \"model\": payload.get(\"model\"),\n \"prompt_len\": len(prompt),\n \"response_keys\": list(data.keys()),\n \"choices_count\": len(choices),\n })\n raise ValueError(\n f\"LLM returned empty content (finish_reason={finish_reason}, \"\n f\"model={payload.get('model')}, prompt_len={len(prompt)})\"\n )\n\n return content\n # #endregion _call_openai_compatible\n\n # #region _parse_llm_response [C:3] [TYPE Function] [SEMANTICS translate,llm,parse]\n # @BRIEF Parse LLM JSON response into dict of row_id -> translation text.\n # @RELATION DEPENDS_ON -> [json]\n @staticmethod\n def _parse_llm_response(response_text: str, expected_count: int) -> Dict[str, str]:\n with belief_scope(\"TranslationExecutor._parse_llm_response\"):\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n # Try to extract from markdown code block\n import re\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n except json.JSONDecodeError:\n raise ValueError(\"LLM response was not valid JSON\")\n else:\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: Dict[str, str] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n translation = item.get(\"translation\")\n if translation is None:\n # Skip NULL translations — they'll be handled by caller\n continue\n if row_id:\n translations[row_id] = str(translation)\n\n return translations\n # #endregion _parse_llm_response\n\n\n# #endregion TranslationExecutor\n# #endregion TranslationExecutor\n" + "body": "# #region TranslationExecutor [C:4] [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# @RATIONALE: Batch processing with retry — independent batches allow partial recovery.\n# @REJECTED: Single monolithic LLM call — would lose all progress on any failure.\n\nimport json\nimport time\nimport uuid\nfrom 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 (\n TranslationBatch,\n TranslationJob,\n TranslationPreviewRecord,\n TranslationPreviewSession,\n TranslationRecord,\n TranslationRun,\n)\nfrom ...services.llm_prompt_templates import render_prompt\nfrom ...services.llm_provider import LLMProviderService\nfrom .dictionary import DictionaryManager\nfrom .preview import DEFAULT_EXECUTION_PROMPT_TEMPLATE\n\n# #region MAX_RETRIES_PER_BATCH [TYPE Constant]\n# @BRIEF Maximum number of retries for a single batch before marking it failed.\nMAX_RETRIES_PER_BATCH = 3\n# #endregion MAX_RETRIES_PER_BATCH\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\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 ) -> 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 \"batch_size\": job.batch_size,\n })\n\n # Mark run as RUNNING\n run.status = \"RUNNING\"\n run.started_at = datetime.now(UTC)\n self.db.flush()\n\n # Fetch source rows from the accepted preview session\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\n if run.trigger_type == \"new_key_only\":\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 # Split into batches\n batch_size = job.batch_size or 50\n batches = [\n source_rows[i:i + batch_size]\n for i in range(0, total_rows, batch_size)\n ]\n\n logger.reason(f\"Processing {len(batches)} batches\", {\n \"run_id\": run.id,\n \"total_rows\": total_rows,\n \"batch_size\": batch_size,\n })\n\n successful_records = 0\n failed_records = 0\n skipped_records = 0\n\n for batch_idx, batch_rows in enumerate(batches):\n batch_result = self._process_batch(\n job=job,\n run_id=run.id,\n batch_index=batch_idx,\n batch_rows=batch_rows,\n )\n successful_records += batch_result[\"successful\"]\n failed_records += batch_result[\"failed\"]\n skipped_records += batch_result[\"skipped\"]\n\n # Update run stats incrementally\n run.successful_records = successful_records\n run.failed_records = failed_records\n run.skipped_records = skipped_records\n self.db.flush()\n\n if self.on_batch_progress:\n self.on_batch_progress(\n run.id, batch_idx + 1, len(batches),\n successful_records, total_rows,\n )\n\n # Update final run status\n if failed_records == 0 and skipped_records == 0:\n run.status = \"COMPLETED\"\n elif successful_records == 0:\n run.status = \"FAILED\"\n else:\n run.status = \"COMPLETED\" # Partial success\n\n run.completed_at = datetime.now(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 # Remove row_limit to get ALL rows; use result_type=\"samples\"\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0].pop(\"row_limit\", None)\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 _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[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\n dict_matches = DictionaryManager.filter_for_batch(\n self.db, source_texts, job.id\n )\n\n # For each row, determine if we need LLM translation or can use approved translation\n rows_for_llm = []\n pre_translated = []\n\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n pre_translated.append(row)\n else:\n rows_for_llm.append(row)\n\n # Handle pre-translated (approved) rows\n for row in pre_translated:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=row.get(\"approved_translation\"),\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n result[\"successful\"] += 1\n\n # Process rows needing LLM translation\n if rows_for_llm:\n llm_result = self._call_llm_for_batch(\n job=job,\n run_id=run_id,\n batch_rows=rows_for_llm,\n dict_matches=dict_matches,\n batch_id=batch_id,\n )\n result[\"successful\"] += llm_result[\"successful\"]\n result[\"failed\"] += llm_result[\"failed\"]\n result[\"skipped\"] += llm_result[\"skipped\"]\n result[\"retries\"] += llm_result.get(\"retries\", 0)\n\n # Update batch status\n batch.successful_records = result[\"successful\"]\n batch.failed_records = result[\"failed\"]\n batch.completed_at = datetime.now(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\n # endregion _process_batch\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 ) -> dict[str, int]:\n with belief_scope(\"TranslationExecutor._call_llm_for_batch\"):\n # Build dictionary section\n dictionary_section = \"\"\n if dict_matches:\n glossary_lines = []\n for m in dict_matches:\n glossary_lines.append(\n f\"- '{m['source_term']}' -> '{m['target_term']}'\"\n f\"{' (' + m['context_notes'] + ')' if m.get('context_notes') else ''}\"\n )\n dictionary_section = (\n \"Terminology dictionary (use these translations when applicable):\\n\"\n + \"\\n\".join(glossary_lines)\n + \"\\n\\n\"\n )\n\n # Build rows JSON for LLM\n rows_json = json.dumps([\n {\n \"row_id\": str(row.get(\"row_index\", idx)),\n \"text\": row.get(\"source_text\", \"\"),\n }\n for idx, row in enumerate(batch_rows)\n ], indent=2)\n\n # Build prompt\n prompt = render_prompt(\n DEFAULT_EXECUTION_PROMPT_TEMPLATE,\n {\n \"source_language\": job.source_dialect or \"SQL\",\n \"target_language\": job.target_language or job.target_dialect or \"en\",\n \"source_dialect\": job.source_dialect or \"\",\n \"target_dialect\": job.target_dialect or \"\",\n \"translation_column\": job.translation_column or \"\",\n \"dictionary_section\": dictionary_section,\n \"rows_json\": rows_json,\n \"row_count\": str(len(batch_rows)),\n },\n )\n\n # Call LLM with retry\n llm_response = None\n last_error = None\n retries = 0\n\n for attempt in range(1, MAX_RETRIES_PER_BATCH + 1):\n try:\n llm_response = self._call_llm(job, prompt)\n break\n except Exception as e:\n last_error = str(e)\n retries += 1\n 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 # Parse LLM response\n try:\n translations = self._parse_llm_response(llm_response, len(batch_rows))\n except ValueError as e:\n # Parse failure — mark all rows as SKIPPED\n 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 = translations.get(row_id)\n\n if translation is None:\n # NULL translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n status=\"SKIPPED\",\n error_message=\"NULL translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n if translation.strip() == \"\":\n # Empty translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n status=\"SKIPPED\",\n error_message=\"Empty translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n successful += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=translation,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n\n return {\n \"successful\": successful,\n \"failed\": failed,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n # endregion _call_llm_for_batch\n\n # region _call_llm [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) -> str:\n with belief_scope(\"TranslationExecutor._call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\n\n provider_svc = LLMProviderService(self.db)\n provider = provider_svc.get_provider(job.provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{job.provider_id}' not found\")\n\n api_key = provider_svc.get_decrypted_api_key(job.provider_id)\n if not api_key:\n raise ValueError(f\"Could not decrypt API key for provider '{job.provider_id}'\")\n\n model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n\n if provider_type in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\"):\n return self._call_openai_compatible(\n base_url=provider.base_url,\n api_key=api_key,\n model=model,\n prompt=prompt,\n provider_type=provider_type,\n )\n else:\n raise ValueError(f\"Unsupported provider type '{provider_type}'\")\n # endregion _call_llm\n\n # region _call_openai_compatible [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 ) -> str:\n with belief_scope(\"TranslationExecutor._call_openai_compatible\"):\n import requests as http_requests\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n payload = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": \"You are a database content translation assistant. Translate the provided text accurately, preserving data semantics.\"},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": 4096,\n }\n # Structured output (response_format) only for native OpenAI — upstream providers routed via\n # Kilo/OpenRouter may not support it (e.g. StepFun returns \"structured_outputs is not supported\")\n if provider_type in (\"openai\", \"openai_compatible\"):\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n logger.reason(\n f\"LLM request model={payload.get('model')} \"\n f\"provider_type={provider_type} \"\n f\"response_format={'yes' if 'response_format' in payload else 'no'} \"\n f\"prompt_len={len(prompt)}\"\n )\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n if not response.ok:\n 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 raise ValueError(\"LLM returned no choices\")\n\n content = choices[0].get(\"message\", {}).get(\"content\", \"\")\n if not content:\n raise ValueError(\"LLM returned empty content\")\n\n return content\n # endregion _call_openai_compatible\n\n # region _parse_llm_response [TYPE Function]\n # @PURPOSE: Parse LLM JSON response into dict of row_id -> translation.\n # @PRE: response_text is valid JSON with {\"rows\": [...]} structure.\n # @POST: Returns dict mapping row_id to translation text.\n @staticmethod\n def _parse_llm_response(response_text: str, expected_count: int) -> dict[str, str]:\n with belief_scope(\"TranslationExecutor._parse_llm_response\"):\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n # Try to extract from markdown code block\n import re\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n except json.JSONDecodeError:\n raise ValueError(\"LLM response was not valid JSON\")\n else:\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: dict[str, str] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n translation = item.get(\"translation\")\n if translation is None:\n # Skip NULL translations — they'll be handled by caller\n continue\n if row_id:\n translations[row_id] = str(translation)\n\n return translations\n # endregion _parse_llm_response\n\n\n# #endregion TranslationExecutor\n# #endregion TranslationExecutor\n" }, { "contract_id": "MAX_RETRIES_PER_BATCH", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 45, - "end_line": 47, + "start_line": 40, + "end_line": 43, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Maximum number of retries for a single batch before marking it failed." }, "relations": [], "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Constant'", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Constant'", "detail": { "actual_type": "Constant", "allowed_types": [ @@ -67880,6 +52359,7 @@ "Class", "Component", "Block", + "ADR", "Skill", "Agent" ] @@ -67887,8 +52367,8 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Constant' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Constant' at C1", "detail": { "actual_complexity": 1, "contract_type": "Constant" @@ -67896,278 +52376,98 @@ } ], "anchor_syntax": "region", - "body": "# #region MAX_RETRIES_PER_BATCH [C:1] [TYPE Constant] [SEMANTICS translate,executor,retry]\nMAX_RETRIES_PER_BATCH = 3\n# #endregion MAX_RETRIES_PER_BATCH\n" - }, - { - "contract_id": "_fetch_source_rows", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 180, - "end_line": 226, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns list of dicts with source data (row_index, source_text, approved_translation, etc).", - "PRE": "job_id exists.", - "PURPOSE": "Fetch source rows from the accepted preview session for this job.", - "SIDE_EFFECT": "Queries preview session and records from DB." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region _fetch_source_rows [C:4] [TYPE Function] [SEMANTICS translate,source,rows]\n # @BRIEF Fetch source rows from the accepted preview session for this job.\n # @PRE job_id exists.\n # @POST Returns list of dicts with source data (row_index, source_text, approved_translation, etc).\n # @SIDE_EFFECT Queries preview session and records from DB.\n def _fetch_source_rows(self, job_id: str, run_id: str) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationExecutor._fetch_source_rows\"):\n # Get the latest APPLIED preview session\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job_id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n log.explore(\"No accepted preview session found\", error=\"Preview session has no accepted rows\", payload={\"job_id\": job_id})\n return []\n\n # Fetch APPROVED or all records from the session\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(\n TranslationPreviewRecord.session_id == session.id,\n TranslationPreviewRecord.status.in_([\"APPROVED\", \"PENDING\"]),\n )\n .all()\n )\n\n source_rows = []\n for rec in records:\n source_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": rec.target_sql if rec.status == \"APPROVED\" else None,\n \"source_object_name\": rec.source_object_name or \"\",\n \"source_data\": rec.source_data or {},\n })\n\n log.reason(f\"Fetched {len(source_rows)} source rows from preview\", payload={\n \"run_id\": run_id,\n \"session_id\": session.id,\n })\n return source_rows\n # #endregion _fetch_source_rows\n" - }, - { - "contract_id": "_fetch_all_rows_from_superset", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 228, - "end_line": 329, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns list of row dicts with row_index, source_text, source_data.", - "PRE": "job has source_datasource_id and environment_id configured.", - "PURPOSE": "Fetch ALL rows from the Superset dataset for full translation (paginated).", - "SIDE_EFFECT": "Calls Superset chart data endpoint (paginated)." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region _fetch_all_rows_from_superset [C:4] [TYPE Function] [SEMANTICS translate,superset,rows]\n # @BRIEF Fetch ALL rows from the Superset dataset for full translation (paginated).\n # @PRE job has source_datasource_id and environment_id configured.\n # @POST Returns list of row dicts with row_index, source_text, source_data.\n # @SIDE_EFFECT Calls Superset chart data endpoint (paginated).\n def _fetch_all_rows_from_superset(self, job: TranslationJob) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationExecutor._fetch_all_rows_from_superset\"):\n env_id = job.environment_id or job.source_dialect or \"\"\n environments = self.config_manager.get_environments()\n env_config = next(\n (e for e in environments if e.id == env_id),\n None,\n )\n if not env_config:\n raise ValueError(f\"Superset environment '{env_id}' not found\")\n\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))\n\n # Build query context (same as preview, but with large row_limit)\n query_context = client.build_dataset_preview_query_context(\n dataset_id=int(job.source_datasource_id),\n dataset_record=dataset_detail,\n template_params={},\n effective_filters=[],\n )\n\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0][\"row_limit\"] = 100000 # Superset max default\n queries[0].pop(\"result_type\", None)\n queries[0].pop(\"columns\", None)\n queries[0][\"metrics\"] = []\n queries[0][\"row_offset\"] = 0\n query_context[\"result_type\"] = \"samples\"\n form_data = query_context.get(\"form_data\", {})\n form_data.pop(\"query_mode\", None)\n\n all_rows: List[Dict[str, Any]] = []\n batch_size_fetch = 10000 # Fetch 10K rows per pagination request\n\n while True:\n # Set pagination for this batch\n if queries:\n queries[0][\"row_limit\"] = min(batch_size_fetch, 100000)\n queries[0][\"row_offset\"] = len(all_rows)\n\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n log.explore(\"Chart data API failed during full fetch\", error=\"Chart data API error\",\n payload={\n \"offset\": len(all_rows),\n \"error\": str(e),\n })\n if not all_rows:\n raise ValueError(f\"Failed to fetch data from Superset: {e}\")\n break # Return what we have\n\n from .preview import TranslationPreview\n rows = TranslationPreview._extract_data_rows(response)\n if not rows:\n break # No more data\n\n all_rows.extend(rows)\n log.reason(f\"Fetched {len(rows)} rows (total: {len(all_rows)})\", payload={\n \"offset\": len(all_rows) - len(rows),\n })\n\n if len(rows) < batch_size_fetch:\n break # Last page\n\n # Convert to the format expected by _process_batch\n source_rows = []\n for idx, row in enumerate(all_rows):\n translation_value = str(row.get(job.translation_column, \"\") or \"\")\n context_values = {}\n if job.context_columns:\n for col in job.context_columns:\n context_values[col] = str(row.get(col, \"\") or \"\")\n # Also include target_key_cols values in context_data\n if job.target_key_cols:\n for col in job.target_key_cols:\n context_values[col] = str(row.get(col, \"\") or \"\")\n source_rows.append({\n \"row_index\": str(idx),\n \"source_text\": translation_value,\n \"source_data\": context_values,\n \"source_object_name\": f\"Row {idx + 1}\",\n \"approved_translation\": None,\n })\n\n log.reason(f\"Prepared {len(source_rows)} source rows for full translation\", payload={\n \"job_id\": job.id,\n })\n return source_rows\n # #endregion _fetch_all_rows_from_superset\n" - }, - { - "contract_id": "_process_batch", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 331, - "end_line": 429, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "TranslationBatch and TranslationRecord rows are created.", - "PRE": "job and batch_rows are valid.", - "PURPOSE": "Process a single batch: filter dictionary, build prompt, call LLM, persist records.", - "SIDE_EFFECT": "LLM API call; DB writes." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region _process_batch [C:4] [TYPE Function] [SEMANTICS translate,batch,process]\n # @BRIEF Process a single batch: filter dictionary, build prompt, call LLM, persist records.\n # @PRE job and batch_rows are valid.\n # @POST TranslationBatch and TranslationRecord rows are created.\n # @SIDE_EFFECT LLM API call; DB writes.\n def _process_batch(\n self,\n job: TranslationJob,\n run_id: str,\n batch_index: int,\n batch_rows: List[Dict[str, Any]],\n ) -> Dict[str, int]:\n with belief_scope(\"TranslationExecutor._process_batch\"):\n batch_start = time.monotonic()\n\n # Create batch record\n batch = TranslationBatch(\n id=str(uuid.uuid4()),\n run_id=run_id,\n batch_index=batch_index,\n status=\"RUNNING\",\n total_records=len(batch_rows),\n started_at=datetime.now(timezone.utc),\n )\n self.db.add(batch)\n self.db.flush()\n batch_id = batch.id\n\n result = {\"successful\": 0, \"failed\": 0, \"skipped\": 0, \"retries\": 0}\n\n # Extract source texts for dict filtering\n source_texts = [\n row.get(\"source_text\", \"\")\n for row in batch_rows\n if row.get(\"source_text\")\n ]\n\n # Filter dictionary entries\n dict_matches = DictionaryManager.filter_for_batch(\n self.db, source_texts, job.id\n )\n\n # For each row, determine if we need LLM translation or can use approved translation\n rows_for_llm = []\n pre_translated = []\n\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n pre_translated.append(row)\n else:\n rows_for_llm.append(row)\n\n # Handle pre-translated (approved) rows\n for row in pre_translated:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=row.get(\"approved_translation\"),\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n result[\"successful\"] += 1\n\n # Process rows needing LLM translation\n if rows_for_llm:\n llm_result = self._call_llm_for_batch(\n job=job,\n run_id=run_id,\n batch_rows=rows_for_llm,\n dict_matches=dict_matches,\n batch_id=batch_id,\n )\n result[\"successful\"] += llm_result[\"successful\"]\n result[\"failed\"] += llm_result[\"failed\"]\n result[\"skipped\"] += llm_result[\"skipped\"]\n result[\"retries\"] += llm_result.get(\"retries\", 0)\n\n # Update batch status\n batch.successful_records = result[\"successful\"]\n batch.failed_records = result[\"failed\"]\n batch.completed_at = datetime.now(timezone.utc)\n batch.status = \"COMPLETED\" if result[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n self.db.flush()\n\n batch_latency = int((time.monotonic() - batch_start) * 1000)\n log.reason(f\"Batch {batch_index} complete\", payload={\n \"batch_id\": batch_id,\n \"latency_ms\": batch_latency,\n **result,\n })\n\n return result\n # #endregion _process_batch\n" - }, - { - "contract_id": "_call_llm_for_batch", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 431, - "end_line": 631, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns dict with successful/failed/skipped counts. Creates TranslationRecord rows.", - "PRE": "job has valid provider_id. batch_rows is non-empty.", - "PURPOSE": "Call LLM for a batch of rows requiring translation. Parse structured JSON response.", - "SIDE_EFFECT": "HTTP call to LLM provider." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region _call_llm_for_batch [C:4] [TYPE Function] [SEMANTICS translate,llm,batch]\n # @BRIEF Call LLM for a batch of rows requiring translation. Parse structured JSON response.\n # @PRE job has valid provider_id. batch_rows is non-empty.\n # @POST Returns dict with successful/failed/skipped counts. Creates TranslationRecord rows.\n # @SIDE_EFFECT HTTP call to LLM provider.\n def _call_llm_for_batch(\n self,\n job: TranslationJob,\n run_id: str,\n batch_rows: List[Dict[str, Any]],\n dict_matches: List[Dict[str, Any]],\n batch_id: str,\n ) -> Dict[str, int]:\n with belief_scope(\"TranslationExecutor._call_llm_for_batch\"):\n # Build dictionary section\n dictionary_section = \"\"\n if dict_matches:\n glossary_lines = []\n for m in dict_matches:\n glossary_lines.append(\n f\"- '{m['source_term']}' -> '{m['target_term']}'\"\n f\"{' (' + m['context_notes'] + ')' if m.get('context_notes') else ''}\"\n )\n dictionary_section = (\n \"Terminology dictionary (use these translations when applicable):\\n\"\n + \"\\n\".join(glossary_lines)\n + \"\\n\\n\"\n )\n\n # Build rows JSON for LLM\n rows_json = json.dumps([\n {\n \"row_id\": str(row.get(\"row_index\", idx)),\n \"text\": row.get(\"source_text\", \"\"),\n }\n for idx, row in enumerate(batch_rows)\n ], indent=2)\n\n # Build prompt\n prompt = render_prompt(\n DEFAULT_EXECUTION_PROMPT_TEMPLATE,\n {\n \"source_language\": job.source_dialect or \"SQL\",\n \"target_language\": job.target_language or job.target_dialect or \"en\",\n \"source_dialect\": job.source_dialect or \"\",\n \"target_dialect\": job.target_dialect or \"\",\n \"translation_column\": job.translation_column or \"\",\n \"dictionary_section\": dictionary_section,\n \"rows_json\": rows_json,\n \"row_count\": str(len(batch_rows)),\n },\n )\n\n # Call LLM with retry\n llm_response = None\n last_error = None\n retries = 0\n\n for attempt in range(1, MAX_RETRIES_PER_BATCH + 1):\n try:\n llm_response = self._call_llm(job, prompt)\n break\n except Exception as e:\n last_error = str(e)\n retries += 1\n log.explore(\"LLM call failed\", error=\"LLM call failed, retrying\",\n payload={\n \"batch_id\": batch_id,\n \"error\": last_error,\n \"attempt\": attempt,\n })\n if attempt < MAX_RETRIES_PER_BATCH:\n time.sleep(2 ** attempt) # Exponential backoff\n else:\n log.explore(\"LLM call exhausted retries\", error=\"LLM retries exhausted\",\n payload={\n \"batch_id\": batch_id,\n \"last_error\": last_error,\n })\n\n if llm_response is None:\n # All retries failed — mark all rows as failed\n for row in batch_rows:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=None,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"FAILED\",\n error_message=f\"LLM call failed after {retries} retries: {last_error}\",\n )\n self.db.add(record)\n return {\"successful\": 0, \"failed\": len(batch_rows), \"skipped\": 0, \"retries\": retries}\n\n # Parse LLM response\n try:\n translations = self._parse_llm_response(llm_response, len(batch_rows))\n except ValueError as e:\n # Parse failure — mark all rows as SKIPPED\n log.explore(\"LLM response parse failed\", error=\"Failed to parse LLM JSON response\",\n payload={\n \"batch_id\": batch_id,\n \"error\": str(e),\n \"response_preview\": llm_response[:500] if llm_response else \"\",\n })\n skipped = len(batch_rows)\n for row in batch_rows:\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=None,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=f\"LLM parse failure: {e}\",\n )\n self.db.add(record)\n return {\n \"successful\": 0,\n \"failed\": 0,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n\n successful = 0\n failed = 0\n skipped = 0\n\n for row in batch_rows:\n row_id = str(row.get(\"row_index\", \"\"))\n translation = translations.get(row_id)\n\n if translation is None:\n # NULL translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=\"NULL translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n if translation.strip() == \"\":\n # Empty translation — skip\n skipped += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=\"\",\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SKIPPED\",\n error_message=\"Empty translation returned by LLM\",\n )\n self.db.add(record)\n continue\n\n successful += 1\n record = TranslationRecord(\n id=str(uuid.uuid4()),\n batch_id=batch_id,\n run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"),\n target_sql=translation,\n source_object_type=\"table_row\",\n source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\", {}),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n\n return {\n \"successful\": successful,\n \"failed\": failed,\n \"skipped\": skipped,\n \"retries\": retries,\n }\n # #endregion _call_llm_for_batch\n" - }, - { - "contract_id": "_call_llm", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 633, - "end_line": 665, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns raw LLM response string.", - "PRE": "job has valid provider_id.", - "PURPOSE": "Call the configured LLM provider with the batch prompt, routing by provider type.", - "SIDE_EFFECT": "HTTP call to LLM provider." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region _call_llm [C:4] [TYPE Function] [SEMANTICS translate,llm,call]\n # @BRIEF Call the configured LLM provider with the batch prompt, routing by provider type.\n # @PRE job has valid provider_id.\n # @POST Returns raw LLM response string.\n # @SIDE_EFFECT HTTP call to LLM provider.\n def _call_llm(self, job: TranslationJob, prompt: str) -> str:\n with belief_scope(\"TranslationExecutor._call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\n\n provider_svc = LLMProviderService(self.db)\n provider = provider_svc.get_provider(job.provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{job.provider_id}' not found\")\n\n api_key = provider_svc.get_decrypted_api_key(job.provider_id)\n if not api_key:\n raise ValueError(f\"Could not decrypt API key for provider '{job.provider_id}'\")\n\n model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n\n if provider_type in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\"):\n return self._call_openai_compatible(\n base_url=provider.base_url,\n api_key=api_key,\n model=model,\n prompt=prompt,\n provider_type=provider_type,\n )\n else:\n raise ValueError(f\"Unsupported provider type '{provider_type}'\")\n # #endregion _call_llm\n" - }, - { - "contract_id": "_call_openai_compatible", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 667, - "end_line": 740, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns response text.", - "PRE": "Valid API endpoint, key, model, and prompt.", - "PURPOSE": "Call OpenAI-compatible API for batch translation with retry and structured output support.", - "SIDE_EFFECT": "HTTP POST to LLM API." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region _call_openai_compatible [C:4] [TYPE Function] [SEMANTICS translate,llm,openai]\n # @BRIEF Call OpenAI-compatible API for batch translation with retry and structured output support.\n # @PRE Valid API endpoint, key, model, and prompt.\n # @POST Returns response text.\n # @SIDE_EFFECT HTTP POST to LLM API.\n @staticmethod\n def _call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n ) -> str:\n with belief_scope(\"TranslationExecutor._call_openai_compatible\"):\n import requests as http_requests\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n payload = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": \"You are a database content translation assistant. Translate the provided text accurately, preserving data semantics.\"},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": 4096,\n }\n # Structured output (response_format) only for native OpenAI — upstream providers routed via\n # Kilo/OpenRouter may not support it (e.g. StepFun returns \"structured_outputs is not supported\")\n if provider_type in (\"openai\", \"openai_compatible\"):\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n log.reason(\n f\"LLM request model={payload.get('model')} \"\n f\"provider_type={provider_type} \"\n f\"response_format={'yes' if 'response_format' in payload else 'no'} \"\n f\"prompt_len={len(prompt)}\"\n )\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n if not response.ok:\n log.explore(\"LLM API error\", error=f\"LLM API returned status {response.status_code}\", payload={\n \"status_code\": response.status_code,\n \"model\": payload.get('model'),\n \"body\": response.text[:2000],\n })\n response.raise_for_status()\n data = response.json()\n\n choices = data.get(\"choices\", [])\n if not choices:\n raise ValueError(\"LLM returned no choices\")\n\n content = choices[0].get(\"message\", {}).get(\"content\", \"\")\n if not content:\n # Log full response for diagnostics\n finish_reason = choices[0].get(\"finish_reason\", \"unknown\")\n log.explore(\"LLM returned empty content\", error=\"Empty response from LLM\",\n payload={\n \"finish_reason\": finish_reason,\n \"model\": payload.get(\"model\"),\n \"prompt_len\": len(prompt),\n \"response_keys\": list(data.keys()),\n \"choices_count\": len(choices),\n })\n raise ValueError(\n f\"LLM returned empty content (finish_reason={finish_reason}, \"\n f\"model={payload.get('model')}, prompt_len={len(prompt)})\"\n )\n\n return content\n # #endregion _call_openai_compatible\n" - }, - { - "contract_id": "_parse_llm_response", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/executor.py", - "start_line": 742, - "end_line": 777, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Parse LLM JSON response into dict of row_id -> translation text." - }, - "relations": [ - { - "source_id": "_parse_llm_response", - "relation_type": "DEPENDS_ON", - "target_id": "json", - "target_ref": "[json]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region _parse_llm_response [C:3] [TYPE Function] [SEMANTICS translate,llm,parse]\n # @BRIEF Parse LLM JSON response into dict of row_id -> translation text.\n # @RELATION DEPENDS_ON -> [json]\n @staticmethod\n def _parse_llm_response(response_text: str, expected_count: int) -> Dict[str, str]:\n with belief_scope(\"TranslationExecutor._parse_llm_response\"):\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n # Try to extract from markdown code block\n import re\n match = re.search(r'```(?:json)?\\s*\\n?(.*?)\\n?```', response_text, re.DOTALL)\n if match:\n try:\n data = json.loads(match.group(1))\n except json.JSONDecodeError:\n raise ValueError(\"LLM response was not valid JSON\")\n else:\n raise ValueError(\"LLM response was not valid JSON\")\n\n rows = data.get(\"rows\", [])\n if not isinstance(rows, list):\n raise ValueError(\"LLM response missing 'rows' array\")\n\n translations: Dict[str, str] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n translation = item.get(\"translation\")\n if translation is None:\n # Skip NULL translations — they'll be handled by caller\n continue\n if row_id:\n translations[row_id] = str(translation)\n\n return translations\n # #endregion _parse_llm_response\n" + "body": "# #region MAX_RETRIES_PER_BATCH [TYPE Constant]\n# @BRIEF Maximum number of retries for a single batch before marking it failed.\nMAX_RETRIES_PER_BATCH = 3\n# #endregion MAX_RETRIES_PER_BATCH\n" }, { "contract_id": "TranslationMetrics", "contract_type": "Module", "file_path": "backend/src/plugins/translate/metrics.py", "start_line": 1, - "end_line": 164, + "end_line": 169, "tier": "TIER_2", "complexity": 3, "metadata": { "COMPLEXITY": 3, "LAYER": "Domain", - "PURPOSE": "Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting." + "POST": "Metrics are aggregated and returned; no side effects.", + "PRE": "Database session is open.", + "PURPOSE": "Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting.", + "RATIONALE": "Live events (<90 days) + MetricSnapshot (>=90 days) fusion for complete picture.", + "REJECTED": "Querying all events for all time — would be slow; MetricSnapshot provides historical summary." }, "relations": [ { "source_id": "TranslationMetrics", "relation_type": "DEPENDS_ON", - "target_id": "TranslationEvent", - "target_ref": "[TranslationEvent]" + "target_id": "TranslationEvent:Class", + "target_ref": "[TranslationEvent:Class]" }, { "source_id": "TranslationMetrics", "relation_type": "DEPENDS_ON", - "target_id": "MetricSnapshot", - "target_ref": "[MetricSnapshot]" + "target_id": "MetricSnapshot:Class", + "target_ref": "[MetricSnapshot:Class]" }, { "source_id": "TranslationMetrics", "relation_type": "DEPENDS_ON", - "target_id": "TranslationRun", - "target_ref": "[TranslationRun]" + "target_id": "TranslationRun:Class", + "target_ref": "[TranslationRun:Class]" }, { "source_id": "TranslationMetrics", "relation_type": "DEPENDS_ON", - "target_id": "TranslationSchedule", - "target_ref": "[TranslationSchedule]" + "target_id": "TranslationSchedule:Class", + "target_ref": "[TranslationSchedule:Class]" + } + ], + "schema_warnings": [ + { + "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": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } } ], - "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslationMetrics [C:3] [TYPE Module] [SEMANTICS translate,metrics,aggregation]\n# @BRIEF Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationEvent]\n# @RELATION DEPENDS_ON -> [MetricSnapshot]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, List, Optional, Tuple\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import func\n\nfrom ...core.logger import logger, belief_scope\nfrom ...models.translate import (\n TranslationEvent,\n MetricSnapshot,\n TranslationRun,\n TranslationSchedule,\n)\n\n\n# #region TranslationMetrics [C:3] [TYPE Class] [SEMANTICS translate,metrics]\n# @BRIEF Aggregate translation metrics from live events and MetricSnapshot.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [MetricSnapshot]\n# @RELATION DEPENDS_ON -> [TranslationEvent]\nclass TranslationMetrics:\n\n def __init__(self, db: Session):\n self.db = db\n\n # #region get_job_metrics [C:2] [TYPE Function] [SEMANTICS translate,metrics,job]\n # @BRIEF Get aggregated metrics for a specific job including run counts, record stats, and duration.\n def get_job_metrics(self, job_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationMetrics.get_job_metrics\"):\n # Run counts from TranslationRun\n run_counts = (\n self.db.query(\n TranslationRun.status,\n func.count(TranslationRun.id),\n )\n .filter(TranslationRun.job_id == job_id)\n .group_by(TranslationRun.status)\n .all()\n )\n\n total_runs = 0\n status_counts: Dict[str, int] = {}\n for status_val, count in run_counts:\n status_counts[status_val] = count\n total_runs += count\n\n # Aggregate record stats from runs\n record_stats = (\n self.db.query(\n func.coalesce(func.sum(TranslationRun.total_records), 0),\n func.coalesce(func.sum(TranslationRun.successful_records), 0),\n func.coalesce(func.sum(TranslationRun.failed_records), 0),\n func.coalesce(func.sum(TranslationRun.skipped_records), 0),\n )\n .filter(TranslationRun.job_id == job_id)\n .first()\n )\n total_records = record_stats[0] if record_stats else 0\n successful_records = record_stats[1] if record_stats else 0\n failed_records = record_stats[2] if record_stats else 0\n skipped_records = record_stats[3] if record_stats else 0\n\n # Average duration from runs\n avg_duration = (\n self.db.query(\n func.avg(\n func.extract('epoch', TranslationRun.completed_at - TranslationRun.started_at) * 1000\n )\n )\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.started_at.isnot(None),\n TranslationRun.completed_at.isnot(None),\n )\n .scalar()\n )\n\n # Last run\n last_run = (\n self.db.query(TranslationRun)\n .filter(TranslationRun.job_id == job_id)\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n\n # Next scheduled run\n next_schedule = (\n self.db.query(TranslationSchedule)\n .filter(\n TranslationSchedule.job_id == job_id,\n TranslationSchedule.is_active == True,\n )\n .first()\n )\n\n # Cumulative tokens/cost from MetricSnapshot + events\n cumulative_tokens = 0\n cumulative_cost = 0.0\n\n # Latest MetricSnapshot\n latest_snapshot = (\n self.db.query(MetricSnapshot)\n .filter(MetricSnapshot.job_id == job_id)\n .order_by(MetricSnapshot.snapshot_date.desc())\n .first()\n )\n if latest_snapshot:\n # MetricSnapshot stores per-snapshot aggregated tokens/cost\n # Here we sum what we stored — in practice MetricSnapshot.covers_events_before\n # indicates cutoff\n pass\n\n # Live events (<90 days) for token/cost\n cutoff = datetime.now(timezone.utc)\n live_events = (\n self.db.query(TranslationEvent)\n .filter(\n TranslationEvent.job_id == job_id,\n TranslationEvent.event_type.in_([\"TRANSLATION_PHASE_COMPLETED\", \"RUN_COMPLETED\"]),\n TranslationEvent.created_at > cutoff, # events newer than snapshot\n )\n .all()\n )\n\n return {\n \"job_id\": job_id,\n \"total_runs\": total_runs,\n \"successful_runs\": status_counts.get(\"COMPLETED\", 0),\n \"failed_runs\": status_counts.get(\"FAILED\", 0),\n \"cancelled_runs\": status_counts.get(\"CANCELLED\", 0),\n \"total_records\": int(total_records),\n \"successful_records\": int(successful_records),\n \"failed_records\": int(failed_records),\n \"skipped_records\": int(skipped_records),\n \"cumulative_tokens\": cumulative_tokens,\n \"cumulative_cost\": cumulative_cost,\n \"avg_duration_ms\": int(avg_duration) if avg_duration else None,\n \"last_run_at\": last_run.created_at.isoformat() if last_run else None,\n \"next_scheduled_run\": next_schedule.last_run_at.isoformat() if next_schedule and next_schedule.last_run_at else None,\n }\n # #endregion get_job_metrics\n\n # #region get_all_metrics [C:2] [TYPE Function] [SEMANTICS translate,metrics,all]\n # @BRIEF Get aggregated metrics for all jobs.\n def get_all_metrics(self) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationMetrics.get_all_metrics\"):\n job_ids = (\n self.db.query(TranslationRun.job_id)\n .distinct()\n .all()\n )\n return [self.get_job_metrics(jid[0]) for jid in job_ids if jid[0]]\n # #endregion get_all_metrics\n\n\n# #endregion TranslationMetrics\n# #endregion TranslationMetrics\n" - }, - { - "contract_id": "get_all_metrics", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/metrics.py", - "start_line": 150, - "end_line": 160, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Get aggregated metrics for all jobs." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region get_all_metrics [C:2] [TYPE Function] [SEMANTICS translate,metrics,all]\n # @BRIEF Get aggregated metrics for all jobs.\n def get_all_metrics(self) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationMetrics.get_all_metrics\"):\n job_ids = (\n self.db.query(TranslationRun.job_id)\n .distinct()\n .all()\n )\n return [self.get_job_metrics(jid[0]) for jid in job_ids if jid[0]]\n # #endregion get_all_metrics\n" + "body": "# #region TranslationMetrics [C:3] [TYPE Module] [SEMANTICS sqlalchemy, translate, metrics, job, statistics]\n# @BRIEF Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TranslationEvent:Class]\n# @RELATION DEPENDS_ON -> [MetricSnapshot:Class]\n# @RELATION DEPENDS_ON -> [TranslationRun:Class]\n# @RELATION DEPENDS_ON -> [TranslationSchedule:Class]\n# @PRE: Database session is open.\n# @POST: Metrics are aggregated and returned; no side effects.\n# @RATIONALE: Live events (<90 days) + MetricSnapshot (>=90 days) fusion for complete picture.\n# @REJECTED: Querying all events for all time — would be slow; MetricSnapshot provides historical summary.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope\nfrom ...models.translate import (\n MetricSnapshot,\n TranslationEvent,\n TranslationRun,\n TranslationSchedule,\n)\n\n\n# #region TranslationMetrics [C:3] [TYPE Class]\n# @BRIEF Aggregate translation metrics from live events and MetricSnapshot.\nclass TranslationMetrics:\n\n def __init__(self, db: Session):\n self.db = db\n\n # region get_job_metrics [TYPE Function]\n # @PURPOSE: Get aggregated metrics for a specific job.\n # @PRE: job_id exists.\n # @POST: Returns dict with metrics from events + latest snapshot.\n def get_job_metrics(self, job_id: str) -> dict[str, Any]:\n with belief_scope(\"TranslationMetrics.get_job_metrics\"):\n # Run counts from TranslationRun\n run_counts = (\n self.db.query(\n TranslationRun.status,\n func.count(TranslationRun.id),\n )\n .filter(TranslationRun.job_id == job_id)\n .group_by(TranslationRun.status)\n .all()\n )\n\n total_runs = 0\n status_counts: dict[str, int] = {}\n for status_val, count in run_counts:\n status_counts[status_val] = count\n total_runs += count\n\n # Aggregate record stats from runs\n record_stats = (\n self.db.query(\n func.coalesce(func.sum(TranslationRun.total_records), 0),\n func.coalesce(func.sum(TranslationRun.successful_records), 0),\n func.coalesce(func.sum(TranslationRun.failed_records), 0),\n func.coalesce(func.sum(TranslationRun.skipped_records), 0),\n )\n .filter(TranslationRun.job_id == job_id)\n .first()\n )\n total_records = record_stats[0] if record_stats else 0\n successful_records = record_stats[1] if record_stats else 0\n failed_records = record_stats[2] if record_stats else 0\n skipped_records = record_stats[3] if record_stats else 0\n\n # Average duration from runs\n avg_duration = (\n self.db.query(\n func.avg(\n func.extract('epoch', TranslationRun.completed_at - TranslationRun.started_at) * 1000\n )\n )\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.started_at.isnot(None),\n TranslationRun.completed_at.isnot(None),\n )\n .scalar()\n )\n\n # Last run\n last_run = (\n self.db.query(TranslationRun)\n .filter(TranslationRun.job_id == job_id)\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n\n # Next scheduled run\n next_schedule = (\n self.db.query(TranslationSchedule)\n .filter(\n TranslationSchedule.job_id == job_id,\n TranslationSchedule.is_active == True,\n )\n .first()\n )\n\n # Cumulative tokens/cost from MetricSnapshot + events\n cumulative_tokens = 0\n cumulative_cost = 0.0\n\n # Latest MetricSnapshot\n latest_snapshot = (\n self.db.query(MetricSnapshot)\n .filter(MetricSnapshot.job_id == job_id)\n .order_by(MetricSnapshot.snapshot_date.desc())\n .first()\n )\n if latest_snapshot:\n # MetricSnapshot stores per-snapshot aggregated tokens/cost\n # Here we sum what we stored — in practice MetricSnapshot.covers_events_before\n # indicates cutoff\n pass\n\n # Live events (<90 days) for token/cost\n cutoff = datetime.now(UTC)\n live_events = (\n self.db.query(TranslationEvent)\n .filter(\n TranslationEvent.job_id == job_id,\n TranslationEvent.event_type.in_([\"TRANSLATION_PHASE_COMPLETED\", \"RUN_COMPLETED\"]),\n TranslationEvent.created_at > cutoff, # events newer than snapshot\n )\n .all()\n )\n\n return {\n \"job_id\": job_id,\n \"total_runs\": total_runs,\n \"successful_runs\": status_counts.get(\"COMPLETED\", 0),\n \"failed_runs\": status_counts.get(\"FAILED\", 0),\n \"cancelled_runs\": status_counts.get(\"CANCELLED\", 0),\n \"total_records\": int(total_records),\n \"successful_records\": int(successful_records),\n \"failed_records\": int(failed_records),\n \"skipped_records\": int(skipped_records),\n \"cumulative_tokens\": cumulative_tokens,\n \"cumulative_cost\": cumulative_cost,\n \"avg_duration_ms\": int(avg_duration) if avg_duration else None,\n \"last_run_at\": last_run.created_at.isoformat() if last_run else None,\n \"next_scheduled_run\": next_schedule.last_run_at.isoformat() if next_schedule and next_schedule.last_run_at else None,\n }\n # endregion get_job_metrics\n\n # region get_all_metrics [TYPE Function]\n # @PURPOSE: Get aggregated metrics for all jobs.\n # @POST: Returns list of per-job metrics.\n def get_all_metrics(self) -> list[dict[str, Any]]:\n with belief_scope(\"TranslationMetrics.get_all_metrics\"):\n job_ids = (\n self.db.query(TranslationRun.job_id)\n .distinct()\n .all()\n )\n return [self.get_job_metrics(jid[0]) for jid in job_ids if jid[0]]\n # endregion get_all_metrics\n\n\n# #endregion TranslationMetrics\n# #endregion TranslationMetrics\n" }, { "contract_id": "TranslationOrchestrator", "contract_type": "Module", "file_path": "backend/src/plugins/translate/orchestrator.py", "start_line": 1, - "end_line": 871, + "end_line": 903, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -68228,318 +52528,22 @@ ], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region TranslationOrchestrator [C:5] [TYPE Module] [SEMANTICS translate,orchestrator,lifecycle,run]\n# @BRIEF Run lifecycle coordination: validate preconditions, dispatch executor, generate SQL, submit to Superset, record events.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationExecutor]\n# @RELATION DEPENDS_ON -> [SQLGenerator]\n# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @PRE Valid job and accepted preview (for manual runs). Superset and LLM are reachable.\n# @POST Translation run is executed, SQL generated and submitted, events recorded.\n# @SIDE_EFFECT Creates TranslationRun; creates batches and records; generates SQL; submits to Superset; records events.\n# @DATA_CONTRACT Input[db, config_manager, current_user] -> Output[TranslationRun]\n# @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED. Exactly one terminal event per run.\n# @RATIONALE C5 orchestrator because preview, execution, event logging, and retry share state within single transaction boundary.\n# @REJECTED Distributed actor model (Celery) — eventual-consistency challenges at current scale.\n# @REJECTED stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.\n\nimport json\nimport time\nimport uuid\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, List, Optional, Callable, Tuple\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger, belief_scope\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.config_manager import ConfigManager\nfrom ...models.translate import (\n TranslationJob,\n TranslationRun,\n TranslationBatch,\n TranslationRecord,\n TranslationPreviewSession,\n)\nfrom ...schemas.translate import TranslationRunResponse\nfrom .executor import TranslationExecutor\nfrom .sql_generator import SQLGenerator\nfrom .superset_executor import SupersetSqlLabExecutor\nfrom .events import TranslationEventLog\nfrom ..translate.service import TranslateJobService\n\nlog = MarkerLogger(\"TranslationOrchestrator\")\n\n# #region TranslationOrchestrator [C:5] [TYPE Class] [SEMANTICS translate,orchestrator]\n# @BRIEF Coordinates full translation run lifecycle: validation, execution, SQL generation, Superset submission, event logging.\n# @PRE DB session and config manager are available.\n# @POST Runs are created, executed, and finalized with event records.\n# @SIDE_EFFECT Creates/modifies TranslationRun, TranslationBatch, TranslationRecord, TranslationEvent rows.\n# @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED.\n# @DATA_CONTRACT Input[db, config_manager, current_user] -> Output[TranslationRun]\nclass TranslationOrchestrator:\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n current_user: Optional[str] = None,\n ):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.event_log = TranslationEventLog(db)\n self._job: Optional[TranslationJob] = None\n\n # #region start_run [C:5] [TYPE Function] [SEMANTICS translate,run,create]\n # @BRIEF Start a new translation run for a job with config snapshot and hash computation.\n # @PRE job_id exists. For manual runs, there must be an accepted preview session.\n # @POST TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.\n # @SIDE_EFFECT DB writes; event logging.\n def start_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: Optional[str] = None,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.start_run\"):\n # Load and validate job\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n self._job = job\n\n log.reason(\"Starting translation run\", payload={\n \"job_id\": job_id,\n \"is_scheduled\": is_scheduled,\n })\n\n # Validate preconditions\n self._validate_preconditions(job, is_scheduled=is_scheduled)\n\n # Compute hashes\n config_hash = self._compute_config_hash(job)\n dict_snapshot_hash = self._compute_dict_snapshot_hash(job_id)\n\n # Build config snapshot\n config_snapshot = {\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"database_dialect\": job.database_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n \"target_schema\": job.target_schema,\n \"target_table\": job.target_table,\n \"source_key_cols\": job.source_key_cols,\n \"target_key_cols\": job.target_key_cols,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n \"dictionary_ids\": self._compute_dict_snapshot_hash(job_id),\n }\n\n # Compute key_hash from source_key_cols\n import hashlib\n key_hash_input = json.dumps({\n \"source_key_cols\": job.source_key_cols,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n }, sort_keys=True)\n key_hash = hashlib.sha256(key_hash_input.encode()).hexdigest()[:16]\n\n # Create run record with all hash/snapshot fields\n run = TranslationRun(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=\"PENDING\",\n trigger_type=trigger_type or (\"scheduled\" if is_scheduled else \"manual\"),\n config_snapshot=config_snapshot,\n key_hash=key_hash,\n config_hash=config_hash,\n dict_snapshot_hash=dict_snapshot_hash,\n created_by=self.current_user,\n created_at=datetime.now(timezone.utc),\n )\n self.db.add(run)\n self.db.flush()\n\n # Record event\n self.event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\n \"is_scheduled\": is_scheduled,\n \"trigger_type\": run.trigger_type,\n \"config_hash\": config_hash,\n \"dict_snapshot_hash\": dict_snapshot_hash,\n \"key_hash\": key_hash,\n },\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Run created\", payload={\n \"run_id\": run.id,\n \"job_id\": job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n })\n return run\n # #endregion start_run\n\n # #region execute_run [C:5] [TYPE Function] [SEMANTICS translate,run,execute]\n # @BRIEF Execute a translation run: dispatch executor, generate SQL, submit to Superset.\n # @PRE run is in PENDING status.\n # @POST Run is executed, SQL generated, Superset submission attempted.\n # @SIDE_EFFECT LLM calls, DB writes, Superset API calls.\n def execute_run(\n self,\n run: TranslationRun,\n on_batch_progress: Optional[Callable[[str, int, int, int, int], None]] = None,\n skip_insert: bool = False,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.execute_run\"):\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n if run.status != \"PENDING\":\n raise ValueError(\n f\"Cannot execute run in status '{run.status}'. \"\n f\"Run must be in PENDING status.\"\n )\n\n log.reason(\"Executing run\", payload={\n \"run_id\": run.id,\n \"job_id\": job.id,\n \"skip_insert\": skip_insert,\n })\n\n # Record translation phase start\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"TRANSLATION_PHASE_STARTED\",\n payload={},\n created_by=self.current_user,\n )\n\n # Dispatch executor\n executor = TranslationExecutor(\n self.db, self.config_manager, self.current_user,\n on_batch_progress=on_batch_progress,\n )\n try:\n run = executor.execute_run(run, llm_progress_callback=None, full_translation=full_translation)\n except Exception as e:\n log.explore(\"Translation execution failed\", error=str(e), payload={\n \"run_id\": run.id,\n })\n run.status = \"FAILED\"\n run.error_message = f\"Translation execution failed: {e}\"\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_FAILED\",\n payload={\"error\": str(e), \"phase\": \"translation\"},\n created_by=self.current_user,\n )\n self.db.commit()\n return run\n\n # Record translation phase complete\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"TRANSLATION_PHASE_COMPLETED\",\n payload={\n \"total\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n \"skipped\": run.skipped_records,\n },\n created_by=self.current_user,\n )\n\n # Skip insert phase if requested (e.g., for preview-only execution)\n if skip_insert:\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_COMPLETED\",\n payload={\"skip_insert\": True},\n created_by=self.current_user,\n )\n self.db.commit()\n return run\n\n # Generate SQL and submit to Superset\n insert_result = self._generate_and_insert_sql(job, run)\n run.insert_status = insert_result.get(\"status\")\n run.superset_execution_id = str(insert_result.get(\"query_id\") or \"\")\n run.superset_execution_log = insert_result\n run.status = \"COMPLETED\" if insert_result.get(\"status\") == \"success\" else \"COMPLETED\"\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n # Record terminal event\n terminal_event = \"RUN_COMPLETED\"\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=terminal_event,\n payload={\n \"insert_status\": insert_result.get(\"status\"),\n \"query_id\": insert_result.get(\"query_id\"),\n \"rows_affected\": insert_result.get(\"rows_affected\"),\n \"total_records\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n \"skipped\": run.skipped_records,\n },\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Run execution complete\", payload={\n \"run_id\": run.id,\n \"status\": run.status,\n \"insert_status\": run.insert_status,\n })\n return run\n # #endregion execute_run\n\n # #region _generate_and_insert_sql [C:5] [TYPE Function] [SEMANTICS translate,sql,insert]\n # @BRIEF Generate INSERT SQL from successful records and submit to Superset SQL Lab.\n # @PRE job has target table configured. run has successful records.\n # @POST SQL is generated and submitted. Returns execution result.\n # @SIDE_EFFECT Superset API call; event logging.\n def _generate_and_insert_sql(\n self,\n job: TranslationJob,\n run: TranslationRun,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator._generate_and_insert_sql\"):\n # Fetch successful records\n records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run.id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n\n if not records:\n log.reason(\"No successful records to insert\", payload={\"run_id\": run.id})\n return {\"status\": \"skipped\", \"reason\": \"no_records\", \"query_id\": None}\n\n log.reason(f\"Generating SQL for {len(records)} records\", payload={\n \"run_id\": run.id,\n \"dialect\": job.database_dialect or job.target_dialect,\n })\n\n # Build rows for SQL generation\n # Only include the translation column — context columns are for LLM only\n columns = []\n if job.translation_column:\n columns.append(job.translation_column)\n\n # Include key columns if present (for UPSERT matching)\n if job.target_key_cols:\n for k in job.target_key_cols:\n if k not in columns:\n columns.append(k)\n\n rows_for_sql = []\n for rec in records:\n row_data = {}\n if job.translation_column:\n row_data[job.translation_column] = rec.target_sql or \"\"\n if job.target_key_cols:\n source_data = rec.source_data or {}\n for k in job.target_key_cols:\n # Use source_data value if available, fall back to empty string\n row_data[k] = source_data.get(k, \"\")\n rows_for_sql.append(row_data)\n\n if not columns:\n # Use target_sql as the sole column\n columns = [job.translation_column or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n # Generate SQL\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=job.database_dialect or job.target_dialect or \"postgresql\",\n target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns,\n rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n except ValueError as e:\n log.explore(\"SQL generation failed\", error=str(e))\n return {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n # Log insert phase start\n log.reason(\"Insert SQL generated\", payload={\n \"run_id\": run.id,\n \"sql_preview\": sql[:500],\n \"sql_length\": len(sql),\n \"row_count\": row_count,\n \"dialect\": job.database_dialect or job.target_dialect or \"postgresql\",\n \"columns\": columns,\n \"has_key_cols\": bool(job.target_key_cols),\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"INSERT_PHASE_STARTED\",\n payload={\"sql_length\": len(sql), \"row_count\": row_count},\n created_by=self.current_user,\n )\n\n # Submit to Superset\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n # Resolve database_id: explicit target_database_id from job (int or UUID), or resolve from environment\n target_db_id = None\n if job.target_database_id:\n try:\n target_db_id = int(job.target_database_id)\n except (ValueError, TypeError):\n # Could be a UUID — pass as-is, executor will resolve it\n target_db_id = job.target_database_id\n log.reason(\"target_database_id is not an integer, will resolve as UUID\", payload={\n \"target_database_id\": job.target_database_id,\n })\n executor = SupersetSqlLabExecutor(self.config_manager, env_id, database_id=target_db_id)\n result = executor.execute_and_poll(\n sql=sql,\n max_polls=30,\n poll_interval_seconds=2.0,\n )\n except Exception as e:\n log.explore(\"Superset SQL submission failed\", error=str(e), payload={\n \"run_id\": run.id,\n })\n result = {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n # Log insert phase complete\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"INSERT_PHASE_COMPLETED\",\n payload=result,\n created_by=self.current_user,\n )\n\n return result\n # #endregion _generate_and_insert_sql\n\n # #region _validate_preconditions [C:5] [TYPE Function] [SEMANTICS translate,validation]\n # @BRIEF Validate preconditions before starting a translation run.\n # @PRE None.\n # @POST Raises ValueError if preconditions are not met.\n # @SIDE_EFFECT None.\n def _validate_preconditions(\n self,\n job: TranslationJob,\n is_scheduled: bool = False,\n ) -> None:\n with belief_scope(\"TranslationOrchestrator._validate_preconditions\"):\n # Job must be in valid status\n if job.status in (\"DRAFT\",):\n raise ValueError(\n f\"Cannot run job '{job.id}' in status '{job.status}'. \"\n f\"Job must be READY, ACTIVE, or COMPLETED.\"\n )\n\n # Must have target table configured\n if not job.target_table:\n raise ValueError(\n f\"Job '{job.id}' has no target table configured. \"\n \"Configure a target table before running.\"\n )\n\n # Must have LLM provider configured\n if not job.provider_id:\n raise ValueError(\n f\"Job '{job.id}' has no LLM provider configured. \"\n \"Select an LLM provider before running.\"\n )\n\n # Must have a translation column\n if not job.translation_column:\n raise ValueError(\n f\"Job '{job.id}' has no translation column configured. \"\n \"Select a translation column before running.\"\n )\n\n # For manual runs, must have accepted preview\n if not is_scheduled:\n accepted_session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job.id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not accepted_session:\n raise ValueError(\n f\"Job '{job.id}' has no accepted preview session. \"\n \"Run and accept a preview before executing a manual translation run.\"\n )\n\n log.reason(\"Preconditions validated\", payload={\n \"job_id\": job.id,\n \"is_scheduled\": is_scheduled,\n })\n # #endregion _validate_preconditions\n\n # #region retry_failed_batches [C:5] [TYPE Function] [SEMANTICS translate,retry,batch]\n # @BRIEF Retry failed batches in a run by re-processing failed records.\n # @PRE run exists and has failed batches.\n # @POST Failed batches are re-processed; run status and stats updated.\n # @SIDE_EFFECT LLM calls; DB writes; event logging.\n def retry_failed_batches(\n self,\n run_id: str,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.retry_failed_batches\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n # Find failed batches\n failed_batches = (\n self.db.query(TranslationBatch)\n .filter(\n TranslationBatch.run_id == run_id,\n TranslationBatch.status.in_([\"FAILED\", \"COMPLETED_WITH_ERRORS\"]),\n )\n .all()\n )\n\n if not failed_batches:\n raise ValueError(f\"No failed batches found for run '{run_id}'\")\n\n log.reason(\"Retrying failed batches\", payload={\n \"run_id\": run_id,\n \"batch_count\": len(failed_batches),\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_RETRYING\",\n payload={\"batch_count\": len(failed_batches)},\n created_by=self.current_user,\n )\n\n # Re-process each failed batch\n executor = TranslationExecutor(self.db, self.config_manager, self.current_user)\n run.status = \"RUNNING\"\n self.db.flush()\n\n for batch in failed_batches:\n # Fetch failed records for this batch\n failed_records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.batch_id == batch.id,\n TranslationRecord.status == \"FAILED\",\n )\n .all()\n )\n\n if not failed_records:\n continue\n\n # Build rows from failed records\n retry_rows = []\n for rec in failed_records:\n retry_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": None,\n \"source_object_name\": rec.source_object_name or \"\",\n })\n\n # Process retry batch\n result = executor._process_batch(\n job=job,\n run_id=run_id,\n batch_index=batch.batch_index,\n batch_rows=retry_rows,\n )\n\n # Update run stats\n run.successful_records = (run.successful_records or 0) + result[\"successful\"]\n run.failed_records = (run.failed_records or 0) + result[\"failed\"]\n run.skipped_records = (run.skipped_records or 0) + result[\"skipped\"]\n self.db.flush()\n\n # Update run status\n if run.failed_records == 0:\n run.status = \"COMPLETED\"\n elif run.successful_records == 0:\n run.status = \"FAILED\"\n else:\n run.status = \"COMPLETED\"\n\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_COMPLETED\",\n payload={\"retry\": True},\n created_by=self.current_user,\n )\n\n self.db.commit()\n log.reflect(\"Retry complete\", payload={\n \"run_id\": run_id,\n \"status\": run.status,\n })\n return run\n # #endregion retry_failed_batches\n\n # #region retry_insert [C:5] [TYPE Function] [SEMANTICS translate,retry,insert]\n # @BRIEF Retry the SQL insert phase for a completed run.\n # @PRE run exists and has successful records.\n # @POST SQL is regenerated and re-submitted to Superset.\n # @SIDE_EFFECT Superset API call; event logging.\n def retry_insert(\n self,\n run_id: str,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.retry_insert\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n log.reason(\"Retrying insert phase\", payload={\n \"run_id\": run_id,\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_RETRY_INSERT\",\n payload={},\n created_by=self.current_user,\n )\n\n # Regenerate SQL and submit\n insert_result = self._generate_and_insert_sql(job, run)\n\n # Update run\n run.insert_status = insert_result.get(\"status\")\n run.superset_execution_id = str(insert_result.get(\"query_id\") or \"\")\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n self.db.flush()\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Insert retry complete\", payload={\n \"run_id\": run_id,\n \"insert_status\": run.insert_status,\n })\n return run\n # #endregion retry_insert\n\n # #region cancel_run [C:5] [TYPE Function] [SEMANTICS translate,run,cancel]\n # @BRIEF Cancel a running translation run.\n # @PRE run is in PENDING or RUNNING status.\n # @POST Run status is set to CANCELLED; event recorded.\n # @SIDE_EFFECT DB write; records event.\n def cancel_run(self, run_id: str) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.cancel_run\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n if run.status not in (\"PENDING\", \"RUNNING\"):\n raise ValueError(\n f\"Cannot cancel run in status '{run.status}'. \"\n f\"Only PENDING or RUNNING runs can be cancelled.\"\n )\n\n run.status = \"CANCELLED\"\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n # Record terminal event directly — we are the source of the terminal event\n self.event_log._create_event_raw(\n job_id=run.job_id,\n run_id=run.id,\n event_type=\"RUN_CANCELLED\",\n payload={},\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Run cancelled\", payload={\n \"run_id\": run_id,\n })\n return run\n # #endregion cancel_run\n\n # #region get_run_status [C:5] [TYPE Function] [SEMANTICS translate,run,status]\n # @BRIEF Get run status with statistics and event invariant checks.\n # @PRE run_id exists.\n # @POST Returns dict with run details.\n def get_run_status(self, run_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator.get_run_status\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n # Count batches\n batch_count = (\n self.db.query(TranslationBatch)\n .filter(TranslationBatch.run_id == run_id)\n .count()\n )\n\n # Get event invariants (lightweight — only fetches event_type column)\n invariants = self.event_log.get_run_event_invariants_lightweight(run_id)\n\n return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"batch_count\": batch_count,\n \"event_invariants\": invariants,\n \"created_by\": run.created_by,\n \"created_at\": run.created_at.isoformat() if run.created_at else None,\n }\n # #endregion get_run_status\n\n # #region get_run_records [C:5] [TYPE Function] [SEMANTICS translate,run,records]\n # @BRIEF Get paginated records for a run with optional status filter.\n # @PRE run_id exists.\n # @POST Returns dict with records and pagination info.\n def get_run_records(\n self,\n run_id: str,\n page: int = 1,\n page_size: int = 50,\n status_filter: Optional[str] = None,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator.get_run_records\"):\n query = self.db.query(TranslationRecord).filter(\n TranslationRecord.run_id == run_id\n )\n\n if status_filter:\n query = query.filter(TranslationRecord.status == status_filter)\n\n total = query.count()\n offset = (page - 1) * page_size\n records = (\n query.order_by(TranslationRecord.created_at.desc())\n .offset(offset)\n .limit(page_size)\n .all()\n )\n\n return {\n \"items\": [\n {\n \"id\": r.id,\n \"batch_id\": r.batch_id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"error_message\": r.error_message,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n }\n for r in records\n ],\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"status_filter\": status_filter,\n }\n # #endregion get_run_records\n\n # #region get_run_history [C:5] [TYPE Function] [SEMANTICS translate,run,history]\n # @BRIEF Get paginated run history for a job.\n # @PRE job_id exists.\n # @POST Returns tuple of (total_count, list_of_runs).\n def get_run_history(\n self,\n job_id: str,\n page: int = 1,\n page_size: int = 20,\n ) -> Tuple[int, List[Dict[str, Any]]]:\n with belief_scope(\"TranslationOrchestrator.get_run_history\"):\n query = self.db.query(TranslationRun).filter(\n TranslationRun.job_id == job_id\n )\n total = query.count()\n runs = (\n query.order_by(TranslationRun.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n\n return total, [\n {\n \"id\": r.id,\n \"job_id\": r.job_id,\n \"status\": r.status,\n \"started_at\": r.started_at.isoformat() if r.started_at else None,\n \"completed_at\": r.completed_at.isoformat() if r.completed_at else None,\n \"error_message\": r.error_message,\n \"total_records\": r.total_records or 0,\n \"successful_records\": r.successful_records or 0,\n \"failed_records\": r.failed_records or 0,\n \"skipped_records\": r.skipped_records or 0,\n \"insert_status\": r.insert_status,\n \"superset_execution_id\": r.superset_execution_id,\n \"created_by\": r.created_by,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n }\n for r in runs\n ]\n # #endregion get_run_history\n\n # #region _compute_config_hash [C:2] [TYPE Function] [SEMANTICS translate,hash]\n # @BRIEF Compute a SHA-256 hash of the job's current configuration for snapshot comparison.\n @staticmethod\n def _compute_config_hash(job: TranslationJob) -> str:\n import hashlib\n config_str = json.dumps({\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n # #endregion _compute_config_hash\n\n # #region _compute_dict_snapshot_hash [C:2] [TYPE Function] [SEMANTICS translate,hash]\n # @BRIEF Compute a SHA-256 hash of dictionary state for snapshot comparison.\n def _compute_dict_snapshot_hash(self, job_id: str) -> str:\n import hashlib\n from ...models.translate import TranslationJobDictionary\n dict_links = (\n self.db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n dict_ids = sorted([dl.dictionary_id for dl in dict_links])\n hash_input = \",\".join(dict_ids)\n return hashlib.sha256(hash_input.encode()).hexdigest()[:16]\n # #endregion _compute_dict_snapshot_hash\n\n\n# #endregion TranslationOrchestrator\n# #endregion TranslationOrchestrator\n" - }, - { - "contract_id": "start_run", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/orchestrator.py", - "start_line": 68, - "end_line": 168, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": 5, - "POST": "TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.", - "PRE": "job_id exists. For manual runs, there must be an accepted preview session.", - "PURPOSE": "Start a new translation run for a job with config snapshot and hash computation.", - "SIDE_EFFECT": "DB writes; event logging." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "body": " # #region start_run [C:5] [TYPE Function] [SEMANTICS translate,run,create]\n # @BRIEF Start a new translation run for a job with config snapshot and hash computation.\n # @PRE job_id exists. For manual runs, there must be an accepted preview session.\n # @POST TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.\n # @SIDE_EFFECT DB writes; event logging.\n def start_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: Optional[str] = None,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.start_run\"):\n # Load and validate job\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n self._job = job\n\n log.reason(\"Starting translation run\", payload={\n \"job_id\": job_id,\n \"is_scheduled\": is_scheduled,\n })\n\n # Validate preconditions\n self._validate_preconditions(job, is_scheduled=is_scheduled)\n\n # Compute hashes\n config_hash = self._compute_config_hash(job)\n dict_snapshot_hash = self._compute_dict_snapshot_hash(job_id)\n\n # Build config snapshot\n config_snapshot = {\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"database_dialect\": job.database_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n \"target_schema\": job.target_schema,\n \"target_table\": job.target_table,\n \"source_key_cols\": job.source_key_cols,\n \"target_key_cols\": job.target_key_cols,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n \"dictionary_ids\": self._compute_dict_snapshot_hash(job_id),\n }\n\n # Compute key_hash from source_key_cols\n import hashlib\n key_hash_input = json.dumps({\n \"source_key_cols\": job.source_key_cols,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n }, sort_keys=True)\n key_hash = hashlib.sha256(key_hash_input.encode()).hexdigest()[:16]\n\n # Create run record with all hash/snapshot fields\n run = TranslationRun(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=\"PENDING\",\n trigger_type=trigger_type or (\"scheduled\" if is_scheduled else \"manual\"),\n config_snapshot=config_snapshot,\n key_hash=key_hash,\n config_hash=config_hash,\n dict_snapshot_hash=dict_snapshot_hash,\n created_by=self.current_user,\n created_at=datetime.now(timezone.utc),\n )\n self.db.add(run)\n self.db.flush()\n\n # Record event\n self.event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\n \"is_scheduled\": is_scheduled,\n \"trigger_type\": run.trigger_type,\n \"config_hash\": config_hash,\n \"dict_snapshot_hash\": dict_snapshot_hash,\n \"key_hash\": key_hash,\n },\n created_by=self.current_user,\n )\n\n self.db.commit()\n self.db.refresh(run)\n\n log.reflect(\"Run created\", payload={\n \"run_id\": run.id,\n \"job_id\": job_id,\n \"status\": run.status,\n \"trigger_type\": run.trigger_type,\n })\n return run\n # #endregion start_run\n" - }, - { - "contract_id": "_generate_and_insert_sql", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/orchestrator.py", - "start_line": 303, - "end_line": 433, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": 5, - "POST": "SQL is generated and submitted. Returns execution result.", - "PRE": "job has target table configured. run has successful records.", - "PURPOSE": "Generate INSERT SQL from successful records and submit to Superset SQL Lab.", - "SIDE_EFFECT": "Superset API call; event logging." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "body": " # #region _generate_and_insert_sql [C:5] [TYPE Function] [SEMANTICS translate,sql,insert]\n # @BRIEF Generate INSERT SQL from successful records and submit to Superset SQL Lab.\n # @PRE job has target table configured. run has successful records.\n # @POST SQL is generated and submitted. Returns execution result.\n # @SIDE_EFFECT Superset API call; event logging.\n def _generate_and_insert_sql(\n self,\n job: TranslationJob,\n run: TranslationRun,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationOrchestrator._generate_and_insert_sql\"):\n # Fetch successful records\n records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run.id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n\n if not records:\n log.reason(\"No successful records to insert\", payload={\"run_id\": run.id})\n return {\"status\": \"skipped\", \"reason\": \"no_records\", \"query_id\": None}\n\n log.reason(f\"Generating SQL for {len(records)} records\", payload={\n \"run_id\": run.id,\n \"dialect\": job.database_dialect or job.target_dialect,\n })\n\n # Build rows for SQL generation\n # Only include the translation column — context columns are for LLM only\n columns = []\n if job.translation_column:\n columns.append(job.translation_column)\n\n # Include key columns if present (for UPSERT matching)\n if job.target_key_cols:\n for k in job.target_key_cols:\n if k not in columns:\n columns.append(k)\n\n rows_for_sql = []\n for rec in records:\n row_data = {}\n if job.translation_column:\n row_data[job.translation_column] = rec.target_sql or \"\"\n if job.target_key_cols:\n source_data = rec.source_data or {}\n for k in job.target_key_cols:\n # Use source_data value if available, fall back to empty string\n row_data[k] = source_data.get(k, \"\")\n rows_for_sql.append(row_data)\n\n if not columns:\n # Use target_sql as the sole column\n columns = [job.translation_column or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n # Generate SQL\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=job.database_dialect or job.target_dialect or \"postgresql\",\n target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns,\n rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n except ValueError as e:\n log.explore(\"SQL generation failed\", error=str(e))\n return {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n # Log insert phase start\n log.reason(\"Insert SQL generated\", payload={\n \"run_id\": run.id,\n \"sql_preview\": sql[:500],\n \"sql_length\": len(sql),\n \"row_count\": row_count,\n \"dialect\": job.database_dialect or job.target_dialect or \"postgresql\",\n \"columns\": columns,\n \"has_key_cols\": bool(job.target_key_cols),\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"INSERT_PHASE_STARTED\",\n payload={\"sql_length\": len(sql), \"row_count\": row_count},\n created_by=self.current_user,\n )\n\n # Submit to Superset\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n # Resolve database_id: explicit target_database_id from job (int or UUID), or resolve from environment\n target_db_id = None\n if job.target_database_id:\n try:\n target_db_id = int(job.target_database_id)\n except (ValueError, TypeError):\n # Could be a UUID — pass as-is, executor will resolve it\n target_db_id = job.target_database_id\n log.reason(\"target_database_id is not an integer, will resolve as UUID\", payload={\n \"target_database_id\": job.target_database_id,\n })\n executor = SupersetSqlLabExecutor(self.config_manager, env_id, database_id=target_db_id)\n result = executor.execute_and_poll(\n sql=sql,\n max_polls=30,\n poll_interval_seconds=2.0,\n )\n except Exception as e:\n log.explore(\"Superset SQL submission failed\", error=str(e), payload={\n \"run_id\": run.id,\n })\n result = {\"status\": \"failed\", \"error_message\": str(e), \"query_id\": None}\n\n # Log insert phase complete\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"INSERT_PHASE_COMPLETED\",\n payload=result,\n created_by=self.current_user,\n )\n\n return result\n # #endregion _generate_and_insert_sql\n" - }, - { - "contract_id": "_validate_preconditions", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/orchestrator.py", - "start_line": 435, - "end_line": 495, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": 5, - "POST": "Raises ValueError if preconditions are not met.", - "PRE": "None.", - "PURPOSE": "Validate preconditions before starting a translation run.", - "SIDE_EFFECT": "None." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "body": " # #region _validate_preconditions [C:5] [TYPE Function] [SEMANTICS translate,validation]\n # @BRIEF Validate preconditions before starting a translation run.\n # @PRE None.\n # @POST Raises ValueError if preconditions are not met.\n # @SIDE_EFFECT None.\n def _validate_preconditions(\n self,\n job: TranslationJob,\n is_scheduled: bool = False,\n ) -> None:\n with belief_scope(\"TranslationOrchestrator._validate_preconditions\"):\n # Job must be in valid status\n if job.status in (\"DRAFT\",):\n raise ValueError(\n f\"Cannot run job '{job.id}' in status '{job.status}'. \"\n f\"Job must be READY, ACTIVE, or COMPLETED.\"\n )\n\n # Must have target table configured\n if not job.target_table:\n raise ValueError(\n f\"Job '{job.id}' has no target table configured. \"\n \"Configure a target table before running.\"\n )\n\n # Must have LLM provider configured\n if not job.provider_id:\n raise ValueError(\n f\"Job '{job.id}' has no LLM provider configured. \"\n \"Select an LLM provider before running.\"\n )\n\n # Must have a translation column\n if not job.translation_column:\n raise ValueError(\n f\"Job '{job.id}' has no translation column configured. \"\n \"Select a translation column before running.\"\n )\n\n # For manual runs, must have accepted preview\n if not is_scheduled:\n accepted_session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job.id,\n TranslationPreviewSession.status == \"APPLIED\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not accepted_session:\n raise ValueError(\n f\"Job '{job.id}' has no accepted preview session. \"\n \"Run and accept a preview before executing a manual translation run.\"\n )\n\n log.reason(\"Preconditions validated\", payload={\n \"job_id\": job.id,\n \"is_scheduled\": is_scheduled,\n })\n # #endregion _validate_preconditions\n" - }, - { - "contract_id": "retry_failed_batches", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/orchestrator.py", - "start_line": 497, - "end_line": 610, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": 5, - "POST": "Failed batches are re-processed; run status and stats updated.", - "PRE": "run exists and has failed batches.", - "PURPOSE": "Retry failed batches in a run by re-processing failed records.", - "SIDE_EFFECT": "LLM calls; DB writes; event logging." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - }, - { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "region", - "body": " # #region retry_failed_batches [C:5] [TYPE Function] [SEMANTICS translate,retry,batch]\n # @BRIEF Retry failed batches in a run by re-processing failed records.\n # @PRE run exists and has failed batches.\n # @POST Failed batches are re-processed; run status and stats updated.\n # @SIDE_EFFECT LLM calls; DB writes; event logging.\n def retry_failed_batches(\n self,\n run_id: str,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.retry_failed_batches\"):\n run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n job = self.db.query(TranslationJob).filter(TranslationJob.id == run.job_id).first()\n if not job:\n raise ValueError(f\"Job '{run.job_id}' not found\")\n self._job = job\n\n # Find failed batches\n failed_batches = (\n self.db.query(TranslationBatch)\n .filter(\n TranslationBatch.run_id == run_id,\n TranslationBatch.status.in_([\"FAILED\", \"COMPLETED_WITH_ERRORS\"]),\n )\n .all()\n )\n\n if not failed_batches:\n raise ValueError(f\"No failed batches found for run '{run_id}'\")\n\n log.reason(\"Retrying failed batches\", payload={\n \"run_id\": run_id,\n \"batch_count\": len(failed_batches),\n })\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_RETRYING\",\n payload={\"batch_count\": len(failed_batches)},\n created_by=self.current_user,\n )\n\n # Re-process each failed batch\n executor = TranslationExecutor(self.db, self.config_manager, self.current_user)\n run.status = \"RUNNING\"\n self.db.flush()\n\n for batch in failed_batches:\n # Fetch failed records for this batch\n failed_records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.batch_id == batch.id,\n TranslationRecord.status == \"FAILED\",\n )\n .all()\n )\n\n if not failed_records:\n continue\n\n # Build rows from failed records\n retry_rows = []\n for rec in failed_records:\n retry_rows.append({\n \"row_index\": rec.source_object_id or \"0\",\n \"source_text\": rec.source_sql or \"\",\n \"approved_translation\": None,\n \"source_object_name\": rec.source_object_name or \"\",\n })\n\n # Process retry batch\n result = executor._process_batch(\n job=job,\n run_id=run_id,\n batch_index=batch.batch_index,\n batch_rows=retry_rows,\n )\n\n # Update run stats\n run.successful_records = (run.successful_records or 0) + result[\"successful\"]\n run.failed_records = (run.failed_records or 0) + result[\"failed\"]\n run.skipped_records = (run.skipped_records or 0) + result[\"skipped\"]\n self.db.flush()\n\n # Update run status\n if run.failed_records == 0:\n run.status = \"COMPLETED\"\n elif run.successful_records == 0:\n run.status = \"FAILED\"\n else:\n run.status = \"COMPLETED\"\n\n run.completed_at = datetime.now(timezone.utc)\n self.db.flush()\n\n self.event_log.log_event(\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_COMPLETED\",\n payload={\"retry\": True},\n created_by=self.current_user,\n )\n\n self.db.commit()\n log.reflect(\"Retry complete\", payload={\n \"run_id\": run_id,\n \"status\": run.status,\n })\n return run\n # #endregion retry_failed_batches\n" - }, - { - "contract_id": "_compute_config_hash", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/orchestrator.py", - "start_line": 835, - "end_line": 852, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Compute a SHA-256 hash of the job's current configuration for snapshot comparison." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region _compute_config_hash [C:2] [TYPE Function] [SEMANTICS translate,hash]\n # @BRIEF Compute a SHA-256 hash of the job's current configuration for snapshot comparison.\n @staticmethod\n def _compute_config_hash(job: TranslationJob) -> str:\n import hashlib\n config_str = json.dumps({\n \"source_dialect\": job.source_dialect,\n \"target_dialect\": job.target_dialect,\n \"source_datasource_id\": job.source_datasource_id,\n \"translation_column\": job.translation_column,\n \"context_columns\": job.context_columns,\n \"target_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n # #endregion _compute_config_hash\n" - }, - { - "contract_id": "_compute_dict_snapshot_hash", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/orchestrator.py", - "start_line": 854, - "end_line": 867, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Compute a SHA-256 hash of dictionary state for snapshot comparison." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region _compute_dict_snapshot_hash [C:2] [TYPE Function] [SEMANTICS translate,hash]\n # @BRIEF Compute a SHA-256 hash of dictionary state for snapshot comparison.\n def _compute_dict_snapshot_hash(self, job_id: str) -> str:\n import hashlib\n from ...models.translate import TranslationJobDictionary\n dict_links = (\n self.db.query(TranslationJobDictionary)\n .filter(TranslationJobDictionary.job_id == job_id)\n .all()\n )\n dict_ids = sorted([dl.dictionary_id for dl in dict_links])\n hash_input = \",\".join(dict_ids)\n return hashlib.sha256(hash_input.encode()).hexdigest()[:16]\n # #endregion _compute_dict_snapshot_hash\n" + "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\nimport json\nimport uuid\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 (\n TranslationBatch,\n TranslationJob,\n TranslationPreviewSession,\n TranslationRecord,\n TranslationRun,\n)\nfrom .events import TranslationEventLog\nfrom .executor import TranslationExecutor\nfrom .sql_generator import SQLGenerator\nfrom .superset_executor import SupersetSqlLabExecutor\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.\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 ) -> 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 \"target_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n \"dictionary_ids\": self._compute_dict_snapshot_hash(job_id),\n }\n\n # Compute key_hash from source_key_cols\n import hashlib\n key_hash_input = json.dumps({\n \"source_key_cols\": job.source_key_cols,\n \"source_datasource_id\": job.source_datasource_id,\n \"source_table\": job.source_table,\n }, sort_keys=True)\n key_hash = hashlib.sha256(key_hash_input.encode()).hexdigest()[:16]\n\n # Create run record with all hash/snapshot fields\n run = TranslationRun(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=\"PENDING\",\n trigger_type=trigger_type or (\"scheduled\" if is_scheduled else \"manual\"),\n config_snapshot=config_snapshot,\n key_hash=key_hash,\n config_hash=config_hash,\n dict_snapshot_hash=dict_snapshot_hash,\n created_by=self.current_user,\n created_at=datetime.now(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 # 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)\n except Exception as e:\n logger.explore(\"Translation execution failed\", {\n \"run_id\": run.id,\n \"error\": str(e),\n })\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 # 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\n records = (\n self.db.query(TranslationRecord)\n .filter(\n TranslationRecord.run_id == run.id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n\n if not records:\n 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 # Determine effective target column for INSERT (defaults to translation_column)\n effective_target = job.target_column or job.translation_column\n\n # Build columns for SQL generation\n columns = job.context_columns or []\n\n # Always include translation_column (original text) if it's different from target\n if job.translation_column and job.translation_column not in columns:\n columns.append(job.translation_column)\n\n # Add target_column separately if it differs from translation_column\n if effective_target and effective_target != job.translation_column and effective_target not in columns:\n columns.append(effective_target)\n\n # Also include key columns if used for upsert\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 source_data = rec.source_data or {}\n\n # Context columns from source data\n if job.context_columns:\n for col in job.context_columns:\n row_data[col] = source_data.get(col, \"\")\n\n # Original text column (translation_column)\n if job.translation_column and job.translation_column not in (job.target_key_cols or []):\n # If target_column differs, keep the original value from source_data;\n # otherwise use the translated value\n if effective_target and effective_target != job.translation_column:\n row_data[job.translation_column] = source_data.get(job.translation_column, \"\")\n else:\n row_data[job.translation_column] = rec.target_sql or \"\"\n\n # Translated text goes into target_column (may be same as translation_column)\n if effective_target:\n row_data[effective_target] = rec.target_sql or \"\"\n\n # Key columns from source data\n if job.target_key_cols:\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 if not columns:\n # Use target_sql as the sole column\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 run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()\n if not run:\n raise ValueError(f\"Run '{run_id}' not found\")\n\n if run.status not in (\"PENDING\", \"RUNNING\"):\n raise ValueError(\n f\"Cannot cancel run in status '{run.status}'. \"\n f\"Only PENDING or RUNNING runs can be cancelled.\"\n )\n\n run.status = \"CANCELLED\"\n run.completed_at = datetime.now(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 return {\n \"id\": run.id,\n \"job_id\": run.job_id,\n \"status\": run.status,\n \"started_at\": run.started_at.isoformat() if run.started_at else None,\n \"completed_at\": run.completed_at.isoformat() if run.completed_at else None,\n \"error_message\": run.error_message,\n \"total_records\": run.total_records or 0,\n \"successful_records\": run.successful_records or 0,\n \"failed_records\": run.failed_records or 0,\n \"skipped_records\": run.skipped_records or 0,\n \"insert_status\": run.insert_status,\n \"superset_execution_id\": run.superset_execution_id,\n \"batch_count\": batch_count,\n \"event_invariants\": {\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 = self.db.query(TranslationRecord).filter(\n TranslationRecord.run_id == run_id\n )\n\n if status_filter:\n query = query.filter(TranslationRecord.status == status_filter)\n\n total = query.count()\n offset = (page - 1) * page_size\n records = (\n query.order_by(TranslationRecord.created_at.desc())\n .offset(offset)\n .limit(page_size)\n .all()\n )\n\n return {\n \"items\": [\n {\n \"id\": r.id,\n \"batch_id\": r.batch_id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"error_message\": r.error_message,\n \"created_at\": r.created_at.isoformat() if r.created_at else None,\n }\n for r in records\n ],\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"status_filter\": status_filter,\n }\n # endregion get_run_records\n\n # region get_run_history [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 _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_language\": job.target_language,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n }, sort_keys=True)\n return hashlib.sha256(config_str.encode()).hexdigest()[:16]\n # endregion _compute_config_hash\n\n # region _compute_dict_snapshot_hash [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\n# #endregion TranslationOrchestrator\n# #endregion TranslationOrchestrator\n" }, { "contract_id": "TranslatePlugin", "contract_type": "Module", "file_path": "backend/src/plugins/translate/plugin.py", "start_line": 1, - "end_line": 58, - "tier": "TIER_2", - "complexity": 3, + "end_line": 61, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": 3, + "COMPLEXITY": 2, "LAYER": "Domain", - "PURPOSE": "TranslatePlugin skeleton for cross-dialect SQL and dashboard translation." + "PURPOSE": "TranslatePlugin skeleton for cross-dialect SQL and dashboard translation.", + "RATIONALE": "Separate plugin avoids bloating LLMAnalysisPlugin beyond fractal limit (<400 lines).", + "REJECTED": "Extending LLMAnalysisPlugin would conflate two distinct feature domains." }, "relations": [ { @@ -68549,27 +52553,55 @@ "target_ref": "[PluginBase]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", + "detail": { + "actual_complexity": 2, + "contract_type": "Module" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslatePlugin [C:3] [TYPE Module] [SEMANTICS plugin,translate,llm,sql,dashboard]\n# @BRIEF TranslatePlugin skeleton for cross-dialect SQL and dashboard translation.\n# @LAYER Domain\n# @RELATION INHERITS -> [PluginBase]\n\nfrom typing import Dict, Any, Optional\nfrom ...core.plugin_base import PluginBase\n\n\n# #region TranslatePlugin [C:3] [TYPE Class] [SEMANTICS plugin,translate]\n# @BRIEF Plugin for translating SQL queries and dashboard definitions across database dialects.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass TranslatePlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"translate\"\n\n @property\n def name(self) -> str:\n return \"LLM Table Translation\"\n\n @property\n def description(self) -> str:\n return \"Cross-dialect SQL and dashboard translation using LLMs.\"\n\n @property\n def version(self) -> str:\n return \"1.0.0\"\n\n def get_schema(self) -> Dict[str, Any]:\n return {\n \"type\": \"object\",\n \"properties\": {\n \"source_dialect\": {\n \"type\": \"string\",\n \"title\": \"Source Dialect\",\n \"description\": \"Source database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"target_dialect\": {\n \"type\": \"string\",\n \"title\": \"Target Dialect\",\n \"description\": \"Target database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"job_id\": {\n \"type\": \"string\",\n \"title\": \"Translation Job ID\",\n \"description\": \"Existing translation job to execute\",\n },\n },\n \"required\": [\"job_id\"],\n }\n\n async def execute(self, params: Dict[str, Any], context: Optional[Any] = None):\n \"\"\"Execute a translation job — implementation deferred to later phases.\"\"\"\n raise NotImplementedError(\"TranslatePlugin.execute not yet implemented\")\n# #endregion TranslatePlugin\n\n# #endregion TranslatePlugin\n" + "body": "# #region TranslatePlugin [C:2] [TYPE Module] [SEMANTICS clickhouse, translate, sql, dashboard, dialect]\n# @BRIEF TranslatePlugin skeleton for cross-dialect SQL and dashboard translation.\n# @LAYER: Domain\n# @RELATION INHERITS -> [PluginBase]\n# @RATIONALE: Separate plugin avoids bloating LLMAnalysisPlugin beyond fractal limit (<400 lines).\n# @REJECTED: Extending LLMAnalysisPlugin would conflate two distinct feature domains.\n\nfrom typing import Any\n\nfrom ...core.plugin_base import PluginBase\n\n\n# #region TranslatePlugin [TYPE Class]\n# @BRIEF Plugin for translating SQL queries and dashboard definitions across database dialects.\n# @RELATION IMPLEMENTS -> backend.src.core.plugin_base.PluginBase\nclass TranslatePlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"translate\"\n\n @property\n def name(self) -> str:\n return \"LLM Table Translation\"\n\n @property\n def description(self) -> str:\n return \"Cross-dialect SQL and dashboard translation using LLMs.\"\n\n @property\n def version(self) -> str:\n return \"1.0.0\"\n\n def get_schema(self) -> dict[str, Any]:\n return {\n \"type\": \"object\",\n \"properties\": {\n \"source_dialect\": {\n \"type\": \"string\",\n \"title\": \"Source Dialect\",\n \"description\": \"Source database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"target_dialect\": {\n \"type\": \"string\",\n \"title\": \"Target Dialect\",\n \"description\": \"Target database dialect (e.g. PostgreSQL, ClickHouse)\",\n },\n \"job_id\": {\n \"type\": \"string\",\n \"title\": \"Translation Job ID\",\n \"description\": \"Existing translation job to execute\",\n },\n },\n \"required\": [\"job_id\"],\n }\n\n async def execute(self, params: dict[str, Any], context: Any | None = None):\n \"\"\"Execute a translation job — implementation deferred to later phases.\"\"\"\n raise NotImplementedError(\"TranslatePlugin.execute not yet implemented\")\n# #endregion TranslatePlugin\n\n# #endregion TranslatePlugin\n" }, { "contract_id": "DEFAULT_EXECUTION_PROMPT_TEMPLATE", "contract_type": "Constant", "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 44, - "end_line": 57, + "start_line": 39, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Default prompt template for batch LLM translation execution (no context columns — faster)." }, "relations": [], "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Constant'", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Constant'", "detail": { "actual_type": "Constant", "allowed_types": [ @@ -68578,6 +52610,7 @@ "Class", "Component", "Block", + "ADR", "Skill", "Agent" ] @@ -68585,8 +52618,8 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Constant' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Constant' at C1", "detail": { "actual_complexity": 1, "contract_type": "Constant" @@ -68594,25 +52627,25 @@ } ], "anchor_syntax": "region", - "body": "# #region DEFAULT_EXECUTION_PROMPT_TEMPLATE [C:1] [TYPE Constant] [SEMANTICS translate,prompt]\nDEFAULT_EXECUTION_PROMPT_TEMPLATE: str = (\n \"Translate the following database content from {source_language} to {target_language}.\\n\\n\"\n \"Source dialect: {source_dialect}\\n\"\n \"Target dialect: {target_dialect}\\n\"\n \"Column to translate: {translation_column}\\n\\n\"\n \"{dictionary_section}\"\n \"For each row, provide an accurate translation 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\": \"\", \"translation\": \"\"}}]}}\\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 [TYPE Constant]\n# @BRIEF Default prompt template for batch LLM translation execution (no context columns — faster).\nDEFAULT_EXECUTION_PROMPT_TEMPLATE: str = (\n \"Translate the following database content from {source_language} to {target_language}.\\n\\n\"\n \"Source dialect: {source_dialect}\\n\"\n \"Target dialect: {target_dialect}\\n\"\n \"Column to translate: {translation_column}\\n\\n\"\n \"{dictionary_section}\"\n \"For each row, provide an accurate translation 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\": \"\", \"translation\": \"\"}}]}}\\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": 60, - "end_line": 74, + "start_line": 56, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Default prompt template for LLM translation preview." }, "relations": [], "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Constant'", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Constant'", "detail": { "actual_type": "Constant", "allowed_types": [ @@ -68621,6 +52654,7 @@ "Class", "Component", "Block", + "ADR", "Skill", "Agent" ] @@ -68628,8 +52662,8 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Constant' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Constant' at C1", "detail": { "actual_complexity": 1, "contract_type": "Constant" @@ -68637,197 +52671,68 @@ } ], "anchor_syntax": "region", - "body": "# #region DEFAULT_PREVIEW_PROMPT_TEMPLATE [C:1] [TYPE Constant] [SEMANTICS translate,prompt]\nDEFAULT_PREVIEW_PROMPT_TEMPLATE: str = (\n \"Translate the following database content from {source_language} to {target_language}.\\n\\n\"\n \"Source dialect: {source_dialect}\\n\"\n \"Target dialect: {target_dialect}\\n\"\n \"Column to translate: {translation_column}\\n\\n\"\n \"{dictionary_section}\"\n \"For each row, provide an accurate translation of the '{translation_column}' value.\\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\": \"\", \"translation\": \"\"}}]}}\\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 DEFAULT_PREVIEW_PROMPT_TEMPLATE [TYPE Constant]\n# @BRIEF Default prompt template for LLM translation preview.\nDEFAULT_PREVIEW_PROMPT_TEMPLATE: str = (\n \"Translate the following database content from {source_language} to {target_language}.\\n\\n\"\n \"Source dialect: {source_dialect}\\n\"\n \"Target dialect: {target_dialect}\\n\"\n \"Column to translate: {translation_column}\\n\\n\"\n \"{dictionary_section}\"\n \"For each row, provide an accurate translation of the '{translation_column}' value.\\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\": \"\", \"translation\": \"\"}}]}}\\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": "TokenEstimator", "contract_type": "Class", "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 77, - "end_line": 108, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Estimate token counts and costs for LLM translation operations using heuristic chars/token ratio." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region TokenEstimator [C:2] [TYPE Class] [SEMANTICS translate,tokens,estimate]\n# @BRIEF Estimate token counts and costs for LLM translation operations using heuristic chars/token ratio.\nclass TokenEstimator:\n \"\"\"Estimate token counts and costs for LLM operations.\"\"\"\n\n CHARS_PER_TOKEN_ESTIMATE: float = 4.0\n OUTPUT_TOKENS_PER_ROW_ESTIMATE: int = 50\n TOKEN_COST_PER_1K: float = 0.002 # Default cost per 1K tokens\n\n # #region estimate_prompt_tokens [C:1] [TYPE Function] [SEMANTICS translate,tokens]\n @staticmethod\n def estimate_prompt_tokens(prompt: str) -> int:\n if not prompt:\n return 0\n return max(1, int(len(prompt) / TokenEstimator.CHARS_PER_TOKEN_ESTIMATE))\n # #endregion estimate_prompt_tokens\n\n # #region estimate_output_tokens [C:1] [TYPE Function] [SEMANTICS translate,tokens]\n @staticmethod\n def estimate_output_tokens(row_count: int) -> int:\n return row_count * TokenEstimator.OUTPUT_TOKENS_PER_ROW_ESTIMATE\n # #endregion estimate_output_tokens\n\n # #region estimate_cost [C:1] [TYPE Function] [SEMANTICS translate,cost]\n @staticmethod\n def estimate_cost(total_tokens: int, cost_per_1k: Optional[float] = None) -> float:\n rate = cost_per_1k if cost_per_1k is not None else TokenEstimator.TOKEN_COST_PER_1K\n return round((total_tokens / 1000) * rate, 6)\n # #endregion estimate_cost\n\n\n# #endregion TokenEstimator\n" - }, - { - "contract_id": "estimate_prompt_tokens", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 86, - "end_line": 92, + "start_line": 74, + "end_line": 116, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region estimate_prompt_tokens [C:1] [TYPE Function] [SEMANTICS translate,tokens]\n @staticmethod\n def estimate_prompt_tokens(prompt: str) -> int:\n if not prompt:\n return 0\n return max(1, int(len(prompt) / TokenEstimator.CHARS_PER_TOKEN_ESTIMATE))\n # #endregion estimate_prompt_tokens\n" - }, - { - "contract_id": "estimate_output_tokens", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 94, - "end_line": 98, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region estimate_output_tokens [C:1] [TYPE Function] [SEMANTICS translate,tokens]\n @staticmethod\n def estimate_output_tokens(row_count: int) -> int:\n return row_count * TokenEstimator.OUTPUT_TOKENS_PER_ROW_ESTIMATE\n # #endregion estimate_output_tokens\n" - }, - { - "contract_id": "estimate_cost", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 100, - "end_line": 105, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": 1 - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region estimate_cost [C:1] [TYPE Function] [SEMANTICS translate,cost]\n @staticmethod\n def estimate_cost(total_tokens: int, cost_per_1k: Optional[float] = None) -> float:\n rate = cost_per_1k if cost_per_1k is not None else TokenEstimator.TOKEN_COST_PER_1K\n return round((total_tokens / 1000) * rate, 6)\n # #endregion estimate_cost\n" - }, - { - "contract_id": "preview_rows", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 130, - "end_line": 342, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns TranslationPreviewResponse with records, cost estimation, and persistent session.", - "PRE": "job_id exists and job has source_datasource_id, translation_column configured.", - "PURPOSE": "Fetch sample rows from Superset dataset, send to LLM for translation, create preview session with records.", - "SIDE_EFFECT": "Fetches data from Superset; calls LLM; creates TranslationPreviewSession and TranslationPreviewRecord rows." + "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." }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", "detail": { - "actual_complexity": 4, - "contract_type": "Function" + "actual_complexity": 1, + "contract_type": "Class" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" } } ], "anchor_syntax": "region", - "body": " # #region preview_rows [C:4] [TYPE Function] [SEMANTICS translate,preview,rows]\n # @BRIEF Fetch sample rows from Superset dataset, send to LLM for translation, create preview session with records.\n # @PRE job_id exists and job has source_datasource_id, translation_column configured.\n # @POST Returns TranslationPreviewResponse with records, cost estimation, and persistent session.\n # @SIDE_EFFECT Fetches data from Superset; calls LLM; creates TranslationPreviewSession and TranslationPreviewRecord rows.\n def preview_rows(\n self,\n job_id: str,\n sample_size: int = 10,\n prompt_template: Optional[str] = None,\n env_id: Optional[str] = None,\n ) -> Dict[str, Any]:\n with belief_scope(\"TranslationPreview.preview_rows\"):\n log.reason(\"Starting preview for job\", payload={\"job_id\": job_id, \"sample_size\": sample_size})\n\n # 1. Load 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 if not job.source_datasource_id:\n raise ValueError(\"Job must have a source datasource configured for preview\")\n if not job.translation_column:\n raise ValueError(\"Job must have a translation column configured for preview\")\n\n # 2. Compute config hash and dict snapshot hash\n config_hash = self._compute_config_hash(job)\n dict_snapshot_hash = self._compute_dict_snapshot_hash(job_id)\n\n # 3. Fetch sample rows from Superset\n log.reason(\"Fetching sample rows from Superset\", payload={\n \"datasource_id\": job.source_datasource_id,\n \"sample_size\": sample_size,\n \"translation_column\": job.translation_column,\n })\n source_rows = self._fetch_sample_rows(\n job=job,\n sample_size=sample_size,\n env_id=env_id,\n )\n if not source_rows:\n raise ValueError(\"No rows returned from datasource for preview\")\n\n actual_row_count = len(source_rows)\n log.reason(f\"Fetched {actual_row_count} sample row(s)\")\n\n # Debug: log first row keys and translation column value\n if source_rows:\n first_row = source_rows[0]\n log.reason(\n f\"First source row keys={list(first_row.keys())} \"\n f\"translation_col={job.translation_column} \"\n f\"val='{first_row.get(job.translation_column, '')}'\"\n )\n\n # 4. Build prompt context from rows\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 # 5. Filter dictionary entries for this batch\n dict_matches = DictionaryManager.filter_for_batch(\n self.db, all_source_texts, job_id\n )\n\n # Build dictionary glossary section\n dictionary_section = \"\"\n if dict_matches:\n glossary_lines = []\n for m in dict_matches:\n glossary_lines.append(\n f\"- '{m['source_term']}' -> '{m['target_term']}'\"\n f\"{' (' + m['context_notes'] + ')' if m.get('context_notes') else ''}\"\n )\n dictionary_section = (\n \"Terminology dictionary (use these translations when applicable):\\n\"\n + \"\\n\".join(glossary_lines)\n + \"\\n\\n\"\n )\n\n # 6. Build LLM prompt\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 template = prompt_template or DEFAULT_PREVIEW_PROMPT_TEMPLATE\n prompt = render_prompt(template, {\n \"source_language\": job.source_dialect or \"SQL\",\n \"target_language\": job.target_language or job.target_dialect or \"en\",\n \"source_dialect\": job.source_dialect or \"\",\n \"target_dialect\": job.target_dialect or \"\",\n \"translation_column\": job.translation_column or \"\",\n \"dictionary_section\": dictionary_section,\n \"rows_json\": rows_json,\n \"row_count\": str(actual_row_count),\n })\n\n # 7. Estimate tokens/cost for sample and full dataset\n sample_prompt_tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n sample_output_tokens = TokenEstimator.estimate_output_tokens(actual_row_count)\n sample_total_tokens = sample_prompt_tokens + sample_output_tokens\n sample_cost = TokenEstimator.estimate_cost(sample_total_tokens)\n\n # Estimate full dataset cost (if we knew total rows)\n total_est_tokens = TokenEstimator.estimate_prompt_tokens(\n prompt.replace(str(actual_row_count), \"{total}\")\n ) + TokenEstimator.estimate_output_tokens(sample_size * 10) # rough extrapolation\n total_est_cost = TokenEstimator.estimate_cost(total_est_tokens)\n\n # 8. Call LLM\n log.reason(\"Calling LLM for preview translation\", payload={\n \"provider_id\": job.provider_id,\n \"row_count\": actual_row_count,\n \"estimated_tokens\": sample_total_tokens,\n })\n llm_response = self._call_llm(\n job=job,\n prompt=prompt,\n )\n\n # 9. Parse LLM response\n translations = self._parse_llm_response(llm_response, actual_row_count)\n\n # 10. Create preview session\n session = TranslationPreviewSession(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=\"ACTIVE\",\n created_by=self.current_user,\n created_at=datetime.now(timezone.utc),\n expires_at=datetime.now(timezone.utc) + timedelta(hours=24),\n )\n self.db.add(session)\n self.db.flush()\n\n # 11. Create preview records\n records = []\n for meta in row_meta:\n idx = meta[\"row_index\"]\n translation = translations.get(str(idx), \"\")\n is_rejected = False\n status = \"PENDING\"\n feedback = None\n\n record = TranslationPreviewRecord(\n id=str(uuid.uuid4()),\n session_id=session.id,\n source_sql=meta[\"source_text\"],\n target_sql=translation,\n source_object_type=\"table_row\",\n source_object_id=str(idx),\n source_object_name=f\"Row {idx + 1}\",\n status=status,\n feedback=feedback,\n source_data=meta.get(\"context_data\"),\n created_at=datetime.now(timezone.utc),\n )\n self.db.add(record)\n self.db.flush()\n records.append({\n \"id\": record.id,\n \"source_sql\": record.source_sql,\n \"target_sql\": record.target_sql,\n \"source_object_type\": record.source_object_type,\n \"source_object_id\": record.source_object_id,\n \"source_object_name\": record.source_object_name,\n \"status\": record.status,\n \"feedback\": record.feedback,\n })\n\n self.db.commit()\n\n result = {\n \"id\": session.id,\n \"job_id\": job_id,\n \"status\": \"ACTIVE\",\n \"created_by\": self.current_user,\n \"created_at\": session.created_at.isoformat(),\n \"expires_at\": session.expires_at.isoformat() if session.expires_at else None,\n \"records\": records,\n \"cost_estimate\": {\n \"sample_size\": actual_row_count,\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 \"estimated_total_rows\": actual_row_count * 10,\n \"estimated_tokens\": total_est_tokens,\n \"estimated_cost\": total_est_cost,\n },\n \"config_hash\": config_hash,\n \"dict_snapshot_hash\": dict_snapshot_hash,\n }\n\n log.reflect(\"Preview completed\", payload={\n \"session_id\": session.id,\n \"row_count\": actual_row_count,\n \"sample_cost\": sample_cost,\n })\n return result\n # #endregion preview_rows\n" - }, - { - "contract_id": "get_preview_session", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 469, - "end_line": 510, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Get the latest preview session for a job with its records." - }, - "relations": [ - { - "source_id": "get_preview_session", - "relation_type": "DEPENDS_ON", - "target_id": "TranslationPreviewSession", - "target_ref": "[TranslationPreviewSession]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region get_preview_session [C:3] [TYPE Function] [SEMANTICS translate,preview,get]\n # @BRIEF Get the latest preview session for a job with its records.\n # @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n def get_preview_session(self, job_id: str) -> Dict[str, Any]:\n with belief_scope(\"TranslationPreview.get_preview_session\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id)\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n raise ValueError(f\"No preview session found for job '{job_id}'\")\n\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id)\n .all()\n )\n\n return {\n \"id\": session.id,\n \"job_id\": job_id,\n \"status\": session.status,\n \"created_by\": session.created_by,\n \"created_at\": session.created_at.isoformat(),\n \"expires_at\": session.expires_at.isoformat() if session.expires_at else None,\n \"records\": [\n {\n \"id\": r.id,\n \"source_sql\": r.source_sql,\n \"target_sql\": r.target_sql,\n \"source_object_type\": r.source_object_type,\n \"source_object_id\": r.source_object_id,\n \"source_object_name\": r.source_object_name,\n \"status\": r.status,\n \"feedback\": r.feedback,\n }\n for r in records\n ],\n }\n # #endregion get_preview_session\n" - }, - { - "contract_id": "_fetch_sample_rows", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 512, - "end_line": 597, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns list of dicts with row data from Superset chart data API.", - "PRE": "job has source_datasource_id and translation_column.", - "PURPOSE": "Fetch sample rows from the Superset dataset for preview.", - "SIDE_EFFECT": "Calls Superset chart data endpoint with pagination." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region _fetch_sample_rows [C:4] [TYPE Function] [SEMANTICS translate,superset,sample]\n # @BRIEF Fetch sample rows from the Superset dataset for preview.\n # @PRE job has source_datasource_id and translation_column.\n # @POST Returns list of dicts with row data from Superset chart data API.\n # @SIDE_EFFECT Calls Superset chart data endpoint with pagination.\n def _fetch_sample_rows(self, job: TranslationJob, sample_size: int = 10, env_id: Optional[str] = None) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationPreview._fetch_sample_rows\"):\n # Determine environment: prefer explicit env_id, then job.environment_id, then job.source_dialect (legacy)\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(\n (e for e in environments if e.id == target_env_id),\n None,\n )\n if not env_config:\n log.explore(\"Could not find environment for datasource\", error=\"Environment not found for datasource\",\n payload={\n \"env_id\": target_env_id,\n })\n # Fallback: try first environment\n if environments:\n env_config = environments[0]\n log.explore(\"Falling back to first available environment\", error=\"Environment fallback used\",\n payload={\n \"env_id\": env_config.id,\n })\n else:\n raise ValueError(\"No Superset environments configured\")\n\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 for chart data endpoint.\n # Virtual columns (e.g. comment_text_ru) are NOT resolved when:\n # - result_type=\"query\" (physical columns only)\n # - query_mode=\"raw\" (virtual columns unavailable in raw mode)\n # Solution: remove both result_type=\"query\" AND query_mode=\"raw\",\n # use aggregate mode with no metrics — this resolves virtual columns.\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 # Modify: use result_type=\"samples\" which returns sample data\n # including all columns (physical + virtual), without needing\n # explicit column objects that trigger validation errors.\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\",\n endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n log.explore(\"Chart data API failed\", error=str(e))\n raise ValueError(f\"Failed to fetch sample data from Superset: {e}\")\n\n # Parse response\n rows = self._extract_data_rows(response)\n log.reason(f\"Extracted {len(rows)} data row(s)\")\n\n # Debug: log first row keys and translation column value\n if rows:\n first_row = rows[0]\n log.reason(\n f\"Row keys={list(first_row.keys())} \"\n f\"target_col={job.translation_column} \"\n f\"val='{first_row.get(job.translation_column, '')}'\"\n )\n\n return rows\n # #endregion _fetch_sample_rows\n" - }, - { - "contract_id": "_extract_data_rows", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/preview.py", - "start_line": 599, - "end_line": 630, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Extract data rows from Superset chart data response, trying multiple response formats." - }, - "relations": [ - { - "source_id": "_extract_data_rows", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetClient", - "target_ref": "[SupersetClient]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region _extract_data_rows [C:3] [TYPE Function] [SEMANTICS translate,superset,data]\n # @BRIEF Extract data rows from Superset chart data response, trying multiple response formats.\n # @RELATION DEPENDS_ON -> [SupersetClient]\n @staticmethod\n def _extract_data_rows(response: Dict[str, Any]) -> List[Dict[str, Any]]:\n with belief_scope(\"TranslationPreview._extract_data_rows\"):\n # Try various response formats\n result = response.get(\"result\")\n if isinstance(result, list):\n for item in result:\n if isinstance(item, dict):\n data = item.get(\"data\")\n if isinstance(data, list) and data:\n return data\n\n # Try flat result\n if isinstance(result, dict):\n data = result.get(\"data\")\n if isinstance(data, list) and data:\n return data\n\n # Legacy: response may have data at top level\n data = response.get(\"data\")\n if isinstance(data, list) and data:\n return data\n\n # Last resort: return response itself wrapped if it looks like a list of rows\n if isinstance(result, list):\n return result\n\n return []\n # #endregion _extract_data_rows\n" + "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 = 4.0\n OUTPUT_TOKENS_PER_ROW_ESTIMATE: int = 50\n TOKEN_COST_PER_1K: float = 0.002 # Default cost per 1K tokens\n\n # region estimate_prompt_tokens [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.\n # @PRE: row_count >= 0.\n # @POST: Returns estimated output token count.\n @staticmethod\n def estimate_output_tokens(row_count: int) -> int:\n return row_count * TokenEstimator.OUTPUT_TOKENS_PER_ROW_ESTIMATE\n # endregion estimate_output_tokens\n\n # region estimate_cost [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\n# #endregion TokenEstimator\n" }, { "contract_id": "TranslationScheduler", "contract_type": "Module", "file_path": "backend/src/plugins/translate/scheduler.py", "start_line": 1, - "end_line": 366, - "tier": "TIER_3", - "complexity": 5, + "end_line": 406, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[job_id, cron_expression] -> Output[TranslationSchedule]", - "INVARIANT": "Concurrency guard: max 1 pending/running run per job at scheduled trigger time.", + "COMPLEXITY": 4, "LAYER": "Domain", - "POST": "TranslationSchedule CRUD persisted; translation runs triggered on schedule.", + "POST": "TranslationSchedule CRUD persisted; APScheduler jobs registered/updated/removed.", "PRE": "Database session and SchedulerService are available.", - "PURPOSE": "Manage TranslationSchedule rows and register them with core SchedulerService for cron-based execution.", + "PURPOSE": "Manage TranslationSchedule rows and register them with core SchedulerService.", "RATIONALE": "Uses existing SchedulerService (APScheduler) to avoid creating a second scheduler instance.", "REJECTED": "Polling-based approach — event-driven APScheduler is more precise.", "SIDE_EFFECT": "Registers APScheduler jobs; runs translations on trigger; creates events." @@ -68836,367 +52741,362 @@ { "source_id": "TranslationScheduler", "relation_type": "DEPENDS_ON", - "target_id": "TranslationSchedule", - "target_ref": "[TranslationSchedule]" + "target_id": "TranslationSchedule:Class", + "target_ref": "[TranslationSchedule:Class]" }, { "source_id": "TranslationScheduler", "relation_type": "DEPENDS_ON", - "target_id": "SchedulerService", - "target_ref": "[SchedulerService]" + "target_id": "SchedulerService:Class", + "target_ref": "[SchedulerService:Class]" }, { "source_id": "TranslationScheduler", "relation_type": "DEPENDS_ON", - "target_id": "TranslationOrchestrator", - "target_ref": "[TranslationOrchestrator]" + "target_id": "TranslationOrchestrator:Class", + "target_ref": "[TranslationOrchestrator:Class]" }, { "source_id": "TranslationScheduler", "relation_type": "DEPENDS_ON", - "target_id": "TranslationEventLog", - "target_ref": "[TranslationEventLog]" + "target_id": "TranslationEventLog:Class", + "target_ref": "[TranslationEventLog:Class]" } ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region TranslationScheduler [C:5] [TYPE Module] [SEMANTICS translate,scheduler,apscheduler,cron]\n# @BRIEF Manage TranslationSchedule rows and register them with core SchedulerService for cron-based execution.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @RELATION DEPENDS_ON -> [SchedulerService]\n# @RELATION DEPENDS_ON -> [TranslationOrchestrator]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @PRE Database session and SchedulerService are available.\n# @POST TranslationSchedule CRUD persisted; translation runs triggered on schedule.\n# @SIDE_EFFECT Registers APScheduler jobs; runs translations on trigger; creates events.\n# @DATA_CONTRACT Input[job_id, cron_expression] -> Output[TranslationSchedule]\n# @INVARIANT Concurrency guard: max 1 pending/running run per job at scheduled trigger time.\n# @RATIONALE Uses existing SchedulerService (APScheduler) to avoid creating a second scheduler instance.\n# @REJECTED Separate scheduler instance would create resource contention.\n# @REJECTED Polling-based approach — event-driven APScheduler is more precise.\n\nimport uuid\nfrom datetime import datetime, timezone, timedelta\nfrom typing import Any, Dict, List, Optional\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.logger import belief_scope\nfrom ...core.config_manager import ConfigManager\nfrom ...models.translate import TranslationSchedule, TranslationJob, TranslationRun\nfrom .events import TranslationEventLog\n\nlog = MarkerLogger(\"TranslationScheduler\")\n\n\n# #region TranslationScheduler [C:5] [TYPE Class] [SEMANTICS translate,scheduler,crud]\n# @BRIEF CRUD for TranslationSchedule rows + APScheduler registration wrappers.\n# @PRE Database session and ConfigManager are available.\n# @POST Schedule rows created/updated/deleted with event logging.\n# @SIDE_EFFECT Writes TranslationSchedule rows; logs SCHEDULE_CREATED/UPDATED/DELETED events.\n# @DATA_CONTRACT Input[job_id, cron_expression, timezone] -> Output[TranslationSchedule]\n# @INVARIANT Exactly one schedule per job (upsert pattern).\nclass TranslationScheduler:\n\n def __init__(self, db: Session, config_manager: ConfigManager, current_user: Optional[str] = None):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n self.event_log = TranslationEventLog(db)\n\n # #region create_schedule [C:4] [TYPE Function] [SEMANTICS translate,schedule,create]\n # @BRIEF Create a new schedule for a job with cron expression and event logging.\n # @PRE job_id exists. cron_expression is valid.\n # @POST TranslationSchedule row created; SCHEDULE_CREATED event logged.\n # @SIDE_EFFECT DB write; event logging.\n def create_schedule(\n self,\n job_id: str,\n cron_expression: str,\n timezone: str = \"UTC\",\n is_active: bool = True,\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.create_schedule\"):\n # Verify job exists\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n\n schedule = TranslationSchedule(\n id=str(uuid.uuid4()),\n job_id=job_id,\n cron_expression=cron_expression,\n timezone=timezone,\n is_active=is_active,\n created_by=self.current_user,\n )\n self.db.add(schedule)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_CREATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": cron_expression,\n \"timezone\": timezone,\n },\n created_by=self.current_user,\n )\n\n log.reflect(\"Schedule created\", payload={\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # #endregion create_schedule\n\n # #region update_schedule [C:4] [TYPE Function] [SEMANTICS translate,schedule,update]\n # @BRIEF Update an existing schedule's cron expression, timezone, or active state.\n # @PRE job_id has an existing schedule.\n # @POST Schedule updated; SCHEDULE_UPDATED event logged.\n # @SIDE_EFFECT DB write; event logging.\n def update_schedule(\n self,\n job_id: str,\n cron_expression: Optional[str] = None,\n timezone_str: Optional[str] = None,\n is_active: Optional[bool] = None,\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.update_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n if cron_expression is not None:\n schedule.cron_expression = cron_expression\n if timezone_str is not None:\n schedule.timezone = timezone_str\n if is_active is not None:\n schedule.is_active = is_active\n schedule.updated_at = datetime.now(timezone.utc)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_UPDATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n },\n created_by=self.current_user,\n )\n\n log.reflect(\"Schedule updated\", payload={\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # #endregion update_schedule\n\n # #region delete_schedule [C:4] [TYPE Function] [SEMANTICS translate,schedule,delete]\n # @BRIEF Delete a schedule for a job with event logging.\n # @PRE job_id has an existing schedule.\n # @POST Schedule deleted; SCHEDULE_DELETED event logged.\n # @SIDE_EFFECT DB write; event logging.\n def delete_schedule(self, job_id: str) -> None:\n with belief_scope(\"TranslationScheduler.delete_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule_id = schedule.id\n self.db.delete(schedule)\n self.db.commit()\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_DELETED\",\n payload={\"schedule_id\": schedule_id},\n created_by=self.current_user,\n )\n\n log.reflect(\"Schedule deleted\", payload={\"schedule_id\": schedule_id, \"job_id\": job_id})\n # #endregion delete_schedule\n\n # #region set_schedule_active [C:4] [TYPE Function] [SEMANTICS translate,schedule,activate]\n # @BRIEF Enable or disable a schedule by setting is_active flag.\n # @PRE job_id has an existing schedule.\n # @POST Schedule is_active updated.\n # @SIDE_EFFECT DB write.\n def set_schedule_active(self, job_id: str, is_active: bool) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.set_schedule_active\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule.is_active = is_active\n schedule.updated_at = datetime.now(timezone.utc)\n self.db.commit()\n self.db.refresh(schedule)\n\n log.reflect(\"Schedule active state set\", payload={\n \"schedule_id\": schedule.id,\n \"job_id\": job_id,\n \"is_active\": is_active,\n })\n return schedule\n # #endregion set_schedule_active\n\n # #region get_schedule [C:2] [TYPE Function] [SEMANTICS translate,schedule,get]\n # @BRIEF Get the schedule for a job by job_id.\n def get_schedule(self, job_id: str) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.get_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n return schedule\n # #endregion get_schedule\n\n # #region list_active_schedules [C:2] [TYPE Function] [SEMANTICS translate,schedule,list]\n # @BRIEF List all active schedules.\n @staticmethod\n def list_active_schedules(db: Session) -> List[TranslationSchedule]:\n return (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active == True)\n .all()\n )\n # #endregion list_active_schedules\n\n # #region get_next_executions [C:2] [TYPE Function] [SEMANTICS translate,schedule,next]\n # @BRIEF Compute next N execution times from a cron expression using APScheduler's CronTrigger.\n def get_next_executions(cron_expression: str, timezone_str: str = \"UTC\", n: int = 3) -> List[str]:\n from zoneinfo import ZoneInfo\n from apscheduler.triggers.cron import CronTrigger\n from apscheduler.triggers.interval import IntervalTrigger\n\n try:\n tz = ZoneInfo(timezone_str)\n trigger = CronTrigger.from_crontab(cron_expression, timezone=tz)\n except (ValueError, KeyError) as e:\n log.explore(\"Invalid cron\", error=str(e))\n return []\n\n now = datetime.now(tz)\n results = []\n next_time = now\n prev = None\n for _ in range(n):\n ft = trigger.get_next_fire_time(prev, next_time)\n if ft is None:\n break\n results.append(ft.isoformat())\n prev = ft\n next_time = ft\n return results\n # #endregion get_next_executions\n\n\n# #endregion TranslationScheduler\n\n\n# #region execute_scheduled_translation [C:5] [TYPE Function] [SEMANTICS translate,schedule,execute]\n# @BRIEF APScheduler job handler — runs scheduled translation with concurrency check and baseline expiry detection.\n# @PRE schedule_id is valid and job exists.\n# @POST Translation run created and executed if no concurrent run exists.\n# @SIDE_EFFECT DB writes; LLM calls; Superset API calls; event logging.\n# @DATA_CONTRACT Input[schedule_id, job_id, db_session_maker, config_manager] -> Output[None]\n# @INVARIANT Concurrency guard: max 1 pending/running run per job at trigger time.\n# @RATIONALE New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.\ndef execute_scheduled_translation(\n schedule_id: str,\n job_id: str,\n db_session_maker,\n config_manager: ConfigManager,\n) -> None:\n \"\"\"APScheduler job callback for scheduled translations.\"\"\"\n db: Session = db_session_maker()\n try:\n with belief_scope(\"execute_scheduled_translation\"):\n # Load schedule\n schedule = db.query(TranslationSchedule).filter(\n TranslationSchedule.id == schedule_id,\n TranslationSchedule.job_id == job_id,\n ).first()\n if not schedule or not schedule.is_active:\n log.reason(\"Schedule inactive/missing\", payload={\"schedule_id\": schedule_id})\n return\n\n log.reason(\"Scheduled translation triggered\", payload={\n \"schedule_id\": schedule_id,\n \"job_id\": job_id,\n })\n\n # Concurrency check: max 1 pending/running run per job\n active_run = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status.in_([\"PENDING\", \"RUNNING\"]),\n )\n .first()\n )\n if active_run:\n log.explore(\"Skipping scheduled run — concurrent run in progress\", error=\"Concurrent run detected\",\n payload={\n \"job_id\": job_id,\n \"existing_run_id\": active_run.id,\n })\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"skipped_concurrent\", \"existing_run_id\": active_run.id},\n )\n return\n\n # Check baseline expiry\n baseline_expired = False\n most_recent = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.insert_status == \"succeeded\",\n )\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n if most_recent:\n age = datetime.now(timezone.utc) - most_recent.created_at\n if age > timedelta(days=90):\n baseline_expired = True\n log.reason(\"Baseline expired — full translation\", payload={\n \"job_id\": job_id,\n \"last_run\": most_recent.created_at.isoformat(),\n \"age_days\": age.days,\n })\n\n # Import and execute\n from .orchestrator import TranslationOrchestrator\n\n orch = TranslationOrchestrator(db, config_manager, current_user=\"scheduler\")\n run = orch.start_run(\n job_id=job_id,\n is_scheduled=True,\n )\n run.trigger_type = \"scheduled\"\n db.flush()\n\n if baseline_expired:\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"baseline_expired\", \"age_days\": age.days},\n )\n\n # Execute in same thread (APScheduler runs in background thread pool)\n try:\n orch.execute_run(run)\n run.insert_status = run.insert_status or \"succeeded\"\n except Exception as exec_err:\n log.explore(\"Scheduled translation execution failed\", error=str(exec_err))\n # Leave schedule enabled — schedule continues on failure\n run.status = \"FAILED\"\n run.error_message = str(exec_err)\n run.completed_at = datetime.now(timezone.utc)\n\n # Update schedule tracking\n schedule.last_run_at = datetime.now(timezone.utc)\n db.commit()\n\n log.reflect(\"Scheduled translation complete\", payload={\n \"schedule_id\": schedule_id,\n \"run_id\": run.id,\n \"status\": run.status,\n })\n except Exception as e:\n log.explore(\"Unexpected error\", error=str(e))\n finally:\n db.close()\n# #endregion execute_scheduled_translation\n# #endregion TranslationScheduler\n" - }, - { - "contract_id": "create_schedule", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/scheduler.py", - "start_line": 47, - "end_line": 90, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "TranslationSchedule row created; SCHEDULE_CREATED event logged.", - "PRE": "job_id exists. cron_expression is valid.", - "PURPOSE": "Create a new schedule for a job with cron expression and event logging.", - "SIDE_EFFECT": "DB write; event logging." - }, - "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C4", "detail": { "actual_complexity": 4, - "contract_type": "Function" + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" } } ], "anchor_syntax": "region", - "body": " # #region create_schedule [C:4] [TYPE Function] [SEMANTICS translate,schedule,create]\n # @BRIEF Create a new schedule for a job with cron expression and event logging.\n # @PRE job_id exists. cron_expression is valid.\n # @POST TranslationSchedule row created; SCHEDULE_CREATED event logged.\n # @SIDE_EFFECT DB write; event logging.\n def create_schedule(\n self,\n job_id: str,\n cron_expression: str,\n timezone: str = \"UTC\",\n is_active: bool = True,\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.create_schedule\"):\n # Verify job exists\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n\n schedule = TranslationSchedule(\n id=str(uuid.uuid4()),\n job_id=job_id,\n cron_expression=cron_expression,\n timezone=timezone,\n is_active=is_active,\n created_by=self.current_user,\n )\n self.db.add(schedule)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_CREATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": cron_expression,\n \"timezone\": timezone,\n },\n created_by=self.current_user,\n )\n\n log.reflect(\"Schedule created\", payload={\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # #endregion create_schedule\n" - }, - { - "contract_id": "update_schedule", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/scheduler.py", - "start_line": 92, - "end_line": 135, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Schedule updated; SCHEDULE_UPDATED event logged.", - "PRE": "job_id has an existing schedule.", - "PURPOSE": "Update an existing schedule's cron expression, timezone, or active state.", - "SIDE_EFFECT": "DB write; event logging." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region update_schedule [C:4] [TYPE Function] [SEMANTICS translate,schedule,update]\n # @BRIEF Update an existing schedule's cron expression, timezone, or active state.\n # @PRE job_id has an existing schedule.\n # @POST Schedule updated; SCHEDULE_UPDATED event logged.\n # @SIDE_EFFECT DB write; event logging.\n def update_schedule(\n self,\n job_id: str,\n cron_expression: Optional[str] = None,\n timezone_str: Optional[str] = None,\n is_active: Optional[bool] = None,\n ) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.update_schedule\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n if cron_expression is not None:\n schedule.cron_expression = cron_expression\n if timezone_str is not None:\n schedule.timezone = timezone_str\n if is_active is not None:\n schedule.is_active = is_active\n schedule.updated_at = datetime.now(timezone.utc)\n self.db.commit()\n self.db.refresh(schedule)\n\n self.event_log.log_event(\n job_id=job_id,\n event_type=\"SCHEDULE_UPDATED\",\n payload={\n \"schedule_id\": schedule.id,\n \"cron_expression\": schedule.cron_expression,\n \"timezone\": schedule.timezone,\n \"is_active\": schedule.is_active,\n },\n created_by=self.current_user,\n )\n\n log.reflect(\"Schedule updated\", payload={\"schedule_id\": schedule.id, \"job_id\": job_id})\n return schedule\n # #endregion update_schedule\n" - }, - { - "contract_id": "set_schedule_active", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/scheduler.py", - "start_line": 164, - "end_line": 188, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Schedule is_active updated.", - "PRE": "job_id has an existing schedule.", - "PURPOSE": "Enable or disable a schedule by setting is_active flag.", - "SIDE_EFFECT": "DB write." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region set_schedule_active [C:4] [TYPE Function] [SEMANTICS translate,schedule,activate]\n # @BRIEF Enable or disable a schedule by setting is_active flag.\n # @PRE job_id has an existing schedule.\n # @POST Schedule is_active updated.\n # @SIDE_EFFECT DB write.\n def set_schedule_active(self, job_id: str, is_active: bool) -> TranslationSchedule:\n with belief_scope(\"TranslationScheduler.set_schedule_active\"):\n schedule = self.db.query(TranslationSchedule).filter(\n TranslationSchedule.job_id == job_id\n ).first()\n if not schedule:\n raise ValueError(f\"No schedule found for job '{job_id}'\")\n\n schedule.is_active = is_active\n schedule.updated_at = datetime.now(timezone.utc)\n self.db.commit()\n self.db.refresh(schedule)\n\n log.reflect(\"Schedule active state set\", payload={\n \"schedule_id\": schedule.id,\n \"job_id\": job_id,\n \"is_active\": is_active,\n })\n return schedule\n # #endregion set_schedule_active\n" - }, - { - "contract_id": "list_active_schedules", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/scheduler.py", - "start_line": 202, - "end_line": 211, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "List all active schedules." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region list_active_schedules [C:2] [TYPE Function] [SEMANTICS translate,schedule,list]\n # @BRIEF List all active schedules.\n @staticmethod\n def list_active_schedules(db: Session) -> List[TranslationSchedule]:\n return (\n db.query(TranslationSchedule)\n .filter(TranslationSchedule.is_active == True)\n .all()\n )\n # #endregion list_active_schedules\n" + "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 .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 == True)\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 [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.\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 # 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 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": 245, - "end_line": 365, - "tier": "TIER_3", - "complexity": 5, + "start_line": 242, + "end_line": 405, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[schedule_id, job_id, db_session_maker, config_manager] -> Output[None]", - "INVARIANT": "Concurrency guard: max 1 pending/running run per job at trigger time.", "POST": "Translation run created and executed if no concurrent run exists.", - "PRE": "schedule_id is valid and job exists.", - "PURPOSE": "APScheduler job handler — runs scheduled translation with concurrency check and baseline expiry detection.", + "PRE": "schedule_id is valid.", + "PURPOSE": "APScheduler job handler — runs scheduled translation with concurrency check and new-key-only fallback.", "RATIONALE": "New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.", - "SIDE_EFFECT": "DB writes; LLM calls; Superset API calls; event logging." + "SIDE_EFFECT": "DB writes; LLM calls; Superset API calls." }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Function' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Function" } }, { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "PRE", + "message": "@PRE is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Function" } } ], "anchor_syntax": "region", - "body": "# #region execute_scheduled_translation [C:5] [TYPE Function] [SEMANTICS translate,schedule,execute]\n# @BRIEF APScheduler job handler — runs scheduled translation with concurrency check and baseline expiry detection.\n# @PRE schedule_id is valid and job exists.\n# @POST Translation run created and executed if no concurrent run exists.\n# @SIDE_EFFECT DB writes; LLM calls; Superset API calls; event logging.\n# @DATA_CONTRACT Input[schedule_id, job_id, db_session_maker, config_manager] -> Output[None]\n# @INVARIANT Concurrency guard: max 1 pending/running run per job at trigger time.\n# @RATIONALE New-key-only mode compares keys against most recent succeeded run; baseline_expired fallback does full.\ndef execute_scheduled_translation(\n schedule_id: str,\n job_id: str,\n db_session_maker,\n config_manager: ConfigManager,\n) -> None:\n \"\"\"APScheduler job callback for scheduled translations.\"\"\"\n db: Session = db_session_maker()\n try:\n with belief_scope(\"execute_scheduled_translation\"):\n # Load schedule\n schedule = db.query(TranslationSchedule).filter(\n TranslationSchedule.id == schedule_id,\n TranslationSchedule.job_id == job_id,\n ).first()\n if not schedule or not schedule.is_active:\n log.reason(\"Schedule inactive/missing\", payload={\"schedule_id\": schedule_id})\n return\n\n log.reason(\"Scheduled translation triggered\", payload={\n \"schedule_id\": schedule_id,\n \"job_id\": job_id,\n })\n\n # Concurrency check: max 1 pending/running run per job\n active_run = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.status.in_([\"PENDING\", \"RUNNING\"]),\n )\n .first()\n )\n if active_run:\n log.explore(\"Skipping scheduled run — concurrent run in progress\", error=\"Concurrent run detected\",\n payload={\n \"job_id\": job_id,\n \"existing_run_id\": active_run.id,\n })\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"skipped_concurrent\", \"existing_run_id\": active_run.id},\n )\n return\n\n # Check baseline expiry\n baseline_expired = False\n most_recent = (\n db.query(TranslationRun)\n .filter(\n TranslationRun.job_id == job_id,\n TranslationRun.insert_status == \"succeeded\",\n )\n .order_by(TranslationRun.created_at.desc())\n .first()\n )\n if most_recent:\n age = datetime.now(timezone.utc) - most_recent.created_at\n if age > timedelta(days=90):\n baseline_expired = True\n log.reason(\"Baseline expired — full translation\", payload={\n \"job_id\": job_id,\n \"last_run\": most_recent.created_at.isoformat(),\n \"age_days\": age.days,\n })\n\n # Import and execute\n from .orchestrator import TranslationOrchestrator\n\n orch = TranslationOrchestrator(db, config_manager, current_user=\"scheduler\")\n run = orch.start_run(\n job_id=job_id,\n is_scheduled=True,\n )\n run.trigger_type = \"scheduled\"\n db.flush()\n\n if baseline_expired:\n event_log = TranslationEventLog(db)\n event_log.log_event(\n job_id=job_id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n payload={\"reason\": \"baseline_expired\", \"age_days\": age.days},\n )\n\n # Execute in same thread (APScheduler runs in background thread pool)\n try:\n orch.execute_run(run)\n run.insert_status = run.insert_status or \"succeeded\"\n except Exception as exec_err:\n log.explore(\"Scheduled translation execution failed\", error=str(exec_err))\n # Leave schedule enabled — schedule continues on failure\n run.status = \"FAILED\"\n run.error_message = str(exec_err)\n run.completed_at = datetime.now(timezone.utc)\n\n # Update schedule tracking\n schedule.last_run_at = datetime.now(timezone.utc)\n db.commit()\n\n log.reflect(\"Scheduled translation complete\", payload={\n \"schedule_id\": schedule_id,\n \"run_id\": run.id,\n \"status\": run.status,\n })\n except Exception as e:\n log.explore(\"Unexpected error\", error=str(e))\n finally:\n db.close()\n# #endregion execute_scheduled_translation\n" - }, - { - "contract_id": "TranslateJobService", - "contract_type": "Module", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 1, - "end_line": 579, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[TranslateJobCreate/Update] -> Output[TranslationJob/TranslateJobResponse]", - "INVARIANT": "Snapshot isolation: in-progress runs use config snapshot; config edits affect future runs only.", - "LAYER": "Domain", - "POST": "Translation jobs are created/updated/deleted with column validation and dialect caching.", - "PRE": "Database session and config manager are available.", - "PURPOSE": "Service layer for translation job CRUD with datasource column validation and database dialect detection.", - "RATIONALE": "Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.", - "REJECTED": "Invalidating in-progress runs on config edit would break scheduled run continuity.", - "SIDE_EFFECT": "Queries Superset for column metadata and database dialect at save time." - }, - "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", - "body": "# #region TranslateJobService [C:5] [TYPE Module] [SEMANTICS translate,service,crud,validation]\n# @BRIEF Service layer for translation job CRUD with datasource column validation and database dialect detection.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Database session and config manager are available.\n# @POST Translation jobs are created/updated/deleted with column validation and dialect caching.\n# @SIDE_EFFECT Queries Superset for column metadata and database dialect at save time.\n# @DATA_CONTRACT Input[TranslateJobCreate/Update] -> Output[TranslationJob/TranslateJobResponse]\n# @INVARIANT Snapshot isolation: in-progress runs use config snapshot; config edits affect future runs only.\n# @RATIONALE Snapshot isolation — in-progress runs use config snapshot; config edits affect future runs only.\n# @REJECTED Invalidating in-progress runs on config edit would break scheduled run continuity.\n\nfrom typing import Any, Dict, List, Optional, Tuple\nfrom sqlalchemy.orm import Session\nfrom datetime import datetime, timezone\nimport uuid\n\nfrom ...core.logger import logger\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.config_manager import ConfigManager\nfrom ...core.superset_client import SupersetClient\nfrom ...models.translate import TranslationJob, TranslationJobDictionary\nfrom ...schemas.translate import (\n TranslateJobCreate,\n TranslateJobUpdate,\n TranslateJobResponse,\n DatasourceColumnsResponse,\n DatasourceColumnResponse,\n)\n\nlog = MarkerLogger(\"TranslateJobService\")\n\n# Supported database dialects for translation\nSUPPORTED_DIALECTS = {\n \"postgresql\", \"mysql\", \"clickhouse\", \"sqlite\", \"mssql\",\n \"oracle\", \"snowflake\", \"bigquery\", \"redshift\", \"presto\",\n \"trino\", \"druid\", \"hive\", \"spark\", \"databricks\",\n}\n\n\n# #region get_dialect_from_database [C:4] [TYPE Function] [SEMANTICS translate,dialect,validation]\n# @BRIEF Extract normalized dialect string from a Superset database record and validate it is supported.\n# @PRE database_record is a dict from Superset API with 'backend' or 'engine' key.\n# @POST Returns normalized dialect string or raises ValueError if unsupported.\n# @SIDE_EFFECT None — pure data extraction.\ndef get_dialect_from_database(database_record: Dict[str, Any]) -> str:\n \"\"\"Extract and validate dialect from Superset database record.\"\"\"\n log.reason(\"Extracting dialect from database record\", payload={\n \"has_backend\": bool(database_record.get(\"backend\") or database_record.get(\"engine\")),\n })\n backend = (\n database_record.get(\"backend\")\n or database_record.get(\"engine\")\n or \"\"\n ).lower().strip()\n\n if not backend:\n log.explore(\"Could not determine database dialect\", error=\"No backend/engine field in database record\", payload={\n \"record_keys\": list(database_record.keys()),\n })\n raise ValueError(\"Could not determine database dialect from connection\")\n\n # Map Superset backend names to normalized dialect\n dialect_map = {\n \"postgresql\": \"postgresql\",\n \"greenplum\": \"postgresql\",\n \"mysql\": \"mysql\",\n \"clickhouse\": \"clickhouse\",\n \"clickhousedb\": \"clickhouse\",\n \"sqlite\": \"sqlite\",\n \"mssql\": \"mssql\",\n \"oracle\": \"oracle\",\n \"snowflake\": \"snowflake\",\n \"bigquery\": \"bigquery\",\n \"redshift\": \"redshift\",\n \"presto\": \"presto\",\n \"trino\": \"trino\",\n \"druid\": \"druid\",\n \"hive\": \"hive\",\n \"spark\": \"spark\",\n \"databricks\": \"databricks\",\n }\n\n normalized = dialect_map.get(backend, backend)\n if normalized not in SUPPORTED_DIALECTS:\n log.explore(\"Unsupported dialect\", payload={\"backend\": backend, \"normalized\": normalized}, error=f\"Unsupported database dialect: {backend}\")\n raise ValueError(\n f\"Unsupported database dialect: '{backend}'. \"\n f\"Supported dialects: {', '.join(sorted(SUPPORTED_DIALECTS))}\"\n )\n log.reflect(\"Dialect validated\", payload={\"dialect\": normalized})\n return normalized\n# #endregion get_dialect_from_database\n\n\n# #region fetch_datasource_metadata [C:4] [TYPE Function] [SEMANTICS translate,datasource,metadata]\n# @BRIEF Fetch datasource columns and database dialect from Superset for a given dataset.\n# @PRE dataset_id is a valid Superset dataset ID and environment has valid credentials.\n# @POST Returns (columns_list, dialect_string) or raises on failure.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\ndef fetch_datasource_metadata(\n dataset_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> Tuple[List[Dict[str, Any]], str]:\n \"\"\"Fetch column metadata and database dialect for a datasource from Superset.\"\"\"\n log.reason(\"Fetching datasource metadata\", payload={\n \"dataset_id\": dataset_id,\n \"env_id\": env_id,\n })\n # Find environment config\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n log.explore(\"Environment not found\", payload={\"env_id\": env_id}, error=f\"Environment {env_id} not configured\")\n raise ValueError(f\"Superset environment '{env_id}' not found in configuration\")\n\n # Create Superset client and fetch dataset detail\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n # Extract columns\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append({\n \"name\": str(col_name),\n \"type\": col.get(\"type\"),\n \"is_physical\": col.get(\"is_physical\", True),\n \"is_dttm\": col.get(\"is_dttm\", False),\n \"description\": col.get(\"description\", \"\"),\n })\n\n # Extract database dialect from datasource\n database_info = dataset_detail.get(\"database\", {})\n if isinstance(database_info, dict):\n dialect = get_dialect_from_database(database_info)\n else:\n # Fallback: try to fetch database directly\n try:\n db_id = dataset_detail.get(\"database_id\")\n if db_id:\n db_record = client.get_database(int(db_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n raise ValueError(\"No database information available for this datasource\")\n except Exception as e:\n log.explore(\"Could not fetch database dialect\", error=str(e))\n raise ValueError(f\"Could not determine database dialect: {e}\")\n\n log.reflect(\"Metadata fetched\", payload={\"columns\": len(columns), \"dialect\": dialect})\n return columns, dialect\n# #endregion fetch_datasource_metadata\n\n\n# #region detect_virtual_columns [C:2] [TYPE Function] [SEMANTICS translate,columns]\n# @BRIEF Identify virtual (calculated) columns from column metadata by filtering non-physical entries.\ndef detect_virtual_columns(columns: List[Dict[str, Any]]) -> List[str]:\n \"\"\"Return names of columns that are virtual (not physical).\"\"\"\n return [col[\"name\"] for col in columns if not col.get(\"is_physical\", True)]\n# #endregion detect_virtual_columns\n\n\n# #region TranslateJobService [C:5] [TYPE Class] [SEMANTICS translate,service,crud]\n# @BRIEF Service for translation job CRUD with validation and Superset integration.\n# @PRE Database session and config manager are available.\n# @POST Translation jobs are managed with full lifecycle (create/update/delete/duplicate).\n# @SIDE_EFFECT Queries Superset for dialect detection and column validation.\n# @DATA_CONTRACT Input[TranslateJobCreate/Update] -> Output[TranslationJob]\n# @INVARIANT Snapshot isolation: in-progress runs unaffected by config edits.\nclass TranslateJobService:\n\n def __init__(self, db: Session, config_manager: ConfigManager, current_user: Optional[str] = None):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n\n # #region list_jobs [C:2] [TYPE Function] [SEMANTICS translate,jobs,query]\n # @BRIEF List translation jobs with optional status filter and pagination.\n def list_jobs(\n self,\n page: int = 1,\n page_size: int = 20,\n status_filter: Optional[str] = None,\n ) -> Tuple[int, List[TranslationJob]]:\n query = self.db.query(TranslationJob)\n\n if status_filter:\n query = query.filter(TranslationJob.status == status_filter)\n\n total = query.count()\n offset = (page - 1) * page_size\n jobs = query.order_by(TranslationJob.created_at.desc()).offset(offset).limit(page_size).all()\n\n return total, jobs\n # #endregion list_jobs\n\n # #region get_job [C:2] [TYPE Function] [SEMANTICS translate,jobs,query]\n # @BRIEF Get a single translation job by ID, raising ValueError if not found.\n def get_job(self, job_id: str) -> TranslationJob:\n job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()\n if not job:\n raise ValueError(f\"Translation job '{job_id}' not found\")\n return job\n # #endregion get_job\n\n # #region create_job [C:4] [TYPE Function] [SEMANTICS translate,jobs,create]\n # @BRIEF Create a new translation job with column validation and dialect detection.\n # @PRE payload contains valid job configuration (name, upsert_strategy, etc).\n # @POST Returns the created TranslationJob with database_dialect cached.\n # @SIDE_EFFECT Validates columns via SupersetClient if source_datasource_id is provided; caches database_dialect.\n def create_job(self, payload: TranslateJobCreate) -> TranslationJob:\n log.reason(\"Creating translation job\", payload={\"name\": payload.name})\n\n # Validate: must have a translation column if datasource is configured\n if payload.source_datasource_id and not payload.translation_column:\n log.explore(\"Missing translation column\", error=\"translation_column required when source_datasource_id is set\", payload={\n \"source_datasource_id\": payload.source_datasource_id,\n })\n raise ValueError(\"A translation column is required when a datasource is selected\")\n\n # Validate upsert strategy\n valid_strategies = {\"MERGE\", \"INSERT\", \"UPDATE\"}\n if payload.upsert_strategy not in valid_strategies:\n raise ValueError(\n f\"Invalid upsert_strategy '{payload.upsert_strategy}'. \"\n f\"Must be one of: {', '.join(sorted(valid_strategies))}\"\n )\n\n # Detect database dialect and validate columns if datasource is specified\n dialect = payload.database_dialect\n if payload.source_datasource_id and (payload.environment_id or payload.source_dialect):\n # If no explicit dialect, try to detect it\n if not dialect:\n try:\n env_id = payload.environment_id or payload.source_dialect\n _, detected_dialect = fetch_datasource_metadata(\n int(payload.source_datasource_id),\n env_id,\n self.config_manager,\n )\n dialect = detected_dialect\n except Exception as e:\n log.explore(\"Dialect detection failed\", error=str(e))\n dialect = payload.source_dialect\n\n # Build job instance\n job = TranslationJob(\n id=str(uuid.uuid4()),\n name=payload.name,\n description=payload.description,\n source_dialect=payload.source_dialect,\n target_dialect=payload.target_dialect,\n database_dialect=dialect,\n source_datasource_id=payload.source_datasource_id,\n source_table=payload.source_table,\n target_schema=payload.target_schema,\n target_table=payload.target_table,\n source_key_cols=payload.source_key_cols or [],\n target_key_cols=payload.target_key_cols or [],\n translation_column=payload.translation_column,\n context_columns=payload.context_columns or [],\n target_language=payload.target_language,\n provider_id=payload.provider_id,\n batch_size=payload.batch_size,\n upsert_strategy=payload.upsert_strategy,\n environment_id=payload.environment_id,\n target_database_id=payload.target_database_id,\n status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(job)\n self.db.flush()\n\n # Attach dictionaries\n if payload.dictionary_ids:\n for dict_id in payload.dictionary_ids:\n assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=job.id,\n dictionary_id=dict_id,\n )\n self.db.add(assoc)\n\n self.db.commit()\n self.db.refresh(job)\n log.reflect(\"Job created\", payload={\"job_id\": job.id})\n return job\n # #endregion create_job\n\n # #region update_job [C:4] [TYPE Function] [SEMANTICS translate,jobs,update]\n # @BRIEF Update an existing translation job with optional dialect re-detection.\n # @PRE payload contains fields to update; job_id exists.\n # @POST Returns the updated TranslationJob with re-detected dialect if datasource changed.\n # @SIDE_EFFECT Re-detects database_dialect if source_datasource_id changed.\n def update_job(self, job_id: str, payload: TranslateJobUpdate) -> TranslationJob:\n log.reason(\"Updating translation job\", payload={\"job_id\": job_id})\n job = self.get_job(job_id)\n\n update_data = payload.model_dump(exclude_unset=True)\n dict_ids = update_data.pop(\"dictionary_ids\", None)\n\n for field, value in update_data.items():\n if hasattr(job, field):\n setattr(job, field, value)\n\n # Re-detect dialect if datasource changed\n if payload.source_datasource_id and not payload.database_dialect:\n try:\n env_id = (payload.environment_id or payload.source_dialect or job.environment_id or job.source_dialect)\n _, detected_dialect = fetch_datasource_metadata(\n int(payload.source_datasource_id),\n env_id,\n self.config_manager,\n )\n job.database_dialect = detected_dialect\n except Exception as e:\n log.explore(\"Dialect re-detection failed\", error=str(e))\n\n job.updated_at = datetime.now(timezone.utc)\n\n # Update dictionary associations if provided\n if dict_ids is not None:\n self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).delete()\n for dict_id in dict_ids:\n assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=job_id,\n dictionary_id=dict_id,\n )\n self.db.add(assoc)\n\n self.db.commit()\n self.db.refresh(job)\n log.reflect(\"Job updated\", payload={\"job_id\": job_id})\n return job\n # #endregion update_job\n\n # #region delete_job [C:4] [TYPE Function] [SEMANTICS translate,jobs,delete]\n # @BRIEF Delete a translation job and its dictionary associations.\n # @PRE job_id must exist.\n # @POST Job and all related TranslationJobDictionary records are deleted.\n # @SIDE_EFFECT Deletes TranslationJob and TranslationJobDictionary rows.\n def delete_job(self, job_id: str) -> None:\n log.reason(\"Deleting translation job\", payload={\"job_id\": job_id})\n job = self.get_job(job_id)\n\n # Delete dictionary associations\n self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).delete()\n\n self.db.delete(job)\n self.db.commit()\n log.reflect(\"Job deleted\", payload={\"job_id\": job_id})\n # #endregion delete_job\n\n # #region duplicate_job [C:4] [TYPE Function] [SEMANTICS translate,jobs,duplicate]\n # @BRIEF Duplicate a translation job with a new name, copying all fields and dictionary associations.\n # @PRE job_id must exist.\n # @POST Returns the duplicated TranslationJob with status DRAFT.\n # @SIDE_EFFECT Creates new TranslationJob and TranslationJobDictionary rows.\n def duplicate_job(self, job_id: str, new_name: Optional[str] = None) -> TranslationJob:\n log.reason(\"Duplicating translation job\", payload={\"job_id\": job_id, \"new_name\": new_name})\n source = self.get_job(job_id)\n\n # Copy all fields except id, created_at, updated_at, status\n new_job = TranslationJob(\n id=str(uuid.uuid4()),\n name=new_name or f\"{source.name} (Copy)\",\n description=source.description,\n source_dialect=source.source_dialect,\n target_dialect=source.target_dialect,\n database_dialect=source.database_dialect,\n source_datasource_id=source.source_datasource_id,\n source_table=source.source_table,\n target_schema=source.target_schema,\n target_table=source.target_table,\n source_key_cols=source.source_key_cols,\n target_key_cols=source.target_key_cols,\n translation_column=source.translation_column,\n context_columns=source.context_columns,\n target_language=source.target_language,\n provider_id=source.provider_id,\n batch_size=source.batch_size,\n upsert_strategy=source.upsert_strategy,\n environment_id=source.environment_id,\n target_database_id=source.target_database_id,\n status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(new_job)\n self.db.flush()\n\n # Copy dictionary associations\n old_dicts = self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).all()\n for assoc in old_dicts:\n new_assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=new_job.id,\n dictionary_id=assoc.dictionary_id,\n )\n self.db.add(new_assoc)\n\n self.db.commit()\n self.db.refresh(new_job)\n log.reflect(\"Job duplicated\", payload={\"new_job_id\": new_job.id, \"source_job_id\": job_id})\n return new_job\n # #endregion duplicate_job\n\n # #region get_job_dictionary_ids [C:1] [TYPE Function] [SEMANTICS translate,jobs,dictionaries]\n def get_job_dictionary_ids(self, job_id: str) -> List[str]:\n assocs = self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).all()\n return [a.dictionary_id for a in assocs]\n # #endregion get_job_dictionary_ids\n\n # #region fetch_available_datasources [C:2] [TYPE Function] [SEMANTICS translate,datasources,query]\n # @BRIEF List available Superset datasets for translation job creation with optional search filter.\n def fetch_available_datasources(self, env_id: str, search: Optional[str] = None) -> list:\n \"\"\"List Superset datasets available for translation.\"\"\"\n from ...core.superset_client import SupersetClient\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment '{env_id}' not found\")\n client = SupersetClient(env)\n _, datasets = client.get_datasets()\n result = []\n for ds in datasets:\n name = ds.get(\"table_name\", \"\")\n if search and search.lower() not in name.lower():\n continue\n db_info = ds.get(\"database\", {})\n backend = db_info.get(\"backend\", \"\")\n dialect = _extract_dialect(backend)\n result.append({\n \"id\": ds.get(\"id\"),\n \"table_name\": name,\n \"schema\": ds.get(\"schema\"),\n \"database_name\": db_info.get(\"database_name\", \"Unknown\"),\n \"database_dialect\": dialect,\n \"description\": ds.get(\"description\", \"\"),\n })\n return result\n # #endregion fetch_available_datasources\n# #endregion TranslateJobService\n\n\n# #region _extract_dialect [C:1] [TYPE Function] [SEMANTICS translate,dialect]\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI (e.g. 'postgresql+psycopg2://...').\"\"\"\n if not backend:\n return \"unknown\"\n try:\n scheme = backend.split(\"://\")[0]\n dialect = scheme.split(\"+\")[0]\n return dialect.lower()\n except Exception:\n return \"unknown\"\n# #endregion _extract_dialect\n\n\n# #region job_to_response [C:2] [TYPE Function] [SEMANTICS translate,serialization]\n# @BRIEF Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.\ndef job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) -> TranslateJobResponse:\n return TranslateJobResponse(\n id=job.id,\n name=job.name,\n description=job.description,\n source_dialect=job.source_dialect,\n target_dialect=job.target_dialect,\n database_dialect=job.database_dialect,\n source_datasource_id=job.source_datasource_id,\n source_table=job.source_table,\n target_schema=job.target_schema,\n target_table=job.target_table,\n source_key_cols=job.source_key_cols or [],\n target_key_cols=job.target_key_cols or [],\n translation_column=job.translation_column,\n context_columns=job.context_columns or [],\n target_language=job.target_language,\n provider_id=job.provider_id,\n batch_size=job.batch_size or 50,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n status=job.status,\n created_by=job.created_by,\n created_at=job.created_at,\n updated_at=job.updated_at,\n dictionary_ids=dict_ids or [],\n environment_id=job.environment_id,\n target_database_id=job.target_database_id,\n )\n# #endregion job_to_response\n\n\n# #region get_datasource_columns [C:4] [TYPE Function] [SEMANTICS translate,datasource,columns]\n# @BRIEF Fetch datasource column metadata from Superset and return structured DatasourceColumnsResponse.\n# @PRE datasource_id is a valid Superset dataset ID.\n# @POST Returns DatasourceColumnsResponse with column metadata and database dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\ndef get_datasource_columns(\n datasource_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> DatasourceColumnsResponse:\n \"\"\"Fetch and return column metadata for a given Superset datasource.\"\"\"\n log.reason(\"Fetching datasource columns\", payload={\"datasource_id\": datasource_id, \"env_id\": env_id})\n\n # Find environment config\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n log.explore(\"Environment not found\", payload={\"env_id\": env_id}, error=f\"Environment {env_id} not configured\")\n raise ValueError(f\"Superset environment '{env_id}' not found\")\n\n # Create Superset client\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(datasource_id)\n\n # Extract database dialect\n database_info = dataset_detail.get(\"database\", {})\n dialect = None\n if isinstance(database_info, dict):\n try:\n dialect = get_dialect_from_database(database_info)\n except (ValueError, KeyError):\n dialect = None\n if dialect is None:\n database_id = dataset_detail.get(\"database_id\")\n if database_id:\n db_record = client.get_database(int(database_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n log.explore(\"Could not determine dialect\", payload={\"datasource_id\": datasource_id}, error=f\"Could not determine dialect for datasource {datasource_id}\")\n raise ValueError(\"Could not determine database dialect for this datasource\")\n\n # Extract columns\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append(DatasourceColumnResponse(\n name=str(col_name),\n type=col.get(\"type\"),\n is_physical=col.get(\"is_physical\", True),\n is_dttm=col.get(\"is_dttm\", False),\n description=col.get(\"description\", \"\"),\n ))\n\n result = DatasourceColumnsResponse(\n datasource_id=datasource_id,\n datasource_name=dataset_detail.get(\"table_name\"),\n schema_name=dataset_detail.get(\"schema\"),\n database_dialect=dialect,\n columns=columns,\n )\n log.reflect(\"Datasource columns fetched\", payload={\n \"datasource_id\": datasource_id,\n \"columns_count\": len(columns),\n \"dialect\": dialect,\n })\n return result\n# #endregion get_datasource_columns\n\n# #endregion TranslateJobService\n" + "body": "# #region execute_scheduled_translation [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.\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 # 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 finally:\n db.close()\n# #endregion execute_scheduled_translation\n" }, { "contract_id": "get_dialect_from_database", "contract_type": "Function", "file_path": "backend/src/plugins/translate/service.py", - "start_line": 43, - "end_line": 95, - "tier": "TIER_2", - "complexity": 4, + "start_line": 39, + "end_line": 82, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 4, "POST": "Returns normalized dialect string or raises ValueError if unsupported.", - "PRE": "database_record is a dict from Superset API with 'backend' or 'engine' key.", - "PURPOSE": "Extract normalized dialect string from a Superset database record and validate it is supported.", - "SIDE_EFFECT": "None — pure data extraction." + "PRE": "database_record is a dict from Superset API.", + "PURPOSE": "Extract normalized dialect string from a Superset database record." }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 4, + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Function" } } ], "anchor_syntax": "region", - "body": "# #region get_dialect_from_database [C:4] [TYPE Function] [SEMANTICS translate,dialect,validation]\n# @BRIEF Extract normalized dialect string from a Superset database record and validate it is supported.\n# @PRE database_record is a dict from Superset API with 'backend' or 'engine' key.\n# @POST Returns normalized dialect string or raises ValueError if unsupported.\n# @SIDE_EFFECT None — pure data extraction.\ndef get_dialect_from_database(database_record: Dict[str, Any]) -> str:\n \"\"\"Extract and validate dialect from Superset database record.\"\"\"\n log.reason(\"Extracting dialect from database record\", payload={\n \"has_backend\": bool(database_record.get(\"backend\") or database_record.get(\"engine\")),\n })\n backend = (\n database_record.get(\"backend\")\n or database_record.get(\"engine\")\n or \"\"\n ).lower().strip()\n\n if not backend:\n log.explore(\"Could not determine database dialect\", error=\"No backend/engine field in database record\", payload={\n \"record_keys\": list(database_record.keys()),\n })\n raise ValueError(\"Could not determine database dialect from connection\")\n\n # Map Superset backend names to normalized dialect\n dialect_map = {\n \"postgresql\": \"postgresql\",\n \"greenplum\": \"postgresql\",\n \"mysql\": \"mysql\",\n \"clickhouse\": \"clickhouse\",\n \"clickhousedb\": \"clickhouse\",\n \"sqlite\": \"sqlite\",\n \"mssql\": \"mssql\",\n \"oracle\": \"oracle\",\n \"snowflake\": \"snowflake\",\n \"bigquery\": \"bigquery\",\n \"redshift\": \"redshift\",\n \"presto\": \"presto\",\n \"trino\": \"trino\",\n \"druid\": \"druid\",\n \"hive\": \"hive\",\n \"spark\": \"spark\",\n \"databricks\": \"databricks\",\n }\n\n normalized = dialect_map.get(backend, backend)\n if normalized not in SUPPORTED_DIALECTS:\n log.explore(\"Unsupported dialect\", payload={\"backend\": backend, \"normalized\": normalized}, error=f\"Unsupported database dialect: {backend}\")\n raise ValueError(\n f\"Unsupported database dialect: '{backend}'. \"\n f\"Supported dialects: {', '.join(sorted(SUPPORTED_DIALECTS))}\"\n )\n log.reflect(\"Dialect validated\", payload={\"dialect\": normalized})\n return normalized\n# #endregion get_dialect_from_database\n" + "body": "# #region get_dialect_from_database [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" }, { "contract_id": "fetch_datasource_metadata", "contract_type": "Function", "file_path": "backend/src/plugins/translate/service.py", - "start_line": 98, - "end_line": 159, - "tier": "TIER_2", - "complexity": 4, + "start_line": 85, + "end_line": 139, + "tier": "TIER_1", + "complexity": 1, "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.", - "PURPOSE": "Fetch datasource columns and database dialect from Superset for a given dataset.", - "SIDE_EFFECT": "Queries Superset API for dataset detail and database info." + "PURPOSE": "Fetch datasource columns and database dialect from Superset." }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 4, + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Function" } } ], "anchor_syntax": "region", - "body": "# #region fetch_datasource_metadata [C:4] [TYPE Function] [SEMANTICS translate,datasource,metadata]\n# @BRIEF Fetch datasource columns and database dialect from Superset for a given dataset.\n# @PRE dataset_id is a valid Superset dataset ID and environment has valid credentials.\n# @POST Returns (columns_list, dialect_string) or raises on failure.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\ndef fetch_datasource_metadata(\n dataset_id: int,\n env_id: str,\n config_manager: ConfigManager,\n) -> Tuple[List[Dict[str, Any]], str]:\n \"\"\"Fetch column metadata and database dialect for a datasource from Superset.\"\"\"\n log.reason(\"Fetching datasource metadata\", payload={\n \"dataset_id\": dataset_id,\n \"env_id\": env_id,\n })\n # Find environment config\n environments = config_manager.get_environments()\n env_config = next((e for e in environments if e.id == env_id), None)\n if not env_config:\n log.explore(\"Environment not found\", payload={\"env_id\": env_id}, error=f\"Environment {env_id} not configured\")\n raise ValueError(f\"Superset environment '{env_id}' not found in configuration\")\n\n # Create Superset client and fetch dataset detail\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n # Extract columns\n raw_columns = dataset_detail.get(\"columns\", [])\n columns = []\n for col in raw_columns:\n col_name = col.get(\"name\") or col.get(\"column_name\")\n if not col_name:\n continue\n columns.append({\n \"name\": str(col_name),\n \"type\": col.get(\"type\"),\n \"is_physical\": col.get(\"is_physical\", True),\n \"is_dttm\": col.get(\"is_dttm\", False),\n \"description\": col.get(\"description\", \"\"),\n })\n\n # Extract database dialect from datasource\n database_info = dataset_detail.get(\"database\", {})\n if isinstance(database_info, dict):\n dialect = get_dialect_from_database(database_info)\n else:\n # Fallback: try to fetch database directly\n try:\n db_id = dataset_detail.get(\"database_id\")\n if db_id:\n db_record = client.get_database(int(db_id))\n db_result = db_record.get(\"result\", db_record) if isinstance(db_record, dict) else db_record\n dialect = get_dialect_from_database(db_result)\n else:\n raise ValueError(\"No database information available for this datasource\")\n except Exception as e:\n log.explore(\"Could not fetch database dialect\", error=str(e))\n raise ValueError(f\"Could not determine database dialect: {e}\")\n\n log.reflect(\"Metadata fetched\", payload={\"columns\": len(columns), \"dialect\": dialect})\n return columns, dialect\n# #endregion fetch_datasource_metadata\n" + "body": "# #region fetch_datasource_metadata [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" }, { "contract_id": "detect_virtual_columns", "contract_type": "Function", "file_path": "backend/src/plugins/translate/service.py", - "start_line": 162, - "end_line": 167, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Identify virtual (calculated) columns from column metadata by filtering non-physical entries." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region detect_virtual_columns [C:2] [TYPE Function] [SEMANTICS translate,columns]\n# @BRIEF Identify virtual (calculated) columns from column metadata by filtering non-physical entries.\ndef detect_virtual_columns(columns: List[Dict[str, Any]]) -> List[str]:\n \"\"\"Return names of columns that are virtual (not physical).\"\"\"\n return [col[\"name\"] for col in columns if not col.get(\"is_physical\", True)]\n# #endregion detect_virtual_columns\n" - }, - { - "contract_id": "get_job_dictionary_ids", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 421, - "end_line": 427, + "start_line": 142, + "end_line": 149, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "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": [], + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "region", - "body": " # #region get_job_dictionary_ids [C:1] [TYPE Function] [SEMANTICS translate,jobs,dictionaries]\n def get_job_dictionary_ids(self, job_id: str) -> List[str]:\n assocs = self.db.query(TranslationJobDictionary).filter(\n TranslationJobDictionary.job_id == job_id\n ).all()\n return [a.dictionary_id for a in assocs]\n # #endregion get_job_dictionary_ids\n" + "body": "# #region detect_virtual_columns [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": "fetch_available_datasources", - "contract_type": "Function", + "contract_id": "TranslateJobService", + "contract_type": "Class", "file_path": "backend/src/plugins/translate/service.py", - "start_line": 429, - "end_line": 456, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "List available Superset datasets for translation job creation with optional search filter." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region fetch_available_datasources [C:2] [TYPE Function] [SEMANTICS translate,datasources,query]\n # @BRIEF List available Superset datasets for translation job creation with optional search filter.\n def fetch_available_datasources(self, env_id: str, search: Optional[str] = None) -> list:\n \"\"\"List Superset datasets available for translation.\"\"\"\n from ...core.superset_client import SupersetClient\n env = self.config_manager.get_environment(env_id)\n if not env:\n raise ValueError(f\"Environment '{env_id}' not found\")\n client = SupersetClient(env)\n _, datasets = client.get_datasets()\n result = []\n for ds in datasets:\n name = ds.get(\"table_name\", \"\")\n if search and search.lower() not in name.lower():\n continue\n db_info = ds.get(\"database\", {})\n backend = db_info.get(\"backend\", \"\")\n dialect = _extract_dialect(backend)\n result.append({\n \"id\": ds.get(\"id\"),\n \"table_name\": name,\n \"schema\": ds.get(\"schema\"),\n \"database_name\": db_info.get(\"database_name\", \"Unknown\"),\n \"database_dialect\": dialect,\n \"description\": ds.get(\"description\", \"\"),\n })\n return result\n # #endregion fetch_available_datasources\n" - }, - { - "contract_id": "_extract_dialect", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/service.py", - "start_line": 460, - "end_line": 471, + "start_line": 152, + "end_line": 434, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Service for translation job CRUD with validation and Superset integration." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region _extract_dialect [C:1] [TYPE Function] [SEMANTICS translate,dialect]\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI (e.g. 'postgresql+psycopg2://...').\"\"\"\n if not backend:\n return \"unknown\"\n try:\n scheme = backend.split(\"://\")[0]\n dialect = scheme.split(\"+\")[0]\n return dialect.lower()\n except Exception:\n return \"unknown\"\n# #endregion _extract_dialect\n" + "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 # 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 context_columns=payload.context_columns or [],\n target_language=payload.target_language,\n provider_id=payload.provider_id,\n batch_size=payload.batch_size,\n upsert_strategy=payload.upsert_strategy,\n environment_id=payload.environment_id,\n target_database_id=payload.target_database_id,\n status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(job)\n self.db.flush()\n\n # Attach dictionaries\n if payload.dictionary_ids:\n for dict_id in payload.dictionary_ids:\n assoc = TranslationJobDictionary(\n id=str(uuid.uuid4()),\n job_id=job.id,\n dictionary_id=dict_id,\n )\n self.db.add(assoc)\n\n self.db.commit()\n self.db.refresh(job)\n 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 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 context_columns=source.context_columns,\n target_language=source.target_language,\n provider_id=source.provider_id,\n batch_size=source.batch_size,\n upsert_strategy=source.upsert_strategy,\n 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" }, { "contract_id": "job_to_response", "contract_type": "Function", "file_path": "backend/src/plugins/translate/service.py", - "start_line": 474, - "end_line": 504, + "start_line": 451, + "end_line": 482, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "COMPLEXITY": 2, "PURPOSE": "Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "region", - "body": "# #region job_to_response [C:2] [TYPE Function] [SEMANTICS translate,serialization]\n# @BRIEF Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.\ndef job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) -> TranslateJobResponse:\n return TranslateJobResponse(\n id=job.id,\n name=job.name,\n description=job.description,\n source_dialect=job.source_dialect,\n target_dialect=job.target_dialect,\n database_dialect=job.database_dialect,\n source_datasource_id=job.source_datasource_id,\n source_table=job.source_table,\n target_schema=job.target_schema,\n target_table=job.target_table,\n source_key_cols=job.source_key_cols or [],\n target_key_cols=job.target_key_cols or [],\n translation_column=job.translation_column,\n context_columns=job.context_columns or [],\n target_language=job.target_language,\n provider_id=job.provider_id,\n batch_size=job.batch_size or 50,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n status=job.status,\n created_by=job.created_by,\n created_at=job.created_at,\n updated_at=job.updated_at,\n dictionary_ids=dict_ids or [],\n environment_id=job.environment_id,\n target_database_id=job.target_database_id,\n )\n# #endregion job_to_response\n" + "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 context_columns=job.context_columns or [],\n target_language=job.target_language,\n provider_id=job.provider_id,\n batch_size=job.batch_size or 50,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n status=job.status,\n created_by=job.created_by,\n created_at=job.created_at,\n updated_at=job.updated_at,\n dictionary_ids=dict_ids or [],\n environment_id=job.environment_id,\n target_database_id=job.target_database_id,\n )\n# #endregion job_to_response\n" + }, + { + "contract_id": "DatasourceColumnsService", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/service.py", + "start_line": 485, + "end_line": 547, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "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": "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "body": "# #region DatasourceColumnsService [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.\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": "SQLGenerator", @@ -69205,13 +53105,16 @@ "start_line": 1, "end_line": 376, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": 4, + "COMPLEXITY": 3, "LAYER": "Domain", "POST": "Returns safe SQL strings for the target dialect.", "PRE": "Job has target_schema and target_table configured. Dialect is one of supported SUPPORTED_DIALECTS.", - "PURPOSE": "Dialect-aware safe SQL generation for INSERT/UPSERT operations with identifier quoting and value encoding." + "PURPOSE": "Dialect-aware safe SQL generation for INSERT/UPSERT operations.", + "RATIONALE": "Dialect-aware SQL uses ON CONFLICT for PostgreSQL; plain INSERT for ClickHouse with documented limitations.", + "REJECTED": "ORM-based insert bypasses Superset's SQL Lab audit trail.", + "SIDE_EFFECT": "None — pure code generation." }, "relations": [ { @@ -69229,24 +53132,60 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Module' at C3", "detail": { - "actual_complexity": 4, + "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": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C3", + "detail": { + "actual_complexity": 3, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED 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", - "body": "# #region SQLGenerator [C:4] [TYPE Module] [SEMANTICS translate,sql,generator,dialect]\n# @BRIEF Dialect-aware safe SQL generation for INSERT/UPSERT operations with identifier quoting and value encoding.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @PRE Job has target_schema and target_table configured. Dialect is one of supported SUPPORTED_DIALECTS.\n# @POST Returns safe SQL strings for the target dialect.\n\nfrom typing import Any, Dict, List, Optional, Tuple\nfrom datetime import datetime, timezone\nfrom ...core.cot_logger import MarkerLogger\n\nfrom ...core.logger import belief_scope\n\nlog = MarkerLogger(\"SqlGenerator\")\n\n# PostgreSQL/Greenplum dialects that support ON CONFLICT\nPOSTGRESQL_DIALECTS = {\"postgresql\", \"redshift\", \"greenplum\"}\n# Dialects that use backtick or no quoting\nCLICKHOUSE_DIALECTS = {\"clickhouse\"}\n\n\n# #region _normalize_timestamp_value [C:2] [TYPE Function] [SEMANTICS translate,sql,timestamp]\n# @BRIEF Detect Unix timestamp strings (seconds or millis) and convert to 'YYYY-MM-DD' for Date columns.\ndef _normalize_timestamp_value(value: Any) -> Optional[str]:\n \"\"\"Detect Unix timestamp values and convert to date string.\n\n Handles:\n - Integer/float Unix timestamps (seconds or milliseconds)\n - String representations of Unix timestamps (e.g. '1726358400000.0')\n\n Returns 'YYYY-MM-DD' if conversion succeeds, None if value is not a timestamp.\n \"\"\"\n # Try numeric conversion first\n try:\n ts = float(value)\n except (ValueError, TypeError):\n return None\n\n # Heuristic: Unix timestamps in seconds are ~10 digits (1e9 range for 2001-2033)\n # Unix timestamps in milliseconds are ~13 digits (1e12 range for 2001-2033)\n if 1e9 <= ts < 1e12:\n # Already in seconds\n pass\n elif 1e12 <= ts < 1e15:\n # Milliseconds — convert to seconds\n ts = ts / 1000.0\n else:\n # Not a plausible Unix timestamp\n return None\n\n try:\n dt = datetime.fromtimestamp(ts, tz=timezone.utc)\n return dt.strftime(\"%Y-%m-%d\")\n except (OSError, OverflowError, ValueError):\n return None\n# #endregion _normalize_timestamp_value\n\n\n# #region _quote_identifier [C:2] [TYPE Function] [SEMANTICS translate,sql,quoting]\n# @BRIEF Quote a SQL identifier per dialect rules — double quotes for PostgreSQL, backticks for ClickHouse.\ndef _quote_identifier(identifier: str, dialect: str) -> str:\n \"\"\"Quote a SQL identifier per dialect rules.\"\"\"\n if not identifier:\n return identifier\n # Remove any existing quotes to avoid double-quoting\n cleaned = identifier.strip().strip('\"').strip('`').strip('[]')\n if dialect in POSTGRESQL_DIALECTS:\n return f'\"{cleaned}\"'\n elif dialect in CLICKHOUSE_DIALECTS:\n return f\"`{cleaned}`\"\n else:\n # Generic ANSI double-quote\n return f'\"{cleaned}\"'\n# #endregion _quote_identifier\n\n\n# #region _encode_sql_value [C:2] [TYPE Function] [SEMANTICS translate,sql,encoding]\n# @BRIEF Encode a Python value into a SQL-safe literal for INSERT VALUES, with ClickHouse timestamp normalization.\ndef _encode_sql_value(value: Any, dialect: Optional[str] = None) -> str:\n \"\"\"Encode a Python value into a SQL-safe literal.\n\n For ClickHouse dialect, attempts to detect Unix timestamp strings\n and convert them to 'YYYY-MM-DD' format for Date column compatibility.\n \"\"\"\n if value is None:\n return \"NULL\"\n if isinstance(value, bool):\n return \"TRUE\" if value else \"FALSE\"\n\n # For ClickHouse: try to normalize timestamp-like string values\n if dialect in CLICKHOUSE_DIALECTS and isinstance(value, str) and value:\n normalized = _normalize_timestamp_value(value)\n if normalized:\n return f\"'{normalized}'\"\n\n if isinstance(value, (int, float)):\n return str(value)\n # String — escape single quotes by doubling them\n escaped = str(value).replace(\"'\", \"''\")\n return f\"'{escaped}'\"\n# #endregion _encode_sql_value\n\n\n# #region _build_values_clause [C:2] [TYPE Function] [SEMANTICS translate,sql,values]\n# @BRIEF Build a VALUES clause for multiple rows with per-column value encoding and dialect-aware quoting.\ndef _build_values_clause(columns: List[str], rows: List[Dict[str, Any]], dialect: Optional[str] = None) -> str:\n \"\"\"Build VALUES (...) clause for multiple rows.\n\n NOTE: columns may be quoted (e.g. '\"col\"' or '`col`') for the SQL column list,\n but row dicts have UNQUOTED keys. Strip quotes before value lookup.\n \"\"\"\n value_groups = []\n for row in rows:\n values = []\n for col in columns:\n # Strip quoting for value lookup — row keys are unquoted\n lookup_key = col.strip('\"').strip('`').strip('[]')\n val = row.get(lookup_key)\n values.append(_encode_sql_value(val, dialect=dialect))\n value_groups.append(f\"({', '.join(values)})\")\n return \",\\n\".join(value_groups)\n# #endregion _build_values_clause\n\n\n# #region generate_insert_sql [C:3] [TYPE Function] [SEMANTICS translate,sql,insert]\n# @BRIEF Generate a dialect-aware plain INSERT SQL statement for the given table, columns, and rows.\n# @RELATION DEPENDS_ON -> [_quote_identifier]\n# @RELATION DEPENDS_ON -> [_build_values_clause]\ndef generate_insert_sql(\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n rows: List[Dict[str, Any]],\n dialect: Optional[str] = None,\n) -> str:\n \"\"\"Generate a plain INSERT SQL.\"\"\"\n with belief_scope(\"generate_insert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for INSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for INSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for INSERT SQL generation\")\n\n col_list = \", \".join(columns)\n values = _build_values_clause(columns, rows, dialect=dialect)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n sql = f\"INSERT INTO {table_ref} ({col_list})\\nVALUES\\n{values};\"\n return sql\n# #endregion generate_insert_sql\n\n\n# #region generate_upsert_sql [C:3] [TYPE Function] [SEMANTICS translate,sql,upsert]\n# @BRIEF Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE / DO NOTHING.\n# @RELATION DEPENDS_ON -> [_quote_identifier]\n# @RELATION DEPENDS_ON -> [_build_values_clause]\ndef generate_upsert_sql(\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n key_columns: List[str],\n rows: List[Dict[str, Any]],\n dialect: Optional[str] = None,\n) -> str:\n \"\"\"Generate INSERT ... ON CONFLICT (key_cols) DO UPDATE SET ... SQL.\"\"\"\n with belief_scope(\"generate_upsert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for UPSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for UPSERT SQL generation\")\n if not key_columns:\n raise ValueError(\"key_columns are required for UPSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for UPSERT SQL generation\")\n\n col_list = \", \".join(columns)\n key_list = \", \".join(key_columns)\n values = _build_values_clause(columns, rows, dialect=dialect)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n # Build SET clause: exclude key columns from update\n update_cols = [c for c in columns if c not in key_columns]\n if not update_cols:\n # If only key columns, use DO NOTHING\n conflict_action = \"DO NOTHING\"\n else:\n set_parts = [f\"{col} = EXCLUDED.{col}\" for col in update_cols]\n conflict_action = \"DO UPDATE SET\\n\" + \",\\n\".join(set_parts)\n\n sql = (\n f\"INSERT INTO {table_ref} ({col_list})\\n\"\n f\"VALUES\\n\"\n f\"{values}\\n\"\n f\"ON CONFLICT ({key_list}) {conflict_action};\"\n )\n return sql\n# #endregion generate_upsert_sql\n\n\n# #region SQLGenerator [C:4] [TYPE Class] [SEMANTICS translate,sql,generator]\n# @BRIEF Generate safe, dialect-appropriate SQL INSERT/UPSERT statements with batching support.\n# @PRE Job has target_schema, target_table, key columns configured.\n# @POST Returns generated SQL string for the target dialect.\n# @SIDE_EFFECT None — pure SQL generation.\n# @RELATION DEPENDS_ON -> [generate_insert_sql]\n# @RELATION DEPENDS_ON -> [generate_upsert_sql]\nclass SQLGenerator:\n\n # #region SQLGenerator.generate [C:4] [TYPE Function] [SEMANTICS translate,sql,generate]\n # @BRIEF Generate SQL for a set of rows, detecting dialect from the job configuration.\n # @PRE dialect is a supported database dialect. columns list is non-empty. rows is non-empty.\n # @POST Returns tuple of (sql_string, statement_count).\n # @SIDE_EFFECT None — pure SQL generation.\n @staticmethod\n def generate(\n dialect: str,\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n rows: List[Dict[str, Any]],\n key_columns: Optional[List[str]] = None,\n upsert_strategy: str = \"MERGE\",\n ) -> Tuple[str, int]:\n \"\"\"Generate dialect-appropriate INSERT/UPSERT SQL.\n\n Args:\n dialect: Target database dialect (e.g. 'postgresql', 'clickhouse').\n target_schema: Optional schema name.\n target_table: Target table name.\n columns: List of column names to insert.\n rows: List of row dicts with column values.\n key_columns: Key columns for conflict resolution (UPSERT).\n upsert_strategy: 'MERGE' (UPSERT), 'INSERT' (plain INSERT).\n\n Returns:\n Tuple of (sql_string, row_count).\n \"\"\"\n with belief_scope(\"SQLGenerator.generate\"):\n log.reason(\"Generating SQL\", payload={\n \"dialect\": dialect,\n \"schema\": target_schema,\n \"table\": target_table,\n \"columns\": len(columns),\n \"rows\": len(rows),\n \"strategy\": upsert_strategy,\n })\n\n # Validate inputs\n if not target_table:\n raise ValueError(\"target_table is required\")\n if not columns:\n raise ValueError(\"At least one column is required\")\n if not rows:\n raise ValueError(\"At least one row is required\")\n\n # Build fully qualified table reference\n table_ref = target_table\n if target_schema:\n quoted_schema = _quote_identifier(target_schema, dialect)\n quoted_table = _quote_identifier(target_table, dialect)\n table_ref = f\"{quoted_schema}.{quoted_table}\"\n else:\n table_ref = _quote_identifier(target_table, dialect)\n\n # Quote columns per dialect\n quoted_columns = [_quote_identifier(c, dialect) for c in columns]\n quoted_key_columns = (\n [_quote_identifier(k, dialect) for k in key_columns]\n if key_columns\n else []\n )\n\n # Generate SQL per dialect and strategy\n use_upsert = upsert_strategy.upper() == \"MERGE\" and key_columns\n\n if dialect in POSTGRESQL_DIALECTS or dialect not in CLICKHOUSE_DIALECTS:\n # PostgreSQL and other ANSI dialects: support UPSERT via ON CONFLICT\n if use_upsert:\n sql = generate_upsert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n key_columns=quoted_key_columns,\n rows=rows,\n dialect=dialect,\n )\n else:\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n elif dialect in CLICKHOUSE_DIALECTS:\n # ClickHouse: plain INSERT, no ON CONFLICT support\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n if use_upsert:\n log.reason(\"ClickHouse UPSERT not supported; using plain INSERT\", payload={\n \"note\": \"ClickHouse does not support ON CONFLICT. Use ReplacingMergeTree for dedup.\",\n })\n else:\n # Fallback: plain INSERT\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n\n log.reflect(\"SQL generated\", payload={\n \"dialect\": dialect,\n \"row_count\": len(rows),\n \"sql_length\": len(sql),\n })\n return sql, len(rows)\n # #endregion SQLGenerator.generate\n\n # #region SQLGenerator.generate_batch [C:3] [TYPE Function] [SEMANTICS translate,sql,batch]\n # @BRIEF Generate separate INSERT statements for each row chunk (batch-safe version).\n # @RELATION DEPENDS_ON -> [SQLGenerator.generate]\n @staticmethod\n def generate_batch(\n dialect: str,\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n rows: List[Dict[str, Any]],\n key_columns: Optional[List[str]] = None,\n upsert_strategy: str = \"MERGE\",\n max_rows_per_statement: int = 500,\n ) -> List[Tuple[str, int]]:\n \"\"\"Generate SQL in batches, splitting large row sets into multiple statements.\n\n Returns:\n List of (sql_string, row_count) tuples.\n \"\"\"\n with belief_scope(\"SQLGenerator.generate_batch\"):\n if not rows:\n return []\n\n statements = []\n for i in range(0, len(rows), max_rows_per_statement):\n chunk = rows[i:i + max_rows_per_statement]\n sql, count = SQLGenerator.generate(\n dialect=dialect,\n target_schema=target_schema,\n target_table=target_table,\n columns=columns,\n rows=chunk,\n key_columns=key_columns,\n upsert_strategy=upsert_strategy,\n )\n statements.append((sql, count))\n\n return statements\n # #endregion SQLGenerator.generate_batch\n\n\n# #endregion SQLGenerator\n# #endregion SQLGenerator\n" + "body": "# #region SQLGenerator [C:3] [TYPE Module] [SEMANTICS clickhouse, translate, sql, insert, generate]\n# @BRIEF Dialect-aware safe SQL generation for INSERT/UPSERT operations.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @PRE: Job has target_schema and target_table configured. Dialect is one of supported SUPPORTED_DIALECTS.\n# @POST: Returns safe SQL strings for the target dialect.\n# @SIDE_EFFECT: None — pure code generation.\n# @RATIONALE: Dialect-aware SQL uses ON CONFLICT for PostgreSQL; plain INSERT for ClickHouse with documented limitations.\n# @REJECTED: UPDATE statements — source is append-only; UPSERT covers overwrite case.\n# @REJECTED: ORM-based insert bypasses Superset's SQL Lab audit trail.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...core.logger import belief_scope, logger\n\n# PostgreSQL/Greenplum dialects that support ON CONFLICT\nPOSTGRESQL_DIALECTS = {\"postgresql\", \"redshift\", \"greenplum\"}\n# Dialects that use backtick or no quoting\nCLICKHOUSE_DIALECTS = {\"clickhouse\"}\n\n\n# #region _normalize_timestamp_value [C:2] [TYPE Function] [SEMANTICS translate,sql,timestamp]\n# @BRIEF Detect Unix timestamp strings (seconds or millis) and convert to 'YYYY-MM-DD' for Date columns.\ndef _normalize_timestamp_value(value: Any) -> str | None:\n \"\"\"Detect Unix timestamp values and convert to date string.\n\n Handles:\n - Integer/float Unix timestamps (seconds or milliseconds)\n - String representations of Unix timestamps (e.g. '1726358400000.0')\n\n Returns 'YYYY-MM-DD' if conversion succeeds, None if value is not a timestamp.\n \"\"\"\n # Try numeric conversion first\n try:\n ts = float(value)\n except (ValueError, TypeError):\n return None\n\n # Heuristic: Unix timestamps in seconds are ~10 digits (1e9 range for 2001-2033)\n # Unix timestamps in milliseconds are ~13 digits (1e12 range for 2001-2033)\n if 1e9 <= ts < 1e12:\n # Already in seconds\n pass\n elif 1e12 <= ts < 1e15:\n # Milliseconds — convert to seconds\n ts = ts / 1000.0\n else:\n # Not a plausible Unix timestamp\n return None\n\n try:\n dt = datetime.fromtimestamp(ts, tz=UTC)\n return dt.strftime(\"%Y-%m-%d\")\n except (OSError, OverflowError, ValueError):\n return None\n# #endregion _normalize_timestamp_value\n\n\n# #region _quote_identifier [TYPE Function]\n# @BRIEF Quote an identifier per dialect rules. PostgreSQL uses double quotes; ClickHouse uses backticks.\n# @PRE: identifier is a non-empty string.\n# @POST: Returns safely quoted identifier.\ndef _quote_identifier(identifier: str, dialect: str) -> str:\n \"\"\"Quote a SQL identifier per dialect rules.\"\"\"\n if not identifier:\n return identifier\n # Remove any existing quotes to avoid double-quoting\n cleaned = identifier.strip().strip('\"').strip('`').strip('[]')\n if dialect in POSTGRESQL_DIALECTS:\n return f'\"{cleaned}\"'\n elif dialect in CLICKHOUSE_DIALECTS:\n return f\"`{cleaned}`\"\n else:\n # Generic ANSI double-quote\n return f'\"{cleaned}\"'\n# #endregion _quote_identifier\n\n\n# #region _encode_sql_value [C:2] [TYPE Function] [SEMANTICS translate,sql,encoding]\n# @BRIEF Encode a Python value into a SQL-safe literal for INSERT VALUES, with ClickHouse timestamp normalization.\ndef _encode_sql_value(value: Any, dialect: str | None = None) -> str:\n \"\"\"Encode a Python value into a SQL-safe literal.\n\n For ClickHouse dialect, attempts to detect Unix timestamp strings\n and convert them to 'YYYY-MM-DD' format for Date column compatibility.\n \"\"\"\n if value is None:\n return \"NULL\"\n if isinstance(value, bool):\n return \"TRUE\" if value else \"FALSE\"\n\n # For ClickHouse: try to normalize timestamp-like values (both string and numeric)\n if dialect in CLICKHOUSE_DIALECTS:\n if (isinstance(value, str) and value) or isinstance(value, (int, float)):\n normalized = _normalize_timestamp_value(value)\n if normalized:\n return f\"'{normalized}'\"\n\n if isinstance(value, (int, float)):\n return str(value)\n # String — escape single quotes by doubling them\n escaped = str(value).replace(\"'\", \"''\")\n return f\"'{escaped}'\"\n# #endregion _encode_sql_value\n\n\n# #region _build_values_clause [C:2] [TYPE Function] [SEMANTICS translate,sql,values]\n# @BRIEF Build a VALUES clause for multiple rows with per-column value encoding and dialect-aware quoting.\ndef _build_values_clause(columns: list[str], rows: list[dict[str, Any]], dialect: str | None = None) -> str:\n \"\"\"Build VALUES (...) clause for multiple rows.\n\n NOTE: columns may be quoted (e.g. '\"col\"' or '`col`') for the SQL column list,\n but row dicts have UNQUOTED keys. Strip quotes before value lookup.\n \"\"\"\n value_groups = []\n for row in rows:\n values = []\n for col in columns:\n # Strip quoting for value lookup — row keys are unquoted\n lookup_key = col.strip('\"').strip('`').strip('[]')\n val = row.get(lookup_key)\n values.append(_encode_sql_value(val, dialect=dialect))\n value_groups.append(f\"({', '.join(values)})\")\n return \",\\n\".join(value_groups)\n# #endregion _build_values_clause\n\n\n# #region generate_insert_sql [C:3] [TYPE Function] [SEMANTICS translate,sql,insert]\n# @BRIEF Generate a dialect-aware plain INSERT SQL statement for the given table, columns, and rows.\n# @RELATION DEPENDS_ON -> [_quote_identifier]\n# @RELATION DEPENDS_ON -> [_build_values_clause]\ndef generate_insert_sql(\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n dialect: str | None = None,\n) -> str:\n \"\"\"Generate a plain INSERT SQL.\"\"\"\n with belief_scope(\"generate_insert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for INSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for INSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for INSERT SQL generation\")\n\n col_list = \", \".join(columns)\n values = _build_values_clause(columns, rows, dialect=dialect)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n sql = f\"INSERT INTO {table_ref} ({col_list})\\nVALUES\\n{values};\"\n return sql\n# #endregion generate_insert_sql\n\n\n# #region generate_upsert_sql [TYPE Function]\n# @BRIEF Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE.\n# @PRE: dialect is postgresql-compatible. target_table, columns, key_columns are non-empty.\n# @POST: Returns UPSERT SQL string or raises ValueError.\ndef generate_upsert_sql(\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n key_columns: list[str],\n rows: list[dict[str, Any]],\n) -> str:\n \"\"\"Generate INSERT ... ON CONFLICT (key_cols) DO UPDATE SET ... SQL.\"\"\"\n with belief_scope(\"generate_upsert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for UPSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for UPSERT SQL generation\")\n if not key_columns:\n raise ValueError(\"key_columns are required for UPSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for UPSERT SQL generation\")\n\n col_list = \", \".join(columns)\n key_list = \", \".join(key_columns)\n values = _build_values_clause(columns, rows)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n # Build SET clause: exclude key columns from update\n update_cols = [c for c in columns if c not in key_columns]\n if not update_cols:\n # If only key columns, use DO NOTHING\n conflict_action = \"DO NOTHING\"\n else:\n set_parts = [f\"{col} = EXCLUDED.{col}\" for col in update_cols]\n conflict_action = \"DO UPDATE SET\\n\" + \",\\n\".join(set_parts)\n\n sql = (\n f\"INSERT INTO {table_ref} ({col_list})\\n\"\n f\"VALUES\\n\"\n f\"{values}\\n\"\n f\"ON CONFLICT ({key_list}) {conflict_action};\"\n )\n return sql\n# #endregion generate_upsert_sql\n\n\n# #region SQLGenerator [C:3] [TYPE Class]\n# @BRIEF Generate safe, dialect-appropriate SQL INSERT/UPSERT statements.\n# @PRE: Job has target_schema, target_table, key columns configured.\n# @POST: Returns generated SQL string for the target dialect.\nclass SQLGenerator:\n\n # region SQLGenerator.generate [TYPE Function]\n # @PURPOSE: Generate SQL for a set of rows, detecting dialect from the job configuration.\n # @PRE: dialect is a supported database dialect. columns list is non-empty. rows is non-empty.\n # @POST: Returns tuple of (sql_string, statement_count).\n # @SIDE_EFFECT: None — pure SQL generation.\n @staticmethod\n def generate(\n dialect: str,\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n key_columns: list[str] | None = None,\n upsert_strategy: str = \"MERGE\",\n ) -> tuple[str, int]:\n \"\"\"Generate dialect-appropriate INSERT/UPSERT SQL.\n\n Args:\n dialect: Target database dialect (e.g. 'postgresql', 'clickhouse').\n target_schema: Optional schema name.\n target_table: Target table name.\n columns: List of column names to insert.\n rows: List of row dicts with column values.\n key_columns: Key columns for conflict resolution (UPSERT).\n upsert_strategy: 'MERGE' (UPSERT), 'INSERT' (plain INSERT).\n\n Returns:\n Tuple of (sql_string, row_count).\n \"\"\"\n with belief_scope(\"SQLGenerator.generate\"):\n logger.reason(\"Generating SQL\", {\n \"dialect\": dialect,\n \"schema\": target_schema,\n \"table\": target_table,\n \"columns\": len(columns),\n \"rows\": len(rows),\n \"strategy\": upsert_strategy,\n })\n\n # Validate inputs\n if not target_table:\n raise ValueError(\"target_table is required\")\n if not columns:\n raise ValueError(\"At least one column is required\")\n if not rows:\n raise ValueError(\"At least one row is required\")\n\n # Build fully qualified table reference\n table_ref = target_table\n if target_schema:\n quoted_schema = _quote_identifier(target_schema, dialect)\n quoted_table = _quote_identifier(target_table, dialect)\n table_ref = f\"{quoted_schema}.{quoted_table}\"\n else:\n table_ref = _quote_identifier(target_table, dialect)\n\n # Quote columns per dialect\n quoted_columns = [_quote_identifier(c, dialect) for c in columns]\n quoted_key_columns = (\n [_quote_identifier(k, dialect) for k in key_columns]\n if key_columns\n else []\n )\n\n # Generate SQL per dialect and strategy\n use_upsert = upsert_strategy.upper() == \"MERGE\" and key_columns\n\n if dialect in POSTGRESQL_DIALECTS or dialect not in CLICKHOUSE_DIALECTS:\n # PostgreSQL and other ANSI dialects: support UPSERT via ON CONFLICT\n if use_upsert:\n sql = generate_upsert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n key_columns=quoted_key_columns,\n rows=rows,\n )\n else:\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n elif dialect in CLICKHOUSE_DIALECTS:\n # ClickHouse: plain INSERT, no ON CONFLICT support\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n if use_upsert:\n logger.reason(\"ClickHouse UPSERT not supported; using plain INSERT\", {\n \"note\": \"ClickHouse does not support ON CONFLICT. Use ReplacingMergeTree for dedup.\",\n })\n else:\n # Fallback: plain INSERT\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n\n logger.reflect(\"SQL generated\", {\n \"dialect\": dialect,\n \"row_count\": len(rows),\n \"sql_length\": len(sql),\n })\n return sql, len(rows)\n # endregion SQLGenerator.generate\n\n # region SQLGenerator.generate_batch [TYPE Function]\n # @PURPOSE: Generate separate INSERT statements for each row (batch-safe version).\n # @PRE: Same as generate().\n # @POST: Returns list of (sql_string, row_index) tuples.\n @staticmethod\n def generate_batch(\n dialect: str,\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n key_columns: list[str] | None = None,\n upsert_strategy: str = \"MERGE\",\n max_rows_per_statement: int = 500,\n ) -> list[tuple[str, int]]:\n \"\"\"Generate SQL in batches, splitting large row sets into multiple statements.\n\n Returns:\n List of (sql_string, row_count) tuples.\n \"\"\"\n with belief_scope(\"SQLGenerator.generate_batch\"):\n if not rows:\n return []\n\n statements = []\n for i in range(0, len(rows), max_rows_per_statement):\n chunk = rows[i:i + max_rows_per_statement]\n sql, count = SQLGenerator.generate(\n dialect=dialect,\n target_schema=target_schema,\n target_table=target_table,\n columns=columns,\n rows=chunk,\n key_columns=key_columns,\n upsert_strategy=upsert_strategy,\n )\n statements.append((sql, count))\n\n return statements\n # endregion SQLGenerator.generate_batch\n\n\n# #endregion SQLGenerator\n# #endregion SQLGenerator\n" }, { "contract_id": "_normalize_timestamp_value", "contract_type": "Function", "file_path": "backend/src/plugins/translate/sql_generator.py", - "start_line": 23, - "end_line": 57, + "start_line": 24, + "end_line": 58, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -69256,31 +53195,60 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region _normalize_timestamp_value [C:2] [TYPE Function] [SEMANTICS translate,sql,timestamp]\n# @BRIEF Detect Unix timestamp strings (seconds or millis) and convert to 'YYYY-MM-DD' for Date columns.\ndef _normalize_timestamp_value(value: Any) -> Optional[str]:\n \"\"\"Detect Unix timestamp values and convert to date string.\n\n Handles:\n - Integer/float Unix timestamps (seconds or milliseconds)\n - String representations of Unix timestamps (e.g. '1726358400000.0')\n\n Returns 'YYYY-MM-DD' if conversion succeeds, None if value is not a timestamp.\n \"\"\"\n # Try numeric conversion first\n try:\n ts = float(value)\n except (ValueError, TypeError):\n return None\n\n # Heuristic: Unix timestamps in seconds are ~10 digits (1e9 range for 2001-2033)\n # Unix timestamps in milliseconds are ~13 digits (1e12 range for 2001-2033)\n if 1e9 <= ts < 1e12:\n # Already in seconds\n pass\n elif 1e12 <= ts < 1e15:\n # Milliseconds — convert to seconds\n ts = ts / 1000.0\n else:\n # Not a plausible Unix timestamp\n return None\n\n try:\n dt = datetime.fromtimestamp(ts, tz=timezone.utc)\n return dt.strftime(\"%Y-%m-%d\")\n except (OSError, OverflowError, ValueError):\n return None\n# #endregion _normalize_timestamp_value\n" + "body": "# #region _normalize_timestamp_value [C:2] [TYPE Function] [SEMANTICS translate,sql,timestamp]\n# @BRIEF Detect Unix timestamp strings (seconds or millis) and convert to 'YYYY-MM-DD' for Date columns.\ndef _normalize_timestamp_value(value: Any) -> str | None:\n \"\"\"Detect Unix timestamp values and convert to date string.\n\n Handles:\n - Integer/float Unix timestamps (seconds or milliseconds)\n - String representations of Unix timestamps (e.g. '1726358400000.0')\n\n Returns 'YYYY-MM-DD' if conversion succeeds, None if value is not a timestamp.\n \"\"\"\n # Try numeric conversion first\n try:\n ts = float(value)\n except (ValueError, TypeError):\n return None\n\n # Heuristic: Unix timestamps in seconds are ~10 digits (1e9 range for 2001-2033)\n # Unix timestamps in milliseconds are ~13 digits (1e12 range for 2001-2033)\n if 1e9 <= ts < 1e12:\n # Already in seconds\n pass\n elif 1e12 <= ts < 1e15:\n # Milliseconds — convert to seconds\n ts = ts / 1000.0\n else:\n # Not a plausible Unix timestamp\n return None\n\n try:\n dt = datetime.fromtimestamp(ts, tz=UTC)\n return dt.strftime(\"%Y-%m-%d\")\n except (OSError, OverflowError, ValueError):\n return None\n# #endregion _normalize_timestamp_value\n" }, { "contract_id": "_quote_identifier", "contract_type": "Function", "file_path": "backend/src/plugins/translate/sql_generator.py", - "start_line": 60, - "end_line": 75, + "start_line": 61, + "end_line": 78, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "COMPLEXITY": 2, - "PURPOSE": "Quote a SQL identifier per dialect rules — double quotes for PostgreSQL, backticks for ClickHouse." + "POST": "Returns safely quoted identifier.", + "PRE": "identifier is a non-empty string.", + "PURPOSE": "Quote an identifier per dialect rules. PostgreSQL uses double quotes; ClickHouse uses backticks." }, "relations": [], - "schema_warnings": [], + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], "anchor_syntax": "region", - "body": "# #region _quote_identifier [C:2] [TYPE Function] [SEMANTICS translate,sql,quoting]\n# @BRIEF Quote a SQL identifier per dialect rules — double quotes for PostgreSQL, backticks for ClickHouse.\ndef _quote_identifier(identifier: str, dialect: str) -> str:\n \"\"\"Quote a SQL identifier per dialect rules.\"\"\"\n if not identifier:\n return identifier\n # Remove any existing quotes to avoid double-quoting\n cleaned = identifier.strip().strip('\"').strip('`').strip('[]')\n if dialect in POSTGRESQL_DIALECTS:\n return f'\"{cleaned}\"'\n elif dialect in CLICKHOUSE_DIALECTS:\n return f\"`{cleaned}`\"\n else:\n # Generic ANSI double-quote\n return f'\"{cleaned}\"'\n# #endregion _quote_identifier\n" + "body": "# #region _quote_identifier [TYPE Function]\n# @BRIEF Quote an identifier per dialect rules. PostgreSQL uses double quotes; ClickHouse uses backticks.\n# @PRE: identifier is a non-empty string.\n# @POST: Returns safely quoted identifier.\ndef _quote_identifier(identifier: str, dialect: str) -> str:\n \"\"\"Quote a SQL identifier per dialect rules.\"\"\"\n if not identifier:\n return identifier\n # Remove any existing quotes to avoid double-quoting\n cleaned = identifier.strip().strip('\"').strip('`').strip('[]')\n if dialect in POSTGRESQL_DIALECTS:\n return f'\"{cleaned}\"'\n elif dialect in CLICKHOUSE_DIALECTS:\n return f\"`{cleaned}`\"\n else:\n # Generic ANSI double-quote\n return f'\"{cleaned}\"'\n# #endregion _quote_identifier\n" }, { "contract_id": "_encode_sql_value", "contract_type": "Function", "file_path": "backend/src/plugins/translate/sql_generator.py", - "start_line": 78, - "end_line": 102, + "start_line": 81, + "end_line": 106, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -69290,14 +53258,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region _encode_sql_value [C:2] [TYPE Function] [SEMANTICS translate,sql,encoding]\n# @BRIEF Encode a Python value into a SQL-safe literal for INSERT VALUES, with ClickHouse timestamp normalization.\ndef _encode_sql_value(value: Any, dialect: Optional[str] = None) -> str:\n \"\"\"Encode a Python value into a SQL-safe literal.\n\n For ClickHouse dialect, attempts to detect Unix timestamp strings\n and convert them to 'YYYY-MM-DD' format for Date column compatibility.\n \"\"\"\n if value is None:\n return \"NULL\"\n if isinstance(value, bool):\n return \"TRUE\" if value else \"FALSE\"\n\n # For ClickHouse: try to normalize timestamp-like string values\n if dialect in CLICKHOUSE_DIALECTS and isinstance(value, str) and value:\n normalized = _normalize_timestamp_value(value)\n if normalized:\n return f\"'{normalized}'\"\n\n if isinstance(value, (int, float)):\n return str(value)\n # String — escape single quotes by doubling them\n escaped = str(value).replace(\"'\", \"''\")\n return f\"'{escaped}'\"\n# #endregion _encode_sql_value\n" + "body": "# #region _encode_sql_value [C:2] [TYPE Function] [SEMANTICS translate,sql,encoding]\n# @BRIEF Encode a Python value into a SQL-safe literal for INSERT VALUES, with ClickHouse timestamp normalization.\ndef _encode_sql_value(value: Any, dialect: str | None = None) -> str:\n \"\"\"Encode a Python value into a SQL-safe literal.\n\n For ClickHouse dialect, attempts to detect Unix timestamp strings\n and convert them to 'YYYY-MM-DD' format for Date column compatibility.\n \"\"\"\n if value is None:\n return \"NULL\"\n if isinstance(value, bool):\n return \"TRUE\" if value else \"FALSE\"\n\n # For ClickHouse: try to normalize timestamp-like values (both string and numeric)\n if dialect in CLICKHOUSE_DIALECTS:\n if (isinstance(value, str) and value) or isinstance(value, (int, float)):\n normalized = _normalize_timestamp_value(value)\n if normalized:\n return f\"'{normalized}'\"\n\n if isinstance(value, (int, float)):\n return str(value)\n # String — escape single quotes by doubling them\n escaped = str(value).replace(\"'\", \"''\")\n return f\"'{escaped}'\"\n# #endregion _encode_sql_value\n" }, { "contract_id": "_build_values_clause", "contract_type": "Function", "file_path": "backend/src/plugins/translate/sql_generator.py", - "start_line": 105, - "end_line": 123, + "start_line": 109, + "end_line": 127, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -69307,14 +53275,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region _build_values_clause [C:2] [TYPE Function] [SEMANTICS translate,sql,values]\n# @BRIEF Build a VALUES clause for multiple rows with per-column value encoding and dialect-aware quoting.\ndef _build_values_clause(columns: List[str], rows: List[Dict[str, Any]], dialect: Optional[str] = None) -> str:\n \"\"\"Build VALUES (...) clause for multiple rows.\n\n NOTE: columns may be quoted (e.g. '\"col\"' or '`col`') for the SQL column list,\n but row dicts have UNQUOTED keys. Strip quotes before value lookup.\n \"\"\"\n value_groups = []\n for row in rows:\n values = []\n for col in columns:\n # Strip quoting for value lookup — row keys are unquoted\n lookup_key = col.strip('\"').strip('`').strip('[]')\n val = row.get(lookup_key)\n values.append(_encode_sql_value(val, dialect=dialect))\n value_groups.append(f\"({', '.join(values)})\")\n return \",\\n\".join(value_groups)\n# #endregion _build_values_clause\n" + "body": "# #region _build_values_clause [C:2] [TYPE Function] [SEMANTICS translate,sql,values]\n# @BRIEF Build a VALUES clause for multiple rows with per-column value encoding and dialect-aware quoting.\ndef _build_values_clause(columns: list[str], rows: list[dict[str, Any]], dialect: str | None = None) -> str:\n \"\"\"Build VALUES (...) clause for multiple rows.\n\n NOTE: columns may be quoted (e.g. '\"col\"' or '`col`') for the SQL column list,\n but row dicts have UNQUOTED keys. Strip quotes before value lookup.\n \"\"\"\n value_groups = []\n for row in rows:\n values = []\n for col in columns:\n # Strip quoting for value lookup — row keys are unquoted\n lookup_key = col.strip('\"').strip('`').strip('[]')\n val = row.get(lookup_key)\n values.append(_encode_sql_value(val, dialect=dialect))\n value_groups.append(f\"({', '.join(values)})\")\n return \",\\n\".join(value_groups)\n# #endregion _build_values_clause\n" }, { "contract_id": "generate_insert_sql", "contract_type": "Function", "file_path": "backend/src/plugins/translate/sql_generator.py", - "start_line": 126, - "end_line": 155, + "start_line": 130, + "end_line": 159, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -69337,108 +53305,68 @@ ], "schema_warnings": [], "anchor_syntax": "region", - "body": "# #region generate_insert_sql [C:3] [TYPE Function] [SEMANTICS translate,sql,insert]\n# @BRIEF Generate a dialect-aware plain INSERT SQL statement for the given table, columns, and rows.\n# @RELATION DEPENDS_ON -> [_quote_identifier]\n# @RELATION DEPENDS_ON -> [_build_values_clause]\ndef generate_insert_sql(\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n rows: List[Dict[str, Any]],\n dialect: Optional[str] = None,\n) -> str:\n \"\"\"Generate a plain INSERT SQL.\"\"\"\n with belief_scope(\"generate_insert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for INSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for INSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for INSERT SQL generation\")\n\n col_list = \", \".join(columns)\n values = _build_values_clause(columns, rows, dialect=dialect)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n sql = f\"INSERT INTO {table_ref} ({col_list})\\nVALUES\\n{values};\"\n return sql\n# #endregion generate_insert_sql\n" + "body": "# #region generate_insert_sql [C:3] [TYPE Function] [SEMANTICS translate,sql,insert]\n# @BRIEF Generate a dialect-aware plain INSERT SQL statement for the given table, columns, and rows.\n# @RELATION DEPENDS_ON -> [_quote_identifier]\n# @RELATION DEPENDS_ON -> [_build_values_clause]\ndef generate_insert_sql(\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n dialect: str | None = None,\n) -> str:\n \"\"\"Generate a plain INSERT SQL.\"\"\"\n with belief_scope(\"generate_insert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for INSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for INSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for INSERT SQL generation\")\n\n col_list = \", \".join(columns)\n values = _build_values_clause(columns, rows, dialect=dialect)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n sql = f\"INSERT INTO {table_ref} ({col_list})\\nVALUES\\n{values};\"\n return sql\n# #endregion generate_insert_sql\n" }, { "contract_id": "generate_upsert_sql", "contract_type": "Function", "file_path": "backend/src/plugins/translate/sql_generator.py", - "start_line": 158, - "end_line": 205, - "tier": "TIER_2", - "complexity": 3, + "start_line": 162, + "end_line": 208, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE / DO NOTHING." - }, - "relations": [ - { - "source_id": "generate_upsert_sql", - "relation_type": "DEPENDS_ON", - "target_id": "_quote_identifier", - "target_ref": "[_quote_identifier]" - }, - { - "source_id": "generate_upsert_sql", - "relation_type": "DEPENDS_ON", - "target_id": "_build_values_clause", - "target_ref": "[_build_values_clause]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region generate_upsert_sql [C:3] [TYPE Function] [SEMANTICS translate,sql,upsert]\n# @BRIEF Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE / DO NOTHING.\n# @RELATION DEPENDS_ON -> [_quote_identifier]\n# @RELATION DEPENDS_ON -> [_build_values_clause]\ndef generate_upsert_sql(\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n key_columns: List[str],\n rows: List[Dict[str, Any]],\n dialect: Optional[str] = None,\n) -> str:\n \"\"\"Generate INSERT ... ON CONFLICT (key_cols) DO UPDATE SET ... SQL.\"\"\"\n with belief_scope(\"generate_upsert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for UPSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for UPSERT SQL generation\")\n if not key_columns:\n raise ValueError(\"key_columns are required for UPSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for UPSERT SQL generation\")\n\n col_list = \", \".join(columns)\n key_list = \", \".join(key_columns)\n values = _build_values_clause(columns, rows, dialect=dialect)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n # Build SET clause: exclude key columns from update\n update_cols = [c for c in columns if c not in key_columns]\n if not update_cols:\n # If only key columns, use DO NOTHING\n conflict_action = \"DO NOTHING\"\n else:\n set_parts = [f\"{col} = EXCLUDED.{col}\" for col in update_cols]\n conflict_action = \"DO UPDATE SET\\n\" + \",\\n\".join(set_parts)\n\n sql = (\n f\"INSERT INTO {table_ref} ({col_list})\\n\"\n f\"VALUES\\n\"\n f\"{values}\\n\"\n f\"ON CONFLICT ({key_list}) {conflict_action};\"\n )\n return sql\n# #endregion generate_upsert_sql\n" - }, - { - "contract_id": "SQLGenerator.generate", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/sql_generator.py", - "start_line": 217, - "end_line": 332, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns tuple of (sql_string, statement_count).", - "PRE": "dialect is a supported database dialect. columns list is non-empty. rows is non-empty.", - "PURPOSE": "Generate SQL for a set of rows, detecting dialect from the job configuration.", - "SIDE_EFFECT": "None — pure SQL generation." + "POST": "Returns UPSERT SQL string or raises ValueError.", + "PRE": "dialect is postgresql-compatible. target_table, columns, key_columns are non-empty.", + "PURPOSE": "Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE." }, "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 4, + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Function" } } ], "anchor_syntax": "region", - "body": " # #region SQLGenerator.generate [C:4] [TYPE Function] [SEMANTICS translate,sql,generate]\n # @BRIEF Generate SQL for a set of rows, detecting dialect from the job configuration.\n # @PRE dialect is a supported database dialect. columns list is non-empty. rows is non-empty.\n # @POST Returns tuple of (sql_string, statement_count).\n # @SIDE_EFFECT None — pure SQL generation.\n @staticmethod\n def generate(\n dialect: str,\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n rows: List[Dict[str, Any]],\n key_columns: Optional[List[str]] = None,\n upsert_strategy: str = \"MERGE\",\n ) -> Tuple[str, int]:\n \"\"\"Generate dialect-appropriate INSERT/UPSERT SQL.\n\n Args:\n dialect: Target database dialect (e.g. 'postgresql', 'clickhouse').\n target_schema: Optional schema name.\n target_table: Target table name.\n columns: List of column names to insert.\n rows: List of row dicts with column values.\n key_columns: Key columns for conflict resolution (UPSERT).\n upsert_strategy: 'MERGE' (UPSERT), 'INSERT' (plain INSERT).\n\n Returns:\n Tuple of (sql_string, row_count).\n \"\"\"\n with belief_scope(\"SQLGenerator.generate\"):\n log.reason(\"Generating SQL\", payload={\n \"dialect\": dialect,\n \"schema\": target_schema,\n \"table\": target_table,\n \"columns\": len(columns),\n \"rows\": len(rows),\n \"strategy\": upsert_strategy,\n })\n\n # Validate inputs\n if not target_table:\n raise ValueError(\"target_table is required\")\n if not columns:\n raise ValueError(\"At least one column is required\")\n if not rows:\n raise ValueError(\"At least one row is required\")\n\n # Build fully qualified table reference\n table_ref = target_table\n if target_schema:\n quoted_schema = _quote_identifier(target_schema, dialect)\n quoted_table = _quote_identifier(target_table, dialect)\n table_ref = f\"{quoted_schema}.{quoted_table}\"\n else:\n table_ref = _quote_identifier(target_table, dialect)\n\n # Quote columns per dialect\n quoted_columns = [_quote_identifier(c, dialect) for c in columns]\n quoted_key_columns = (\n [_quote_identifier(k, dialect) for k in key_columns]\n if key_columns\n else []\n )\n\n # Generate SQL per dialect and strategy\n use_upsert = upsert_strategy.upper() == \"MERGE\" and key_columns\n\n if dialect in POSTGRESQL_DIALECTS or dialect not in CLICKHOUSE_DIALECTS:\n # PostgreSQL and other ANSI dialects: support UPSERT via ON CONFLICT\n if use_upsert:\n sql = generate_upsert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n key_columns=quoted_key_columns,\n rows=rows,\n dialect=dialect,\n )\n else:\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n elif dialect in CLICKHOUSE_DIALECTS:\n # ClickHouse: plain INSERT, no ON CONFLICT support\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n if use_upsert:\n log.reason(\"ClickHouse UPSERT not supported; using plain INSERT\", payload={\n \"note\": \"ClickHouse does not support ON CONFLICT. Use ReplacingMergeTree for dedup.\",\n })\n else:\n # Fallback: plain INSERT\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n\n log.reflect(\"SQL generated\", payload={\n \"dialect\": dialect,\n \"row_count\": len(rows),\n \"sql_length\": len(sql),\n })\n return sql, len(rows)\n # #endregion SQLGenerator.generate\n" - }, - { - "contract_id": "SQLGenerator.generate_batch", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/sql_generator.py", - "start_line": 334, - "end_line": 372, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": 3, - "PURPOSE": "Generate separate INSERT statements for each row chunk (batch-safe version)." - }, - "relations": [ - { - "source_id": "SQLGenerator.generate_batch", - "relation_type": "DEPENDS_ON", - "target_id": "SQLGenerator.generate", - "target_ref": "[SQLGenerator.generate]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": " # #region SQLGenerator.generate_batch [C:3] [TYPE Function] [SEMANTICS translate,sql,batch]\n # @BRIEF Generate separate INSERT statements for each row chunk (batch-safe version).\n # @RELATION DEPENDS_ON -> [SQLGenerator.generate]\n @staticmethod\n def generate_batch(\n dialect: str,\n target_schema: Optional[str],\n target_table: str,\n columns: List[str],\n rows: List[Dict[str, Any]],\n key_columns: Optional[List[str]] = None,\n upsert_strategy: str = \"MERGE\",\n max_rows_per_statement: int = 500,\n ) -> List[Tuple[str, int]]:\n \"\"\"Generate SQL in batches, splitting large row sets into multiple statements.\n\n Returns:\n List of (sql_string, row_count) tuples.\n \"\"\"\n with belief_scope(\"SQLGenerator.generate_batch\"):\n if not rows:\n return []\n\n statements = []\n for i in range(0, len(rows), max_rows_per_statement):\n chunk = rows[i:i + max_rows_per_statement]\n sql, count = SQLGenerator.generate(\n dialect=dialect,\n target_schema=target_schema,\n target_table=target_table,\n columns=columns,\n rows=chunk,\n key_columns=key_columns,\n upsert_strategy=upsert_strategy,\n )\n statements.append((sql, count))\n\n return statements\n # #endregion SQLGenerator.generate_batch\n" + "body": "# #region generate_upsert_sql [TYPE Function]\n# @BRIEF Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE.\n# @PRE: dialect is postgresql-compatible. target_table, columns, key_columns are non-empty.\n# @POST: Returns UPSERT SQL string or raises ValueError.\ndef generate_upsert_sql(\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n key_columns: list[str],\n rows: list[dict[str, Any]],\n) -> str:\n \"\"\"Generate INSERT ... ON CONFLICT (key_cols) DO UPDATE SET ... SQL.\"\"\"\n with belief_scope(\"generate_upsert_sql\"):\n if not target_table:\n raise ValueError(\"target_table is required for UPSERT SQL generation\")\n if not columns:\n raise ValueError(\"At least one column is required for UPSERT SQL generation\")\n if not key_columns:\n raise ValueError(\"key_columns are required for UPSERT SQL generation\")\n if not rows:\n raise ValueError(\"At least one row is required for UPSERT SQL generation\")\n\n col_list = \", \".join(columns)\n key_list = \", \".join(key_columns)\n values = _build_values_clause(columns, rows)\n\n table_ref = target_table\n if target_schema:\n table_ref = f\"{target_schema}.{target_table}\"\n\n # Build SET clause: exclude key columns from update\n update_cols = [c for c in columns if c not in key_columns]\n if not update_cols:\n # If only key columns, use DO NOTHING\n conflict_action = \"DO NOTHING\"\n else:\n set_parts = [f\"{col} = EXCLUDED.{col}\" for col in update_cols]\n conflict_action = \"DO UPDATE SET\\n\" + \",\\n\".join(set_parts)\n\n sql = (\n f\"INSERT INTO {table_ref} ({col_list})\\n\"\n f\"VALUES\\n\"\n f\"{values}\\n\"\n f\"ON CONFLICT ({key_list}) {conflict_action};\"\n )\n return sql\n# #endregion generate_upsert_sql\n" }, { "contract_id": "SupersetSqlLabExecutor", "contract_type": "Module", "file_path": "backend/src/plugins/translate/superset_executor.py", "start_line": 1, - "end_line": 363, - "tier": "TIER_3", - "complexity": 5, + "end_line": 420, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "COMPLEXITY": 5, - "DATA_CONTRACT": "Input[sql:str, database_id:Optional] -> Output[Dict with query_id, status, error_message]", - "INVARIANT": "Polling respects max_polls limit; timeout returns structured error.", + "COMPLEXITY": 4, "LAYER": "Infra", "POST": "SQL is submitted to Superset SQL Lab; execution reference is returned.", "PRE": "Valid Superset environment configuration and authenticated client.", - "PURPOSE": "Submit SQL to Superset SQL Lab API and poll execution status with retry.", + "PURPOSE": "Submit SQL to Superset SQL Lab API and poll execution status.", "RATIONALE": "Direct SQL Lab API submission provides audit trail and execution monitoring within Superset.", "REJECTED": "Direct database connection bypass — would skip Superset's SQL Lab audit and RBAC.", "SIDE_EFFECT": "Makes HTTP calls to Superset /api/v1/sqllab/execute/; polls status." @@ -69457,207 +53385,46 @@ "target_ref": "[ConfigManager]" } ], - "schema_warnings": [], - "anchor_syntax": "region", - "body": "# #region SupersetSqlLabExecutor [C:5] [TYPE Module] [SEMANTICS translate,superset,sqllab,execute]\n# @BRIEF Submit SQL to Superset SQL Lab API and poll execution status with retry.\n# @LAYER Infra\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Valid Superset environment configuration and authenticated client.\n# @POST SQL is submitted to Superset SQL Lab; execution reference is returned.\n# @SIDE_EFFECT Makes HTTP calls to Superset /api/v1/sqllab/execute/; polls status.\n# @DATA_CONTRACT Input[sql:str, database_id:Optional] -> Output[Dict with query_id, status, error_message]\n# @INVARIANT Polling respects max_polls limit; timeout returns structured error.\n# @RATIONALE Direct SQL Lab API submission provides audit trail and execution monitoring within Superset.\n# @REJECTED Direct database connection bypass — would skip Superset's SQL Lab audit and RBAC.\n\nfrom typing import Any, Dict, List, Optional, Tuple, Union\nfrom datetime import datetime, timezone\nimport json\nimport time\nimport uuid\n\nfrom ...core.logger import logger, belief_scope\nfrom ...core.cot_logger import MarkerLogger\nfrom ...core.config_manager import ConfigManager\nfrom ...core.superset_client import SupersetClient\n\nlog = MarkerLogger(\"SupersetExecutor\")\n\n# #region SupersetSqlLabExecutor [C:5] [TYPE Class] [SEMANTICS translate,superset,sqllab]\n# @BRIEF Submit SQL to Superset SQL Lab API with polling and status tracking.\n# @PRE Valid environment ID and ConfigManager.\n# @POST SQL submitted to Superset; execution reference recorded; polling completes.\n# @SIDE_EFFECT Makes HTTP POST to /api/v1/sqllab/execute/; polls GET for status.\n# @DATA_CONTRACT Input[env_id:str, database_id:Optional] -> Output[Dict with query_id, status]\n# @INVARIANT database_id resolved lazily; polling capped at max_polls.\nclass SupersetSqlLabExecutor:\n\n def __init__(self, config_manager: ConfigManager, env_id: str, database_id: Optional[int] = None):\n self.config_manager = config_manager\n self.env_id = env_id\n self._client: Optional[SupersetClient] = None\n self._database_id: Optional[int] = database_id\n\n # #region _get_client [C:4] [TYPE Function] [SEMANTICS translate,superset,client]\n # @BRIEF Lazy-initialize SupersetClient for the configured environment.\n # @PRE env_id must correspond to a valid environment config.\n # @POST Returns authenticated SupersetClient.\n # @SIDE_EFFECT Authenticates against Superset API on first call.\n def _get_client(self) -> SupersetClient:\n with belief_scope(\"SupersetSqlLabExecutor._get_client\"):\n if self._client is not None:\n return self._client\n\n environments = self.config_manager.get_environments()\n env_config = next((e for e in environments if e.id == self.env_id), None)\n if not env_config:\n raise ValueError(f\"Superset environment '{self.env_id}' not found\")\n\n self._client = SupersetClient(env_config)\n return self._client\n # #endregion _get_client\n\n # #region resolve_database_id [C:4] [TYPE Function] [SEMANTICS translate,database,resolve]\n # @BRIEF Resolve the target database ID from the environment by name or default.\n # @PRE Superset environment is reachable and has databases.\n # @POST Returns database_id integer or raises ValueError.\n # @SIDE_EFFECT Fetches databases list from Superset API.\n def resolve_database_id(self, database_name: Optional[str] = None) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_id\"):\n client = self._get_client()\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"database_name\"]}\n )\n if not databases:\n raise ValueError(\"No databases found in Superset environment\")\n\n if database_name:\n for db in databases:\n if db.get(\"database_name\", \"\").lower() == database_name.lower():\n self._database_id = db[\"id\"]\n log.reason(\"Resolved database ID by name\", payload={\n \"database_name\": database_name,\n \"database_id\": self._database_id,\n })\n return self._database_id\n raise ValueError(\n f\"Database '{database_name}' not found in Superset environment\"\n )\n\n # Default: use first database\n self._database_id = databases[0][\"id\"]\n log.reason(\"Using default database\", payload={\n \"database_name\": databases[0].get(\"database_name\"),\n \"database_id\": self._database_id,\n })\n return self._database_id\n # #endregion resolve_database_id\n\n # #region resolve_database_uuid [C:4] [TYPE Function] [SEMANTICS translate,database,uuid]\n # @BRIEF Resolve a database UUID to a numeric database ID from Superset.\n # @PRE uuid_str is a valid Superset database UUID.\n # @POST Returns numeric database_id or raises ValueError if not found.\n # @SIDE_EFFECT Fetches databases list from Superset API.\n def resolve_database_uuid(self, uuid_str: str) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_uuid\"):\n client = self._get_client()\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"uuid\", \"database_name\"]}\n )\n if not databases:\n raise ValueError(\"No databases found in Superset environment\")\n\n for db in databases:\n db_uuid = db.get(\"uuid\") or \"\"\n if db_uuid.lower() == uuid_str.lower():\n db_id = db[\"id\"]\n self._database_id = db_id\n log.reason(\"Resolved database ID by UUID\", payload={\n \"uuid\": uuid_str,\n \"database_id\": db_id,\n \"database_name\": db.get(\"database_name\"),\n })\n return db_id\n\n raise ValueError(\n f\"Database with UUID '{uuid_str}' not found in Superset environment\"\n )\n # #endregion resolve_database_uuid\n\n # #region execute_sql [C:4] [TYPE Function] [SEMANTICS translate,sql,execute]\n # @BRIEF Submit SQL to Superset SQL Lab and return execution tracking info.\n # @PRE sql is valid SQL string. database_id is a valid Superset DB ID.\n # @POST Returns execution result dict with query_id and status.\n # @SIDE_EFFECT Makes HTTP POST to /api/v1/sqllab/execute/.\n def execute_sql(\n self,\n sql: str,\n database_id: Optional[Union[int, str]] = None,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.execute_sql\"):\n client = self._get_client()\n db_id = database_id or self._database_id\n if db_id is None:\n db_id = self.resolve_database_id()\n # Handle UUID string passed as database_id — resolve to numeric ID\n if isinstance(db_id, str):\n try:\n db_id = int(db_id)\n except (ValueError, TypeError):\n db_id = self.resolve_database_uuid(db_id)\n\n log.reason(\"Submitting SQL to Superset SQL Lab\", payload={\n \"database_id\": db_id,\n \"sql_length\": len(sql),\n })\n\n payload = {\n \"database_id\": db_id,\n \"sql\": sql,\n \"client_id\": uuid.uuid4().hex[:10],\n \"schema\": None,\n }\n\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=\"/api/v1/sqllab/execute/\",\n data=json.dumps(payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n log.explore(\"SQL Lab execute failed\", error=str(e))\n raise ValueError(f\"Superset SQL Lab execute failed: {e}\")\n\n # Parse response — try multiple formats\n result = response if isinstance(response, dict) else {}\n query_id = (result.get(\"query_id\")\n or result.get(\"id\")\n or result.get(\"sql_lab_session_ref\")\n or (result.get(\"result\") or {}).get(\"query_id\")\n or (result.get(\"result\") or {}).get(\"id\"))\n status = result.get(\"status\", \"unknown\")\n\n log.reason(\"SQL Lab execute response\", payload={\n \"query_id\": query_id,\n \"status\": status,\n \"has_query_id\": query_id is not None,\n \"has_errors\": \"errors\" in result or \"error\" in result,\n \"response_keys\": list(result.keys()) if isinstance(result, dict) else type(result).__name__,\n \"response_preview\": str(result)[:1000] if isinstance(result, dict) else str(result)[:1000],\n })\n\n return {\n \"query_id\": query_id,\n \"status\": status,\n \"raw_response\": result,\n \"database_id\": db_id,\n }\n # #endregion execute_sql\n\n # #region poll_execution_status [C:4] [TYPE Function] [SEMANTICS translate,poll,status]\n # @BRIEF Poll Superset for SQL execution status until completion or timeout.\n # @PRE query_id is a valid Superset query ID.\n # @POST Returns final execution status dict with success/failure/timeout.\n # @SIDE_EFFECT Makes HTTP GET requests to Superset API.\n def poll_execution_status(\n self,\n query_id: str,\n max_polls: int = 60,\n poll_interval_seconds: float = 2.0,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.poll_execution_status\"):\n client = self._get_client()\n\n log.reason(\"Polling SQL execution status\", payload={\n \"query_id\": query_id,\n \"max_polls\": max_polls,\n \"interval\": poll_interval_seconds,\n })\n\n for attempt in range(max_polls):\n try:\n response = client.network.request(\n method=\"GET\",\n endpoint=f\"/api/v1/query/{query_id}\",\n )\n result = response if isinstance(response, dict) else {}\n status = result.get(\"status\", \"unknown\")\n state = result.get(\"state\", result.get(\"status\", \"\"))\n\n # Terminal states\n if state in (\"success\", \"finished\", \"completed\"):\n log.reason(\"SQL execution completed\", payload={\n \"query_id\": query_id,\n \"attempt\": attempt + 1,\n \"rows_affected\": result.get(\"rows\"),\n })\n return {\n \"query_id\": query_id,\n \"status\": \"success\",\n \"state\": state,\n \"rows_affected\": result.get(\"rows\"),\n \"error_message\": result.get(\"error_message\"),\n \"results\": result.get(\"results\"),\n \"completed_on\": result.get(\"completed_on\"),\n }\n\n if state in (\"failed\", \"error\", \"stopped\"):\n error_msg = result.get(\"error_message\", \"Unknown error\")\n log.explore(\"SQL execution failed\", error=\"SQL query execution returned failed/error state\",\n payload={\n \"query_id\": query_id,\n \"state\": state,\n \"error\": error_msg,\n })\n return {\n \"query_id\": query_id,\n \"status\": \"failed\",\n \"state\": state,\n \"error_message\": error_msg,\n \"results\": None,\n }\n\n if state in (\"pending\", \"running\", \"started\"):\n time.sleep(poll_interval_seconds)\n continue\n\n # Unknown state — treat as still running\n time.sleep(poll_interval_seconds)\n\n except Exception as e:\n log.explore(\"Polling error, retrying\", error=\"Polling encountered error, will retry\",\n payload={\n \"query_id\": query_id,\n \"attempt\": attempt + 1,\n \"error\": str(e),\n })\n time.sleep(poll_interval_seconds)\n continue\n\n # Timeout\n log.explore(\"SQL execution polling timed out\", error=\"Polling timed out\", payload={\n \"query_id\": query_id,\n \"max_polls\": max_polls,\n })\n return {\n \"query_id\": query_id,\n \"status\": \"timeout\",\n \"error_message\": f\"Polling timed out after {max_polls} attempts\",\n \"results\": None,\n }\n # #endregion poll_execution_status\n\n # #region execute_and_poll [C:4] [TYPE Function] [SEMANTICS translate,sql,execute,poll]\n # @BRIEF Execute SQL and wait for completion. One-shot convenience method.\n # @PRE sql is valid SQL.\n # @POST Returns final execution result.\n # @SIDE_EFFECT Makes HTTP calls to Superset API.\n def execute_and_poll(\n self,\n sql: str,\n database_id: Optional[Union[int, str]] = None,\n max_polls: int = 60,\n poll_interval_seconds: float = 2.0,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.execute_and_poll\"):\n exec_result = self.execute_sql(\n sql=sql,\n database_id=database_id,\n )\n\n query_id = exec_result.get(\"query_id\")\n if not query_id:\n log.explore(\"No query_id from SQL Lab execute\", error=\"No query_id returned\", payload={\n \"raw_response\": exec_result.get(\"raw_response\"),\n })\n return {\n \"status\": \"failed\",\n \"error_message\": \"No query_id returned from SQL Lab\",\n \"query_id\": None,\n }\n\n return self.poll_execution_status(\n query_id=str(query_id),\n max_polls=max_polls,\n poll_interval_seconds=poll_interval_seconds,\n )\n # #endregion execute_and_poll\n\n # #region get_query_results [C:4] [TYPE Function] [SEMANTICS translate,query,results]\n # @BRIEF Fetch the results of a completed query from Superset.\n # @PRE query_id is a valid Superset query ID.\n # @POST Returns query results if available, or error dict.\n # @SIDE_EFFECT Makes HTTP GET to Superset API.\n def get_query_results(self, query_id: str) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.get_query_results\"):\n client = self._get_client()\n try:\n response = client.network.request(\n method=\"GET\",\n endpoint=f\"/api/v1/query/{query_id}/results\",\n )\n result = response if isinstance(response, dict) else {}\n return {\n \"query_id\": query_id,\n \"status\": \"success\" if result.get(\"results\") else \"no_results\",\n \"results\": result.get(\"results\"),\n }\n except Exception as e:\n log.explore(\"Failed to fetch query results\", error=\"Failed to fetch query results from Superset\",\n payload={\n \"query_id\": query_id,\n \"error\": str(e),\n })\n return {\n \"query_id\": query_id,\n \"status\": \"failed\",\n \"error_message\": str(e),\n \"results\": None,\n }\n # #endregion get_query_results\n\n\n# #endregion SupersetSqlLabExecutor\n# #endregion SupersetSqlLabExecutor\n" - }, - { - "contract_id": "resolve_database_id", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/superset_executor.py", - "start_line": 61, - "end_line": 95, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns database_id integer or raises ValueError.", - "PRE": "Superset environment is reachable and has databases.", - "PURPOSE": "Resolve the target database ID from the environment by name or default.", - "SIDE_EFFECT": "Fetches databases list from Superset API." - }, - "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "RATIONALE", + "message": "@RATIONALE is forbidden for contract type 'Module' at C4", "detail": { "actual_complexity": 4, - "contract_type": "Function" + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "REJECTED", + "message": "@REJECTED is forbidden for contract type 'Module' at C4", + "detail": { + "actual_complexity": 4, + "contract_type": "Module" } } ], "anchor_syntax": "region", - "body": " # #region resolve_database_id [C:4] [TYPE Function] [SEMANTICS translate,database,resolve]\n # @BRIEF Resolve the target database ID from the environment by name or default.\n # @PRE Superset environment is reachable and has databases.\n # @POST Returns database_id integer or raises ValueError.\n # @SIDE_EFFECT Fetches databases list from Superset API.\n def resolve_database_id(self, database_name: Optional[str] = None) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_id\"):\n client = self._get_client()\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"database_name\"]}\n )\n if not databases:\n raise ValueError(\"No databases found in Superset environment\")\n\n if database_name:\n for db in databases:\n if db.get(\"database_name\", \"\").lower() == database_name.lower():\n self._database_id = db[\"id\"]\n log.reason(\"Resolved database ID by name\", payload={\n \"database_name\": database_name,\n \"database_id\": self._database_id,\n })\n return self._database_id\n raise ValueError(\n f\"Database '{database_name}' not found in Superset environment\"\n )\n\n # Default: use first database\n self._database_id = databases[0][\"id\"]\n log.reason(\"Using default database\", payload={\n \"database_name\": databases[0].get(\"database_name\"),\n \"database_id\": self._database_id,\n })\n return self._database_id\n # #endregion resolve_database_id\n" - }, - { - "contract_id": "resolve_database_uuid", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/superset_executor.py", - "start_line": 97, - "end_line": 126, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns numeric database_id or raises ValueError if not found.", - "PRE": "uuid_str is a valid Superset database UUID.", - "PURPOSE": "Resolve a database UUID to a numeric database ID from Superset.", - "SIDE_EFFECT": "Fetches databases list from Superset API." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region resolve_database_uuid [C:4] [TYPE Function] [SEMANTICS translate,database,uuid]\n # @BRIEF Resolve a database UUID to a numeric database ID from Superset.\n # @PRE uuid_str is a valid Superset database UUID.\n # @POST Returns numeric database_id or raises ValueError if not found.\n # @SIDE_EFFECT Fetches databases list from Superset API.\n def resolve_database_uuid(self, uuid_str: str) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_uuid\"):\n client = self._get_client()\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"uuid\", \"database_name\"]}\n )\n if not databases:\n raise ValueError(\"No databases found in Superset environment\")\n\n for db in databases:\n db_uuid = db.get(\"uuid\") or \"\"\n if db_uuid.lower() == uuid_str.lower():\n db_id = db[\"id\"]\n self._database_id = db_id\n log.reason(\"Resolved database ID by UUID\", payload={\n \"uuid\": uuid_str,\n \"database_id\": db_id,\n \"database_name\": db.get(\"database_name\"),\n })\n return db_id\n\n raise ValueError(\n f\"Database with UUID '{uuid_str}' not found in Superset environment\"\n )\n # #endregion resolve_database_uuid\n" - }, - { - "contract_id": "execute_sql", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/superset_executor.py", - "start_line": 128, - "end_line": 197, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns execution result dict with query_id and status.", - "PRE": "sql is valid SQL string. database_id is a valid Superset DB ID.", - "PURPOSE": "Submit SQL to Superset SQL Lab and return execution tracking info.", - "SIDE_EFFECT": "Makes HTTP POST to /api/v1/sqllab/execute/." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region execute_sql [C:4] [TYPE Function] [SEMANTICS translate,sql,execute]\n # @BRIEF Submit SQL to Superset SQL Lab and return execution tracking info.\n # @PRE sql is valid SQL string. database_id is a valid Superset DB ID.\n # @POST Returns execution result dict with query_id and status.\n # @SIDE_EFFECT Makes HTTP POST to /api/v1/sqllab/execute/.\n def execute_sql(\n self,\n sql: str,\n database_id: Optional[Union[int, str]] = None,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.execute_sql\"):\n client = self._get_client()\n db_id = database_id or self._database_id\n if db_id is None:\n db_id = self.resolve_database_id()\n # Handle UUID string passed as database_id — resolve to numeric ID\n if isinstance(db_id, str):\n try:\n db_id = int(db_id)\n except (ValueError, TypeError):\n db_id = self.resolve_database_uuid(db_id)\n\n log.reason(\"Submitting SQL to Superset SQL Lab\", payload={\n \"database_id\": db_id,\n \"sql_length\": len(sql),\n })\n\n payload = {\n \"database_id\": db_id,\n \"sql\": sql,\n \"client_id\": uuid.uuid4().hex[:10],\n \"schema\": None,\n }\n\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=\"/api/v1/sqllab/execute/\",\n data=json.dumps(payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n log.explore(\"SQL Lab execute failed\", error=str(e))\n raise ValueError(f\"Superset SQL Lab execute failed: {e}\")\n\n # Parse response — try multiple formats\n result = response if isinstance(response, dict) else {}\n query_id = (result.get(\"query_id\")\n or result.get(\"id\")\n or result.get(\"sql_lab_session_ref\")\n or (result.get(\"result\") or {}).get(\"query_id\")\n or (result.get(\"result\") or {}).get(\"id\"))\n status = result.get(\"status\", \"unknown\")\n\n log.reason(\"SQL Lab execute response\", payload={\n \"query_id\": query_id,\n \"status\": status,\n \"has_query_id\": query_id is not None,\n \"has_errors\": \"errors\" in result or \"error\" in result,\n \"response_keys\": list(result.keys()) if isinstance(result, dict) else type(result).__name__,\n \"response_preview\": str(result)[:1000] if isinstance(result, dict) else str(result)[:1000],\n })\n\n return {\n \"query_id\": query_id,\n \"status\": status,\n \"raw_response\": result,\n \"database_id\": db_id,\n }\n # #endregion execute_sql\n" - }, - { - "contract_id": "poll_execution_status", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/superset_executor.py", - "start_line": 199, - "end_line": 290, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns final execution status dict with success/failure/timeout.", - "PRE": "query_id is a valid Superset query ID.", - "PURPOSE": "Poll Superset for SQL execution status until completion or timeout.", - "SIDE_EFFECT": "Makes HTTP GET requests to Superset API." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region poll_execution_status [C:4] [TYPE Function] [SEMANTICS translate,poll,status]\n # @BRIEF Poll Superset for SQL execution status until completion or timeout.\n # @PRE query_id is a valid Superset query ID.\n # @POST Returns final execution status dict with success/failure/timeout.\n # @SIDE_EFFECT Makes HTTP GET requests to Superset API.\n def poll_execution_status(\n self,\n query_id: str,\n max_polls: int = 60,\n poll_interval_seconds: float = 2.0,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.poll_execution_status\"):\n client = self._get_client()\n\n log.reason(\"Polling SQL execution status\", payload={\n \"query_id\": query_id,\n \"max_polls\": max_polls,\n \"interval\": poll_interval_seconds,\n })\n\n for attempt in range(max_polls):\n try:\n response = client.network.request(\n method=\"GET\",\n endpoint=f\"/api/v1/query/{query_id}\",\n )\n result = response if isinstance(response, dict) else {}\n status = result.get(\"status\", \"unknown\")\n state = result.get(\"state\", result.get(\"status\", \"\"))\n\n # Terminal states\n if state in (\"success\", \"finished\", \"completed\"):\n log.reason(\"SQL execution completed\", payload={\n \"query_id\": query_id,\n \"attempt\": attempt + 1,\n \"rows_affected\": result.get(\"rows\"),\n })\n return {\n \"query_id\": query_id,\n \"status\": \"success\",\n \"state\": state,\n \"rows_affected\": result.get(\"rows\"),\n \"error_message\": result.get(\"error_message\"),\n \"results\": result.get(\"results\"),\n \"completed_on\": result.get(\"completed_on\"),\n }\n\n if state in (\"failed\", \"error\", \"stopped\"):\n error_msg = result.get(\"error_message\", \"Unknown error\")\n log.explore(\"SQL execution failed\", error=\"SQL query execution returned failed/error state\",\n payload={\n \"query_id\": query_id,\n \"state\": state,\n \"error\": error_msg,\n })\n return {\n \"query_id\": query_id,\n \"status\": \"failed\",\n \"state\": state,\n \"error_message\": error_msg,\n \"results\": None,\n }\n\n if state in (\"pending\", \"running\", \"started\"):\n time.sleep(poll_interval_seconds)\n continue\n\n # Unknown state — treat as still running\n time.sleep(poll_interval_seconds)\n\n except Exception as e:\n log.explore(\"Polling error, retrying\", error=\"Polling encountered error, will retry\",\n payload={\n \"query_id\": query_id,\n \"attempt\": attempt + 1,\n \"error\": str(e),\n })\n time.sleep(poll_interval_seconds)\n continue\n\n # Timeout\n log.explore(\"SQL execution polling timed out\", error=\"Polling timed out\", payload={\n \"query_id\": query_id,\n \"max_polls\": max_polls,\n })\n return {\n \"query_id\": query_id,\n \"status\": \"timeout\",\n \"error_message\": f\"Polling timed out after {max_polls} attempts\",\n \"results\": None,\n }\n # #endregion poll_execution_status\n" - }, - { - "contract_id": "execute_and_poll", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/superset_executor.py", - "start_line": 292, - "end_line": 326, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns final execution result.", - "PRE": "sql is valid SQL.", - "PURPOSE": "Execute SQL and wait for completion. One-shot convenience method.", - "SIDE_EFFECT": "Makes HTTP calls to Superset API." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region execute_and_poll [C:4] [TYPE Function] [SEMANTICS translate,sql,execute,poll]\n # @BRIEF Execute SQL and wait for completion. One-shot convenience method.\n # @PRE sql is valid SQL.\n # @POST Returns final execution result.\n # @SIDE_EFFECT Makes HTTP calls to Superset API.\n def execute_and_poll(\n self,\n sql: str,\n database_id: Optional[Union[int, str]] = None,\n max_polls: int = 60,\n poll_interval_seconds: float = 2.0,\n ) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.execute_and_poll\"):\n exec_result = self.execute_sql(\n sql=sql,\n database_id=database_id,\n )\n\n query_id = exec_result.get(\"query_id\")\n if not query_id:\n log.explore(\"No query_id from SQL Lab execute\", error=\"No query_id returned\", payload={\n \"raw_response\": exec_result.get(\"raw_response\"),\n })\n return {\n \"status\": \"failed\",\n \"error_message\": \"No query_id returned from SQL Lab\",\n \"query_id\": None,\n }\n\n return self.poll_execution_status(\n query_id=str(query_id),\n max_polls=max_polls,\n poll_interval_seconds=poll_interval_seconds,\n )\n # #endregion execute_and_poll\n" - }, - { - "contract_id": "get_query_results", - "contract_type": "Function", - "file_path": "backend/src/plugins/translate/superset_executor.py", - "start_line": 328, - "end_line": 359, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": 4, - "POST": "Returns query results if available, or error dict.", - "PRE": "query_id is a valid Superset query ID.", - "PURPOSE": "Fetch the results of a completed query from Superset.", - "SIDE_EFFECT": "Makes HTTP GET to Superset API." - }, - "relations": [], - "schema_warnings": [ - { - "code": "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", - "body": " # #region get_query_results [C:4] [TYPE Function] [SEMANTICS translate,query,results]\n # @BRIEF Fetch the results of a completed query from Superset.\n # @PRE query_id is a valid Superset query ID.\n # @POST Returns query results if available, or error dict.\n # @SIDE_EFFECT Makes HTTP GET to Superset API.\n def get_query_results(self, query_id: str) -> Dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.get_query_results\"):\n client = self._get_client()\n try:\n response = client.network.request(\n method=\"GET\",\n endpoint=f\"/api/v1/query/{query_id}/results\",\n )\n result = response if isinstance(response, dict) else {}\n return {\n \"query_id\": query_id,\n \"status\": \"success\" if result.get(\"results\") else \"no_results\",\n \"results\": result.get(\"results\"),\n }\n except Exception as e:\n log.explore(\"Failed to fetch query results\", error=\"Failed to fetch query results from Superset\",\n payload={\n \"query_id\": query_id,\n \"error\": str(e),\n })\n return {\n \"query_id\": query_id,\n \"status\": \"failed\",\n \"error_message\": str(e),\n \"results\": None,\n }\n # #endregion get_query_results\n" + "body": "# #region SupersetSqlLabExecutor [C:4] [TYPE Module] [SEMANTICS translate, superset, sqllab, execute, query]\n# @BRIEF Submit SQL to Superset SQL Lab API and poll execution status.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE: Valid Superset environment configuration and authenticated client.\n# @POST: SQL is submitted to Superset SQL Lab; execution reference is returned.\n# @SIDE_EFFECT: Makes HTTP calls to Superset /api/v1/sqllab/execute/; polls status.\n# @RATIONALE: Direct SQL Lab API submission provides audit trail and execution monitoring within Superset.\n# @REJECTED: Direct database connection bypass — would skip Superset's SQL Lab audit and RBAC.\n\nimport json\nimport time\nimport uuid\nfrom typing import Any\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\n\n\n# #region SupersetSqlLabExecutor [C:4] [TYPE Class]\n# @BRIEF Submit SQL to Superset SQL Lab API with polling and status tracking.\n# @PRE: Valid environment ID and ConfigManager.\n# @POST: SQL submitted to Superset; execution reference recorded.\n# @SIDE_EFFECT: Makes HTTP POST to /api/v1/sqllab/execute/; polls GET for status.\nclass SupersetSqlLabExecutor:\n\n def __init__(self, config_manager: ConfigManager, env_id: str):\n self.config_manager = config_manager\n self.env_id = env_id\n self._client: SupersetClient | None = None\n self._database_id: int | None = None\n self._database_backend: str | None = None\n\n # region _get_client [TYPE Function]\n # @PURPOSE: Lazy-initialize SupersetClient for the configured environment.\n # @PRE: env_id must correspond to a valid environment config.\n # @POST: Returns authenticated SupersetClient.\n # @SIDE_EFFECT: Authenticates against Superset API.\n def _get_client(self) -> SupersetClient:\n with belief_scope(\"SupersetSqlLabExecutor._get_client\"):\n if self._client is not None:\n return self._client\n\n environments = self.config_manager.get_environments()\n env_config = next((e for e in environments if e.id == self.env_id), None)\n if not env_config:\n raise ValueError(f\"Superset environment '{self.env_id}' not found\")\n\n self._client = SupersetClient(env_config)\n return self._client\n # endregion _get_client\n\n # region resolve_database_id [TYPE Function]\n # @PURPOSE: Resolve the target database ID from the environment and fetch its backend engine.\n # @PRE: database_name or database_id should be known.\n # @POST: Returns database_id integer or raises ValueError. Also sets self._database_backend.\n # @SIDE_EFFECT: Fetches databases list from Superset; optionally fetches single DB for backend.\n def resolve_database_id(\n self,\n database_name: str | None = None,\n target_database_id: str | None = None,\n ) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_id\"):\n client = self._get_client()\n\n # If a specific target_database_id is provided, use it directly\n if target_database_id:\n try:\n db_id = int(target_database_id)\n # Fetch full DB info to get backend\n db_info = client.get_database(db_id)\n result = db_info.get(\"result\", db_info)\n self._database_id = result.get(\"id\", db_id)\n self._database_backend = result.get(\"backend\") or result.get(\"engine\")\n logger.reason(\"Resolved database ID by target_database_id\", {\n \"target_database_id\": target_database_id,\n \"database_id\": self._database_id,\n \"backend\": self._database_backend,\n })\n return self._database_id\n except Exception as e:\n logger.explore(\"Failed to resolve by target_database_id, falling back\", {\n \"target_database_id\": target_database_id,\n \"error\": str(e),\n })\n # Fall through to default resolution\n\n # Fetch full database list with backend info\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"database_name\", \"backend\"]}\n )\n if not databases:\n raise ValueError(\"No databases found in Superset environment\")\n\n if database_name:\n for db in databases:\n if db.get(\"database_name\", \"\").lower() == database_name.lower():\n self._database_id = db[\"id\"]\n self._database_backend = db.get(\"backend\")\n logger.reason(\"Resolved database ID by name\", {\n \"database_name\": database_name,\n \"database_id\": self._database_id,\n \"backend\": self._database_backend,\n })\n return self._database_id\n raise ValueError(\n f\"Database '{database_name}' not found in Superset environment\"\n )\n\n # Default: use first database\n self._database_id = databases[0][\"id\"]\n self._database_backend = databases[0].get(\"backend\")\n logger.reason(\"Using default database\", {\n \"database_name\": databases[0].get(\"database_name\"),\n \"database_id\": self._database_id,\n \"backend\": self._database_backend,\n })\n return self._database_id\n # endregion resolve_database_id\n\n # region get_database_backend [TYPE Function]\n # @PURPOSE: Return the cached database backend/engine string.\n # @POST: Returns backend string or None if not yet resolved.\n def get_database_backend(self) -> str | None:\n return self._database_backend\n # endregion get_database_backend\n\n # region execute_sql [TYPE Function]\n # @PURPOSE: Submit SQL to Superset SQL Lab and return execution tracking info.\n # @PRE: sql is valid SQL string. database_id is a valid Superset DB ID.\n # @POST: Returns execution result dict with query_id and status.\n # @SIDE_EFFECT: Makes HTTP POST to /api/v1/sqllab/execute/.\n def execute_sql(\n self,\n sql: str,\n database_id: int | None = None,\n run_async: bool = True,\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.execute_sql\"):\n client = self._get_client()\n db_id = database_id or self._database_id\n if not db_id:\n db_id = self.resolve_database_id()\n\n logger.reason(\"Submitting SQL to Superset SQL Lab\", {\n \"database_id\": db_id,\n \"sql_length\": len(sql),\n \"run_async\": run_async,\n })\n\n payload = {\n \"database_id\": db_id,\n \"sql\": sql,\n \"runAsync\": run_async,\n \"schema\": None,\n \"tab\": \"translation-insert\",\n \"client_id\": f\"trl-{uuid.uuid4().hex[:7]}\", # max 11 chars for Superset varchar(11)\n }\n\n # Try multiple SQL Lab endpoints — some Superset versions use /sqllab/execute/,\n # others use /sql_lab/execute/ (with underscore)\n candidate_endpoints = [\"/api/v1/sqllab/execute/\", \"/api/v1/sql_lab/execute/\"]\n response = None\n last_error = None\n for endpoint in candidate_endpoints:\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=endpoint,\n data=json.dumps(payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n if isinstance(response, dict) and response:\n logger.reason(\"SQL Lab endpoint succeeded\", extra={\n \"endpoint\": endpoint,\n })\n break\n except Exception as ep_err:\n last_error = ep_err\n logger.explore(\"SQL Lab endpoint failed, trying next\", extra={\n \"error\": str(ep_err),\n \"endpoint\": endpoint,\n })\n response = None\n\n if response is None:\n error_msg = f\"All SQL Lab endpoints failed. Last error: {last_error}\"\n logger.explore(\"SQL Lab execute failed\", extra={\"error\": error_msg})\n raise ValueError(f\"Superset SQL Lab execute failed: {error_msg}\")\n\n # Parse response — try multiple known Superset response formats\n result = response if isinstance(response, dict) else {}\n\n # Superset may return query_id at top level, nested in \"result\", as \"id\", or in \"query\" block\n query_id = (\n result.get(\"query_id\")\n or result.get(\"id\")\n or (result.get(\"result\") or {}).get(\"query_id\")\n or (result.get(\"result\") or {}).get(\"id\")\n or (result.get(\"query\") or {}).get(\"query_id\")\n or (result.get(\"query\") or {}).get(\"id\")\n )\n status = result.get(\"status\", \"unknown\")\n\n # Log full response for debugging if query_id is missing\n if not query_id:\n logger.explore(\"No query_id from SQL Lab execute\", extra={\n \"database_id\": db_id,\n \"status\": status,\n \"raw_response_keys\": list(result.keys()),\n \"raw_response_preview\": json.dumps(result)[:2000],\n })\n logger.reason(\"SQL Lab execute response (no query_id)\", extra={\n \"database_id\": db_id,\n \"status\": status,\n \"response_keys\": list(result.keys()),\n \"has_result\": \"result\" in result,\n \"has_query\": \"query\" in result,\n \"raw_preview\": json.dumps(result)[:2000],\n })\n\n logger.reason(\"SQL Lab execute response\", {\n \"query_id\": query_id,\n \"status\": status,\n \"response_keys\": list(result.keys()),\n })\n\n return {\n \"query_id\": query_id,\n \"status\": status,\n \"raw_response\": result,\n \"database_id\": db_id,\n }\n # endregion execute_sql\n\n # region poll_execution_status [TYPE Function]\n # @PURPOSE: Poll Superset for SQL execution status until completion or timeout.\n # @PRE: query_id is a valid Superset query ID.\n # @POST: Returns final execution status dict.\n # @SIDE_EFFECT: Makes HTTP GET requests to Superset.\n def poll_execution_status(\n self,\n query_id: str,\n max_polls: int = 60,\n poll_interval_seconds: float = 2.0,\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.poll_execution_status\"):\n client = self._get_client()\n\n logger.reason(\"Polling SQL execution status\", {\n \"query_id\": query_id,\n \"max_polls\": max_polls,\n \"interval\": poll_interval_seconds,\n })\n\n for attempt in range(max_polls):\n try:\n response = client.network.request(\n method=\"GET\",\n endpoint=f\"/api/v1/query/{query_id}\",\n )\n raw = response if isinstance(response, dict) else {}\n # Superset wraps query data in a nested \"result\" field\n result = raw.get(\"result\", raw)\n status = result.get(\"status\", \"unknown\")\n state = result.get(\"state\", result.get(\"status\", \"\"))\n\n # Terminal states\n if state in (\"success\", \"finished\", \"completed\"):\n logger.reason(\"SQL execution completed\", {\n \"query_id\": query_id,\n \"attempt\": attempt + 1,\n \"rows_affected\": result.get(\"rows\"),\n })\n return {\n \"query_id\": query_id,\n \"status\": \"success\",\n \"state\": state,\n \"rows_affected\": result.get(\"rows\"),\n \"error_message\": result.get(\"error_message\"),\n \"results\": result.get(\"results\"),\n \"completed_on\": result.get(\"completed_on\"),\n }\n\n if state in (\"failed\", \"error\", \"stopped\"):\n error_msg = result.get(\"error_message\", \"Unknown error\")\n logger.explore(\"SQL execution failed\", {\n \"query_id\": query_id,\n \"state\": state,\n \"error\": error_msg,\n })\n return {\n \"query_id\": query_id,\n \"status\": \"failed\",\n \"state\": state,\n \"error_message\": error_msg,\n \"results\": None,\n }\n\n if state in (\"pending\", \"running\", \"started\"):\n time.sleep(poll_interval_seconds)\n continue\n\n # Unknown state — treat as still running\n time.sleep(poll_interval_seconds)\n\n except Exception as e:\n logger.explore(\"Polling error, retrying\", {\n \"query_id\": query_id,\n \"attempt\": attempt + 1,\n \"error\": str(e),\n })\n time.sleep(poll_interval_seconds)\n continue\n\n # Timeout\n logger.explore(\"SQL execution polling timed out\", {\n \"query_id\": query_id,\n \"max_polls\": max_polls,\n })\n return {\n \"query_id\": query_id,\n \"status\": \"timeout\",\n \"error_message\": f\"Polling timed out after {max_polls} attempts\",\n \"results\": None,\n }\n # endregion poll_execution_status\n\n # region execute_and_poll [TYPE Function]\n # @PURPOSE: Execute SQL and wait for completion. One-shot convenience method.\n # @PRE: sql is valid SQL.\n # @POST: Returns final execution result.\n # @SIDE_EFFECT: Makes HTTP calls to Superset API.\n def execute_and_poll(\n self,\n sql: str,\n database_id: int | None = None,\n max_polls: int = 60,\n poll_interval_seconds: float = 2.0,\n ) -> dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.execute_and_poll\"):\n exec_result = self.execute_sql(\n sql=sql,\n database_id=database_id,\n run_async=False, # Sync mode — Superset returns result directly\n )\n\n status = exec_result.get(\"status\", \"unknown\")\n\n # Sync mode: response already has the final status and data\n if status == \"success\":\n logger.reason(\"SQL execution completed (sync)\", {\n \"query_id\": exec_result.get(\"query_id\"),\n })\n return {\n \"query_id\": exec_result.get(\"query_id\"),\n \"status\": \"success\",\n \"state\": \"success\",\n \"rows_affected\": exec_result.get(\"raw_response\", {}).get(\"query\", {}).get(\"rows\"),\n \"error_message\": None,\n \"results\": exec_result.get(\"raw_response\", {}).get(\"data\"),\n \"completed_on\": exec_result.get(\"raw_response\", {}).get(\"query\", {}).get(\"endDttm\"),\n }\n\n # For async mode (fallback): poll for completion\n query_id = exec_result.get(\"query_id\")\n if not query_id:\n logger.explore(\"No query_id from SQL Lab execute\", {\n \"raw_response\": exec_result.get(\"raw_response\"),\n })\n return {\n \"status\": \"failed\",\n \"error_message\": \"No query_id returned from SQL Lab\",\n \"query_id\": None,\n }\n\n return self.poll_execution_status(\n query_id=str(query_id),\n max_polls=max_polls,\n poll_interval_seconds=poll_interval_seconds,\n )\n # endregion execute_and_poll\n\n # region get_query_results [TYPE Function]\n # @PURPOSE: Fetch the results of a completed query.\n # @PRE: query_id is a valid Superset query ID.\n # @POST: Returns query results if available.\n # @SIDE_EFFECT: Makes HTTP GET to Superset.\n def get_query_results(self, query_id: str) -> dict[str, Any]:\n with belief_scope(\"SupersetSqlLabExecutor.get_query_results\"):\n client = self._get_client()\n try:\n response = client.network.request(\n method=\"GET\",\n endpoint=f\"/api/v1/query/{query_id}/results\",\n )\n result = response if isinstance(response, dict) else {}\n return {\n \"query_id\": query_id,\n \"status\": \"success\" if result.get(\"results\") else \"no_results\",\n \"results\": result.get(\"results\"),\n }\n except Exception as e:\n logger.explore(\"Failed to fetch query results\", {\n \"query_id\": query_id,\n \"error\": str(e),\n })\n return {\n \"query_id\": query_id,\n \"status\": \"failed\",\n \"error_message\": str(e),\n \"results\": None,\n }\n # endregion get_query_results\n\n\n# #endregion SupersetSqlLabExecutor\n# #endregion SupersetSqlLabExecutor\n" }, { "contract_id": "SchemasPackage", "contract_type": "Package", "file_path": "backend/src/schemas/__init__.py", "start_line": 1, - "end_line": 2, + "end_line": 3, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "API schema package root." }, "relations": [], "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Package'", + "tag": "PURPOSE", + "message": "@PURPOSE is not allowed for contract type 'Package'", "detail": { "actual_type": "Package", "allowed_types": [ @@ -69666,6 +53433,7 @@ "Class", "Component", "Block", + "ADR", "Skill", "Agent" ] @@ -69673,8 +53441,8 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Package' at C1", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Package' at C1", "detail": { "actual_complexity": 1, "contract_type": "Package" @@ -69682,57 +53450,14 @@ } ], "anchor_syntax": "region", - "body": "# #region SchemasPackage [C:1] [TYPE Package]\n# #endregion SchemasPackage\n" - }, - { - "contract_id": "TestSettingsAndHealthSchemas", - "contract_type": "Module", - "file_path": "backend/src/schemas/__tests__/test_settings_and_health_schemas.py", - "start_line": 1, - "end_line": 88, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Regression tests for settings and health schema contracts updated in 026 fix batch." - }, - "relations": [ - { - "source_id": "TestSettingsAndHealthSchemas", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestSettingsAndHealthSchemas:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @PURPOSE: Regression tests for settings and health schema contracts updated in 026 fix batch.\n\nimport pytest\nfrom pydantic import ValidationError\n\nfrom src.schemas.health import DashboardHealthItem\nfrom src.schemas.settings import ValidationPolicyCreate\n\n\n# [DEF:test_validation_policy_create_accepts_structured_custom_channels:Function]\n# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas\n# @PURPOSE: Ensure policy schema accepts structured custom channel objects with type/target fields.\ndef test_validation_policy_create_accepts_structured_custom_channels():\n payload = {\n \"name\": \"Daily Health\",\n \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"10\", \"11\"],\n \"schedule_days\": [0, 1, 2],\n \"window_start\": \"01:00:00\",\n \"window_end\": \"03:00:00\",\n \"notify_owners\": True,\n \"custom_channels\": [{\"type\": \"SLACK\", \"target\": \"#alerts\"}],\n \"alert_condition\": \"FAIL_ONLY\",\n }\n\n policy = ValidationPolicyCreate(**payload)\n\n assert policy.custom_channels is not None\n assert len(policy.custom_channels) == 1\n assert policy.custom_channels[0].type == \"SLACK\"\n assert policy.custom_channels[0].target == \"#alerts\"\n# [/DEF:test_validation_policy_create_accepts_structured_custom_channels:Function]\n\n\n# [DEF:test_validation_policy_create_rejects_legacy_string_custom_channels:Function]\n# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas\n# @PURPOSE: Ensure legacy list[str] custom channel payload is rejected by typed channel contract.\ndef test_validation_policy_create_rejects_legacy_string_custom_channels():\n payload = {\n \"name\": \"Daily Health\",\n \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"10\"],\n \"schedule_days\": [0],\n \"window_start\": \"01:00:00\",\n \"window_end\": \"02:00:00\",\n \"notify_owners\": False,\n \"custom_channels\": [\"SLACK:#alerts\"],\n }\n\n with pytest.raises(ValidationError):\n ValidationPolicyCreate(**payload)\n# [/DEF:test_validation_policy_create_rejects_legacy_string_custom_channels:Function]\n\n\n# [DEF:test_dashboard_health_item_status_accepts_only_whitelisted_values:Function]\n# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas\n# @PURPOSE: Verify strict grouped regex only accepts PASS/WARN/FAIL/UNKNOWN exact statuses.\ndef test_dashboard_health_item_status_accepts_only_whitelisted_values():\n valid = DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"PASS\",\n last_check=\"2026-03-10T10:00:00\",\n )\n assert valid.status == \"PASS\"\n\n with pytest.raises(ValidationError):\n DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"PASSING\",\n last_check=\"2026-03-10T10:00:00\",\n )\n\n with pytest.raises(ValidationError):\n DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"FAIL \",\n last_check=\"2026-03-10T10:00:00\",\n )\n# [/DEF:test_dashboard_health_item_status_accepts_only_whitelisted_values:Function]\n\n\n# [/DEF:TestSettingsAndHealthSchemas:Module]\n" + "body": "# #region SchemasPackage [TYPE Package] [SEMANTICS schema, package, init]\n# @BRIEF API schema package root.\n# #endregion SchemasPackage\n" }, { "contract_id": "test_validation_policy_create_accepts_structured_custom_channels", "contract_type": "Function", "file_path": "backend/src/schemas/__tests__/test_settings_and_health_schemas.py", - "start_line": 13, - "end_line": 35, + "start_line": 11, + "end_line": 31, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -69757,15 +53482,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_validation_policy_create_accepts_structured_custom_channels:Function]\n# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas\n# @PURPOSE: Ensure policy schema accepts structured custom channel objects with type/target fields.\ndef test_validation_policy_create_accepts_structured_custom_channels():\n payload = {\n \"name\": \"Daily Health\",\n \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"10\", \"11\"],\n \"schedule_days\": [0, 1, 2],\n \"window_start\": \"01:00:00\",\n \"window_end\": \"03:00:00\",\n \"notify_owners\": True,\n \"custom_channels\": [{\"type\": \"SLACK\", \"target\": \"#alerts\"}],\n \"alert_condition\": \"FAIL_ONLY\",\n }\n\n policy = ValidationPolicyCreate(**payload)\n\n assert policy.custom_channels is not None\n assert len(policy.custom_channels) == 1\n assert policy.custom_channels[0].type == \"SLACK\"\n assert policy.custom_channels[0].target == \"#alerts\"\n# [/DEF:test_validation_policy_create_accepts_structured_custom_channels:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_validation_policy_create_accepts_structured_custom_channels [TYPE Function]\n# @RELATION BINDS_TO -> TestSettingsAndHealthSchemas\n# @BRIEF Ensure policy schema accepts structured custom channel objects with type/target fields.\ndef test_validation_policy_create_accepts_structured_custom_channels():\n payload = {\n \"name\": \"Daily Health\",\n \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"10\", \"11\"],\n \"schedule_days\": [0, 1, 2],\n \"window_start\": \"01:00:00\",\n \"window_end\": \"03:00:00\",\n \"notify_owners\": True,\n \"custom_channels\": [{\"type\": \"SLACK\", \"target\": \"#alerts\"}],\n \"alert_condition\": \"FAIL_ONLY\",\n }\n policy = ValidationPolicyCreate(**payload)\n assert policy.custom_channels is not None\n assert len(policy.custom_channels) == 1\n assert policy.custom_channels[0].type == \"SLACK\"\n assert policy.custom_channels[0].target == \"#alerts\"\n# #endregion test_validation_policy_create_accepts_structured_custom_channels\n" }, { "contract_id": "test_validation_policy_create_rejects_legacy_string_custom_channels", "contract_type": "Function", "file_path": "backend/src/schemas/__tests__/test_settings_and_health_schemas.py", - "start_line": 38, - "end_line": 55, + "start_line": 32, + "end_line": 48, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -69790,15 +53515,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_validation_policy_create_rejects_legacy_string_custom_channels:Function]\n# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas\n# @PURPOSE: Ensure legacy list[str] custom channel payload is rejected by typed channel contract.\ndef test_validation_policy_create_rejects_legacy_string_custom_channels():\n payload = {\n \"name\": \"Daily Health\",\n \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"10\"],\n \"schedule_days\": [0],\n \"window_start\": \"01:00:00\",\n \"window_end\": \"02:00:00\",\n \"notify_owners\": False,\n \"custom_channels\": [\"SLACK:#alerts\"],\n }\n\n with pytest.raises(ValidationError):\n ValidationPolicyCreate(**payload)\n# [/DEF:test_validation_policy_create_rejects_legacy_string_custom_channels:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_validation_policy_create_rejects_legacy_string_custom_channels [TYPE Function]\n# @RELATION BINDS_TO -> TestSettingsAndHealthSchemas\n# @BRIEF Ensure legacy list[str] custom channel payload is rejected by typed channel contract.\ndef test_validation_policy_create_rejects_legacy_string_custom_channels():\n payload = {\n \"name\": \"Daily Health\",\n \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"10\"],\n \"schedule_days\": [0],\n \"window_start\": \"01:00:00\",\n \"window_end\": \"02:00:00\",\n \"notify_owners\": False,\n \"custom_channels\": [\"SLACK:#alerts\"],\n }\n with pytest.raises(ValidationError):\n ValidationPolicyCreate(**payload)\n# #endregion test_validation_policy_create_rejects_legacy_string_custom_channels\n" }, { "contract_id": "test_dashboard_health_item_status_accepts_only_whitelisted_values", "contract_type": "Function", "file_path": "backend/src/schemas/__tests__/test_settings_and_health_schemas.py", - "start_line": 58, - "end_line": 85, + "start_line": 49, + "end_line": 74, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -69823,29 +53548,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:test_dashboard_health_item_status_accepts_only_whitelisted_values:Function]\n# @RELATION: BINDS_TO -> TestSettingsAndHealthSchemas\n# @PURPOSE: Verify strict grouped regex only accepts PASS/WARN/FAIL/UNKNOWN exact statuses.\ndef test_dashboard_health_item_status_accepts_only_whitelisted_values():\n valid = DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"PASS\",\n last_check=\"2026-03-10T10:00:00\",\n )\n assert valid.status == \"PASS\"\n\n with pytest.raises(ValidationError):\n DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"PASSING\",\n last_check=\"2026-03-10T10:00:00\",\n )\n\n with pytest.raises(ValidationError):\n DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"FAIL \",\n last_check=\"2026-03-10T10:00:00\",\n )\n# [/DEF:test_dashboard_health_item_status_accepts_only_whitelisted_values:Function]\n" + "anchor_syntax": "region", + "body": "# #region test_dashboard_health_item_status_accepts_only_whitelisted_values [TYPE Function]\n# @RELATION BINDS_TO -> TestSettingsAndHealthSchemas\n# @BRIEF Verify strict grouped regex only accepts PASS/WARN/FAIL/UNKNOWN exact statuses.\ndef test_dashboard_health_item_status_accepts_only_whitelisted_values():\n valid = DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"PASS\",\n last_check=\"2026-03-10T10:00:00\",\n )\n assert valid.status == \"PASS\"\n with pytest.raises(ValidationError):\n DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"PASSING\",\n last_check=\"2026-03-10T10:00:00\",\n )\n with pytest.raises(ValidationError):\n DashboardHealthItem(\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"FAIL \",\n last_check=\"2026-03-10T10:00:00\",\n )\n# #endregion test_dashboard_health_item_status_accepts_only_whitelisted_values\n" }, { "contract_id": "AuthSchemas", "contract_type": "Module", "file_path": "backend/src/schemas/auth.py", "start_line": 1, - "end_line": 164, + "end_line": 157, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Sensitive fields like password must not be included in response schemas.", "LAYER": "API", - "PURPOSE": "Pydantic schemas for authentication requests and responses.", - "SEMANTICS": [ - "auth", - "schemas", - "pydantic", - "user", - "token" - ] + "PURPOSE": "Pydantic schemas for authentication requests and responses." }, "relations": [ { @@ -69866,19 +53584,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:AuthSchemas:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: auth, schemas, pydantic, user, token\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom typing import List, Optional\nfrom pydantic import BaseModel, EmailStr\nfrom datetime import datetime\n# [/SECTION]\n\n\n# [DEF:Token:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a JWT access token response.\nclass Token(BaseModel):\n access_token: str\n token_type: str\n\n\n# [/DEF:Token:Class]\n\n\n# [DEF:TokenData:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents the data encoded in a JWT token.\nclass TokenData(BaseModel):\n username: Optional[str] = None\n scopes: List[str] = []\n\n\n# [/DEF:TokenData:Class]\n\n\n# [DEF:PermissionSchema:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: Optional[str] = None\n resource: str\n action: str\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:PermissionSchema:Class]\n\n\n# [DEF:RoleSchema:Class]\n# @PURPOSE: Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n permissions: List[PermissionSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:RoleSchema:Class]\n\n\n# [DEF:RoleCreate:Class]\n# @PURPOSE: Schema for creating a new role.\nclass RoleCreate(BaseModel):\n name: str\n description: Optional[str] = None\n permissions: List[str] = [] # List of permission IDs or \"resource:action\" strings\n\n\n# [/DEF:RoleCreate:Class]\n\n\n# [DEF:RoleUpdate:Class]\n# @PURPOSE: Schema for updating an existing role.\nclass RoleUpdate(BaseModel):\n name: Optional[str] = None\n description: Optional[str] = None\n permissions: Optional[List[str]] = None\n\n\n# [/DEF:RoleUpdate:Class]\n\n\n# [DEF:ADGroupMappingSchema:Class]\n# @PURPOSE: Represents an AD Group to Role mapping in API responses.\nclass ADGroupMappingSchema(BaseModel):\n id: str\n ad_group: str\n role_id: str\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ADGroupMappingSchema:Class]\n\n\n# [DEF:ADGroupMappingCreate:Class]\n# @PURPOSE: Schema for creating an AD Group mapping.\nclass ADGroupMappingCreate(BaseModel):\n ad_group: str\n role_id: str\n\n\n# [/DEF:ADGroupMappingCreate:Class]\n\n\n# [DEF:UserBase:Class]\n# @PURPOSE: Base schema for user data.\nclass UserBase(BaseModel):\n username: str\n email: Optional[EmailStr] = None\n is_active: bool = True\n\n\n# [/DEF:UserBase:Class]\n\n\n# [DEF:UserCreate:Class]\n# @PURPOSE: Schema for creating a new user.\nclass UserCreate(UserBase):\n password: str\n roles: List[str] = []\n\n\n# [/DEF:UserCreate:Class]\n\n\n# [DEF:UserUpdate:Class]\n# @PURPOSE: Schema for updating an existing user.\nclass UserUpdate(BaseModel):\n email: Optional[EmailStr] = None\n password: Optional[str] = None\n is_active: Optional[bool] = None\n roles: Optional[List[str]] = None\n\n\n# [/DEF:UserUpdate:Class]\n\n\n# [DEF:User:Class]\n# @PURPOSE: Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: Optional[datetime] = None\n roles: List[RoleSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:User:Class]\n\n# [/DEF:AuthSchemas:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "Token", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 18, - "end_line": 26, + "start_line": 14, + "end_line": 21, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Represents a JWT access token response." }, "relations": [], @@ -69893,19 +53611,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:Token:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a JWT access token response.\nclass Token(BaseModel):\n access_token: str\n token_type: str\n\n\n# [/DEF:Token:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "TokenData", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 29, - "end_line": 37, + "start_line": 24, + "end_line": 31, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Represents the data encoded in a JWT token." }, "relations": [], @@ -69920,19 +53638,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TokenData:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents the data encoded in a JWT token.\nclass TokenData(BaseModel):\n username: Optional[str] = None\n scopes: List[str] = []\n\n\n# [/DEF:TokenData:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "PermissionSchema", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 40, - "end_line": 52, + "start_line": 34, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Represents a permission in API responses." }, "relations": [], @@ -69947,15 +53665,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PermissionSchema:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: Optional[str] = None\n resource: str\n action: str\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:PermissionSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region PermissionSchema [C:1] [TYPE Class]\n# @BRIEF Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: str | None = None\n resource: str\n action: str\n\n class Config:\n from_attributes = True\n\n\n# #endregion PermissionSchema\n" }, { "contract_id": "RoleSchema", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 55, - "end_line": 67, + "start_line": 48, + "end_line": 60, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -69973,15 +53691,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RoleSchema:Class]\n# @PURPOSE: Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n permissions: List[PermissionSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:RoleSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region RoleSchema [TYPE Class]\n# @BRIEF Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: str | None = None\n permissions: list[PermissionSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion RoleSchema\n" }, { "contract_id": "RoleCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 70, - "end_line": 78, + "start_line": 63, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -69999,15 +53717,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RoleCreate:Class]\n# @PURPOSE: Schema for creating a new role.\nclass RoleCreate(BaseModel):\n name: str\n description: Optional[str] = None\n permissions: List[str] = [] # List of permission IDs or \"resource:action\" strings\n\n\n# [/DEF:RoleCreate:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "RoleUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 81, - "end_line": 89, + "start_line": 74, + "end_line": 82, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70025,15 +53743,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:RoleUpdate:Class]\n# @PURPOSE: Schema for updating an existing role.\nclass RoleUpdate(BaseModel):\n name: Optional[str] = None\n description: Optional[str] = None\n permissions: Optional[List[str]] = None\n\n\n# [/DEF:RoleUpdate:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ADGroupMappingSchema", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 92, - "end_line": 103, + "start_line": 85, + "end_line": 96, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70051,15 +53769,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ADGroupMappingSchema:Class]\n# @PURPOSE: Represents an AD Group to Role mapping in API responses.\nclass ADGroupMappingSchema(BaseModel):\n id: str\n ad_group: str\n role_id: str\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ADGroupMappingSchema:Class]\n" + "anchor_syntax": "region", + "body": "# #region ADGroupMappingSchema [TYPE Class]\n# @BRIEF Represents an AD Group to Role mapping in API responses.\nclass ADGroupMappingSchema(BaseModel):\n id: str\n ad_group: str\n role_id: str\n\n class Config:\n from_attributes = True\n\n\n# #endregion ADGroupMappingSchema\n" }, { "contract_id": "ADGroupMappingCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 106, - "end_line": 113, + "start_line": 99, + "end_line": 106, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70077,15 +53795,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ADGroupMappingCreate:Class]\n# @PURPOSE: Schema for creating an AD Group mapping.\nclass ADGroupMappingCreate(BaseModel):\n ad_group: str\n role_id: str\n\n\n# [/DEF:ADGroupMappingCreate:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "UserBase", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 116, - "end_line": 124, + "start_line": 109, + "end_line": 117, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70103,15 +53821,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:UserBase:Class]\n# @PURPOSE: Base schema for user data.\nclass UserBase(BaseModel):\n username: str\n email: Optional[EmailStr] = None\n is_active: bool = True\n\n\n# [/DEF:UserBase:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "UserCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 127, - "end_line": 134, + "start_line": 120, + "end_line": 127, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70129,15 +53847,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:UserCreate:Class]\n# @PURPOSE: Schema for creating a new user.\nclass UserCreate(UserBase):\n password: str\n roles: List[str] = []\n\n\n# [/DEF:UserCreate:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "UserUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 137, - "end_line": 146, + "start_line": 130, + "end_line": 139, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70155,15 +53873,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:UserUpdate:Class]\n# @PURPOSE: Schema for updating an existing user.\nclass UserUpdate(BaseModel):\n email: Optional[EmailStr] = None\n password: Optional[str] = None\n is_active: Optional[bool] = None\n roles: Optional[List[str]] = None\n\n\n# [/DEF:UserUpdate:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "User", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 149, - "end_line": 162, + "start_line": 142, + "end_line": 155, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70181,31 +53899,23 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:User:Class]\n# @PURPOSE: Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: Optional[datetime] = None\n roles: List[RoleSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:User:Class]\n" + "anchor_syntax": "region", + "body": "# #region User [TYPE Class]\n# @BRIEF Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: datetime | None = None\n roles: list[RoleSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion User\n" }, { "contract_id": "DatasetReviewSchemas", "contract_type": "Module", "file_path": "backend/src/schemas/dataset_review.py", "start_line": 1, - "end_line": 30, + "end_line": 28, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "API", "PURPOSE": "Thin facade re-exporting all dataset review API schemas from decomposed sub-modules.", "RATIONALE": "Original 419-line file exceeded INV_7 (400-line module limit). Decomposed into DTO and composite sub-modules.", - "REJECTED": "Keeping all schemas in a single file because it exceeded the fractal limit.", - "SEMANTICS": [ - "dataset_review", - "schemas", - "pydantic", - "session", - "profile", - "findings" - ] + "REJECTED": "Keeping all schemas in a single file because it exceeded the fractal limit." }, "relations": [], "schema_warnings": [ @@ -70228,19 +53938,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewSchemas:Module]\n# @COMPLEXITY: 2\n# @SEMANTICS: dataset_review, schemas, pydantic, session, profile, findings\n# @PURPOSE: Thin facade re-exporting all dataset review API schemas from decomposed sub-modules.\n# @LAYER: API\n# @RATIONALE: Original 419-line file exceeded INV_7 (400-line module limit). Decomposed into DTO and composite sub-modules.\n# @REJECTED: Keeping all schemas in a single file because it exceeded the fractal limit.\n\nfrom src.schemas.dataset_review_pkg._dtos import ( # noqa: F401\n SessionCollaboratorDto,\n DatasetProfileDto,\n ValidationFindingDto,\n SemanticSourceDto,\n SemanticCandidateDto,\n SemanticFieldEntryDto,\n ImportedFilterDto,\n TemplateVariableDto,\n ExecutionMappingDto,\n)\nfrom src.schemas.dataset_review_pkg._composites import ( # noqa: F401\n ClarificationOptionDto,\n ClarificationAnswerDto,\n ClarificationQuestionDto,\n ClarificationSessionDto,\n CompiledPreviewDto,\n DatasetRunContextDto,\n SessionSummary,\n SessionDetail,\n)\n# [/DEF:DatasetReviewSchemas:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewSchemas [C:2] [TYPE Module] [SEMANTICS dataset, review, schema, facade]\n# @BRIEF Thin facade re-exporting all dataset review API schemas from decomposed sub-modules.\n# @LAYER: API\n# @RATIONALE: Original 419-line file exceeded INV_7 (400-line module limit). Decomposed into DTO and composite sub-modules.\n# @REJECTED: Keeping all schemas in a single file because it exceeded the fractal limit.\n\nfrom src.schemas.dataset_review_pkg._composites import ( # noqa: F401\n ClarificationAnswerDto,\n ClarificationOptionDto,\n ClarificationQuestionDto,\n ClarificationSessionDto,\n CompiledPreviewDto,\n DatasetRunContextDto,\n SessionDetail,\n SessionSummary,\n)\nfrom src.schemas.dataset_review_pkg._dtos import ( # noqa: F401\n DatasetProfileDto,\n ExecutionMappingDto,\n ImportedFilterDto,\n SemanticCandidateDto,\n SemanticFieldEntryDto,\n SemanticSourceDto,\n SessionCollaboratorDto,\n TemplateVariableDto,\n ValidationFindingDto,\n)\n# #endregion DatasetReviewSchemas\n" }, { "contract_id": "DatasetReviewSchemaComposites", "contract_type": "Module", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", "start_line": 1, - "end_line": 219, + "end_line": 210, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "API", "PURPOSE": "Composite Pydantic DTOs for clarification, preview, run context, and session summary/detail responses." }, @@ -70263,19 +53973,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewSchemaComposites:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Composite Pydantic DTOs for clarification, preview, run context, and session summary/detail responses.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DatasetReviewSchemaDtos]\n\nfrom datetime import datetime\nfrom typing import Any, List, Optional\n\nfrom pydantic import BaseModel\n\nfrom src.models.dataset_review import (\n ClarificationStatus,\n QuestionState,\n AnswerKind,\n PreviewStatus,\n LaunchStatus,\n SessionStatus,\n SessionPhase,\n ReadinessState,\n RecommendedAction,\n)\nfrom src.schemas.dataset_review_pkg._dtos import (\n SessionCollaboratorDto,\n DatasetProfileDto,\n ValidationFindingDto,\n SemanticSourceDto,\n SemanticFieldEntryDto,\n ImportedFilterDto,\n TemplateVariableDto,\n ExecutionMappingDto,\n)\n\n\n# [DEF:ClarificationOptionDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Clarification option DTO.\nclass ClarificationOptionDto(BaseModel):\n option_id: str\n question_id: str\n label: str\n value: str\n is_recommended: bool\n display_order: int\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationOptionDto:Class]\n\n\n# [DEF:ClarificationAnswerDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: Optional[str] = None\n answered_by_user_id: str\n impact_summary: Optional[str] = None\n user_feedback: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationAnswerDto:Class]\n\n\n# [DEF:ClarificationQuestionDto:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Clarification question DTO with nested options and answer.\nclass ClarificationQuestionDto(BaseModel):\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: Optional[str] = None\n priority: int\n state: QuestionState\n created_at: datetime\n updated_at: datetime\n options: List[ClarificationOptionDto] = []\n answer: Optional[ClarificationAnswerDto] = None\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationQuestionDto:Class]\n\n\n# [DEF:ClarificationSessionDto:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Clarification session DTO with nested questions.\nclass ClarificationSessionDto(BaseModel):\n clarification_session_id: str\n session_id: str\n status: ClarificationStatus\n current_question_id: Optional[str] = None\n resolved_count: int\n remaining_count: int\n summary_delta: Optional[str] = None\n started_at: datetime\n updated_at: datetime\n completed_at: Optional[datetime] = None\n questions: List[ClarificationQuestionDto] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationSessionDto:Class]\n\n\n# [DEF:CompiledPreviewDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: Optional[int] = None\n preview_status: PreviewStatus\n compiled_sql: Optional[str] = None\n preview_fingerprint: str\n compiled_by: str\n error_code: Optional[str] = None\n error_details: Optional[str] = None\n compiled_at: Optional[datetime] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:CompiledPreviewDto:Class]\n\n\n# [DEF:DatasetRunContextDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Run context DTO with launch audit data and session version.\nclass DatasetRunContextDto(BaseModel):\n run_context_id: str\n session_id: str\n session_version: Optional[int] = None\n dataset_ref: str\n environment_id: str\n preview_id: str\n sql_lab_session_ref: str\n effective_filters: Any\n template_params: Any\n approved_mapping_ids: List[str]\n semantic_decision_refs: List[str]\n open_warning_refs: List[str]\n launch_status: LaunchStatus\n launch_error: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:DatasetRunContextDto:Class]\n\n\n# [DEF:SessionSummary:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Lightweight session summary DTO for list responses.\nclass SessionSummary(BaseModel):\n session_id: str\n user_id: str\n environment_id: str\n source_kind: str\n source_input: str\n dataset_ref: str\n dataset_id: Optional[int] = None\n version: int = 0\n session_version: int = 0\n readiness_state: ReadinessState\n recommended_action: RecommendedAction\n status: SessionStatus\n current_phase: SessionPhase\n created_at: datetime\n updated_at: datetime\n last_activity_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SessionSummary:Class]\n\n\n# [DEF:SessionDetail:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Full session detail DTO with all nested aggregates for detail views.\n# @RELATION: INHERITS -> [SessionSummary]\nclass SessionDetail(SessionSummary):\n collaborators: List[SessionCollaboratorDto] = []\n profile: Optional[DatasetProfileDto] = None\n findings: List[ValidationFindingDto] = []\n semantic_sources: List[SemanticSourceDto] = []\n semantic_fields: List[SemanticFieldEntryDto] = []\n imported_filters: List[ImportedFilterDto] = []\n template_variables: List[TemplateVariableDto] = []\n execution_mappings: List[ExecutionMappingDto] = []\n clarification_sessions: List[ClarificationSessionDto] = []\n previews: List[CompiledPreviewDto] = []\n run_contexts: List[DatasetRunContextDto] = []\n\n\n# [/DEF:SessionDetail:Class]\n\n\n# [/DEF:DatasetReviewSchemaComposites:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewSchemaComposites [C:2] [TYPE Module] [SEMANTICS pydantic, dataset, review, composite, schema]\n# @BRIEF Composite Pydantic DTOs for clarification, preview, run context, and session summary/detail responses.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [DatasetReviewSchemaDtos]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationStatus,\n LaunchStatus,\n PreviewStatus,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SessionPhase,\n SessionStatus,\n)\nfrom src.schemas.dataset_review_pkg._dtos import (\n DatasetProfileDto,\n ExecutionMappingDto,\n ImportedFilterDto,\n SemanticFieldEntryDto,\n SemanticSourceDto,\n SessionCollaboratorDto,\n TemplateVariableDto,\n ValidationFindingDto,\n)\n\n\n# #region ClarificationOptionDto [C:1] [TYPE Class]\n# @BRIEF Clarification option DTO.\nclass ClarificationOptionDto(BaseModel):\n option_id: str\n question_id: str\n label: str\n value: str\n is_recommended: bool\n display_order: int\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationOptionDto\n\n\n# #region ClarificationAnswerDto [C:1] [TYPE Class]\n# @BRIEF Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None = None\n answered_by_user_id: str\n impact_summary: str | None = None\n user_feedback: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationAnswerDto\n\n\n# #region ClarificationQuestionDto [C:2] [TYPE Class]\n# @BRIEF Clarification question DTO with nested options and answer.\nclass ClarificationQuestionDto(BaseModel):\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None = None\n priority: int\n state: QuestionState\n created_at: datetime\n updated_at: datetime\n options: list[ClarificationOptionDto] = []\n answer: ClarificationAnswerDto | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationQuestionDto\n\n\n# #region ClarificationSessionDto [C:2] [TYPE Class]\n# @BRIEF Clarification session DTO with nested questions.\nclass ClarificationSessionDto(BaseModel):\n clarification_session_id: str\n session_id: str\n status: ClarificationStatus\n current_question_id: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n started_at: datetime\n updated_at: datetime\n completed_at: datetime | None = None\n questions: list[ClarificationQuestionDto] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationSessionDto\n\n\n# #region CompiledPreviewDto [C:1] [TYPE Class]\n# @BRIEF Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: int | None = None\n preview_status: PreviewStatus\n compiled_sql: str | None = None\n preview_fingerprint: str\n compiled_by: str\n error_code: str | None = None\n error_details: str | None = None\n compiled_at: datetime | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion CompiledPreviewDto\n\n\n# #region DatasetRunContextDto [C:1] [TYPE Class]\n# @BRIEF Run context DTO with launch audit data and session version.\nclass DatasetRunContextDto(BaseModel):\n run_context_id: str\n session_id: str\n session_version: int | None = None\n dataset_ref: str\n environment_id: str\n preview_id: str\n sql_lab_session_ref: str\n effective_filters: Any\n template_params: Any\n approved_mapping_ids: list[str]\n semantic_decision_refs: list[str]\n open_warning_refs: list[str]\n launch_status: LaunchStatus\n launch_error: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion DatasetRunContextDto\n\n\n# #region SessionSummary [C:2] [TYPE Class]\n# @BRIEF Lightweight session summary DTO for list responses.\nclass SessionSummary(BaseModel):\n session_id: str\n user_id: str\n environment_id: str\n source_kind: str\n source_input: str\n dataset_ref: str\n dataset_id: int | None = None\n version: int = 0\n session_version: int = 0\n readiness_state: ReadinessState\n recommended_action: RecommendedAction\n status: SessionStatus\n current_phase: SessionPhase\n created_at: datetime\n updated_at: datetime\n last_activity_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SessionSummary\n\n\n# #region SessionDetail [C:2] [TYPE Class]\n# @BRIEF Full session detail DTO with all nested aggregates for detail views.\n# @RELATION INHERITS -> [SessionSummary]\nclass SessionDetail(SessionSummary):\n collaborators: list[SessionCollaboratorDto] = []\n profile: DatasetProfileDto | None = None\n findings: list[ValidationFindingDto] = []\n semantic_sources: list[SemanticSourceDto] = []\n semantic_fields: list[SemanticFieldEntryDto] = []\n imported_filters: list[ImportedFilterDto] = []\n template_variables: list[TemplateVariableDto] = []\n execution_mappings: list[ExecutionMappingDto] = []\n clarification_sessions: list[ClarificationSessionDto] = []\n previews: list[CompiledPreviewDto] = []\n run_contexts: list[DatasetRunContextDto] = []\n\n\n# #endregion SessionDetail\n\n\n# #endregion DatasetReviewSchemaComposites\n" }, { "contract_id": "ClarificationOptionDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 35, - "end_line": 50, + "start_line": 34, + "end_line": 48, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Clarification option DTO." }, "relations": [], @@ -70290,19 +54000,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationOptionDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Clarification option DTO.\nclass ClarificationOptionDto(BaseModel):\n option_id: str\n question_id: str\n label: str\n value: str\n is_recommended: bool\n display_order: int\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationOptionDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationOptionDto [C:1] [TYPE Class]\n# @BRIEF Clarification option DTO.\nclass ClarificationOptionDto(BaseModel):\n option_id: str\n question_id: str\n label: str\n value: str\n is_recommended: bool\n display_order: int\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationOptionDto\n" }, { "contract_id": "ClarificationAnswerDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 53, - "end_line": 70, + "start_line": 51, + "end_line": 67, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Clarification answer DTO with feedback." }, "relations": [], @@ -70317,53 +54027,53 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationAnswerDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: Optional[str] = None\n answered_by_user_id: str\n impact_summary: Optional[str] = None\n user_feedback: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationAnswerDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationAnswerDto [C:1] [TYPE Class]\n# @BRIEF Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None = None\n answered_by_user_id: str\n impact_summary: str | None = None\n user_feedback: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationAnswerDto\n" }, { "contract_id": "ClarificationQuestionDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 73, - "end_line": 94, + "start_line": 70, + "end_line": 90, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Clarification question DTO with nested options and answer." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationQuestionDto:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Clarification question DTO with nested options and answer.\nclass ClarificationQuestionDto(BaseModel):\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: Optional[str] = None\n priority: int\n state: QuestionState\n created_at: datetime\n updated_at: datetime\n options: List[ClarificationOptionDto] = []\n answer: Optional[ClarificationAnswerDto] = None\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationQuestionDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationQuestionDto [C:2] [TYPE Class]\n# @BRIEF Clarification question DTO with nested options and answer.\nclass ClarificationQuestionDto(BaseModel):\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None = None\n priority: int\n state: QuestionState\n created_at: datetime\n updated_at: datetime\n options: list[ClarificationOptionDto] = []\n answer: ClarificationAnswerDto | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationQuestionDto\n" }, { "contract_id": "ClarificationSessionDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 97, - "end_line": 117, + "start_line": 93, + "end_line": 112, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Clarification session DTO with nested questions." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationSessionDto:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Clarification session DTO with nested questions.\nclass ClarificationSessionDto(BaseModel):\n clarification_session_id: str\n session_id: str\n status: ClarificationStatus\n current_question_id: Optional[str] = None\n resolved_count: int\n remaining_count: int\n summary_delta: Optional[str] = None\n started_at: datetime\n updated_at: datetime\n completed_at: Optional[datetime] = None\n questions: List[ClarificationQuestionDto] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ClarificationSessionDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationSessionDto [C:2] [TYPE Class]\n# @BRIEF Clarification session DTO with nested questions.\nclass ClarificationSessionDto(BaseModel):\n clarification_session_id: str\n session_id: str\n status: ClarificationStatus\n current_question_id: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n started_at: datetime\n updated_at: datetime\n completed_at: datetime | None = None\n questions: list[ClarificationQuestionDto] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationSessionDto\n" }, { "contract_id": "CompiledPreviewDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 120, - "end_line": 140, + "start_line": 115, + "end_line": 134, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Compiled preview DTO with fingerprint and session version." }, "relations": [], @@ -70378,19 +54088,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CompiledPreviewDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: Optional[int] = None\n preview_status: PreviewStatus\n compiled_sql: Optional[str] = None\n preview_fingerprint: str\n compiled_by: str\n error_code: Optional[str] = None\n error_details: Optional[str] = None\n compiled_at: Optional[datetime] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:CompiledPreviewDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region CompiledPreviewDto [C:1] [TYPE Class]\n# @BRIEF Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: int | None = None\n preview_status: PreviewStatus\n compiled_sql: str | None = None\n preview_fingerprint: str\n compiled_by: str\n error_code: str | None = None\n error_details: str | None = None\n compiled_at: datetime | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion CompiledPreviewDto\n" }, { "contract_id": "DatasetRunContextDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 143, - "end_line": 167, + "start_line": 137, + "end_line": 160, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Run context DTO with launch audit data and session version." }, "relations": [], @@ -70405,36 +54115,36 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetRunContextDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Run context DTO with launch audit data and session version.\nclass DatasetRunContextDto(BaseModel):\n run_context_id: str\n session_id: str\n session_version: Optional[int] = None\n dataset_ref: str\n environment_id: str\n preview_id: str\n sql_lab_session_ref: str\n effective_filters: Any\n template_params: Any\n approved_mapping_ids: List[str]\n semantic_decision_refs: List[str]\n open_warning_refs: List[str]\n launch_status: LaunchStatus\n launch_error: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:DatasetRunContextDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region DatasetRunContextDto [C:1] [TYPE Class]\n# @BRIEF Run context DTO with launch audit data and session version.\nclass DatasetRunContextDto(BaseModel):\n run_context_id: str\n session_id: str\n session_version: int | None = None\n dataset_ref: str\n environment_id: str\n preview_id: str\n sql_lab_session_ref: str\n effective_filters: Any\n template_params: Any\n approved_mapping_ids: list[str]\n semantic_decision_refs: list[str]\n open_warning_refs: list[str]\n launch_status: LaunchStatus\n launch_error: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion DatasetRunContextDto\n" }, { "contract_id": "SessionSummary", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 170, - "end_line": 195, + "start_line": 163, + "end_line": 187, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Lightweight session summary DTO for list responses." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SessionSummary:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Lightweight session summary DTO for list responses.\nclass SessionSummary(BaseModel):\n session_id: str\n user_id: str\n environment_id: str\n source_kind: str\n source_input: str\n dataset_ref: str\n dataset_id: Optional[int] = None\n version: int = 0\n session_version: int = 0\n readiness_state: ReadinessState\n recommended_action: RecommendedAction\n status: SessionStatus\n current_phase: SessionPhase\n created_at: datetime\n updated_at: datetime\n last_activity_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SessionSummary:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionSummary [C:2] [TYPE Class]\n# @BRIEF Lightweight session summary DTO for list responses.\nclass SessionSummary(BaseModel):\n session_id: str\n user_id: str\n environment_id: str\n source_kind: str\n source_input: str\n dataset_ref: str\n dataset_id: int | None = None\n version: int = 0\n session_version: int = 0\n readiness_state: ReadinessState\n recommended_action: RecommendedAction\n status: SessionStatus\n current_phase: SessionPhase\n created_at: datetime\n updated_at: datetime\n last_activity_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SessionSummary\n" }, { "contract_id": "SessionDetail", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 198, - "end_line": 216, + "start_line": 190, + "end_line": 207, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Full session detail DTO with all nested aggregates for detail views." }, "relations": [ @@ -70456,19 +54166,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionDetail:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Full session detail DTO with all nested aggregates for detail views.\n# @RELATION: INHERITS -> [SessionSummary]\nclass SessionDetail(SessionSummary):\n collaborators: List[SessionCollaboratorDto] = []\n profile: Optional[DatasetProfileDto] = None\n findings: List[ValidationFindingDto] = []\n semantic_sources: List[SemanticSourceDto] = []\n semantic_fields: List[SemanticFieldEntryDto] = []\n imported_filters: List[ImportedFilterDto] = []\n template_variables: List[TemplateVariableDto] = []\n execution_mappings: List[ExecutionMappingDto] = []\n clarification_sessions: List[ClarificationSessionDto] = []\n previews: List[CompiledPreviewDto] = []\n run_contexts: List[DatasetRunContextDto] = []\n\n\n# [/DEF:SessionDetail:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionDetail [C:2] [TYPE Class]\n# @BRIEF Full session detail DTO with all nested aggregates for detail views.\n# @RELATION INHERITS -> [SessionSummary]\nclass SessionDetail(SessionSummary):\n collaborators: list[SessionCollaboratorDto] = []\n profile: DatasetProfileDto | None = None\n findings: list[ValidationFindingDto] = []\n semantic_sources: list[SemanticSourceDto] = []\n semantic_fields: list[SemanticFieldEntryDto] = []\n imported_filters: list[ImportedFilterDto] = []\n template_variables: list[TemplateVariableDto] = []\n execution_mappings: list[ExecutionMappingDto] = []\n clarification_sessions: list[ClarificationSessionDto] = []\n previews: list[CompiledPreviewDto] = []\n run_contexts: list[DatasetRunContextDto] = []\n\n\n# #endregion SessionDetail\n" }, { "contract_id": "DatasetReviewSchemaDtos", "contract_type": "Module", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", "start_line": 1, - "end_line": 262, + "end_line": 248, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "API", "PURPOSE": "Pydantic DTOs for session, profile, findings, collaborators, and semantic field API payloads." }, @@ -70491,19 +54201,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewSchemaDtos:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Pydantic DTOs for session, profile, findings, collaborators, and semantic field API payloads.\n# @LAYER: API\n# @RELATION: DEPENDS_ON -> [DatasetReviewModels]\n\nfrom datetime import datetime\nfrom typing import List, Optional, Any\n\nfrom pydantic import BaseModel\n\nfrom src.models.dataset_review import (\n SessionStatus,\n SessionPhase,\n ReadinessState,\n RecommendedAction,\n SessionCollaboratorRole,\n BusinessSummarySource,\n ConfidenceState,\n FindingArea,\n FindingSeverity,\n ResolutionState,\n SemanticSourceType,\n TrustLevel,\n SemanticSourceStatus,\n FieldKind,\n FieldProvenance,\n CandidateMatchType,\n CandidateStatus,\n FilterSource,\n FilterConfidenceState,\n FilterRecoveryStatus,\n VariableKind,\n MappingStatus,\n MappingMethod,\n MappingWarningLevel,\n ApprovalState,\n)\n\n\n# [DEF:SessionCollaboratorDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Collaborator DTO for session access control.\nclass SessionCollaboratorDto(BaseModel):\n user_id: str\n role: SessionCollaboratorRole\n added_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SessionCollaboratorDto:Class]\n\n\n# [DEF:DatasetProfileDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Dataset profile DTO with business summary and confidence metadata.\nclass DatasetProfileDto(BaseModel):\n profile_id: str\n session_id: str\n dataset_name: str\n schema_name: Optional[str] = None\n database_name: Optional[str] = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: Optional[str] = None\n dataset_type: Optional[str] = None\n is_sqllab_view: bool\n completeness_score: Optional[float] = None\n confidence_state: ConfidenceState\n has_blocking_findings: bool\n has_warning_findings: bool\n manual_summary_locked: bool\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:DatasetProfileDto:Class]\n\n\n# [DEF:ValidationFindingDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Validation finding DTO with resolution tracking.\nclass ValidationFindingDto(BaseModel):\n finding_id: str\n session_id: str\n area: FindingArea\n severity: FindingSeverity\n code: str\n title: str\n message: str\n resolution_state: ResolutionState\n resolution_note: Optional[str] = None\n caused_by_ref: Optional[str] = None\n created_at: datetime\n resolved_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ValidationFindingDto:Class]\n\n\n# [DEF:SemanticSourceDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Semantic source DTO with trust level and status.\nclass SemanticSourceDto(BaseModel):\n source_id: str\n session_id: str\n source_type: SemanticSourceType\n source_ref: str\n source_version: str\n display_name: str\n trust_level: TrustLevel\n schema_overlap_score: Optional[float] = None\n status: SemanticSourceStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SemanticSourceDto:Class]\n\n\n# [DEF:SemanticCandidateDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: Optional[str] = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: Optional[str] = None\n proposed_description: Optional[str] = None\n proposed_display_format: Optional[str] = None\n status: CandidateStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SemanticCandidateDto:Class]\n\n\n# [DEF:SemanticFieldEntryDto:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Semantic field entry DTO with nested candidates and session version.\nclass SemanticFieldEntryDto(BaseModel):\n field_id: str\n session_id: str\n session_version: Optional[int] = None\n field_name: str\n field_kind: FieldKind\n verbose_name: Optional[str] = None\n description: Optional[str] = None\n display_format: Optional[str] = None\n provenance: FieldProvenance\n source_id: Optional[str] = None\n source_version: Optional[str] = None\n confidence_rank: Optional[int] = None\n is_locked: bool\n has_conflict: bool\n needs_review: bool\n last_changed_by: str\n user_feedback: Optional[str] = None\n created_at: datetime\n updated_at: datetime\n candidates: List[SemanticCandidateDto] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SemanticFieldEntryDto:Class]\n\n\n# [DEF:ImportedFilterDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Imported filter DTO with confidence and recovery status.\nclass ImportedFilterDto(BaseModel):\n filter_id: str\n session_id: str\n filter_name: str\n display_name: Optional[str] = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Optional[Any] = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: Optional[str] = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ImportedFilterDto:Class]\n\n\n# [DEF:TemplateVariableDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Template variable DTO with mapping status.\nclass TemplateVariableDto(BaseModel):\n variable_id: str\n session_id: str\n variable_name: str\n expression_source: str\n variable_kind: VariableKind\n is_required: bool\n default_value: Optional[Any] = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:TemplateVariableDto:Class]\n\n\n# [DEF:ExecutionMappingDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: Optional[int] = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Optional[Any] = None\n transformation_note: Optional[str] = None\n warning_level: Optional[MappingWarningLevel] = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: Optional[str] = None\n approved_at: Optional[datetime] = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ExecutionMappingDto:Class]\n\n\n# [/DEF:DatasetReviewSchemaDtos:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewSchemaDtos [C:2] [TYPE Module] [SEMANTICS pydantic, dataset, review, dto, schema]\n# @BRIEF Pydantic DTOs for session, profile, findings, collaborators, and semantic field API payloads.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom src.models.dataset_review import (\n ApprovalState,\n BusinessSummarySource,\n CandidateMatchType,\n CandidateStatus,\n ConfidenceState,\n FieldKind,\n FieldProvenance,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n MappingMethod,\n MappingStatus,\n MappingWarningLevel,\n ResolutionState,\n SemanticSourceStatus,\n SemanticSourceType,\n SessionCollaboratorRole,\n TrustLevel,\n VariableKind,\n)\n\n\n# #region SessionCollaboratorDto [C:1] [TYPE Class]\n# @BRIEF Collaborator DTO for session access control.\nclass SessionCollaboratorDto(BaseModel):\n user_id: str\n role: SessionCollaboratorRole\n added_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SessionCollaboratorDto\n\n\n# #region DatasetProfileDto [C:1] [TYPE Class]\n# @BRIEF Dataset profile DTO with business summary and confidence metadata.\nclass DatasetProfileDto(BaseModel):\n profile_id: str\n session_id: str\n dataset_name: str\n schema_name: str | None = None\n database_name: str | None = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: str | None = None\n dataset_type: str | None = None\n is_sqllab_view: bool\n completeness_score: float | None = None\n confidence_state: ConfidenceState\n has_blocking_findings: bool\n has_warning_findings: bool\n manual_summary_locked: bool\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion DatasetProfileDto\n\n\n# #region ValidationFindingDto [C:1] [TYPE Class]\n# @BRIEF Validation finding DTO with resolution tracking.\nclass ValidationFindingDto(BaseModel):\n finding_id: str\n session_id: str\n area: FindingArea\n severity: FindingSeverity\n code: str\n title: str\n message: str\n resolution_state: ResolutionState\n resolution_note: str | None = None\n caused_by_ref: str | None = None\n created_at: datetime\n resolved_at: datetime | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion ValidationFindingDto\n\n\n# #region SemanticSourceDto [C:1] [TYPE Class]\n# @BRIEF Semantic source DTO with trust level and status.\nclass SemanticSourceDto(BaseModel):\n source_id: str\n session_id: str\n source_type: SemanticSourceType\n source_ref: str\n source_version: str\n display_name: str\n trust_level: TrustLevel\n schema_overlap_score: float | None = None\n status: SemanticSourceStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticSourceDto\n\n\n# #region SemanticCandidateDto [C:1] [TYPE Class]\n# @BRIEF Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: str | None = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: str | None = None\n proposed_description: str | None = None\n proposed_display_format: str | None = None\n status: CandidateStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticCandidateDto\n\n\n# #region SemanticFieldEntryDto [C:2] [TYPE Class]\n# @BRIEF Semantic field entry DTO with nested candidates and session version.\nclass SemanticFieldEntryDto(BaseModel):\n field_id: str\n session_id: str\n session_version: int | None = None\n field_name: str\n field_kind: FieldKind\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n provenance: FieldProvenance\n source_id: str | None = None\n source_version: str | None = None\n confidence_rank: int | None = None\n is_locked: bool\n has_conflict: bool\n needs_review: bool\n last_changed_by: str\n user_feedback: str | None = None\n created_at: datetime\n updated_at: datetime\n candidates: list[SemanticCandidateDto] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticFieldEntryDto\n\n\n# #region ImportedFilterDto [C:1] [TYPE Class]\n# @BRIEF Imported filter DTO with confidence and recovery status.\nclass ImportedFilterDto(BaseModel):\n filter_id: str\n session_id: str\n filter_name: str\n display_name: str | None = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Any | None = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: str | None = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ImportedFilterDto\n\n\n# #region TemplateVariableDto [C:1] [TYPE Class]\n# @BRIEF Template variable DTO with mapping status.\nclass TemplateVariableDto(BaseModel):\n variable_id: str\n session_id: str\n variable_name: str\n expression_source: str\n variable_kind: VariableKind\n is_required: bool\n default_value: Any | None = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion TemplateVariableDto\n\n\n# #region ExecutionMappingDto [C:1] [TYPE Class]\n# @BRIEF Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: int | None = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Any | None = None\n transformation_note: str | None = None\n warning_level: MappingWarningLevel | None = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: str | None = None\n approved_at: datetime | None = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ExecutionMappingDto\n\n\n# #endregion DatasetReviewSchemaDtos\n" }, { "contract_id": "SessionCollaboratorDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 41, - "end_line": 53, + "start_line": 36, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Collaborator DTO for session access control." }, "relations": [], @@ -70518,19 +54228,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionCollaboratorDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Collaborator DTO for session access control.\nclass SessionCollaboratorDto(BaseModel):\n user_id: str\n role: SessionCollaboratorRole\n added_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SessionCollaboratorDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionCollaboratorDto [C:1] [TYPE Class]\n# @BRIEF Collaborator DTO for session access control.\nclass SessionCollaboratorDto(BaseModel):\n user_id: str\n role: SessionCollaboratorRole\n added_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SessionCollaboratorDto\n" }, { "contract_id": "DatasetProfileDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 56, - "end_line": 82, + "start_line": 50, + "end_line": 75, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Dataset profile DTO with business summary and confidence metadata." }, "relations": [], @@ -70545,19 +54255,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DatasetProfileDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Dataset profile DTO with business summary and confidence metadata.\nclass DatasetProfileDto(BaseModel):\n profile_id: str\n session_id: str\n dataset_name: str\n schema_name: Optional[str] = None\n database_name: Optional[str] = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: Optional[str] = None\n dataset_type: Optional[str] = None\n is_sqllab_view: bool\n completeness_score: Optional[float] = None\n confidence_state: ConfidenceState\n has_blocking_findings: bool\n has_warning_findings: bool\n manual_summary_locked: bool\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:DatasetProfileDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region DatasetProfileDto [C:1] [TYPE Class]\n# @BRIEF Dataset profile DTO with business summary and confidence metadata.\nclass DatasetProfileDto(BaseModel):\n profile_id: str\n session_id: str\n dataset_name: str\n schema_name: str | None = None\n database_name: str | None = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: str | None = None\n dataset_type: str | None = None\n is_sqllab_view: bool\n completeness_score: float | None = None\n confidence_state: ConfidenceState\n has_blocking_findings: bool\n has_warning_findings: bool\n manual_summary_locked: bool\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion DatasetProfileDto\n" }, { "contract_id": "ValidationFindingDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 85, - "end_line": 106, + "start_line": 78, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Validation finding DTO with resolution tracking." }, "relations": [], @@ -70572,19 +54282,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationFindingDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Validation finding DTO with resolution tracking.\nclass ValidationFindingDto(BaseModel):\n finding_id: str\n session_id: str\n area: FindingArea\n severity: FindingSeverity\n code: str\n title: str\n message: str\n resolution_state: ResolutionState\n resolution_note: Optional[str] = None\n caused_by_ref: Optional[str] = None\n created_at: datetime\n resolved_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ValidationFindingDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationFindingDto [C:1] [TYPE Class]\n# @BRIEF Validation finding DTO with resolution tracking.\nclass ValidationFindingDto(BaseModel):\n finding_id: str\n session_id: str\n area: FindingArea\n severity: FindingSeverity\n code: str\n title: str\n message: str\n resolution_state: ResolutionState\n resolution_note: str | None = None\n caused_by_ref: str | None = None\n created_at: datetime\n resolved_at: datetime | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion ValidationFindingDto\n" }, { "contract_id": "SemanticSourceDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 109, - "end_line": 128, + "start_line": 101, + "end_line": 119, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Semantic source DTO with trust level and status." }, "relations": [], @@ -70599,19 +54309,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SemanticSourceDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Semantic source DTO with trust level and status.\nclass SemanticSourceDto(BaseModel):\n source_id: str\n session_id: str\n source_type: SemanticSourceType\n source_ref: str\n source_version: str\n display_name: str\n trust_level: TrustLevel\n schema_overlap_score: Optional[float] = None\n status: SemanticSourceStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SemanticSourceDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region SemanticSourceDto [C:1] [TYPE Class]\n# @BRIEF Semantic source DTO with trust level and status.\nclass SemanticSourceDto(BaseModel):\n source_id: str\n session_id: str\n source_type: SemanticSourceType\n source_ref: str\n source_version: str\n display_name: str\n trust_level: TrustLevel\n schema_overlap_score: float | None = None\n status: SemanticSourceStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticSourceDto\n" }, { "contract_id": "SemanticCandidateDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 131, - "end_line": 151, + "start_line": 122, + "end_line": 141, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Semantic candidate DTO with match type and confidence score." }, "relations": [], @@ -70626,36 +54336,36 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SemanticCandidateDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: Optional[str] = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: Optional[str] = None\n proposed_description: Optional[str] = None\n proposed_display_format: Optional[str] = None\n status: CandidateStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SemanticCandidateDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region SemanticCandidateDto [C:1] [TYPE Class]\n# @BRIEF Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: str | None = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: str | None = None\n proposed_description: str | None = None\n proposed_display_format: str | None = None\n status: CandidateStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticCandidateDto\n" }, { "contract_id": "SemanticFieldEntryDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 154, - "end_line": 183, + "start_line": 144, + "end_line": 172, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Semantic field entry DTO with nested candidates and session version." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SemanticFieldEntryDto:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Semantic field entry DTO with nested candidates and session version.\nclass SemanticFieldEntryDto(BaseModel):\n field_id: str\n session_id: str\n session_version: Optional[int] = None\n field_name: str\n field_kind: FieldKind\n verbose_name: Optional[str] = None\n description: Optional[str] = None\n display_format: Optional[str] = None\n provenance: FieldProvenance\n source_id: Optional[str] = None\n source_version: Optional[str] = None\n confidence_rank: Optional[int] = None\n is_locked: bool\n has_conflict: bool\n needs_review: bool\n last_changed_by: str\n user_feedback: Optional[str] = None\n created_at: datetime\n updated_at: datetime\n candidates: List[SemanticCandidateDto] = []\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:SemanticFieldEntryDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region SemanticFieldEntryDto [C:2] [TYPE Class]\n# @BRIEF Semantic field entry DTO with nested candidates and session version.\nclass SemanticFieldEntryDto(BaseModel):\n field_id: str\n session_id: str\n session_version: int | None = None\n field_name: str\n field_kind: FieldKind\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n provenance: FieldProvenance\n source_id: str | None = None\n source_version: str | None = None\n confidence_rank: int | None = None\n is_locked: bool\n has_conflict: bool\n needs_review: bool\n last_changed_by: str\n user_feedback: str | None = None\n created_at: datetime\n updated_at: datetime\n candidates: list[SemanticCandidateDto] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticFieldEntryDto\n" }, { "contract_id": "ImportedFilterDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 186, - "end_line": 209, + "start_line": 175, + "end_line": 197, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Imported filter DTO with confidence and recovery status." }, "relations": [], @@ -70670,19 +54380,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ImportedFilterDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Imported filter DTO with confidence and recovery status.\nclass ImportedFilterDto(BaseModel):\n filter_id: str\n session_id: str\n filter_name: str\n display_name: Optional[str] = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Optional[Any] = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: Optional[str] = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ImportedFilterDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region ImportedFilterDto [C:1] [TYPE Class]\n# @BRIEF Imported filter DTO with confidence and recovery status.\nclass ImportedFilterDto(BaseModel):\n filter_id: str\n session_id: str\n filter_name: str\n display_name: str | None = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Any | None = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: str | None = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ImportedFilterDto\n" }, { "contract_id": "TemplateVariableDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 212, - "end_line": 231, + "start_line": 200, + "end_line": 218, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Template variable DTO with mapping status." }, "relations": [], @@ -70697,19 +54407,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TemplateVariableDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Template variable DTO with mapping status.\nclass TemplateVariableDto(BaseModel):\n variable_id: str\n session_id: str\n variable_name: str\n expression_source: str\n variable_kind: VariableKind\n is_required: bool\n default_value: Optional[Any] = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:TemplateVariableDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region TemplateVariableDto [C:1] [TYPE Class]\n# @BRIEF Template variable DTO with mapping status.\nclass TemplateVariableDto(BaseModel):\n variable_id: str\n session_id: str\n variable_name: str\n expression_source: str\n variable_kind: VariableKind\n is_required: bool\n default_value: Any | None = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion TemplateVariableDto\n" }, { "contract_id": "ExecutionMappingDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 234, - "end_line": 259, + "start_line": 221, + "end_line": 245, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Execution mapping DTO with approval state and session version." }, "relations": [], @@ -70724,26 +54434,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ExecutionMappingDto:Class]\n# @COMPLEXITY: 1\n# @PURPOSE: Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: Optional[int] = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Optional[Any] = None\n transformation_note: Optional[str] = None\n warning_level: Optional[MappingWarningLevel] = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: Optional[str] = None\n approved_at: Optional[datetime] = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ExecutionMappingDto:Class]\n" + "anchor_syntax": "region", + "body": "# #region ExecutionMappingDto [C:1] [TYPE Class]\n# @BRIEF Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: int | None = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Any | None = None\n transformation_note: str | None = None\n warning_level: MappingWarningLevel | None = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: str | None = None\n approved_at: datetime | None = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ExecutionMappingDto\n" }, { "contract_id": "HealthSchemas", "contract_type": "Module", "file_path": "backend/src/schemas/health.py", "start_line": 1, - "end_line": 42, + "end_line": 40, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", - "PURPOSE": "Pydantic schemas for dashboard health summary.", - "SEMANTICS": [ - "health", - "schemas", - "pydantic" - ] + "PURPOSE": "Pydantic schemas for dashboard health summary." }, "relations": [ { @@ -70754,15 +54459,15 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:HealthSchemas:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: health, schemas, pydantic\n# @PURPOSE: Pydantic schemas for dashboard health summary.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> pydantic\n\nfrom pydantic import BaseModel, Field\nfrom typing import List, Optional\nfrom datetime import datetime\n\n\n# [DEF:DashboardHealthItem:Class]\n# @PURPOSE: Represents the latest health status of a single dashboard.\nclass DashboardHealthItem(BaseModel):\n record_id: Optional[str] = None\n dashboard_id: str\n dashboard_slug: Optional[str] = None\n dashboard_title: Optional[str] = None\n environment_id: str\n status: str = Field(..., pattern=\"^(PASS|WARN|FAIL|UNKNOWN)$\")\n last_check: datetime\n task_id: Optional[str] = None\n summary: Optional[str] = None\n\n\n# [/DEF:DashboardHealthItem:Class]\n\n\n# [DEF:HealthSummaryResponse:Class]\n# @PURPOSE: Aggregated health summary for all dashboards.\nclass HealthSummaryResponse(BaseModel):\n items: List[DashboardHealthItem]\n pass_count: int\n warn_count: int\n fail_count: int\n unknown_count: int\n\n\n# [/DEF:HealthSummaryResponse:Class]\n\n# [/DEF:HealthSchemas:Module]\n" + "anchor_syntax": "region", + "body": "# #region HealthSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, health, schema, dashboard, dashboard-health-item]\n# @BRIEF Pydantic schemas for dashboard health summary.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> pydantic\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n\n# #region DashboardHealthItem [TYPE Class]\n# @BRIEF Represents the latest health status of a single dashboard.\nclass DashboardHealthItem(BaseModel):\n record_id: str | None = None\n dashboard_id: str\n dashboard_slug: str | None = None\n dashboard_title: str | None = None\n environment_id: str\n status: str = Field(..., pattern=\"^(PASS|WARN|FAIL|UNKNOWN)$\")\n last_check: datetime\n task_id: str | None = None\n summary: str | None = None\n\n\n# #endregion DashboardHealthItem\n\n\n# #region HealthSummaryResponse [TYPE Class]\n# @BRIEF Aggregated health summary for all dashboards.\nclass HealthSummaryResponse(BaseModel):\n items: list[DashboardHealthItem]\n pass_count: int\n warn_count: int\n fail_count: int\n unknown_count: int\n\n\n# #endregion HealthSummaryResponse\n\n# #endregion HealthSchemas\n" }, { "contract_id": "DashboardHealthItem", "contract_type": "Class", "file_path": "backend/src/schemas/health.py", - "start_line": 13, - "end_line": 27, + "start_line": 11, + "end_line": 25, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70780,15 +54485,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DashboardHealthItem:Class]\n# @PURPOSE: Represents the latest health status of a single dashboard.\nclass DashboardHealthItem(BaseModel):\n record_id: Optional[str] = None\n dashboard_id: str\n dashboard_slug: Optional[str] = None\n dashboard_title: Optional[str] = None\n environment_id: str\n status: str = Field(..., pattern=\"^(PASS|WARN|FAIL|UNKNOWN)$\")\n last_check: datetime\n task_id: Optional[str] = None\n summary: Optional[str] = None\n\n\n# [/DEF:DashboardHealthItem:Class]\n" + "anchor_syntax": "region", + "body": "# #region DashboardHealthItem [TYPE Class]\n# @BRIEF Represents the latest health status of a single dashboard.\nclass DashboardHealthItem(BaseModel):\n record_id: str | None = None\n dashboard_id: str\n dashboard_slug: str | None = None\n dashboard_title: str | None = None\n environment_id: str\n status: str = Field(..., pattern=\"^(PASS|WARN|FAIL|UNKNOWN)$\")\n last_check: datetime\n task_id: str | None = None\n summary: str | None = None\n\n\n# #endregion DashboardHealthItem\n" }, { "contract_id": "HealthSummaryResponse", "contract_type": "Class", "file_path": "backend/src/schemas/health.py", - "start_line": 30, - "end_line": 40, + "start_line": 28, + "end_line": 38, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70806,33 +54511,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:HealthSummaryResponse:Class]\n# @PURPOSE: Aggregated health summary for all dashboards.\nclass HealthSummaryResponse(BaseModel):\n items: List[DashboardHealthItem]\n pass_count: int\n warn_count: int\n fail_count: int\n unknown_count: int\n\n\n# [/DEF:HealthSummaryResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region HealthSummaryResponse [TYPE Class]\n# @BRIEF Aggregated health summary for all dashboards.\nclass HealthSummaryResponse(BaseModel):\n items: list[DashboardHealthItem]\n pass_count: int\n warn_count: int\n fail_count: int\n unknown_count: int\n\n\n# #endregion HealthSummaryResponse\n" }, { "contract_id": "ProfileSchemas", "contract_type": "Module", "file_path": "backend/src/schemas/profile.py", "start_line": 1, - "end_line": 204, + "end_line": 191, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Schema shapes stay stable for profile UI states and backend preference contracts.", "LAYER": "API", - "PURPOSE": "Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup.", - "SEMANTICS": [ - "profile", - "schemas", - "pydantic", - "preferences", - "superset", - "lookup", - "security", - "git", - "ux" - ] + "PURPOSE": "Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup." }, "relations": [ { @@ -70853,19 +54547,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ProfileSchemas:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: profile, schemas, pydantic, preferences, superset, lookup, security, git, ux\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom typing import List, Literal, Optional\nfrom pydantic import BaseModel, Field\n# [/SECTION]\n\n\n# [DEF:ProfilePermissionState:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:ProfilePermissionState:Class]\n\n\n# [DEF:ProfileSecuritySummary:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = None\n current_role: Optional[str] = None\n role_source: Optional[str] = None\n roles: List[str] = Field(default_factory=list)\n permissions: List[ProfilePermissionState] = Field(default_factory=list)\n\n\n# [/DEF:ProfileSecuritySummary:Class]\n\n\n# [DEF:ProfilePreference:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Represents persisted profile preference for a single authenticated user.\n# @RELATION: DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: Optional[str] = None\n superset_username_normalized: Optional[str] = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: Optional[str] = None\n git_email: Optional[str] = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: Optional[str] = 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: Optional[str] = None\n email_address: Optional[str] = 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# [/DEF:ProfilePreference:Class]\n\n\n# [DEF:ProfilePreferenceUpdateRequest:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Request payload for updating current user's profile settings.\n# @RELATION: DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceUpdateRequest(BaseModel):\n superset_username: Optional[str] = Field(\n default=None,\n description=\"Apache Superset username bound to current user profile.\",\n )\n show_only_my_dashboards: Optional[bool] = Field(\n default=None,\n description='When true, \"/dashboards\" can auto-apply profile filter in main context.',\n )\n show_only_slug_dashboards: Optional[bool] = Field(\n default=None,\n description='When true, \"/dashboards\" hides dashboards without slug by default.',\n )\n git_username: Optional[str] = Field(\n default=None,\n description=\"Git author username used for commit signature.\",\n )\n git_email: Optional[str] = Field(\n default=None,\n description=\"Git author email used for commit signature.\",\n )\n git_personal_access_token: Optional[str] = Field(\n default=None,\n description=\"Personal Access Token value. Empty string clears existing token.\",\n )\n start_page: Optional[\n Literal[\"dashboards\", \"datasets\", \"reports\", \"reports-logs\"]\n ] = Field(\n default=None,\n description=\"Preferred start page after login.\",\n )\n auto_open_task_drawer: Optional[bool] = Field(\n default=None,\n description=\"Auto-open task drawer when long-running tasks start.\",\n )\n dashboards_table_density: Optional[Literal[\"compact\", \"comfortable\", \"free\"]] = (\n Field(\n default=None,\n description=\"Preferred table density for dashboard listings.\",\n )\n )\n telegram_id: Optional[str] = Field(\n default=None,\n description=\"Telegram ID for notifications.\",\n )\n email_address: Optional[str] = Field(\n default=None,\n description=\"Email address for notifications (overrides system email).\",\n )\n notify_on_fail: Optional[bool] = Field(\n default=None,\n description=\"Whether to send notifications on validation failure.\",\n )\n\n\n# [/DEF:ProfilePreferenceUpdateRequest:Class]\n\n\n# [DEF:ProfilePreferenceResponse:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Response envelope for profile preference read/update endpoints.\n# @RELATION: DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceResponse(BaseModel):\n status: Literal[\"success\", \"error\"] = \"success\"\n message: Optional[str] = None\n validation_errors: List[str] = Field(default_factory=list)\n preference: ProfilePreference\n security: ProfileSecuritySummary = Field(default_factory=ProfileSecuritySummary)\n\n\n# [/DEF:ProfilePreferenceResponse:Class]\n\n\n# [DEF:SupersetAccountLookupRequest:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Query contract for Superset account lookup by selected environment.\n# @RELATION: DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupRequest(BaseModel):\n environment_id: str\n search: Optional[str] = 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# [/DEF:SupersetAccountLookupRequest:Class]\n\n\n# [DEF:SupersetAccountCandidate:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = None\n email: Optional[str] = None\n is_active: Optional[bool] = None\n\n\n# [/DEF:SupersetAccountCandidate:Class]\n\n\n# [DEF:SupersetAccountLookupResponse:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = None\n items: List[SupersetAccountCandidate] = Field(default_factory=list)\n\n\n# [/DEF:SupersetAccountLookupResponse:Class]\n\n# [/DEF:ProfileSchemas:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ProfilePermissionState", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 18, - "end_line": 27, + "start_line": 15, + "end_line": 23, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Represents one permission badge state for profile read-only security view." }, "relations": [ @@ -70877,19 +54571,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ProfilePermissionState:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:ProfilePermissionState:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ProfileSecuritySummary", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 30, - "end_line": 43, + "start_line": 26, + "end_line": 38, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Read-only security and access snapshot for current user." }, "relations": [ @@ -70901,19 +54595,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ProfileSecuritySummary:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = None\n current_role: Optional[str] = None\n role_source: Optional[str] = None\n roles: List[str] = Field(default_factory=list)\n permissions: List[ProfilePermissionState] = Field(default_factory=list)\n\n\n# [/DEF:ProfileSecuritySummary:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ProfilePreference", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 46, - "end_line": 77, + "start_line": 41, + "end_line": 71, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Represents persisted profile preference for a single authenticated user." }, "relations": [ @@ -70925,19 +54619,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ProfilePreference:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Represents persisted profile preference for a single authenticated user.\n# @RELATION: DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: Optional[str] = None\n superset_username_normalized: Optional[str] = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: Optional[str] = None\n git_email: Optional[str] = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: Optional[str] = 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: Optional[str] = None\n email_address: Optional[str] = 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# [/DEF:ProfilePreference:Class]\n" + "anchor_syntax": "region", + "body": "# #region ProfilePreference [C:3] [TYPE Class]\n# @BRIEF Represents persisted profile preference for a single authenticated user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: str | None = None\n superset_username_normalized: str | None = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: str | None = None\n git_email: str | None = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: str | None = None\n\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\"] = \"dashboards\"\n auto_open_task_drawer: bool = True\n dashboards_table_density: Literal[\"compact\", \"comfortable\"] = \"comfortable\"\n\n telegram_id: str | None = None\n email_address: str | None = None\n notify_on_fail: bool = True\n\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ProfilePreference\n" }, { "contract_id": "ProfilePreferenceUpdateRequest", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 80, - "end_line": 139, + "start_line": 74, + "end_line": 130, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Request payload for updating current user's profile settings." }, "relations": [ @@ -70949,19 +54643,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ProfilePreferenceUpdateRequest:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Request payload for updating current user's profile settings.\n# @RELATION: DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceUpdateRequest(BaseModel):\n superset_username: Optional[str] = Field(\n default=None,\n description=\"Apache Superset username bound to current user profile.\",\n )\n show_only_my_dashboards: Optional[bool] = Field(\n default=None,\n description='When true, \"/dashboards\" can auto-apply profile filter in main context.',\n )\n show_only_slug_dashboards: Optional[bool] = Field(\n default=None,\n description='When true, \"/dashboards\" hides dashboards without slug by default.',\n )\n git_username: Optional[str] = Field(\n default=None,\n description=\"Git author username used for commit signature.\",\n )\n git_email: Optional[str] = Field(\n default=None,\n description=\"Git author email used for commit signature.\",\n )\n git_personal_access_token: Optional[str] = Field(\n default=None,\n description=\"Personal Access Token value. Empty string clears existing token.\",\n )\n start_page: Optional[\n Literal[\"dashboards\", \"datasets\", \"reports\", \"reports-logs\"]\n ] = Field(\n default=None,\n description=\"Preferred start page after login.\",\n )\n auto_open_task_drawer: Optional[bool] = Field(\n default=None,\n description=\"Auto-open task drawer when long-running tasks start.\",\n )\n dashboards_table_density: Optional[Literal[\"compact\", \"comfortable\", \"free\"]] = (\n Field(\n default=None,\n description=\"Preferred table density for dashboard listings.\",\n )\n )\n telegram_id: Optional[str] = Field(\n default=None,\n description=\"Telegram ID for notifications.\",\n )\n email_address: Optional[str] = Field(\n default=None,\n description=\"Email address for notifications (overrides system email).\",\n )\n notify_on_fail: Optional[bool] = Field(\n default=None,\n description=\"Whether to send notifications on validation failure.\",\n )\n\n\n# [/DEF:ProfilePreferenceUpdateRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ProfilePreferenceResponse", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 142, - "end_line": 154, + "start_line": 133, + "end_line": 144, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Response envelope for profile preference read/update endpoints." }, "relations": [ @@ -70973,19 +54667,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ProfilePreferenceResponse:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Response envelope for profile preference read/update endpoints.\n# @RELATION: DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceResponse(BaseModel):\n status: Literal[\"success\", \"error\"] = \"success\"\n message: Optional[str] = None\n validation_errors: List[str] = Field(default_factory=list)\n preference: ProfilePreference\n security: ProfileSecuritySummary = Field(default_factory=ProfileSecuritySummary)\n\n\n# [/DEF:ProfilePreferenceResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SupersetAccountLookupRequest", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 157, - "end_line": 170, + "start_line": 147, + "end_line": 159, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Query contract for Superset account lookup by selected environment." }, "relations": [ @@ -70997,19 +54691,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetAccountLookupRequest:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Query contract for Superset account lookup by selected environment.\n# @RELATION: DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupRequest(BaseModel):\n environment_id: str\n search: Optional[str] = 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# [/DEF:SupersetAccountLookupRequest:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SupersetAccountCandidate", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 173, - "end_line": 185, + "start_line": 162, + "end_line": 173, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Canonical account candidate projected from Superset users payload." }, "relations": [ @@ -71021,19 +54715,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetAccountCandidate:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = None\n email: Optional[str] = None\n is_active: Optional[bool] = None\n\n\n# [/DEF:SupersetAccountCandidate:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SupersetAccountLookupResponse", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 188, - "end_line": 202, + "start_line": 176, + "end_line": 189, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Response envelope for Superset account lookup (success or degraded mode)." }, "relations": [ @@ -71045,27 +54739,21 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SupersetAccountLookupResponse:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: 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: Optional[str] = None\n items: List[SupersetAccountCandidate] = Field(default_factory=list)\n\n\n# [/DEF:SupersetAccountLookupResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SettingsSchemas", "contract_type": "Module", "file_path": "backend/src/schemas/settings.py", "start_line": 1, - "end_line": 97, + "end_line": 117, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", - "PURPOSE": "Pydantic schemas for application settings and automation policies.", - "SEMANTICS": [ - "settings", - "schemas", - "pydantic", - "validation" - ] + "PURPOSE": "Pydantic schemas for application settings and automation policies." }, "relations": [ { @@ -71076,15 +54764,15 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SettingsSchemas:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: settings, schemas, pydantic, validation\n# @PURPOSE: Pydantic schemas for application settings and automation policies.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> pydantic\n\nfrom pydantic import BaseModel, Field\nfrom typing import List, Optional\nfrom datetime import datetime, time\n\n\n# [DEF:NotificationChannel:Class]\n# @PURPOSE: Structured notification channel definition for policy-level custom routing.\nclass NotificationChannel(BaseModel):\n type: str = Field(\n ..., description=\"Notification channel type (e.g., SLACK, SMTP, TELEGRAM)\"\n )\n target: str = Field(\n ..., description=\"Notification destination (e.g., #alerts, chat id, email)\"\n )\n\n\n# [/DEF:NotificationChannel:Class]\n\n\n# [DEF:ValidationPolicyBase:Class]\n# @PURPOSE: Base schema for validation policy data.\nclass ValidationPolicyBase(BaseModel):\n name: str = Field(..., description=\"Name of the policy\")\n environment_id: str = Field(..., description=\"Target Superset environment ID\")\n is_active: bool = Field(True, description=\"Whether the policy is currently active\")\n dashboard_ids: List[str] = Field(\n ..., description=\"List of dashboard IDs to validate\"\n )\n schedule_days: List[int] = Field(\n ..., description=\"Days of the week (0-6, 0=Sunday) to run\"\n )\n window_start: time = Field(..., description=\"Start of the execution window\")\n window_end: time = Field(..., description=\"End of the execution window\")\n notify_owners: bool = Field(\n True, description=\"Whether to notify dashboard owners on failure\"\n )\n custom_channels: Optional[List[NotificationChannel]] = Field(\n None,\n description=\"List of additional structured notification channels\",\n )\n alert_condition: str = Field(\n \"FAIL_ONLY\",\n description=\"Condition to trigger alerts: FAIL_ONLY, WARN_AND_FAIL, ALWAYS\",\n )\n\n\n# [/DEF:ValidationPolicyBase:Class]\n\n\n# [DEF:ValidationPolicyCreate:Class]\n# @PURPOSE: Schema for creating a new validation policy.\nclass ValidationPolicyCreate(ValidationPolicyBase):\n pass\n\n\n# [/DEF:ValidationPolicyCreate:Class]\n\n\n# [DEF:ValidationPolicyUpdate:Class]\n# @PURPOSE: Schema for updating an existing validation policy.\nclass ValidationPolicyUpdate(BaseModel):\n name: Optional[str] = None\n environment_id: Optional[str] = None\n is_active: Optional[bool] = None\n dashboard_ids: Optional[List[str]] = None\n schedule_days: Optional[List[int]] = None\n window_start: Optional[time] = None\n window_end: Optional[time] = None\n notify_owners: Optional[bool] = None\n custom_channels: Optional[List[NotificationChannel]] = None\n alert_condition: Optional[str] = None\n\n\n# [/DEF:ValidationPolicyUpdate:Class]\n\n\n# [DEF:ValidationPolicyResponse:Class]\n# @PURPOSE: Schema for validation policy response data.\nclass ValidationPolicyResponse(ValidationPolicyBase):\n id: str\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ValidationPolicyResponse:Class]\n\n# [/DEF:SettingsSchemas:Module]\n" + "anchor_syntax": "region", + "body": "# #region SettingsSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, schema, validate, notification-channel]\n# @BRIEF Pydantic schemas for application settings and automation policies.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> pydantic\n\nfrom datetime import datetime, time\n\nfrom pydantic import BaseModel, Field\n\n\n# #region NotificationChannel [TYPE Class]\n# @BRIEF Structured notification channel definition for policy-level custom routing.\nclass NotificationChannel(BaseModel):\n type: str = Field(\n ..., description=\"Notification channel type (e.g., SLACK, SMTP, TELEGRAM)\"\n )\n target: str = Field(\n ..., description=\"Notification destination (e.g., #alerts, chat id, email)\"\n )\n\n\n# #endregion NotificationChannel\n\n\n# #region ValidationPolicyBase [TYPE Class]\n# @BRIEF Base schema for validation policy data.\nclass ValidationPolicyBase(BaseModel):\n name: str = Field(..., description=\"Name of the policy\")\n environment_id: str = Field(..., description=\"Target Superset environment ID\")\n is_active: bool = Field(True, description=\"Whether the policy is currently active\")\n dashboard_ids: list[str] = Field(\n ..., description=\"List of dashboard IDs to validate\"\n )\n schedule_days: list[int] = Field(\n ..., description=\"Days of the week (0-6, 0=Sunday) to run\"\n )\n window_start: time = Field(..., description=\"Start of the execution window\")\n window_end: time = Field(..., description=\"End of the execution window\")\n notify_owners: bool = Field(\n True, description=\"Whether to notify dashboard owners on failure\"\n )\n custom_channels: list[NotificationChannel] | None = Field(\n None,\n description=\"List of additional structured notification channels\",\n )\n alert_condition: str = Field(\n \"FAIL_ONLY\",\n description=\"Condition to trigger alerts: FAIL_ONLY, WARN_AND_FAIL, ALWAYS\",\n )\n\n\n# #endregion ValidationPolicyBase\n\n\n# #region ValidationPolicyCreate [TYPE Class]\n# @BRIEF Schema for creating a new validation policy.\nclass ValidationPolicyCreate(ValidationPolicyBase):\n pass\n\n\n# #endregion ValidationPolicyCreate\n\n\n# #region ValidationPolicyUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing validation policy.\nclass ValidationPolicyUpdate(BaseModel):\n name: str | None = None\n environment_id: str | None = None\n is_active: bool | None = None\n dashboard_ids: list[str] | None = None\n schedule_days: list[int] | None = None\n window_start: time | None = None\n window_end: time | None = None\n notify_owners: bool | None = None\n custom_channels: list[NotificationChannel] | None = None\n alert_condition: str | None = None\n\n\n# #endregion ValidationPolicyUpdate\n\n\n# #region ValidationPolicyResponse [TYPE Class]\n# @BRIEF Schema for validation policy response data.\nclass ValidationPolicyResponse(ValidationPolicyBase):\n id: str\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ValidationPolicyResponse\n\n\n# #region TranslationScheduleItem [C:1] [TYPE Class]\n# @BRIEF Response schema for a translation schedule item with joined job name.\nclass TranslationScheduleItem(BaseModel):\n schedule_id: str\n job_id: str\n job_name: str | None = None\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion TranslationScheduleItem\n\n# #endregion SettingsSchemas\n" }, { "contract_id": "NotificationChannel", "contract_type": "Class", "file_path": "backend/src/schemas/settings.py", - "start_line": 13, - "end_line": 24, + "start_line": 11, + "end_line": 22, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71102,15 +54790,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:NotificationChannel:Class]\n# @PURPOSE: Structured notification channel definition for policy-level custom routing.\nclass NotificationChannel(BaseModel):\n type: str = Field(\n ..., description=\"Notification channel type (e.g., SLACK, SMTP, TELEGRAM)\"\n )\n target: str = Field(\n ..., description=\"Notification destination (e.g., #alerts, chat id, email)\"\n )\n\n\n# [/DEF:NotificationChannel:Class]\n" + "anchor_syntax": "region", + "body": "# #region NotificationChannel [TYPE Class]\n# @BRIEF Structured notification channel definition for policy-level custom routing.\nclass NotificationChannel(BaseModel):\n type: str = Field(\n ..., description=\"Notification channel type (e.g., SLACK, SMTP, TELEGRAM)\"\n )\n target: str = Field(\n ..., description=\"Notification destination (e.g., #alerts, chat id, email)\"\n )\n\n\n# #endregion NotificationChannel\n" }, { "contract_id": "ValidationPolicyBase", "contract_type": "Class", "file_path": "backend/src/schemas/settings.py", - "start_line": 27, - "end_line": 54, + "start_line": 25, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71128,15 +54816,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationPolicyBase:Class]\n# @PURPOSE: Base schema for validation policy data.\nclass ValidationPolicyBase(BaseModel):\n name: str = Field(..., description=\"Name of the policy\")\n environment_id: str = Field(..., description=\"Target Superset environment ID\")\n is_active: bool = Field(True, description=\"Whether the policy is currently active\")\n dashboard_ids: List[str] = Field(\n ..., description=\"List of dashboard IDs to validate\"\n )\n schedule_days: List[int] = Field(\n ..., description=\"Days of the week (0-6, 0=Sunday) to run\"\n )\n window_start: time = Field(..., description=\"Start of the execution window\")\n window_end: time = Field(..., description=\"End of the execution window\")\n notify_owners: bool = Field(\n True, description=\"Whether to notify dashboard owners on failure\"\n )\n custom_channels: Optional[List[NotificationChannel]] = Field(\n None,\n description=\"List of additional structured notification channels\",\n )\n alert_condition: str = Field(\n \"FAIL_ONLY\",\n description=\"Condition to trigger alerts: FAIL_ONLY, WARN_AND_FAIL, ALWAYS\",\n )\n\n\n# [/DEF:ValidationPolicyBase:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationPolicyBase [TYPE Class]\n# @BRIEF Base schema for validation policy data.\nclass ValidationPolicyBase(BaseModel):\n name: str = Field(..., description=\"Name of the policy\")\n environment_id: str = Field(..., description=\"Target Superset environment ID\")\n is_active: bool = Field(True, description=\"Whether the policy is currently active\")\n dashboard_ids: list[str] = Field(\n ..., description=\"List of dashboard IDs to validate\"\n )\n schedule_days: list[int] = Field(\n ..., description=\"Days of the week (0-6, 0=Sunday) to run\"\n )\n window_start: time = Field(..., description=\"Start of the execution window\")\n window_end: time = Field(..., description=\"End of the execution window\")\n notify_owners: bool = Field(\n True, description=\"Whether to notify dashboard owners on failure\"\n )\n custom_channels: list[NotificationChannel] | None = Field(\n None,\n description=\"List of additional structured notification channels\",\n )\n alert_condition: str = Field(\n \"FAIL_ONLY\",\n description=\"Condition to trigger alerts: FAIL_ONLY, WARN_AND_FAIL, ALWAYS\",\n )\n\n\n# #endregion ValidationPolicyBase\n" }, { "contract_id": "ValidationPolicyCreate", "contract_type": "Class", "file_path": "backend/src/schemas/settings.py", - "start_line": 57, - "end_line": 63, + "start_line": 55, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71154,15 +54842,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationPolicyCreate:Class]\n# @PURPOSE: Schema for creating a new validation policy.\nclass ValidationPolicyCreate(ValidationPolicyBase):\n pass\n\n\n# [/DEF:ValidationPolicyCreate:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationPolicyCreate [TYPE Class]\n# @BRIEF Schema for creating a new validation policy.\nclass ValidationPolicyCreate(ValidationPolicyBase):\n pass\n\n\n# #endregion ValidationPolicyCreate\n" }, { "contract_id": "ValidationPolicyUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/settings.py", - "start_line": 66, - "end_line": 81, + "start_line": 64, + "end_line": 79, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71180,15 +54868,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationPolicyUpdate:Class]\n# @PURPOSE: Schema for updating an existing validation policy.\nclass ValidationPolicyUpdate(BaseModel):\n name: Optional[str] = None\n environment_id: Optional[str] = None\n is_active: Optional[bool] = None\n dashboard_ids: Optional[List[str]] = None\n schedule_days: Optional[List[int]] = None\n window_start: Optional[time] = None\n window_end: Optional[time] = None\n notify_owners: Optional[bool] = None\n custom_channels: Optional[List[NotificationChannel]] = None\n alert_condition: Optional[str] = None\n\n\n# [/DEF:ValidationPolicyUpdate:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationPolicyUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing validation policy.\nclass ValidationPolicyUpdate(BaseModel):\n name: str | None = None\n environment_id: str | None = None\n is_active: bool | None = None\n dashboard_ids: list[str] | None = None\n schedule_days: list[int] | None = None\n window_start: time | None = None\n window_end: time | None = None\n notify_owners: bool | None = None\n custom_channels: list[NotificationChannel] | None = None\n alert_condition: str | None = None\n\n\n# #endregion ValidationPolicyUpdate\n" }, { "contract_id": "ValidationPolicyResponse", "contract_type": "Class", "file_path": "backend/src/schemas/settings.py", - "start_line": 84, - "end_line": 95, + "start_line": 82, + "end_line": 93, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71206,522 +54894,876 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ValidationPolicyResponse:Class]\n# @PURPOSE: Schema for validation policy response data.\nclass ValidationPolicyResponse(ValidationPolicyBase):\n id: str\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# [/DEF:ValidationPolicyResponse:Class]\n" + "anchor_syntax": "region", + "body": "# #region ValidationPolicyResponse [TYPE Class]\n# @BRIEF Schema for validation policy response data.\nclass ValidationPolicyResponse(ValidationPolicyBase):\n id: str\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ValidationPolicyResponse\n" + }, + { + "contract_id": "TranslationScheduleItem", + "contract_type": "Class", + "file_path": "backend/src/schemas/settings.py", + "start_line": 96, + "end_line": 115, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Response schema for a translation schedule item with joined job name." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], + "anchor_syntax": "region", + "body": "# #region TranslationScheduleItem [C:1] [TYPE Class]\n# @BRIEF Response schema for a translation schedule item with joined job name.\nclass TranslationScheduleItem(BaseModel):\n schedule_id: str\n job_id: str\n job_name: str | None = None\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion TranslationScheduleItem\n" }, { "contract_id": "TranslateSchemas", "contract_type": "Module", "file_path": "backend/src/schemas/translate.py", "start_line": 1, - "end_line": 465, + "end_line": 503, "tier": "TIER_1", "complexity": 2, "metadata": { "COMPLEXITY": 2, - "LAYER": "Domain", + "LAYER": "API", "PURPOSE": "Pydantic v2 schemas for translation API request/response serialization." }, - "relations": [], - "schema_warnings": [], + "relations": [ + { + "source_id": "TranslateSchemas", + "relation_type": "DEPENDS_ON", + "target_id": "pydantic", + "target_ref": "pydantic" + } + ], + "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", - "body": "# #region TranslateSchemas [C:2] [TYPE Module]\n# @BRIEF Pydantic v2 schemas for translation API request/response serialization.\n# @LAYER Domain\n\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import BaseModel, Field\nimport json\n\n\n# #region TranslateJobCreate [C:1] [TYPE Class]\nclass TranslateJobCreate(BaseModel):\n name: str\n description: Optional[str] = None\n source_dialect: str = Field(..., description=\"Source database dialect (e.g. postgresql, clickhouse)\")\n target_dialect: str = Field(..., description=\"Target database dialect (e.g. postgresql, clickhouse)\")\n database_dialect: Optional[str] = Field(None, description=\"Detected dialect from Superset connection at save time\")\n source_datasource_id: Optional[str] = Field(None, description=\"Superset datasource ID\")\n source_table: Optional[str] = Field(None, description=\"Source table name\")\n target_schema: Optional[str] = Field(None, description=\"Target table schema\")\n target_table: Optional[str] = Field(None, description=\"Target table name\")\n source_key_cols: Optional[List[str]] = Field(default_factory=list, description=\"Source key column names\")\n target_key_cols: Optional[List[str]] = Field(default_factory=list, description=\"Target key column names\")\n translation_column: Optional[str] = Field(None, description=\"Column to translate\")\n context_columns: Optional[List[str]] = Field(default_factory=list, description=\"Context column names\")\n target_language: Optional[str] = Field(None, description=\"Target language code\")\n provider_id: Optional[str] = Field(None, description=\"LLM provider ID\")\n batch_size: int = Field(50, description=\"Records per batch\")\n upsert_strategy: str = Field(\"MERGE\", description=\"UPSERT strategy: MERGE, INSERT, UPDATE\")\n dictionary_ids: Optional[List[str]] = Field(default_factory=list, description=\"Associated terminology dictionary IDs\")\n environment_id: Optional[str] = Field(None, description=\"Superset environment ID\")\n target_database_id: Optional[str] = Field(None, description=\"Superset database ID for INSERT via SQL Lab\")\n# #endregion TranslateJobCreate\n\n\n# #region TranslateJobUpdate [C:1] [TYPE Class]\nclass TranslateJobUpdate(BaseModel):\n name: Optional[str] = None\n description: Optional[str] = None\n source_dialect: Optional[str] = None\n target_dialect: Optional[str] = None\n database_dialect: Optional[str] = None\n source_datasource_id: Optional[str] = None\n source_table: Optional[str] = None\n target_schema: Optional[str] = None\n target_table: Optional[str] = None\n source_key_cols: Optional[List[str]] = None\n target_key_cols: Optional[List[str]] = None\n translation_column: Optional[str] = None\n context_columns: Optional[List[str]] = None\n target_language: Optional[str] = None\n provider_id: Optional[str] = None\n batch_size: Optional[int] = None\n upsert_strategy: Optional[str] = None\n status: Optional[str] = None\n dictionary_ids: Optional[List[str]] = None\n environment_id: Optional[str] = None\n target_database_id: Optional[str] = None\n# #endregion TranslateJobUpdate\n\n\n# #region TranslateJobResponse [C:1] [TYPE Class]\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n database_dialect: Optional[str] = None\n source_datasource_id: Optional[str] = None\n source_table: Optional[str] = None\n target_schema: Optional[str] = None\n target_table: Optional[str] = None\n source_key_cols: Optional[List[str]] = None\n target_key_cols: Optional[List[str]] = None\n translation_column: Optional[str] = None\n context_columns: Optional[List[str]] = None\n target_language: Optional[str] = None\n provider_id: Optional[str] = None\n batch_size: int = 50\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n dictionary_ids: Optional[List[str]] = None\n environment_id: Optional[str] = None\n target_database_id: Optional[str] = None\n\n class Config:\n from_attributes = True\n# #endregion TranslateJobResponse\n\n\n# #region DatasourceColumnResponse [C:1] [TYPE Class]\nclass DatasourceColumnResponse(BaseModel):\n name: str\n type: Optional[str] = None\n is_physical: bool = True\n is_dttm: bool = False\n description: Optional[str] = None\n# #endregion DatasourceColumnResponse\n\n\n# #region DatasourceColumnsResponse [C:1] [TYPE Class]\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: Optional[str] = None\n schema_name: Optional[str] = None\n database_dialect: str\n columns: List[DatasourceColumnResponse] = []\n\n class Config:\n from_attributes = True\n# #endregion DatasourceColumnsResponse\n\n\n# #region DuplicateJobResponse [C:1] [TYPE Class]\nclass DuplicateJobResponse(BaseModel):\n id: str\n name: str\n message: str = \"Job duplicated successfully\"\n\n class Config:\n from_attributes = True\n# #endregion DuplicateJobResponse\n\n\n# #region DictionaryCreate [C:1] [TYPE Class]\nclass DictionaryCreate(BaseModel):\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n is_active: bool = True\n# #endregion DictionaryCreate\n\n\n# #region DictionaryImport [C:1] [TYPE Class]\nclass DictionaryImport(BaseModel):\n content: str = Field(..., description=\"CSV or TSV content as raw string\")\n delimiter: Optional[str] = Field(None, description=\"Detected or forced delimiter: ',' or '\\\\t'. Auto-detect if omitted.\")\n on_conflict: str = Field(\"overwrite\", description=\"'overwrite' or 'keep_existing' or 'cancel'\")\n preview_only: bool = Field(False, description=\"If true, return preview without applying\")\n# #endregion DictionaryImport\n\n\n# #region DictionaryResponse [C:1] [TYPE Class]\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n entry_count: Optional[int] = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryResponse\n\n\n# #region DictionaryEntryCreate [C:1] [TYPE Class]\nclass DictionaryEntryCreate(BaseModel):\n source_term: str = Field(..., description=\"Source term to translate\")\n target_term: str = Field(..., description=\"Target/translated term\")\n context_notes: Optional[str] = Field(None, description=\"Optional context notes\")\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryCreate\n\n\n# #region DictionaryEntryResponse [C:1] [TYPE Class]\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n context_notes: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryResponse\n\n\n# #region DictionaryImportResult [C:1] [TYPE Class]\nclass DictionaryImportResult(BaseModel):\n total: int = 0\n created: int = 0\n updated: int = 0\n skipped: int = 0\n errors: List[Dict[str, Any]] = Field(default_factory=list)\n preview: List[Dict[str, Any]] = Field(default_factory=list, description=\"Preview rows with conflict flags\")\n# #endregion DictionaryImportResult\n\n\n# #region PreviewRequest [C:1] [TYPE Class]\nclass PreviewRequest(BaseModel):\n sample_size: int = Field(10, ge=1, le=100, description=\"Number of sample rows to preview\")\n prompt_template: Optional[str] = Field(None, description=\"Optional custom prompt template\")\n env_id: Optional[str] = Field(None, description=\"Superset environment ID for preview data fetch\")\n# #endregion PreviewRequest\n\n\n# #region PreviewRowUpdate [C:1] [TYPE Class]\nclass PreviewRowUpdate(BaseModel):\n action: str = Field(..., description=\"'approve', 'reject', or 'edit'\")\n translation: Optional[str] = Field(None, description=\"Edited translation (required for 'edit' action)\")\n feedback: Optional[str] = Field(None, description=\"Optional feedback/comment\")\n# #endregion PreviewRowUpdate\n\n\n# #region PreviewAcceptResponse [C:1] [TYPE Class]\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n expires_at: Optional[datetime] = None\n records: List['PreviewRow'] = []\n\n class Config:\n from_attributes = True\n# #endregion PreviewAcceptResponse\n\n\n# #region CostEstimate [C:1] [TYPE Class]\nclass CostEstimate(BaseModel):\n sample_size: int = 0\n sample_prompt_tokens: int = 0\n sample_output_tokens: int = 0\n sample_total_tokens: int = 0\n sample_cost: float = 0.0\n estimated_total_rows: int = 0\n estimated_tokens: int = 0\n estimated_cost: float = 0.0\n# #endregion CostEstimate\n\n\n# #region TermCorrectionSubmit [C:1] [TYPE Class]\nclass TermCorrectionSubmit(BaseModel):\n source_term: str\n incorrect_target_term: str\n corrected_target_term: str\n dictionary_id: Optional[str] = Field(None, description=\"Target dictionary ID (language-filtered)\")\n origin_run_id: Optional[str] = Field(None, description=\"Run ID from which this correction originated\")\n origin_row_key: Optional[str] = Field(None, description=\"Row key within the run\")\n# #endregion TermCorrectionSubmit\n\n\n# #region TermCorrectionBulkSubmit [C:1] [TYPE Class]\nclass TermCorrectionBulkSubmit(BaseModel):\n corrections: List[TermCorrectionSubmit]\n dictionary_id: str = Field(..., description=\"Target dictionary ID for all corrections\")\n# #endregion TermCorrectionBulkSubmit\n\n\n# #region CorrectionConflictResult [C:1] [TYPE Class]\nclass CorrectionConflictResult(BaseModel):\n source_term: str\n existing_target_term: str\n submitted_target_term: str\n action: str = \"keep_existing\"\n# #endregion CorrectionConflictResult\n\n\n# #region CorrectionSubmitResponse [C:1] [TYPE Class]\nclass CorrectionSubmitResponse(BaseModel):\n entry_id: Optional[str] = None\n action: str # \"created\", \"updated\", \"conflict_detected\", \"skipped\"\n source_term: str\n target_term: str\n conflict: Optional[CorrectionConflictResult] = None\n message: Optional[str] = None\n# #endregion CorrectionSubmitResponse\n\n\n# #region ScheduleConfig [C:1] [TYPE Class]\nclass ScheduleConfig(BaseModel):\n cron_expression: str = Field(..., description=\"Cron expression for scheduling (e.g. '0 2 * * *')\")\n timezone: str = Field(\"UTC\", description=\"Timezone for the cron schedule (e.g. 'UTC', 'Europe/Moscow')\")\n is_active: bool = True\n# #endregion ScheduleConfig\n\n\n# #region ScheduleResponse [C:1] [TYPE Class]\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n last_run_at: Optional[datetime] = None\n next_run_at: Optional[datetime] = None\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n# #endregion ScheduleResponse\n\n\n# #region NextExecutionResponse [C:1] [TYPE Class]\nclass NextExecutionResponse(BaseModel):\n job_id: str\n cron_expression: str\n timezone: str\n next_executions: List[str] = []\n# #endregion NextExecutionResponse\n\n\n# #region TranslationRunResponse [C:1] [TYPE Class]\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: Optional[str] = None\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n error_message: Optional[str] = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: Optional[str] = None\n superset_execution_id: Optional[str] = None\n config_snapshot: Optional[Dict[str, Any]] = None\n key_hash: Optional[str] = None\n config_hash: Optional[str] = None\n dict_snapshot_hash: Optional[str] = None\n created_by: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunResponse\n\n\n# #region RunDetailResponse [C:1] [TYPE Class]\nclass RunDetailResponse(BaseModel):\n run: TranslationRunResponse\n records: List[Dict[str, Any]] = Field(default_factory=list, description=\"Paginated translation records\")\n events: List[Dict[str, Any]] = Field(default_factory=list, description=\"Run events\")\n event_invariants: Optional[Dict[str, Any]] = None\n batch_count: int = 0\n# #endregion RunDetailResponse\n\n\n# #region RunHistoryFilter [C:1] [TYPE Class]\nclass RunHistoryFilter(BaseModel):\n job_id: Optional[str] = None\n status: Optional[str] = None\n trigger_type: Optional[str] = None\n created_by: Optional[str] = None\n date_from: Optional[datetime] = None\n date_to: Optional[datetime] = None\n page: int = 1\n page_size: int = 20\n# #endregion RunHistoryFilter\n\n\n# #region RunListResponse [C:1] [TYPE Class]\nclass RunListResponse(BaseModel):\n items: List[TranslationRunResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion RunListResponse\n\n\n# #region AggregatedMetricsResponse [C:1] [TYPE Class]\nclass AggregatedMetricsResponse(BaseModel):\n job_id: str\n total_runs: int = 0\n successful_runs: int = 0\n failed_runs: int = 0\n cancelled_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n cumulative_tokens: Optional[int] = None\n cumulative_cost: Optional[float] = None\n avg_duration_ms: Optional[float] = None\n last_run_at: Optional[datetime] = None\n next_scheduled_run: Optional[datetime] = None\n# #endregion AggregatedMetricsResponse\n\n\n# #region PreviewRow [C:1] [TYPE Class]\nclass PreviewRow(BaseModel):\n id: str\n source_sql: Optional[str] = None\n target_sql: Optional[str] = None\n source_object_type: Optional[str] = None\n source_object_id: Optional[str] = None\n source_object_name: Optional[str] = None\n status: str = \"PENDING\"\n feedback: Optional[str] = None\n# #endregion PreviewRow\n\n\n# #region TranslationPreviewResponse [C:1] [TYPE Class]\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: Optional[str] = None\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n expires_at: Optional[datetime] = None\n records: List[PreviewRow] = []\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewResponse\n\n\n# #region TranslationBatchResponse [C:1] [TYPE Class]\nclass TranslationBatchResponse(BaseModel):\n id: str\n run_id: str\n batch_index: int\n status: str\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationBatchResponse\n\n\n# #region MetricsResponse [C:1] [TYPE Class]\nclass MetricsResponse(BaseModel):\n job_id: str\n snapshot_date: datetime\n total_jobs: int = 0\n total_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n avg_duration_ms: Optional[int] = None\n p50_duration_ms: Optional[int] = None\n p95_duration_ms: Optional[int] = None\n p99_duration_ms: Optional[int] = None\n\n class Config:\n from_attributes = True\n# #endregion MetricsResponse\n\n# #endregion TranslateSchemas\n" + "body": "# #region TranslateSchemas [C:2] [TYPE Module] [SEMANTICS pydantic, translate, schema, translate-job-create]\n# @BRIEF Pydantic v2 schemas for translation API request/response serialization.\n# @LAYER: API\n# @RELATION DEPENDS_ON -> pydantic\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region TranslateJobCreate [TYPE Class]\n# @BRIEF Schema for creating a new translation job.\nclass TranslateJobCreate(BaseModel):\n name: str\n description: str | None = None\n source_dialect: str = Field(..., description=\"Source database dialect (e.g. postgresql, clickhouse)\")\n target_dialect: str = Field(..., description=\"Target database dialect (e.g. postgresql, clickhouse)\")\n database_dialect: str | None = Field(None, description=\"Detected dialect from Superset connection at save time\")\n source_datasource_id: str | None = Field(None, description=\"Superset datasource ID\")\n source_table: str | None = Field(None, description=\"Source table name\")\n target_schema: str | None = Field(None, description=\"Target table schema\")\n target_table: str | None = Field(None, description=\"Target table name\")\n source_key_cols: list[str] | None = Field(default_factory=list, description=\"Source key column names\")\n target_key_cols: list[str] | None = Field(default_factory=list, description=\"Target key column names\")\n translation_column: str | None = Field(None, description=\"Source column to translate\")\n target_column: str | None = Field(None, description=\"Target column for translated output (defaults to translation_column)\")\n context_columns: list[str] | None = Field(default_factory=list, description=\"Context column names\")\n target_language: str | None = Field(None, description=\"Target language code\")\n provider_id: str | None = Field(None, description=\"LLM provider ID\")\n batch_size: int = Field(50, description=\"Records per batch\")\n upsert_strategy: str = Field(\"MERGE\", description=\"UPSERT strategy: MERGE, INSERT, UPDATE\")\n dictionary_ids: list[str] | None = Field(default_factory=list, description=\"Associated terminology dictionary IDs\")\n environment_id: str | None = Field(None, description=\"Superset environment ID\")\n target_database_id: str | None = Field(None, description=\"Superset database ID for SQL Lab insert target\")\n# #endregion TranslateJobCreate\n\n\n# #region TranslateJobUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing translation job.\nclass TranslateJobUpdate(BaseModel):\n name: str | None = None\n description: str | None = None\n source_dialect: str | None = None\n target_dialect: str | None = None\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n context_columns: list[str] | None = None\n target_language: str | None = None\n provider_id: str | None = None\n batch_size: int | None = None\n upsert_strategy: str | None = None\n status: str | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n# #endregion TranslateJobUpdate\n\n\n# #region TranslateJobResponse [TYPE Class]\n# @BRIEF Schema for translation job API responses.\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n context_columns: list[str] | None = None\n target_language: str | None = None\n provider_id: str | None = None\n batch_size: int = 50\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion TranslateJobResponse\n\n\n# #region DatasourceColumnResponse [TYPE Class]\n# @BRIEF Schema for datasource column metadata response.\nclass DatasourceColumnResponse(BaseModel):\n name: str\n type: str | None = None\n is_physical: bool = True\n is_dttm: bool = False\n description: str | None = None\n\n# #endregion DatasourceColumnResponse\n\n# #region DatasourceColumnsResponse [TYPE Class]\n# @BRIEF Schema for datasource columns endpoint response.\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: str | None = None\n schema_name: str | None = None\n database_dialect: str\n columns: list[DatasourceColumnResponse] = []\n\n class Config:\n from_attributes = True\n# #endregion DatasourceColumnsResponse\n\n\n# #region DuplicateJobResponse [TYPE Class]\n# @BRIEF Schema for duplicate job response.\nclass DuplicateJobResponse(BaseModel):\n id: str\n name: str\n message: str = \"Job duplicated successfully\"\n\n class Config:\n from_attributes = True\n# #endregion DuplicateJobResponse\n\n\n# #region DictionaryCreate [TYPE Class]\n# @BRIEF Schema for creating a new terminology dictionary.\nclass DictionaryCreate(BaseModel):\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool = True\n# #endregion DictionaryCreate\n\n\n# #region DictionaryImport [TYPE Class]\n# @BRIEF Schema for importing entries into a terminology dictionary.\nclass DictionaryImport(BaseModel):\n content: str = Field(..., description=\"CSV or TSV content as raw string\")\n delimiter: str | None = Field(None, description=\"Detected or forced delimiter: ',' or '\\\\t'. Auto-detect if omitted.\")\n on_conflict: str = Field(\"overwrite\", description=\"'overwrite' or 'keep_existing' or 'cancel'\")\n preview_only: bool = Field(False, description=\"If true, return preview without applying\")\n# #endregion DictionaryImport\n\n\n# #region DictionaryResponse [TYPE Class]\n# @BRIEF Schema for terminology dictionary API responses.\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n entry_count: int | None = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryResponse\n\n\n# #region DictionaryEntryCreate [TYPE Class]\n# @BRIEF Schema for adding/editing a dictionary entry.\nclass DictionaryEntryCreate(BaseModel):\n source_term: str = Field(..., description=\"Source term to translate\")\n target_term: str = Field(..., description=\"Target/translated term\")\n context_notes: str | None = Field(None, description=\"Optional context notes\")\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryCreate\n\n\n# #region DictionaryEntryResponse [TYPE Class]\n# @BRIEF Schema for dictionary entry API responses.\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n context_notes: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryResponse\n\n\n# #region DictionaryImportResult [TYPE Class]\n# @BRIEF Schema for dictionary import result.\nclass DictionaryImportResult(BaseModel):\n total: int = 0\n created: int = 0\n updated: int = 0\n skipped: int = 0\n errors: list[dict[str, Any]] = Field(default_factory=list)\n preview: list[dict[str, Any]] = Field(default_factory=list, description=\"Preview rows with conflict flags\")\n# #endregion DictionaryImportResult\n\n\n# #region PreviewRequest [TYPE Class]\n# @BRIEF Schema for triggering a translation preview.\nclass PreviewRequest(BaseModel):\n sample_size: int = Field(10, ge=1, le=100, description=\"Number of sample rows to preview\")\n prompt_template: str | None = Field(None, description=\"Optional custom prompt template\")\n env_id: str | None = Field(None, description=\"Superset environment ID for preview data fetch\")\n# #endregion PreviewRequest\n\n\n# #region PreviewRowUpdate [TYPE Class]\n# @BRIEF Schema for approving/editing/rejecting a preview row.\nclass PreviewRowUpdate(BaseModel):\n action: str = Field(..., description=\"'approve', 'reject', or 'edit'\")\n translation: str | None = Field(None, description=\"Edited translation (required for 'edit' action)\")\n feedback: str | None = Field(None, description=\"Optional feedback/comment\")\n# #endregion PreviewRowUpdate\n\n\n# #region PreviewAcceptResponse [TYPE Class]\n# @BRIEF Schema for preview accept response.\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list['PreviewRow'] = []\n\n class Config:\n from_attributes = True\n# #endregion PreviewAcceptResponse\n\n\n# #region CostEstimate [TYPE Class]\n# @BRIEF Schema for cost estimation in preview response.\nclass CostEstimate(BaseModel):\n sample_size: int = 0\n sample_prompt_tokens: int = 0\n sample_output_tokens: int = 0\n sample_total_tokens: int = 0\n sample_cost: float = 0.0\n estimated_total_rows: int = 0\n estimated_tokens: int = 0\n estimated_cost: float = 0.0\n# #endregion CostEstimate\n\n\n# #region TermCorrectionSubmit [TYPE Class]\n# @BRIEF Schema for submitting a term correction in a translation preview.\nclass TermCorrectionSubmit(BaseModel):\n source_term: str\n incorrect_target_term: str\n corrected_target_term: str\n dictionary_id: str | None = Field(None, description=\"Target dictionary ID (language-filtered)\")\n origin_run_id: str | None = Field(None, description=\"Run ID from which this correction originated\")\n origin_row_key: str | None = Field(None, description=\"Row key within the run\")\n# #endregion TermCorrectionSubmit\n\n\n# #region TermCorrectionBulkSubmit [TYPE Class]\n# @BRIEF Schema for submitting multiple term corrections atomically.\nclass TermCorrectionBulkSubmit(BaseModel):\n corrections: list[TermCorrectionSubmit]\n dictionary_id: str = Field(..., description=\"Target dictionary ID for all corrections\")\n# #endregion TermCorrectionBulkSubmit\n\n\n# #region CorrectionConflictResult [TYPE Class]\n# @BRIEF Schema for reporting conflicts in correction submission.\nclass CorrectionConflictResult(BaseModel):\n source_term: str\n existing_target_term: str\n submitted_target_term: str\n action: str = \"keep_existing\"\n# #endregion CorrectionConflictResult\n\n\n# #region CorrectionSubmitResponse [TYPE Class]\n# @BRIEF Schema for correction submission response.\nclass CorrectionSubmitResponse(BaseModel):\n entry_id: str | None = None\n action: str # \"created\", \"updated\", \"conflict_detected\", \"skipped\"\n source_term: str\n target_term: str\n conflict: CorrectionConflictResult | None = None\n message: str | None = None\n# #endregion CorrectionSubmitResponse\n\n\n# #region ScheduleConfig [TYPE Class]\n# @BRIEF Schema for configuring a recurring translation schedule.\nclass ScheduleConfig(BaseModel):\n cron_expression: str = Field(..., description=\"Cron expression for scheduling (e.g. '0 2 * * *')\")\n timezone: str = Field(\"UTC\", description=\"Timezone for the cron schedule (e.g. 'UTC', 'Europe/Moscow')\")\n is_active: bool = True\n execution_mode: str = Field(\"full\", description=\"Translation execution mode: 'full' or 'new_key_only'\")\n# #endregion ScheduleConfig\n\n\n# #region ScheduleResponse [TYPE Class]\n# @BRIEF Schema for schedule API responses.\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion ScheduleResponse\n\n\n# #region NextExecutionResponse [TYPE Class]\n# @BRIEF Schema for next execution preview.\nclass NextExecutionResponse(BaseModel):\n job_id: str\n cron_expression: str\n timezone: str\n next_executions: list[str] = []\n# #endregion NextExecutionResponse\n\n\n# #region TranslationRunResponse [TYPE Class]\n# @BRIEF Schema for translation run API responses.\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: str | None = None\n started_at: datetime | None = None\n completed_at: datetime | None = None\n error_message: str | None = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: str | None = None\n superset_execution_id: str | None = None\n config_snapshot: dict[str, Any] | None = None\n key_hash: str | None = None\n config_hash: str | None = None\n dict_snapshot_hash: str | None = None\n created_by: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunResponse\n\n\n# #region RunDetailResponse [TYPE Class]\n# @BRIEF Schema for detailed run response including records, events, and config snapshot.\nclass RunDetailResponse(BaseModel):\n run: TranslationRunResponse\n records: list[dict[str, Any]] = Field(default_factory=list, description=\"Paginated translation records\")\n events: list[dict[str, Any]] = Field(default_factory=list, description=\"Run events\")\n event_invariants: dict[str, Any] | None = None\n batch_count: int = 0\n# #endregion RunDetailResponse\n\n\n# #region RunHistoryFilter [TYPE Class]\n# @BRIEF Schema for filtering run history.\nclass RunHistoryFilter(BaseModel):\n job_id: str | None = None\n status: str | None = None\n trigger_type: str | None = None\n created_by: str | None = None\n date_from: datetime | None = None\n date_to: datetime | None = None\n page: int = 1\n page_size: int = 20\n# #endregion RunHistoryFilter\n\n\n# #region RunListResponse [TYPE Class]\n# @BRIEF Schema for paginated run list response.\nclass RunListResponse(BaseModel):\n items: list[TranslationRunResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion RunListResponse\n\n\n# #region AggregatedMetricsResponse [TYPE Class]\n# @BRIEF Schema for aggregated per-job metrics.\nclass AggregatedMetricsResponse(BaseModel):\n job_id: str\n total_runs: int = 0\n successful_runs: int = 0\n failed_runs: int = 0\n cancelled_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n cumulative_tokens: int | None = None\n cumulative_cost: float | None = None\n avg_duration_ms: float | None = None\n last_run_at: datetime | None = None\n next_scheduled_run: datetime | None = None\n# #endregion AggregatedMetricsResponse\n\n\n# #region PreviewRow [TYPE Class]\n# @BRIEF A single row in a translation preview showing original and translated content side by side.\nclass PreviewRow(BaseModel):\n id: str\n source_sql: str | None = None\n target_sql: str | None = None\n source_object_type: str | None = None\n source_object_id: str | None = None\n source_object_name: str | None = None\n status: str = \"PENDING\"\n feedback: str | None = None\n# #endregion PreviewRow\n\n\n# #region TranslationPreviewResponse [TYPE Class]\n# @BRIEF Schema for translation preview session API responses.\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: str | None = None\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list[PreviewRow] = []\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewResponse\n\n\n# #region TranslationBatchResponse [TYPE Class]\n# @BRIEF Schema for translation batch API responses.\nclass TranslationBatchResponse(BaseModel):\n id: str\n run_id: str\n batch_index: int\n status: str\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n started_at: datetime | None = None\n completed_at: datetime | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationBatchResponse\n\n\n# #region MetricsResponse [TYPE Class]\n# @BRIEF Schema for translation metrics API responses.\nclass MetricsResponse(BaseModel):\n job_id: str\n snapshot_date: datetime\n total_jobs: int = 0\n total_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n avg_duration_ms: int | None = None\n p50_duration_ms: int | None = None\n p95_duration_ms: int | None = None\n p99_duration_ms: int | None = None\n\n class Config:\n from_attributes = True\n# #endregion MetricsResponse\n\n# #endregion TranslateSchemas\n" }, { "contract_id": "TranslateJobCreate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 11, - "end_line": 33, + "start_line": 12, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for creating a new translation job." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslateJobCreate [C:1] [TYPE Class]\nclass TranslateJobCreate(BaseModel):\n name: str\n description: Optional[str] = None\n source_dialect: str = Field(..., description=\"Source database dialect (e.g. postgresql, clickhouse)\")\n target_dialect: str = Field(..., description=\"Target database dialect (e.g. postgresql, clickhouse)\")\n database_dialect: Optional[str] = Field(None, description=\"Detected dialect from Superset connection at save time\")\n source_datasource_id: Optional[str] = Field(None, description=\"Superset datasource ID\")\n source_table: Optional[str] = Field(None, description=\"Source table name\")\n target_schema: Optional[str] = Field(None, description=\"Target table schema\")\n target_table: Optional[str] = Field(None, description=\"Target table name\")\n source_key_cols: Optional[List[str]] = Field(default_factory=list, description=\"Source key column names\")\n target_key_cols: Optional[List[str]] = Field(default_factory=list, description=\"Target key column names\")\n translation_column: Optional[str] = Field(None, description=\"Column to translate\")\n context_columns: Optional[List[str]] = Field(default_factory=list, description=\"Context column names\")\n target_language: Optional[str] = Field(None, description=\"Target language code\")\n provider_id: Optional[str] = Field(None, description=\"LLM provider ID\")\n batch_size: int = Field(50, description=\"Records per batch\")\n upsert_strategy: str = Field(\"MERGE\", description=\"UPSERT strategy: MERGE, INSERT, UPDATE\")\n dictionary_ids: Optional[List[str]] = Field(default_factory=list, description=\"Associated terminology dictionary IDs\")\n environment_id: Optional[str] = Field(None, description=\"Superset environment ID\")\n target_database_id: Optional[str] = Field(None, description=\"Superset database ID for INSERT via SQL Lab\")\n# #endregion TranslateJobCreate\n" + "body": "# #region TranslateJobCreate [TYPE Class]\n# @BRIEF Schema for creating a new translation job.\nclass TranslateJobCreate(BaseModel):\n name: str\n description: str | None = None\n source_dialect: str = Field(..., description=\"Source database dialect (e.g. postgresql, clickhouse)\")\n target_dialect: str = Field(..., description=\"Target database dialect (e.g. postgresql, clickhouse)\")\n database_dialect: str | None = Field(None, description=\"Detected dialect from Superset connection at save time\")\n source_datasource_id: str | None = Field(None, description=\"Superset datasource ID\")\n source_table: str | None = Field(None, description=\"Source table name\")\n target_schema: str | None = Field(None, description=\"Target table schema\")\n target_table: str | None = Field(None, description=\"Target table name\")\n source_key_cols: list[str] | None = Field(default_factory=list, description=\"Source key column names\")\n target_key_cols: list[str] | None = Field(default_factory=list, description=\"Target key column names\")\n translation_column: str | None = Field(None, description=\"Source column to translate\")\n target_column: str | None = Field(None, description=\"Target column for translated output (defaults to translation_column)\")\n context_columns: list[str] | None = Field(default_factory=list, description=\"Context column names\")\n target_language: str | None = Field(None, description=\"Target language code\")\n provider_id: str | None = Field(None, description=\"LLM provider ID\")\n batch_size: int = Field(50, description=\"Records per batch\")\n upsert_strategy: str = Field(\"MERGE\", description=\"UPSERT strategy: MERGE, INSERT, UPDATE\")\n dictionary_ids: list[str] | None = Field(default_factory=list, description=\"Associated terminology dictionary IDs\")\n environment_id: str | None = Field(None, description=\"Superset environment ID\")\n target_database_id: str | None = Field(None, description=\"Superset database ID for SQL Lab insert target\")\n# #endregion TranslateJobCreate\n" }, { "contract_id": "TranslateJobUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 36, - "end_line": 59, + "start_line": 39, + "end_line": 64, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for updating an existing translation job." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslateJobUpdate [C:1] [TYPE Class]\nclass TranslateJobUpdate(BaseModel):\n name: Optional[str] = None\n description: Optional[str] = None\n source_dialect: Optional[str] = None\n target_dialect: Optional[str] = None\n database_dialect: Optional[str] = None\n source_datasource_id: Optional[str] = None\n source_table: Optional[str] = None\n target_schema: Optional[str] = None\n target_table: Optional[str] = None\n source_key_cols: Optional[List[str]] = None\n target_key_cols: Optional[List[str]] = None\n translation_column: Optional[str] = None\n context_columns: Optional[List[str]] = None\n target_language: Optional[str] = None\n provider_id: Optional[str] = None\n batch_size: Optional[int] = None\n upsert_strategy: Optional[str] = None\n status: Optional[str] = None\n dictionary_ids: Optional[List[str]] = None\n environment_id: Optional[str] = None\n target_database_id: Optional[str] = None\n# #endregion TranslateJobUpdate\n" + "body": "# #region TranslateJobUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing translation job.\nclass TranslateJobUpdate(BaseModel):\n name: str | None = None\n description: str | None = None\n source_dialect: str | None = None\n target_dialect: str | None = None\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n context_columns: list[str] | None = None\n target_language: str | None = None\n provider_id: str | None = None\n batch_size: int | None = None\n upsert_strategy: str | None = None\n status: str | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n# #endregion TranslateJobUpdate\n" }, { "contract_id": "TranslateJobResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 62, - "end_line": 92, + "start_line": 67, + "end_line": 99, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for translation job API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslateJobResponse [C:1] [TYPE Class]\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n database_dialect: Optional[str] = None\n source_datasource_id: Optional[str] = None\n source_table: Optional[str] = None\n target_schema: Optional[str] = None\n target_table: Optional[str] = None\n source_key_cols: Optional[List[str]] = None\n target_key_cols: Optional[List[str]] = None\n translation_column: Optional[str] = None\n context_columns: Optional[List[str]] = None\n target_language: Optional[str] = None\n provider_id: Optional[str] = None\n batch_size: int = 50\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n dictionary_ids: Optional[List[str]] = None\n environment_id: Optional[str] = None\n target_database_id: Optional[str] = None\n\n class Config:\n from_attributes = True\n# #endregion TranslateJobResponse\n" + "body": "# #region TranslateJobResponse [TYPE Class]\n# @BRIEF Schema for translation job API responses.\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n context_columns: list[str] | None = None\n target_language: str | None = None\n provider_id: str | None = None\n batch_size: int = 50\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion TranslateJobResponse\n" }, { "contract_id": "DatasourceColumnResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 95, - "end_line": 102, + "start_line": 102, + "end_line": 111, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for datasource column metadata response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DatasourceColumnResponse [C:1] [TYPE Class]\nclass DatasourceColumnResponse(BaseModel):\n name: str\n type: Optional[str] = None\n is_physical: bool = True\n is_dttm: bool = False\n description: Optional[str] = None\n# #endregion DatasourceColumnResponse\n" + "body": "# #region DatasourceColumnResponse [TYPE Class]\n# @BRIEF Schema for datasource column metadata response.\nclass DatasourceColumnResponse(BaseModel):\n name: str\n type: str | None = None\n is_physical: bool = True\n is_dttm: bool = False\n description: str | None = None\n\n# #endregion DatasourceColumnResponse\n" }, { "contract_id": "DatasourceColumnsResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 105, - "end_line": 115, + "start_line": 113, + "end_line": 124, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for datasource columns endpoint response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DatasourceColumnsResponse [C:1] [TYPE Class]\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: Optional[str] = None\n schema_name: Optional[str] = None\n database_dialect: str\n columns: List[DatasourceColumnResponse] = []\n\n class Config:\n from_attributes = True\n# #endregion DatasourceColumnsResponse\n" + "body": "# #region DatasourceColumnsResponse [TYPE Class]\n# @BRIEF Schema for datasource columns endpoint response.\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: str | None = None\n schema_name: str | None = None\n database_dialect: str\n columns: list[DatasourceColumnResponse] = []\n\n class Config:\n from_attributes = True\n# #endregion DatasourceColumnsResponse\n" }, { "contract_id": "DuplicateJobResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 118, - "end_line": 126, + "start_line": 127, + "end_line": 136, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for duplicate job response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DuplicateJobResponse [C:1] [TYPE Class]\nclass DuplicateJobResponse(BaseModel):\n id: str\n name: str\n message: str = \"Job duplicated successfully\"\n\n class Config:\n from_attributes = True\n# #endregion DuplicateJobResponse\n" + "body": "# #region DuplicateJobResponse [TYPE Class]\n# @BRIEF Schema for duplicate job response.\nclass DuplicateJobResponse(BaseModel):\n id: str\n name: str\n message: str = \"Job duplicated successfully\"\n\n class Config:\n from_attributes = True\n# #endregion DuplicateJobResponse\n" }, { "contract_id": "DictionaryCreate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 129, - "end_line": 136, + "start_line": 139, + "end_line": 147, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for creating a new terminology dictionary." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DictionaryCreate [C:1] [TYPE Class]\nclass DictionaryCreate(BaseModel):\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n is_active: bool = True\n# #endregion DictionaryCreate\n" + "body": "# #region DictionaryCreate [TYPE Class]\n# @BRIEF Schema for creating a new terminology dictionary.\nclass DictionaryCreate(BaseModel):\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool = True\n# #endregion DictionaryCreate\n" }, { "contract_id": "DictionaryImport", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 139, - "end_line": 145, + "start_line": 150, + "end_line": 157, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for importing entries into a terminology dictionary." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DictionaryImport [C:1] [TYPE Class]\nclass DictionaryImport(BaseModel):\n content: str = Field(..., description=\"CSV or TSV content as raw string\")\n delimiter: Optional[str] = Field(None, description=\"Detected or forced delimiter: ',' or '\\\\t'. Auto-detect if omitted.\")\n on_conflict: str = Field(\"overwrite\", description=\"'overwrite' or 'keep_existing' or 'cancel'\")\n preview_only: bool = Field(False, description=\"If true, return preview without applying\")\n# #endregion DictionaryImport\n" + "body": "# #region DictionaryImport [TYPE Class]\n# @BRIEF Schema for importing entries into a terminology dictionary.\nclass DictionaryImport(BaseModel):\n content: str = Field(..., description=\"CSV or TSV content as raw string\")\n delimiter: str | None = Field(None, description=\"Detected or forced delimiter: ',' or '\\\\t'. Auto-detect if omitted.\")\n on_conflict: str = Field(\"overwrite\", description=\"'overwrite' or 'keep_existing' or 'cancel'\")\n preview_only: bool = Field(False, description=\"If true, return preview without applying\")\n# #endregion DictionaryImport\n" }, { "contract_id": "DictionaryResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 148, - "end_line": 163, + "start_line": 160, + "end_line": 176, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for terminology dictionary API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DictionaryResponse [C:1] [TYPE Class]\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: Optional[str] = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n entry_count: Optional[int] = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryResponse\n" + "body": "# #region DictionaryResponse [TYPE Class]\n# @BRIEF Schema for terminology dictionary API responses.\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n entry_count: int | None = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryResponse\n" }, { "contract_id": "DictionaryEntryCreate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 166, - "end_line": 174, + "start_line": 179, + "end_line": 188, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for adding/editing a dictionary entry." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DictionaryEntryCreate [C:1] [TYPE Class]\nclass DictionaryEntryCreate(BaseModel):\n source_term: str = Field(..., description=\"Source term to translate\")\n target_term: str = Field(..., description=\"Target/translated term\")\n context_notes: Optional[str] = Field(None, description=\"Optional context notes\")\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryCreate\n" + "body": "# #region DictionaryEntryCreate [TYPE Class]\n# @BRIEF Schema for adding/editing a dictionary entry.\nclass DictionaryEntryCreate(BaseModel):\n source_term: str = Field(..., description=\"Source term to translate\")\n target_term: str = Field(..., description=\"Target/translated term\")\n context_notes: str | None = Field(None, description=\"Optional context notes\")\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryCreate\n" }, { "contract_id": "DictionaryEntryResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 177, - "end_line": 190, + "start_line": 191, + "end_line": 205, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for dictionary entry API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DictionaryEntryResponse [C:1] [TYPE Class]\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n context_notes: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryResponse\n" + "body": "# #region DictionaryEntryResponse [TYPE Class]\n# @BRIEF Schema for dictionary entry API responses.\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n context_notes: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryResponse\n" }, { "contract_id": "DictionaryImportResult", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 193, - "end_line": 201, + "start_line": 208, + "end_line": 217, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for dictionary import result." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region DictionaryImportResult [C:1] [TYPE Class]\nclass DictionaryImportResult(BaseModel):\n total: int = 0\n created: int = 0\n updated: int = 0\n skipped: int = 0\n errors: List[Dict[str, Any]] = Field(default_factory=list)\n preview: List[Dict[str, Any]] = Field(default_factory=list, description=\"Preview rows with conflict flags\")\n# #endregion DictionaryImportResult\n" + "body": "# #region DictionaryImportResult [TYPE Class]\n# @BRIEF Schema for dictionary import result.\nclass DictionaryImportResult(BaseModel):\n total: int = 0\n created: int = 0\n updated: int = 0\n skipped: int = 0\n errors: list[dict[str, Any]] = Field(default_factory=list)\n preview: list[dict[str, Any]] = Field(default_factory=list, description=\"Preview rows with conflict flags\")\n# #endregion DictionaryImportResult\n" }, { "contract_id": "PreviewRequest", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 204, - "end_line": 209, + "start_line": 220, + "end_line": 226, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for triggering a translation preview." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region PreviewRequest [C:1] [TYPE Class]\nclass PreviewRequest(BaseModel):\n sample_size: int = Field(10, ge=1, le=100, description=\"Number of sample rows to preview\")\n prompt_template: Optional[str] = Field(None, description=\"Optional custom prompt template\")\n env_id: Optional[str] = Field(None, description=\"Superset environment ID for preview data fetch\")\n# #endregion PreviewRequest\n" + "body": "# #region PreviewRequest [TYPE Class]\n# @BRIEF Schema for triggering a translation preview.\nclass PreviewRequest(BaseModel):\n sample_size: int = Field(10, ge=1, le=100, description=\"Number of sample rows to preview\")\n prompt_template: str | None = Field(None, description=\"Optional custom prompt template\")\n env_id: str | None = Field(None, description=\"Superset environment ID for preview data fetch\")\n# #endregion PreviewRequest\n" }, { "contract_id": "PreviewRowUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 212, - "end_line": 217, + "start_line": 229, + "end_line": 235, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for approving/editing/rejecting a preview row." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region PreviewRowUpdate [C:1] [TYPE Class]\nclass PreviewRowUpdate(BaseModel):\n action: str = Field(..., description=\"'approve', 'reject', or 'edit'\")\n translation: Optional[str] = Field(None, description=\"Edited translation (required for 'edit' action)\")\n feedback: Optional[str] = Field(None, description=\"Optional feedback/comment\")\n# #endregion PreviewRowUpdate\n" + "body": "# #region PreviewRowUpdate [TYPE Class]\n# @BRIEF Schema for approving/editing/rejecting a preview row.\nclass PreviewRowUpdate(BaseModel):\n action: str = Field(..., description=\"'approve', 'reject', or 'edit'\")\n translation: str | None = Field(None, description=\"Edited translation (required for 'edit' action)\")\n feedback: str | None = Field(None, description=\"Optional feedback/comment\")\n# #endregion PreviewRowUpdate\n" }, { "contract_id": "PreviewAcceptResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 220, - "end_line": 232, + "start_line": 238, + "end_line": 251, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for preview accept response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region PreviewAcceptResponse [C:1] [TYPE Class]\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n expires_at: Optional[datetime] = None\n records: List['PreviewRow'] = []\n\n class Config:\n from_attributes = True\n# #endregion PreviewAcceptResponse\n" + "body": "# #region PreviewAcceptResponse [TYPE Class]\n# @BRIEF Schema for preview accept response.\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list['PreviewRow'] = []\n\n class Config:\n from_attributes = True\n# #endregion PreviewAcceptResponse\n" }, { "contract_id": "CostEstimate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 235, - "end_line": 245, + "start_line": 254, + "end_line": 265, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for cost estimation in preview response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region CostEstimate [C:1] [TYPE Class]\nclass CostEstimate(BaseModel):\n sample_size: int = 0\n sample_prompt_tokens: int = 0\n sample_output_tokens: int = 0\n sample_total_tokens: int = 0\n sample_cost: float = 0.0\n estimated_total_rows: int = 0\n estimated_tokens: int = 0\n estimated_cost: float = 0.0\n# #endregion CostEstimate\n" + "body": "# #region CostEstimate [TYPE Class]\n# @BRIEF Schema for cost estimation in preview response.\nclass CostEstimate(BaseModel):\n sample_size: int = 0\n sample_prompt_tokens: int = 0\n sample_output_tokens: int = 0\n sample_total_tokens: int = 0\n sample_cost: float = 0.0\n estimated_total_rows: int = 0\n estimated_tokens: int = 0\n estimated_cost: float = 0.0\n# #endregion CostEstimate\n" }, { "contract_id": "TermCorrectionSubmit", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 248, - "end_line": 256, + "start_line": 268, + "end_line": 277, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for submitting a term correction in a translation preview." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TermCorrectionSubmit [C:1] [TYPE Class]\nclass TermCorrectionSubmit(BaseModel):\n source_term: str\n incorrect_target_term: str\n corrected_target_term: str\n dictionary_id: Optional[str] = Field(None, description=\"Target dictionary ID (language-filtered)\")\n origin_run_id: Optional[str] = Field(None, description=\"Run ID from which this correction originated\")\n origin_row_key: Optional[str] = Field(None, description=\"Row key within the run\")\n# #endregion TermCorrectionSubmit\n" + "body": "# #region TermCorrectionSubmit [TYPE Class]\n# @BRIEF Schema for submitting a term correction in a translation preview.\nclass TermCorrectionSubmit(BaseModel):\n source_term: str\n incorrect_target_term: str\n corrected_target_term: str\n dictionary_id: str | None = Field(None, description=\"Target dictionary ID (language-filtered)\")\n origin_run_id: str | None = Field(None, description=\"Run ID from which this correction originated\")\n origin_row_key: str | None = Field(None, description=\"Row key within the run\")\n# #endregion TermCorrectionSubmit\n" }, { "contract_id": "TermCorrectionBulkSubmit", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 259, - "end_line": 263, + "start_line": 280, + "end_line": 285, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for submitting multiple term corrections atomically." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TermCorrectionBulkSubmit [C:1] [TYPE Class]\nclass TermCorrectionBulkSubmit(BaseModel):\n corrections: List[TermCorrectionSubmit]\n dictionary_id: str = Field(..., description=\"Target dictionary ID for all corrections\")\n# #endregion TermCorrectionBulkSubmit\n" + "body": "# #region TermCorrectionBulkSubmit [TYPE Class]\n# @BRIEF Schema for submitting multiple term corrections atomically.\nclass TermCorrectionBulkSubmit(BaseModel):\n corrections: list[TermCorrectionSubmit]\n dictionary_id: str = Field(..., description=\"Target dictionary ID for all corrections\")\n# #endregion TermCorrectionBulkSubmit\n" }, { "contract_id": "CorrectionConflictResult", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 266, - "end_line": 272, + "start_line": 288, + "end_line": 295, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for reporting conflicts in correction submission." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region CorrectionConflictResult [C:1] [TYPE Class]\nclass CorrectionConflictResult(BaseModel):\n source_term: str\n existing_target_term: str\n submitted_target_term: str\n action: str = \"keep_existing\"\n# #endregion CorrectionConflictResult\n" + "body": "# #region CorrectionConflictResult [TYPE Class]\n# @BRIEF Schema for reporting conflicts in correction submission.\nclass CorrectionConflictResult(BaseModel):\n source_term: str\n existing_target_term: str\n submitted_target_term: str\n action: str = \"keep_existing\"\n# #endregion CorrectionConflictResult\n" }, { "contract_id": "CorrectionSubmitResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 275, - "end_line": 283, + "start_line": 298, + "end_line": 307, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for correction submission response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region CorrectionSubmitResponse [C:1] [TYPE Class]\nclass CorrectionSubmitResponse(BaseModel):\n entry_id: Optional[str] = None\n action: str # \"created\", \"updated\", \"conflict_detected\", \"skipped\"\n source_term: str\n target_term: str\n conflict: Optional[CorrectionConflictResult] = None\n message: Optional[str] = None\n# #endregion CorrectionSubmitResponse\n" + "body": "# #region CorrectionSubmitResponse [TYPE Class]\n# @BRIEF Schema for correction submission response.\nclass CorrectionSubmitResponse(BaseModel):\n entry_id: str | None = None\n action: str # \"created\", \"updated\", \"conflict_detected\", \"skipped\"\n source_term: str\n target_term: str\n conflict: CorrectionConflictResult | None = None\n message: str | None = None\n# #endregion CorrectionSubmitResponse\n" }, { "contract_id": "ScheduleResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 294, - "end_line": 309, + "start_line": 320, + "end_line": 337, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for schedule API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region ScheduleResponse [C:1] [TYPE Class]\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n last_run_at: Optional[datetime] = None\n next_run_at: Optional[datetime] = None\n created_by: Optional[str] = None\n created_at: datetime\n updated_at: Optional[datetime] = None\n\n class Config:\n from_attributes = True\n# #endregion ScheduleResponse\n" + "body": "# #region ScheduleResponse [TYPE Class]\n# @BRIEF Schema for schedule API responses.\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion ScheduleResponse\n" }, { "contract_id": "NextExecutionResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 312, - "end_line": 318, + "start_line": 340, + "end_line": 347, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for next execution preview." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region NextExecutionResponse [C:1] [TYPE Class]\nclass NextExecutionResponse(BaseModel):\n job_id: str\n cron_expression: str\n timezone: str\n next_executions: List[str] = []\n# #endregion NextExecutionResponse\n" + "body": "# #region NextExecutionResponse [TYPE Class]\n# @BRIEF Schema for next execution preview.\nclass NextExecutionResponse(BaseModel):\n job_id: str\n cron_expression: str\n timezone: str\n next_executions: list[str] = []\n# #endregion NextExecutionResponse\n" }, { "contract_id": "TranslationRunResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 321, - "end_line": 345, + "start_line": 350, + "end_line": 375, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for translation run API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationRunResponse [C:1] [TYPE Class]\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: Optional[str] = None\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n error_message: Optional[str] = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: Optional[str] = None\n superset_execution_id: Optional[str] = None\n config_snapshot: Optional[Dict[str, Any]] = None\n key_hash: Optional[str] = None\n config_hash: Optional[str] = None\n dict_snapshot_hash: Optional[str] = None\n created_by: Optional[str] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunResponse\n" + "body": "# #region TranslationRunResponse [TYPE Class]\n# @BRIEF Schema for translation run API responses.\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: str | None = None\n started_at: datetime | None = None\n completed_at: datetime | None = None\n error_message: str | None = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: str | None = None\n superset_execution_id: str | None = None\n config_snapshot: dict[str, Any] | None = None\n key_hash: str | None = None\n config_hash: str | None = None\n dict_snapshot_hash: str | None = None\n created_by: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunResponse\n" }, { "contract_id": "RunDetailResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 348, - "end_line": 355, + "start_line": 378, + "end_line": 386, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for detailed run response including records, events, and config snapshot." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region RunDetailResponse [C:1] [TYPE Class]\nclass RunDetailResponse(BaseModel):\n run: TranslationRunResponse\n records: List[Dict[str, Any]] = Field(default_factory=list, description=\"Paginated translation records\")\n events: List[Dict[str, Any]] = Field(default_factory=list, description=\"Run events\")\n event_invariants: Optional[Dict[str, Any]] = None\n batch_count: int = 0\n# #endregion RunDetailResponse\n" + "body": "# #region RunDetailResponse [TYPE Class]\n# @BRIEF Schema for detailed run response including records, events, and config snapshot.\nclass RunDetailResponse(BaseModel):\n run: TranslationRunResponse\n records: list[dict[str, Any]] = Field(default_factory=list, description=\"Paginated translation records\")\n events: list[dict[str, Any]] = Field(default_factory=list, description=\"Run events\")\n event_invariants: dict[str, Any] | None = None\n batch_count: int = 0\n# #endregion RunDetailResponse\n" }, { "contract_id": "RunHistoryFilter", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 358, - "end_line": 368, + "start_line": 389, + "end_line": 400, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for filtering run history." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region RunHistoryFilter [C:1] [TYPE Class]\nclass RunHistoryFilter(BaseModel):\n job_id: Optional[str] = None\n status: Optional[str] = None\n trigger_type: Optional[str] = None\n created_by: Optional[str] = None\n date_from: Optional[datetime] = None\n date_to: Optional[datetime] = None\n page: int = 1\n page_size: int = 20\n# #endregion RunHistoryFilter\n" + "body": "# #region RunHistoryFilter [TYPE Class]\n# @BRIEF Schema for filtering run history.\nclass RunHistoryFilter(BaseModel):\n job_id: str | None = None\n status: str | None = None\n trigger_type: str | None = None\n created_by: str | None = None\n date_from: datetime | None = None\n date_to: datetime | None = None\n page: int = 1\n page_size: int = 20\n# #endregion RunHistoryFilter\n" }, { "contract_id": "RunListResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 371, - "end_line": 377, + "start_line": 403, + "end_line": 410, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for paginated run list response." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region RunListResponse [C:1] [TYPE Class]\nclass RunListResponse(BaseModel):\n items: List[TranslationRunResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion RunListResponse\n" + "body": "# #region RunListResponse [TYPE Class]\n# @BRIEF Schema for paginated run list response.\nclass RunListResponse(BaseModel):\n items: list[TranslationRunResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion RunListResponse\n" }, { "contract_id": "AggregatedMetricsResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 380, - "end_line": 396, + "start_line": 413, + "end_line": 430, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for aggregated per-job metrics." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region AggregatedMetricsResponse [C:1] [TYPE Class]\nclass AggregatedMetricsResponse(BaseModel):\n job_id: str\n total_runs: int = 0\n successful_runs: int = 0\n failed_runs: int = 0\n cancelled_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n cumulative_tokens: Optional[int] = None\n cumulative_cost: Optional[float] = None\n avg_duration_ms: Optional[float] = None\n last_run_at: Optional[datetime] = None\n next_scheduled_run: Optional[datetime] = None\n# #endregion AggregatedMetricsResponse\n" + "body": "# #region AggregatedMetricsResponse [TYPE Class]\n# @BRIEF Schema for aggregated per-job metrics.\nclass AggregatedMetricsResponse(BaseModel):\n job_id: str\n total_runs: int = 0\n successful_runs: int = 0\n failed_runs: int = 0\n cancelled_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n cumulative_tokens: int | None = None\n cumulative_cost: float | None = None\n avg_duration_ms: float | None = None\n last_run_at: datetime | None = None\n next_scheduled_run: datetime | None = None\n# #endregion AggregatedMetricsResponse\n" }, { "contract_id": "PreviewRow", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 399, - "end_line": 409, + "start_line": 433, + "end_line": 444, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "A single row in a translation preview showing original and translated content side by side." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region PreviewRow [C:1] [TYPE Class]\nclass PreviewRow(BaseModel):\n id: str\n source_sql: Optional[str] = None\n target_sql: Optional[str] = None\n source_object_type: Optional[str] = None\n source_object_id: Optional[str] = None\n source_object_name: Optional[str] = None\n status: str = \"PENDING\"\n feedback: Optional[str] = None\n# #endregion PreviewRow\n" + "body": "# #region PreviewRow [TYPE Class]\n# @BRIEF A single row in a translation preview showing original and translated content side by side.\nclass PreviewRow(BaseModel):\n id: str\n source_sql: str | None = None\n target_sql: str | None = None\n source_object_type: str | None = None\n source_object_id: str | None = None\n source_object_name: str | None = None\n status: str = \"PENDING\"\n feedback: str | None = None\n# #endregion PreviewRow\n" }, { "contract_id": "TranslationPreviewResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 412, - "end_line": 425, + "start_line": 447, + "end_line": 461, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for translation preview session API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationPreviewResponse [C:1] [TYPE Class]\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: Optional[str] = None\n status: str\n created_by: Optional[str] = None\n created_at: datetime\n expires_at: Optional[datetime] = None\n records: List[PreviewRow] = []\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewResponse\n" + "body": "# #region TranslationPreviewResponse [TYPE Class]\n# @BRIEF Schema for translation preview session API responses.\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: str | None = None\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list[PreviewRow] = []\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewResponse\n" }, { "contract_id": "TranslationBatchResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 428, - "end_line": 443, + "start_line": 464, + "end_line": 480, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for translation batch API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region TranslationBatchResponse [C:1] [TYPE Class]\nclass TranslationBatchResponse(BaseModel):\n id: str\n run_id: str\n batch_index: int\n status: str\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationBatchResponse\n" + "body": "# #region TranslationBatchResponse [TYPE Class]\n# @BRIEF Schema for translation batch API responses.\nclass TranslationBatchResponse(BaseModel):\n id: str\n run_id: str\n batch_index: int\n status: str\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n started_at: datetime | None = None\n completed_at: datetime | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationBatchResponse\n" }, { "contract_id": "MetricsResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 446, - "end_line": 463, + "start_line": 483, + "end_line": 501, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": 1 + "PURPOSE": "Schema for translation metrics API responses." }, "relations": [], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + } + ], "anchor_syntax": "region", - "body": "# #region MetricsResponse [C:1] [TYPE Class]\nclass MetricsResponse(BaseModel):\n job_id: str\n snapshot_date: datetime\n total_jobs: int = 0\n total_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n avg_duration_ms: Optional[int] = None\n p50_duration_ms: Optional[int] = None\n p95_duration_ms: Optional[int] = None\n p99_duration_ms: Optional[int] = None\n\n class Config:\n from_attributes = True\n# #endregion MetricsResponse\n" + "body": "# #region MetricsResponse [TYPE Class]\n# @BRIEF Schema for translation metrics API responses.\nclass MetricsResponse(BaseModel):\n job_id: str\n snapshot_date: datetime\n total_jobs: int = 0\n total_runs: int = 0\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n avg_duration_ms: int | None = None\n p50_duration_ms: int | None = None\n p95_duration_ms: int | None = None\n p99_duration_ms: int | None = None\n\n class Config:\n from_attributes = True\n# #endregion MetricsResponse\n" }, { "contract_id": "ScriptsPackage", @@ -71764,8 +55806,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ScriptsPackage:Package]\n# @PURPOSE: Script entrypoint package root.\n# [/DEF:ScriptsPackage:Package]\n" + "anchor_syntax": "region", + "body": "# #region ScriptsPackage [TYPE Package]\n# @BRIEF Script entrypoint package root.\n# #endregion ScriptsPackage\n" }, { "contract_id": "CleanReleaseCliScript", @@ -71776,16 +55818,9 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Scripts", - "PURPOSE": "Provide headless CLI commands for candidate registration, artifact import and manifest build.", - "SEMANTICS": [ - "cli", - "clean-release", - "candidate", - "artifacts", - "manifest" - ] + "PURPOSE": "Provide headless CLI commands for candidate registration, artifact import and manifest build." }, "relations": [ { @@ -71796,15 +55831,15 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseCliScript:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: cli, clean-release, candidate, artifacts, manifest\n# @PURPOSE: Provide headless CLI commands for candidate registration, artifact import and manifest build.\n# @LAYER: Scripts\n# @RELATION: CALLS -> ComplianceOrchestrator\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nfrom datetime import date, datetime, timezone\nfrom typing import Any, Dict, List, Optional\n\nfrom ..models.clean_release import CandidateArtifact, ReleaseCandidate\nfrom ..services.clean_release.approval_service import (\n approve_candidate,\n reject_candidate,\n)\nfrom ..services.clean_release.compliance_execution_service import (\n ComplianceExecutionService,\n)\nfrom ..services.clean_release.enums import CandidateStatus\nfrom ..services.clean_release.publication_service import (\n publish_candidate,\n revoke_publication,\n)\n\n\n# [DEF:build_parser:Function]\n# @PURPOSE: Build argparse parser for clean release CLI.\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(prog=\"clean-release-cli\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n register = subparsers.add_parser(\"candidate-register\")\n register.add_argument(\"--candidate-id\", required=True)\n register.add_argument(\"--version\", required=True)\n register.add_argument(\"--source-snapshot-ref\", required=True)\n register.add_argument(\"--created-by\", default=\"cli-operator\")\n\n artifact_import = subparsers.add_parser(\"artifact-import\")\n artifact_import.add_argument(\"--candidate-id\", required=True)\n artifact_import.add_argument(\"--artifact-id\", required=True)\n artifact_import.add_argument(\"--path\", required=True)\n artifact_import.add_argument(\"--sha256\", required=True)\n artifact_import.add_argument(\"--size\", type=int, required=True)\n\n manifest_build = subparsers.add_parser(\"manifest-build\")\n manifest_build.add_argument(\"--candidate-id\", required=True)\n manifest_build.add_argument(\"--created-by\", default=\"cli-operator\")\n\n compliance_run = subparsers.add_parser(\"compliance-run\")\n compliance_run.add_argument(\"--candidate-id\", required=True)\n compliance_run.add_argument(\"--manifest-id\", required=False, default=None)\n compliance_run.add_argument(\"--actor\", default=\"cli-operator\")\n compliance_run.add_argument(\"--json\", action=\"store_true\")\n\n compliance_status = subparsers.add_parser(\"compliance-status\")\n compliance_status.add_argument(\"--run-id\", required=True)\n compliance_status.add_argument(\"--json\", action=\"store_true\")\n\n compliance_report = subparsers.add_parser(\"compliance-report\")\n compliance_report.add_argument(\"--run-id\", required=True)\n compliance_report.add_argument(\"--json\", action=\"store_true\")\n\n compliance_violations = subparsers.add_parser(\"compliance-violations\")\n compliance_violations.add_argument(\"--run-id\", required=True)\n compliance_violations.add_argument(\"--json\", action=\"store_true\")\n\n approve = subparsers.add_parser(\"approve\")\n approve.add_argument(\"--candidate-id\", required=True)\n approve.add_argument(\"--report-id\", required=True)\n approve.add_argument(\"--actor\", default=\"cli-operator\")\n approve.add_argument(\"--comment\", required=False, default=None)\n approve.add_argument(\"--json\", action=\"store_true\")\n\n reject = subparsers.add_parser(\"reject\")\n reject.add_argument(\"--candidate-id\", required=True)\n reject.add_argument(\"--report-id\", required=True)\n reject.add_argument(\"--actor\", default=\"cli-operator\")\n reject.add_argument(\"--comment\", required=False, default=None)\n reject.add_argument(\"--json\", action=\"store_true\")\n\n publish = subparsers.add_parser(\"publish\")\n publish.add_argument(\"--candidate-id\", required=True)\n publish.add_argument(\"--report-id\", required=True)\n publish.add_argument(\"--actor\", default=\"cli-operator\")\n publish.add_argument(\"--target-channel\", required=True)\n publish.add_argument(\"--publication-ref\", required=False, default=None)\n publish.add_argument(\"--json\", action=\"store_true\")\n\n revoke = subparsers.add_parser(\"revoke\")\n revoke.add_argument(\"--publication-id\", required=True)\n revoke.add_argument(\"--actor\", default=\"cli-operator\")\n revoke.add_argument(\"--comment\", required=False, default=None)\n revoke.add_argument(\"--json\", action=\"store_true\")\n\n return parser\n\n\n# [/DEF:build_parser:Function]\n\n\n# [DEF:run_candidate_register:Function]\n# @PURPOSE: Register candidate in repository via CLI command.\n# @PRE: Candidate ID must be unique.\n# @POST: Candidate is persisted in DRAFT status.\ndef run_candidate_register(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n existing = repository.get_candidate(args.candidate_id)\n if existing is not None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate already exists\"}))\n return 1\n\n candidate = ReleaseCandidate(\n id=args.candidate_id,\n version=args.version,\n source_snapshot_ref=args.source_snapshot_ref,\n created_by=args.created_by,\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n print(json.dumps({\"status\": \"ok\", \"candidate_id\": candidate.id}))\n return 0\n\n\n# [/DEF:run_candidate_register:Function]\n\n\n# [DEF:run_artifact_import:Function]\n# @PURPOSE: Import single artifact for existing candidate.\n# @PRE: Candidate must exist.\n# @POST: Artifact is persisted for candidate.\ndef run_artifact_import(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n candidate = repository.get_candidate(args.candidate_id)\n if candidate is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate not found\"}))\n return 1\n\n artifact = CandidateArtifact(\n id=args.artifact_id,\n candidate_id=args.candidate_id,\n path=args.path,\n sha256=args.sha256,\n size=args.size,\n )\n repository.save_artifact(artifact)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n print(json.dumps({\"status\": \"ok\", \"artifact_id\": artifact.id}))\n return 0\n\n\n# [/DEF:run_artifact_import:Function]\n\n\n# [DEF:run_manifest_build:Function]\n# @PURPOSE: Build immutable manifest snapshot for candidate.\n# @PRE: Candidate must exist.\n# @POST: New manifest version is persisted.\ndef run_manifest_build(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n from ..services.clean_release.manifest_service import build_manifest_snapshot\n\n repository = get_clean_release_repository()\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=args.candidate_id,\n created_by=args.created_by,\n )\n except ValueError as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 1\n\n print(\n json.dumps(\n {\n \"status\": \"ok\",\n \"manifest_id\": manifest.id,\n \"version\": manifest.manifest_version,\n }\n )\n )\n return 0\n\n\n# [/DEF:run_manifest_build:Function]\n\n\n# [DEF:run_compliance_run:Function]\n# @PURPOSE: Execute compliance run for candidate with optional manifest fallback.\n# @PRE: Candidate exists and trusted snapshots are configured.\n# @POST: Returns run payload and exit code 0 on success.\ndef run_compliance_run(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository, get_config_manager\n\n repository = get_clean_release_repository()\n config_manager = get_config_manager()\n service = ComplianceExecutionService(\n repository=repository, config_manager=config_manager\n )\n\n try:\n result = service.execute_run(\n candidate_id=args.candidate_id,\n requested_by=args.actor,\n manifest_id=args.manifest_id,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n payload = {\n \"status\": \"ok\",\n \"run_id\": result.run.id,\n \"candidate_id\": result.run.candidate_id,\n \"run_status\": result.run.status,\n \"final_status\": result.run.final_status,\n \"task_id\": getattr(result.run, \"task_id\", None),\n \"report_id\": getattr(result.run, \"report_id\", None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# [/DEF:run_compliance_run:Function]\n\n\n# [DEF:run_compliance_status:Function]\n# @PURPOSE: Read run status by run id.\n# @PRE: Run exists.\n# @POST: Returns run status payload.\ndef run_compliance_status(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n payload = {\n \"status\": \"ok\",\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n \"task_id\": getattr(run, \"task_id\", None),\n \"report_id\": getattr(run, \"report_id\", None) or (report.id if report else None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# [/DEF:run_compliance_status:Function]\n\n\n# [DEF:_to_payload:Function]\n# @PURPOSE: Serialize domain models for CLI JSON output across SQLAlchemy/Pydantic variants.\n# @PRE: value is serializable model or primitive object.\n# @POST: Returns dictionary payload without mutating value.\ndef _to_payload(value: Any) -> Dict[str, Any]:\n def _normalize(raw: Any) -> Any:\n if isinstance(raw, datetime):\n return raw.isoformat()\n if isinstance(raw, date):\n return raw.isoformat()\n if isinstance(raw, dict):\n return {str(key): _normalize(item) for key, item in raw.items()}\n if isinstance(raw, list):\n return [_normalize(item) for item in raw]\n if isinstance(raw, tuple):\n return [_normalize(item) for item in raw]\n return raw\n\n if hasattr(value, \"model_dump\"):\n return _normalize(value.model_dump())\n table = getattr(value, \"__table__\", None)\n if table is not None:\n row = {column.name: getattr(value, column.name) for column in table.columns}\n return _normalize(row)\n raise TypeError(f\"unsupported payload type: {type(value)!r}\")\n\n\n# [/DEF:_to_payload:Function]\n\n\n# [DEF:run_compliance_report:Function]\n# @PURPOSE: Read immutable report by run id.\n# @PRE: Run and report exist.\n# @POST: Returns report payload.\ndef run_compliance_report(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n if report is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"report not found\"}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"report\": _to_payload(report)}))\n return 0\n\n\n# [/DEF:run_compliance_report:Function]\n\n\n# [DEF:run_compliance_violations:Function]\n# @PURPOSE: Read run violations by run id.\n# @PRE: Run exists.\n# @POST: Returns violations payload.\ndef run_compliance_violations(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n violations = repository.get_violations_by_run(args.run_id)\n print(\n json.dumps(\n {\"status\": \"ok\", \"items\": [_to_payload(item) for item in violations]}\n )\n )\n return 0\n\n\n# [/DEF:run_compliance_violations:Function]\n\n\n# [DEF:run_approve:Function]\n# @PURPOSE: Approve candidate based on immutable PASSED report.\n# @PRE: Candidate and report exist; report is PASSED.\n# @POST: Persists APPROVED decision and returns success payload.\ndef run_approve(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# [/DEF:run_approve:Function]\n\n\n# [DEF:run_reject:Function]\n# @PURPOSE: Reject candidate without mutating compliance evidence.\n# @PRE: Candidate and report exist.\n# @POST: Persists REJECTED decision and returns success payload.\ndef run_reject(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# [/DEF:run_reject:Function]\n\n\n# [DEF:run_publish:Function]\n# @PURPOSE: Publish approved candidate to target channel.\n# @PRE: Candidate is approved and report belongs to candidate.\n# @POST: Appends ACTIVE publication record and returns payload.\ndef run_publish(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n published_by=args.actor,\n target_channel=args.target_channel,\n publication_ref=args.publication_ref,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# [/DEF:run_publish:Function]\n\n\n# [DEF:run_revoke:Function]\n# @PURPOSE: Revoke active publication record.\n# @PRE: Publication id exists and is ACTIVE.\n# @POST: Publication record status becomes REVOKED.\ndef run_revoke(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=args.publication_id,\n revoked_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# [/DEF:run_revoke:Function]\n\n\n# [DEF:main:Function]\n# @PURPOSE: CLI entrypoint for clean release commands.\ndef main(argv: Optional[List[str]] = None) -> int:\n parser = build_parser()\n args = parser.parse_args(argv)\n\n if args.command == \"candidate-register\":\n return run_candidate_register(args)\n if args.command == \"artifact-import\":\n return run_artifact_import(args)\n if args.command == \"manifest-build\":\n return run_manifest_build(args)\n if args.command == \"compliance-run\":\n return run_compliance_run(args)\n if args.command == \"compliance-status\":\n return run_compliance_status(args)\n if args.command == \"compliance-report\":\n return run_compliance_report(args)\n if args.command == \"compliance-violations\":\n return run_compliance_violations(args)\n if args.command == \"approve\":\n return run_approve(args)\n if args.command == \"reject\":\n return run_reject(args)\n if args.command == \"publish\":\n return run_publish(args)\n if args.command == \"revoke\":\n return run_revoke(args)\n\n print(json.dumps({\"status\": \"error\", \"message\": \"unknown command\"}))\n return 2\n\n\n# [/DEF:main:Function]\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n\n# [/DEF:CleanReleaseCliScript:Module]\n" + "anchor_syntax": "region", + "body": "# #region CleanReleaseCliScript [C:3] [TYPE Module] [SEMANTICS clean-release, artifact, manifest, candidate, cli]\n# @BRIEF Provide headless CLI commands for candidate registration, artifact import and manifest build.\n# @LAYER: Scripts\n# @RELATION CALLS -> ComplianceOrchestrator\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nfrom datetime import UTC, date, datetime\nfrom typing import Any\n\nfrom ..core.cot_logger import seed_trace_id\nfrom ..models.clean_release import CandidateArtifact, ReleaseCandidate\nfrom ..services.clean_release.approval_service import (\n approve_candidate,\n reject_candidate,\n)\nfrom ..services.clean_release.compliance_execution_service import (\n ComplianceExecutionService,\n)\nfrom ..services.clean_release.enums import CandidateStatus\nfrom ..services.clean_release.publication_service import (\n publish_candidate,\n revoke_publication,\n)\n\n\n# #region build_parser [TYPE Function]\n# @BRIEF Build argparse parser for clean release CLI.\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(prog=\"clean-release-cli\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n register = subparsers.add_parser(\"candidate-register\")\n register.add_argument(\"--candidate-id\", required=True)\n register.add_argument(\"--version\", required=True)\n register.add_argument(\"--source-snapshot-ref\", required=True)\n register.add_argument(\"--created-by\", default=\"cli-operator\")\n\n artifact_import = subparsers.add_parser(\"artifact-import\")\n artifact_import.add_argument(\"--candidate-id\", required=True)\n artifact_import.add_argument(\"--artifact-id\", required=True)\n artifact_import.add_argument(\"--path\", required=True)\n artifact_import.add_argument(\"--sha256\", required=True)\n artifact_import.add_argument(\"--size\", type=int, required=True)\n\n manifest_build = subparsers.add_parser(\"manifest-build\")\n manifest_build.add_argument(\"--candidate-id\", required=True)\n manifest_build.add_argument(\"--created-by\", default=\"cli-operator\")\n\n compliance_run = subparsers.add_parser(\"compliance-run\")\n compliance_run.add_argument(\"--candidate-id\", required=True)\n compliance_run.add_argument(\"--manifest-id\", required=False, default=None)\n compliance_run.add_argument(\"--actor\", default=\"cli-operator\")\n compliance_run.add_argument(\"--json\", action=\"store_true\")\n\n compliance_status = subparsers.add_parser(\"compliance-status\")\n compliance_status.add_argument(\"--run-id\", required=True)\n compliance_status.add_argument(\"--json\", action=\"store_true\")\n\n compliance_report = subparsers.add_parser(\"compliance-report\")\n compliance_report.add_argument(\"--run-id\", required=True)\n compliance_report.add_argument(\"--json\", action=\"store_true\")\n\n compliance_violations = subparsers.add_parser(\"compliance-violations\")\n compliance_violations.add_argument(\"--run-id\", required=True)\n compliance_violations.add_argument(\"--json\", action=\"store_true\")\n\n approve = subparsers.add_parser(\"approve\")\n approve.add_argument(\"--candidate-id\", required=True)\n approve.add_argument(\"--report-id\", required=True)\n approve.add_argument(\"--actor\", default=\"cli-operator\")\n approve.add_argument(\"--comment\", required=False, default=None)\n approve.add_argument(\"--json\", action=\"store_true\")\n\n reject = subparsers.add_parser(\"reject\")\n reject.add_argument(\"--candidate-id\", required=True)\n reject.add_argument(\"--report-id\", required=True)\n reject.add_argument(\"--actor\", default=\"cli-operator\")\n reject.add_argument(\"--comment\", required=False, default=None)\n reject.add_argument(\"--json\", action=\"store_true\")\n\n publish = subparsers.add_parser(\"publish\")\n publish.add_argument(\"--candidate-id\", required=True)\n publish.add_argument(\"--report-id\", required=True)\n publish.add_argument(\"--actor\", default=\"cli-operator\")\n publish.add_argument(\"--target-channel\", required=True)\n publish.add_argument(\"--publication-ref\", required=False, default=None)\n publish.add_argument(\"--json\", action=\"store_true\")\n\n revoke = subparsers.add_parser(\"revoke\")\n revoke.add_argument(\"--publication-id\", required=True)\n revoke.add_argument(\"--actor\", default=\"cli-operator\")\n revoke.add_argument(\"--comment\", required=False, default=None)\n revoke.add_argument(\"--json\", action=\"store_true\")\n\n return parser\n\n\n# #endregion build_parser\n\n\n# #region run_candidate_register [TYPE Function]\n# @BRIEF Register candidate in repository via CLI command.\n# @PRE: Candidate ID must be unique.\n# @POST: Candidate is persisted in DRAFT status.\ndef run_candidate_register(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n existing = repository.get_candidate(args.candidate_id)\n if existing is not None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate already exists\"}))\n return 1\n\n candidate = ReleaseCandidate(\n id=args.candidate_id,\n version=args.version,\n source_snapshot_ref=args.source_snapshot_ref,\n created_by=args.created_by,\n created_at=datetime.now(UTC),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n print(json.dumps({\"status\": \"ok\", \"candidate_id\": candidate.id}))\n return 0\n\n\n# #endregion run_candidate_register\n\n\n# #region run_artifact_import [TYPE Function]\n# @BRIEF Import single artifact for existing candidate.\n# @PRE: Candidate must exist.\n# @POST: Artifact is persisted for candidate.\ndef run_artifact_import(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n candidate = repository.get_candidate(args.candidate_id)\n if candidate is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate not found\"}))\n return 1\n\n artifact = CandidateArtifact(\n id=args.artifact_id,\n candidate_id=args.candidate_id,\n path=args.path,\n sha256=args.sha256,\n size=args.size,\n )\n repository.save_artifact(artifact)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n print(json.dumps({\"status\": \"ok\", \"artifact_id\": artifact.id}))\n return 0\n\n\n# #endregion run_artifact_import\n\n\n# #region run_manifest_build [TYPE Function]\n# @BRIEF Build immutable manifest snapshot for candidate.\n# @PRE: Candidate must exist.\n# @POST: New manifest version is persisted.\ndef run_manifest_build(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n from ..services.clean_release.manifest_service import build_manifest_snapshot\n\n repository = get_clean_release_repository()\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=args.candidate_id,\n created_by=args.created_by,\n )\n except ValueError as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 1\n\n print(\n json.dumps(\n {\n \"status\": \"ok\",\n \"manifest_id\": manifest.id,\n \"version\": manifest.manifest_version,\n }\n )\n )\n return 0\n\n\n# #endregion run_manifest_build\n\n\n# #region run_compliance_run [TYPE Function]\n# @BRIEF Execute compliance run for candidate with optional manifest fallback.\n# @PRE: Candidate exists and trusted snapshots are configured.\n# @POST: Returns run payload and exit code 0 on success.\ndef run_compliance_run(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository, get_config_manager\n\n repository = get_clean_release_repository()\n config_manager = get_config_manager()\n service = ComplianceExecutionService(\n repository=repository, config_manager=config_manager\n )\n\n try:\n result = service.execute_run(\n candidate_id=args.candidate_id,\n requested_by=args.actor,\n manifest_id=args.manifest_id,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n payload = {\n \"status\": \"ok\",\n \"run_id\": result.run.id,\n \"candidate_id\": result.run.candidate_id,\n \"run_status\": result.run.status,\n \"final_status\": result.run.final_status,\n \"task_id\": getattr(result.run, \"task_id\", None),\n \"report_id\": getattr(result.run, \"report_id\", None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# #endregion run_compliance_run\n\n\n# #region run_compliance_status [TYPE Function]\n# @BRIEF Read run status by run id.\n# @PRE: Run exists.\n# @POST: Returns run status payload.\ndef run_compliance_status(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n payload = {\n \"status\": \"ok\",\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n \"task_id\": getattr(run, \"task_id\", None),\n \"report_id\": getattr(run, \"report_id\", None) or (report.id if report else None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# #endregion run_compliance_status\n\n\n# #region _to_payload [TYPE Function]\n# @BRIEF Serialize domain models for CLI JSON output across SQLAlchemy/Pydantic variants.\n# @PRE: value is serializable model or primitive object.\n# @POST: Returns dictionary payload without mutating value.\ndef _to_payload(value: Any) -> dict[str, Any]:\n def _normalize(raw: Any) -> Any:\n if isinstance(raw, datetime):\n return raw.isoformat()\n if isinstance(raw, date):\n return raw.isoformat()\n if isinstance(raw, dict):\n return {str(key): _normalize(item) for key, item in raw.items()}\n if isinstance(raw, list):\n return [_normalize(item) for item in raw]\n if isinstance(raw, tuple):\n return [_normalize(item) for item in raw]\n return raw\n\n if hasattr(value, \"model_dump\"):\n return _normalize(value.model_dump())\n table = getattr(value, \"__table__\", None)\n if table is not None:\n row = {column.name: getattr(value, column.name) for column in table.columns}\n return _normalize(row)\n raise TypeError(f\"unsupported payload type: {type(value)!r}\")\n\n\n# #endregion _to_payload\n\n\n# #region run_compliance_report [TYPE Function]\n# @BRIEF Read immutable report by run id.\n# @PRE: Run and report exist.\n# @POST: Returns report payload.\ndef run_compliance_report(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n if report is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"report not found\"}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"report\": _to_payload(report)}))\n return 0\n\n\n# #endregion run_compliance_report\n\n\n# #region run_compliance_violations [TYPE Function]\n# @BRIEF Read run violations by run id.\n# @PRE: Run exists.\n# @POST: Returns violations payload.\ndef run_compliance_violations(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n violations = repository.get_violations_by_run(args.run_id)\n print(\n json.dumps(\n {\"status\": \"ok\", \"items\": [_to_payload(item) for item in violations]}\n )\n )\n return 0\n\n\n# #endregion run_compliance_violations\n\n\n# #region run_approve [TYPE Function]\n# @BRIEF Approve candidate based on immutable PASSED report.\n# @PRE: Candidate and report exist; report is PASSED.\n# @POST: Persists APPROVED decision and returns success payload.\ndef run_approve(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# #endregion run_approve\n\n\n# #region run_reject [TYPE Function]\n# @BRIEF Reject candidate without mutating compliance evidence.\n# @PRE: Candidate and report exist.\n# @POST: Persists REJECTED decision and returns success payload.\ndef run_reject(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# #endregion run_reject\n\n\n# #region run_publish [TYPE Function]\n# @BRIEF Publish approved candidate to target channel.\n# @PRE: Candidate is approved and report belongs to candidate.\n# @POST: Appends ACTIVE publication record and returns payload.\ndef run_publish(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n published_by=args.actor,\n target_channel=args.target_channel,\n publication_ref=args.publication_ref,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# #endregion run_publish\n\n\n# #region run_revoke [TYPE Function]\n# @BRIEF Revoke active publication record.\n# @PRE: Publication id exists and is ACTIVE.\n# @POST: Publication record status becomes REVOKED.\ndef run_revoke(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=args.publication_id,\n revoked_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# #endregion run_revoke\n\n\n# #region main [TYPE Function]\n# @BRIEF CLI entrypoint for clean release commands.\ndef main(argv: list[str] | None = None) -> int:\n seed_trace_id()\n parser = build_parser()\n args = parser.parse_args(argv)\n\n if args.command == \"candidate-register\":\n return run_candidate_register(args)\n if args.command == \"artifact-import\":\n return run_artifact_import(args)\n if args.command == \"manifest-build\":\n return run_manifest_build(args)\n if args.command == \"compliance-run\":\n return run_compliance_run(args)\n if args.command == \"compliance-status\":\n return run_compliance_status(args)\n if args.command == \"compliance-report\":\n return run_compliance_report(args)\n if args.command == \"compliance-violations\":\n return run_compliance_violations(args)\n if args.command == \"approve\":\n return run_approve(args)\n if args.command == \"reject\":\n return run_reject(args)\n if args.command == \"publish\":\n return run_publish(args)\n if args.command == \"revoke\":\n return run_revoke(args)\n\n print(json.dumps({\"status\": \"error\", \"message\": \"unknown command\"}))\n return 2\n\n\n# #endregion main\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n\n# #endregion CleanReleaseCliScript\n" }, { "contract_id": "build_parser", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 30, - "end_line": 102, + "start_line": 29, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71822,15 +55857,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_parser:Function]\n# @PURPOSE: Build argparse parser for clean release CLI.\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(prog=\"clean-release-cli\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n register = subparsers.add_parser(\"candidate-register\")\n register.add_argument(\"--candidate-id\", required=True)\n register.add_argument(\"--version\", required=True)\n register.add_argument(\"--source-snapshot-ref\", required=True)\n register.add_argument(\"--created-by\", default=\"cli-operator\")\n\n artifact_import = subparsers.add_parser(\"artifact-import\")\n artifact_import.add_argument(\"--candidate-id\", required=True)\n artifact_import.add_argument(\"--artifact-id\", required=True)\n artifact_import.add_argument(\"--path\", required=True)\n artifact_import.add_argument(\"--sha256\", required=True)\n artifact_import.add_argument(\"--size\", type=int, required=True)\n\n manifest_build = subparsers.add_parser(\"manifest-build\")\n manifest_build.add_argument(\"--candidate-id\", required=True)\n manifest_build.add_argument(\"--created-by\", default=\"cli-operator\")\n\n compliance_run = subparsers.add_parser(\"compliance-run\")\n compliance_run.add_argument(\"--candidate-id\", required=True)\n compliance_run.add_argument(\"--manifest-id\", required=False, default=None)\n compliance_run.add_argument(\"--actor\", default=\"cli-operator\")\n compliance_run.add_argument(\"--json\", action=\"store_true\")\n\n compliance_status = subparsers.add_parser(\"compliance-status\")\n compliance_status.add_argument(\"--run-id\", required=True)\n compliance_status.add_argument(\"--json\", action=\"store_true\")\n\n compliance_report = subparsers.add_parser(\"compliance-report\")\n compliance_report.add_argument(\"--run-id\", required=True)\n compliance_report.add_argument(\"--json\", action=\"store_true\")\n\n compliance_violations = subparsers.add_parser(\"compliance-violations\")\n compliance_violations.add_argument(\"--run-id\", required=True)\n compliance_violations.add_argument(\"--json\", action=\"store_true\")\n\n approve = subparsers.add_parser(\"approve\")\n approve.add_argument(\"--candidate-id\", required=True)\n approve.add_argument(\"--report-id\", required=True)\n approve.add_argument(\"--actor\", default=\"cli-operator\")\n approve.add_argument(\"--comment\", required=False, default=None)\n approve.add_argument(\"--json\", action=\"store_true\")\n\n reject = subparsers.add_parser(\"reject\")\n reject.add_argument(\"--candidate-id\", required=True)\n reject.add_argument(\"--report-id\", required=True)\n reject.add_argument(\"--actor\", default=\"cli-operator\")\n reject.add_argument(\"--comment\", required=False, default=None)\n reject.add_argument(\"--json\", action=\"store_true\")\n\n publish = subparsers.add_parser(\"publish\")\n publish.add_argument(\"--candidate-id\", required=True)\n publish.add_argument(\"--report-id\", required=True)\n publish.add_argument(\"--actor\", default=\"cli-operator\")\n publish.add_argument(\"--target-channel\", required=True)\n publish.add_argument(\"--publication-ref\", required=False, default=None)\n publish.add_argument(\"--json\", action=\"store_true\")\n\n revoke = subparsers.add_parser(\"revoke\")\n revoke.add_argument(\"--publication-id\", required=True)\n revoke.add_argument(\"--actor\", default=\"cli-operator\")\n revoke.add_argument(\"--comment\", required=False, default=None)\n revoke.add_argument(\"--json\", action=\"store_true\")\n\n return parser\n\n\n# [/DEF:build_parser:Function]\n" + "anchor_syntax": "region", + "body": "# #region build_parser [TYPE Function]\n# @BRIEF Build argparse parser for clean release CLI.\ndef build_parser() -> argparse.ArgumentParser:\n parser = argparse.ArgumentParser(prog=\"clean-release-cli\")\n subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n register = subparsers.add_parser(\"candidate-register\")\n register.add_argument(\"--candidate-id\", required=True)\n register.add_argument(\"--version\", required=True)\n register.add_argument(\"--source-snapshot-ref\", required=True)\n register.add_argument(\"--created-by\", default=\"cli-operator\")\n\n artifact_import = subparsers.add_parser(\"artifact-import\")\n artifact_import.add_argument(\"--candidate-id\", required=True)\n artifact_import.add_argument(\"--artifact-id\", required=True)\n artifact_import.add_argument(\"--path\", required=True)\n artifact_import.add_argument(\"--sha256\", required=True)\n artifact_import.add_argument(\"--size\", type=int, required=True)\n\n manifest_build = subparsers.add_parser(\"manifest-build\")\n manifest_build.add_argument(\"--candidate-id\", required=True)\n manifest_build.add_argument(\"--created-by\", default=\"cli-operator\")\n\n compliance_run = subparsers.add_parser(\"compliance-run\")\n compliance_run.add_argument(\"--candidate-id\", required=True)\n compliance_run.add_argument(\"--manifest-id\", required=False, default=None)\n compliance_run.add_argument(\"--actor\", default=\"cli-operator\")\n compliance_run.add_argument(\"--json\", action=\"store_true\")\n\n compliance_status = subparsers.add_parser(\"compliance-status\")\n compliance_status.add_argument(\"--run-id\", required=True)\n compliance_status.add_argument(\"--json\", action=\"store_true\")\n\n compliance_report = subparsers.add_parser(\"compliance-report\")\n compliance_report.add_argument(\"--run-id\", required=True)\n compliance_report.add_argument(\"--json\", action=\"store_true\")\n\n compliance_violations = subparsers.add_parser(\"compliance-violations\")\n compliance_violations.add_argument(\"--run-id\", required=True)\n compliance_violations.add_argument(\"--json\", action=\"store_true\")\n\n approve = subparsers.add_parser(\"approve\")\n approve.add_argument(\"--candidate-id\", required=True)\n approve.add_argument(\"--report-id\", required=True)\n approve.add_argument(\"--actor\", default=\"cli-operator\")\n approve.add_argument(\"--comment\", required=False, default=None)\n approve.add_argument(\"--json\", action=\"store_true\")\n\n reject = subparsers.add_parser(\"reject\")\n reject.add_argument(\"--candidate-id\", required=True)\n reject.add_argument(\"--report-id\", required=True)\n reject.add_argument(\"--actor\", default=\"cli-operator\")\n reject.add_argument(\"--comment\", required=False, default=None)\n reject.add_argument(\"--json\", action=\"store_true\")\n\n publish = subparsers.add_parser(\"publish\")\n publish.add_argument(\"--candidate-id\", required=True)\n publish.add_argument(\"--report-id\", required=True)\n publish.add_argument(\"--actor\", default=\"cli-operator\")\n publish.add_argument(\"--target-channel\", required=True)\n publish.add_argument(\"--publication-ref\", required=False, default=None)\n publish.add_argument(\"--json\", action=\"store_true\")\n\n revoke = subparsers.add_parser(\"revoke\")\n revoke.add_argument(\"--publication-id\", required=True)\n revoke.add_argument(\"--actor\", default=\"cli-operator\")\n revoke.add_argument(\"--comment\", required=False, default=None)\n revoke.add_argument(\"--json\", action=\"store_true\")\n\n return parser\n\n\n# #endregion build_parser\n" }, { "contract_id": "run_candidate_register", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 105, - "end_line": 131, + "start_line": 104, + "end_line": 130, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71868,15 +55903,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_candidate_register:Function]\n# @PURPOSE: Register candidate in repository via CLI command.\n# @PRE: Candidate ID must be unique.\n# @POST: Candidate is persisted in DRAFT status.\ndef run_candidate_register(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n existing = repository.get_candidate(args.candidate_id)\n if existing is not None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate already exists\"}))\n return 1\n\n candidate = ReleaseCandidate(\n id=args.candidate_id,\n version=args.version,\n source_snapshot_ref=args.source_snapshot_ref,\n created_by=args.created_by,\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n print(json.dumps({\"status\": \"ok\", \"candidate_id\": candidate.id}))\n return 0\n\n\n# [/DEF:run_candidate_register:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_candidate_register [TYPE Function]\n# @BRIEF Register candidate in repository via CLI command.\n# @PRE: Candidate ID must be unique.\n# @POST: Candidate is persisted in DRAFT status.\ndef run_candidate_register(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n existing = repository.get_candidate(args.candidate_id)\n if existing is not None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate already exists\"}))\n return 1\n\n candidate = ReleaseCandidate(\n id=args.candidate_id,\n version=args.version,\n source_snapshot_ref=args.source_snapshot_ref,\n created_by=args.created_by,\n created_at=datetime.now(UTC),\n status=CandidateStatus.DRAFT.value,\n )\n repository.save_candidate(candidate)\n print(json.dumps({\"status\": \"ok\", \"candidate_id\": candidate.id}))\n return 0\n\n\n# #endregion run_candidate_register\n" }, { "contract_id": "run_artifact_import", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 134, - "end_line": 164, + "start_line": 133, + "end_line": 163, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71914,15 +55949,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_artifact_import:Function]\n# @PURPOSE: Import single artifact for existing candidate.\n# @PRE: Candidate must exist.\n# @POST: Artifact is persisted for candidate.\ndef run_artifact_import(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n candidate = repository.get_candidate(args.candidate_id)\n if candidate is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate not found\"}))\n return 1\n\n artifact = CandidateArtifact(\n id=args.artifact_id,\n candidate_id=args.candidate_id,\n path=args.path,\n sha256=args.sha256,\n size=args.size,\n )\n repository.save_artifact(artifact)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n print(json.dumps({\"status\": \"ok\", \"artifact_id\": artifact.id}))\n return 0\n\n\n# [/DEF:run_artifact_import:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_artifact_import [TYPE Function]\n# @BRIEF Import single artifact for existing candidate.\n# @PRE: Candidate must exist.\n# @POST: Artifact is persisted for candidate.\ndef run_artifact_import(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n candidate = repository.get_candidate(args.candidate_id)\n if candidate is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"candidate not found\"}))\n return 1\n\n artifact = CandidateArtifact(\n id=args.artifact_id,\n candidate_id=args.candidate_id,\n path=args.path,\n sha256=args.sha256,\n size=args.size,\n )\n repository.save_artifact(artifact)\n\n if candidate.status == CandidateStatus.DRAFT.value:\n candidate.transition_to(CandidateStatus.PREPARED)\n repository.save_candidate(candidate)\n\n print(json.dumps({\"status\": \"ok\", \"artifact_id\": artifact.id}))\n return 0\n\n\n# #endregion run_artifact_import\n" }, { "contract_id": "run_manifest_build", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 167, - "end_line": 198, + "start_line": 166, + "end_line": 197, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71960,15 +55995,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_manifest_build:Function]\n# @PURPOSE: Build immutable manifest snapshot for candidate.\n# @PRE: Candidate must exist.\n# @POST: New manifest version is persisted.\ndef run_manifest_build(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n from ..services.clean_release.manifest_service import build_manifest_snapshot\n\n repository = get_clean_release_repository()\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=args.candidate_id,\n created_by=args.created_by,\n )\n except ValueError as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 1\n\n print(\n json.dumps(\n {\n \"status\": \"ok\",\n \"manifest_id\": manifest.id,\n \"version\": manifest.manifest_version,\n }\n )\n )\n return 0\n\n\n# [/DEF:run_manifest_build:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_manifest_build [TYPE Function]\n# @BRIEF Build immutable manifest snapshot for candidate.\n# @PRE: Candidate must exist.\n# @POST: New manifest version is persisted.\ndef run_manifest_build(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n from ..services.clean_release.manifest_service import build_manifest_snapshot\n\n repository = get_clean_release_repository()\n try:\n manifest = build_manifest_snapshot(\n repository=repository,\n candidate_id=args.candidate_id,\n created_by=args.created_by,\n )\n except ValueError as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 1\n\n print(\n json.dumps(\n {\n \"status\": \"ok\",\n \"manifest_id\": manifest.id,\n \"version\": manifest.manifest_version,\n }\n )\n )\n return 0\n\n\n# #endregion run_manifest_build\n" }, { "contract_id": "run_compliance_run", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 201, - "end_line": 237, + "start_line": 200, + "end_line": 236, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72006,15 +56041,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_compliance_run:Function]\n# @PURPOSE: Execute compliance run for candidate with optional manifest fallback.\n# @PRE: Candidate exists and trusted snapshots are configured.\n# @POST: Returns run payload and exit code 0 on success.\ndef run_compliance_run(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository, get_config_manager\n\n repository = get_clean_release_repository()\n config_manager = get_config_manager()\n service = ComplianceExecutionService(\n repository=repository, config_manager=config_manager\n )\n\n try:\n result = service.execute_run(\n candidate_id=args.candidate_id,\n requested_by=args.actor,\n manifest_id=args.manifest_id,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n payload = {\n \"status\": \"ok\",\n \"run_id\": result.run.id,\n \"candidate_id\": result.run.candidate_id,\n \"run_status\": result.run.status,\n \"final_status\": result.run.final_status,\n \"task_id\": getattr(result.run, \"task_id\", None),\n \"report_id\": getattr(result.run, \"report_id\", None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# [/DEF:run_compliance_run:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_compliance_run [TYPE Function]\n# @BRIEF Execute compliance run for candidate with optional manifest fallback.\n# @PRE: Candidate exists and trusted snapshots are configured.\n# @POST: Returns run payload and exit code 0 on success.\ndef run_compliance_run(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository, get_config_manager\n\n repository = get_clean_release_repository()\n config_manager = get_config_manager()\n service = ComplianceExecutionService(\n repository=repository, config_manager=config_manager\n )\n\n try:\n result = service.execute_run(\n candidate_id=args.candidate_id,\n requested_by=args.actor,\n manifest_id=args.manifest_id,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n payload = {\n \"status\": \"ok\",\n \"run_id\": result.run.id,\n \"candidate_id\": result.run.candidate_id,\n \"run_status\": result.run.status,\n \"final_status\": result.run.final_status,\n \"task_id\": getattr(result.run, \"task_id\", None),\n \"report_id\": getattr(result.run, \"report_id\", None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# #endregion run_compliance_run\n" }, { "contract_id": "run_compliance_status", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 240, - "end_line": 269, + "start_line": 239, + "end_line": 268, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72052,15 +56087,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_compliance_status:Function]\n# @PURPOSE: Read run status by run id.\n# @PRE: Run exists.\n# @POST: Returns run status payload.\ndef run_compliance_status(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n payload = {\n \"status\": \"ok\",\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n \"task_id\": getattr(run, \"task_id\", None),\n \"report_id\": getattr(run, \"report_id\", None) or (report.id if report else None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# [/DEF:run_compliance_status:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_compliance_status [TYPE Function]\n# @BRIEF Read run status by run id.\n# @PRE: Run exists.\n# @POST: Returns run status payload.\ndef run_compliance_status(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n payload = {\n \"status\": \"ok\",\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n \"task_id\": getattr(run, \"task_id\", None),\n \"report_id\": getattr(run, \"report_id\", None) or (report.id if report else None),\n }\n print(json.dumps(payload))\n return 0\n\n\n# #endregion run_compliance_status\n" }, { "contract_id": "_to_payload", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 272, - "end_line": 299, + "start_line": 271, + "end_line": 298, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72098,15 +56133,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_to_payload:Function]\n# @PURPOSE: Serialize domain models for CLI JSON output across SQLAlchemy/Pydantic variants.\n# @PRE: value is serializable model or primitive object.\n# @POST: Returns dictionary payload without mutating value.\ndef _to_payload(value: Any) -> Dict[str, Any]:\n def _normalize(raw: Any) -> Any:\n if isinstance(raw, datetime):\n return raw.isoformat()\n if isinstance(raw, date):\n return raw.isoformat()\n if isinstance(raw, dict):\n return {str(key): _normalize(item) for key, item in raw.items()}\n if isinstance(raw, list):\n return [_normalize(item) for item in raw]\n if isinstance(raw, tuple):\n return [_normalize(item) for item in raw]\n return raw\n\n if hasattr(value, \"model_dump\"):\n return _normalize(value.model_dump())\n table = getattr(value, \"__table__\", None)\n if table is not None:\n row = {column.name: getattr(value, column.name) for column in table.columns}\n return _normalize(row)\n raise TypeError(f\"unsupported payload type: {type(value)!r}\")\n\n\n# [/DEF:_to_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #region _to_payload [TYPE Function]\n# @BRIEF Serialize domain models for CLI JSON output across SQLAlchemy/Pydantic variants.\n# @PRE: value is serializable model or primitive object.\n# @POST: Returns dictionary payload without mutating value.\ndef _to_payload(value: Any) -> dict[str, Any]:\n def _normalize(raw: Any) -> Any:\n if isinstance(raw, datetime):\n return raw.isoformat()\n if isinstance(raw, date):\n return raw.isoformat()\n if isinstance(raw, dict):\n return {str(key): _normalize(item) for key, item in raw.items()}\n if isinstance(raw, list):\n return [_normalize(item) for item in raw]\n if isinstance(raw, tuple):\n return [_normalize(item) for item in raw]\n return raw\n\n if hasattr(value, \"model_dump\"):\n return _normalize(value.model_dump())\n table = getattr(value, \"__table__\", None)\n if table is not None:\n row = {column.name: getattr(value, column.name) for column in table.columns}\n return _normalize(row)\n raise TypeError(f\"unsupported payload type: {type(value)!r}\")\n\n\n# #endregion _to_payload\n" }, { "contract_id": "run_compliance_report", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 302, - "end_line": 326, + "start_line": 301, + "end_line": 325, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72144,15 +56179,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_compliance_report:Function]\n# @PURPOSE: Read immutable report by run id.\n# @PRE: Run and report exist.\n# @POST: Returns report payload.\ndef run_compliance_report(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n if report is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"report not found\"}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"report\": _to_payload(report)}))\n return 0\n\n\n# [/DEF:run_compliance_report:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_compliance_report [TYPE Function]\n# @BRIEF Read immutable report by run id.\n# @PRE: Run and report exist.\n# @POST: Returns report payload.\ndef run_compliance_report(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n report = next(\n (item for item in repository.reports.values() if item.run_id == run.id), None\n )\n if report is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"report not found\"}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"report\": _to_payload(report)}))\n return 0\n\n\n# #endregion run_compliance_report\n" }, { "contract_id": "run_compliance_violations", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 329, - "end_line": 351, + "start_line": 328, + "end_line": 350, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72190,15 +56225,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_compliance_violations:Function]\n# @PURPOSE: Read run violations by run id.\n# @PRE: Run exists.\n# @POST: Returns violations payload.\ndef run_compliance_violations(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n violations = repository.get_violations_by_run(args.run_id)\n print(\n json.dumps(\n {\"status\": \"ok\", \"items\": [_to_payload(item) for item in violations]}\n )\n )\n return 0\n\n\n# [/DEF:run_compliance_violations:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_compliance_violations [TYPE Function]\n# @BRIEF Read run violations by run id.\n# @PRE: Run exists.\n# @POST: Returns violations payload.\ndef run_compliance_violations(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n run = repository.get_check_run(args.run_id)\n if run is None:\n print(json.dumps({\"status\": \"error\", \"message\": \"run not found\"}))\n return 2\n\n violations = repository.get_violations_by_run(args.run_id)\n print(\n json.dumps(\n {\"status\": \"ok\", \"items\": [_to_payload(item) for item in violations]}\n )\n )\n return 0\n\n\n# #endregion run_compliance_violations\n" }, { "contract_id": "run_approve", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 354, - "end_line": 382, + "start_line": 353, + "end_line": 381, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72236,15 +56271,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_approve:Function]\n# @PURPOSE: Approve candidate based on immutable PASSED report.\n# @PRE: Candidate and report exist; report is PASSED.\n# @POST: Persists APPROVED decision and returns success payload.\ndef run_approve(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# [/DEF:run_approve:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_approve [TYPE Function]\n# @BRIEF Approve candidate based on immutable PASSED report.\n# @PRE: Candidate and report exist; report is PASSED.\n# @POST: Persists APPROVED decision and returns success payload.\ndef run_approve(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = approve_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# #endregion run_approve\n" }, { "contract_id": "run_reject", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 385, - "end_line": 413, + "start_line": 384, + "end_line": 412, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72282,15 +56317,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_reject:Function]\n# @PURPOSE: Reject candidate without mutating compliance evidence.\n# @PRE: Candidate and report exist.\n# @POST: Persists REJECTED decision and returns success payload.\ndef run_reject(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# [/DEF:run_reject:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_reject [TYPE Function]\n# @BRIEF Reject candidate without mutating compliance evidence.\n# @PRE: Candidate and report exist.\n# @POST: Persists REJECTED decision and returns success payload.\ndef run_reject(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n decision = reject_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n decided_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(\n json.dumps(\n {\"status\": \"ok\", \"decision\": decision.decision, \"decision_id\": decision.id}\n )\n )\n return 0\n\n\n# #endregion run_reject\n" }, { "contract_id": "run_publish", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 416, - "end_line": 441, + "start_line": 415, + "end_line": 440, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72328,15 +56363,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_publish:Function]\n# @PURPOSE: Publish approved candidate to target channel.\n# @PRE: Candidate is approved and report belongs to candidate.\n# @POST: Appends ACTIVE publication record and returns payload.\ndef run_publish(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n published_by=args.actor,\n target_channel=args.target_channel,\n publication_ref=args.publication_ref,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# [/DEF:run_publish:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_publish [TYPE Function]\n# @BRIEF Publish approved candidate to target channel.\n# @PRE: Candidate is approved and report belongs to candidate.\n# @POST: Appends ACTIVE publication record and returns payload.\ndef run_publish(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = publish_candidate(\n repository=repository,\n candidate_id=args.candidate_id,\n report_id=args.report_id,\n published_by=args.actor,\n target_channel=args.target_channel,\n publication_ref=args.publication_ref,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# #endregion run_publish\n" }, { "contract_id": "run_revoke", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_cli.py", - "start_line": 444, - "end_line": 467, + "start_line": 443, + "end_line": 466, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72374,8 +56409,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_revoke:Function]\n# @PURPOSE: Revoke active publication record.\n# @PRE: Publication id exists and is ACTIVE.\n# @POST: Publication record status becomes REVOKED.\ndef run_revoke(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=args.publication_id,\n revoked_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc: # noqa: BLE001\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# [/DEF:run_revoke:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_revoke [TYPE Function]\n# @BRIEF Revoke active publication record.\n# @PRE: Publication id exists and is ACTIVE.\n# @POST: Publication record status becomes REVOKED.\ndef run_revoke(args: argparse.Namespace) -> int:\n from ..dependencies import get_clean_release_repository\n\n repository = get_clean_release_repository()\n try:\n publication = revoke_publication(\n repository=repository,\n publication_id=args.publication_id,\n revoked_by=args.actor,\n comment=args.comment,\n )\n except Exception as exc:\n print(json.dumps({\"status\": \"error\", \"message\": str(exc)}))\n return 2\n\n print(json.dumps({\"status\": \"ok\", \"publication\": _to_payload(publication)}))\n return 0\n\n\n# #endregion run_revoke\n" }, { "contract_id": "CleanReleaseTuiScript", @@ -72386,16 +56421,10 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "TUI refuses startup in non-TTY environments; headless flow is CLI/API only.", "LAYER": "UI", - "PURPOSE": "Interactive terminal interface for Enterprise Clean Release compliance validation.", - "SEMANTICS": [ - "clean-release", - "tui", - "ncurses", - "interactive-validator" - ] + "PURPOSE": "Interactive terminal interface for Enterprise Clean Release compliance validation." }, "relations": [ { @@ -72422,15 +56451,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseTuiScript:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, tui, ncurses, interactive-validator\n# @PURPOSE: 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 datetime, timezone\nfrom types import SimpleNamespace\nfrom typing import List, Optional, Any, Dict\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.models.clean_release import (\n CandidateArtifact,\n CheckFinalStatus,\n CheckStageName,\n CheckStageStatus,\n CleanProfilePolicy,\n ComplianceViolation,\n ProfileType,\n ReleaseCandidate,\n ResourceSourceEntry,\n ResourceSourceRegistry,\n RegistryStatus,\n ReleaseCandidateStatus,\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# [DEF:TuiFacadeAdapter:Class]\n# @PURPOSE: 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# [/DEF:TuiFacadeAdapter:Class]\n\n\n# [DEF:CleanReleaseTUI:Class]\n# @PURPOSE: 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: Optional[str] = None\n self.last_error: Optional[str] = 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(timezone.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 setattr(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 setattr(registry, \"immutable\", True)\n setattr(registry, \"allowed_hosts\", [\"internal-repo.company.com\"])\n setattr(registry, \"allowed_schemes\", [\"https\"])\n setattr(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, \"r\", encoding=\"utf-8\") as bootstrap_file:\n payload = json.load(bootstrap_file)\n\n now = datetime.now(timezone.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: # noqa: BLE001\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: # noqa: BLE001\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: # noqa: BLE001\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: # noqa: BLE001\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: # noqa: BLE001\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# [/DEF:CleanReleaseTUI:Class]\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 # 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# [/DEF:CleanReleaseTuiScript:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "TuiFacadeAdapter", "contract_type": "Class", "file_path": "backend/src/scripts/clean_release_tui.py", - "start_line": 49, - "end_line": 192, + "start_line": 48, + "end_line": 191, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72468,15 +56497,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TuiFacadeAdapter:Class]\n# @PURPOSE: 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# [/DEF:TuiFacadeAdapter:Class]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "CleanReleaseTUI", "contract_type": "Class", "file_path": "backend/src/scripts/clean_release_tui.py", - "start_line": 195, - "end_line": 657, + "start_line": 194, + "end_line": 656, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72536,15 +56565,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseTUI:Class]\n# @PURPOSE: 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: Optional[str] = None\n self.last_error: Optional[str] = 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(timezone.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 setattr(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 setattr(registry, \"immutable\", True)\n setattr(registry, \"allowed_hosts\", [\"internal-repo.company.com\"])\n setattr(registry, \"allowed_schemes\", [\"https\"])\n setattr(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, \"r\", encoding=\"utf-8\") as bootstrap_file:\n payload = json.load(bootstrap_file)\n\n now = datetime.now(timezone.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: # noqa: BLE001\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: # noqa: BLE001\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: # noqa: BLE001\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: # noqa: BLE001\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: # noqa: BLE001\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# [/DEF:CleanReleaseTUI:Class]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "run_checks", "contract_type": "Function", "file_path": "backend/src/scripts/clean_release_tui.py", - "start_line": 511, - "end_line": 555, + "start_line": 510, + "end_line": 554, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -72583,28 +56612,21 @@ } ], "anchor_syntax": "def", - "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: # noqa: BLE001\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\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" }, { "contract_id": "CreateAdminScript", "contract_type": "Module", "file_path": "backend/src/scripts/create_admin.py", "start_line": 1, - "end_line": 99, + "end_line": 94, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Admin user must have the \"Admin\" role.", "LAYER": "Scripts", - "PURPOSE": "CLI tool for creating the initial admin user.", - "SEMANTICS": [ - "admin", - "setup", - "user", - "auth", - "cli" - ] + "PURPOSE": "CLI tool for creating the initial admin user." }, "relations": [ { @@ -72691,31 +56713,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CreateAdminScript:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: admin, setup, user, auth, cli\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nimport sys\nimport argparse\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.database import AuthSessionLocal, init_db\nfrom src.core.auth.security import get_password_hash\nfrom src.models.auth import User, Role\nfrom src.core.logger import logger, belief_scope\n# [/SECTION]\n\n\n# [DEF:create_admin:Function]\n# @PURPOSE: 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#\n# @PARAM: username (str) - Admin username.\n# @PARAM: password (str) - Admin password.\n# @PARAM: email (str | None) - Optional admin email.\ndef create_admin(username, password, email=None):\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# [/DEF:create_admin:Function]\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# [/DEF:CreateAdminScript:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "create_admin", "contract_type": "Function", "file_path": "backend/src/scripts/create_admin.py", - "start_line": 28, - "end_line": 82, + "start_line": 25, + "end_line": 77, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "email (str | None) - Optional admin email.", "POST": "Admin user exists in auth.db.", "PRE": "username and password provided via CLI.", "PURPOSE": "Creates an admin user and necessary roles/permissions." }, "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", @@ -72744,28 +56759,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:create_admin:Function]\n# @PURPOSE: 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#\n# @PARAM: username (str) - Admin username.\n# @PARAM: password (str) - Admin password.\n# @PARAM: email (str | None) - Optional admin email.\ndef create_admin(username, password, email=None):\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# [/DEF:create_admin:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "InitAuthDbScript", "contract_type": "Module", "file_path": "backend/src/scripts/init_auth_db.py", "start_line": 1, - "end_line": 55, + "end_line": 52, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "Safe to run multiple times (idempotent).", "LAYER": "Scripts", - "PURPOSE": "Initializes the auth database and creates the necessary tables.", - "SEMANTICS": [ - "setup", - "database", - "auth", - "migration" - ] + "PURPOSE": "Initializes the auth database and creates the necessary tables." }, "relations": [ { @@ -72807,19 +56816,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:InitAuthDbScript:Module]\n#\n# @SEMANTICS: setup, database, auth, migration\n# @PURPOSE: Initializes the auth database and creates the necessary tables.\n# @COMPLEXITY: 2\n# @LAYER: Scripts\n# @RELATION: CALLS -> init_db\n# @RELATION: CALLS -> ensure_encryption_key\n# @RELATION: CALLS -> seed_permissions\n#\n# @INVARIANT: Safe to run multiple times (idempotent).\n\n# [SECTION: IMPORTS]\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.database import init_db\nfrom src.core.encryption_key import ensure_encryption_key\nfrom src.core.logger import logger, belief_scope\nfrom src.scripts.seed_permissions import seed_permissions\n# [/SECTION]\n\n\n# [DEF:run_init:Function]\n# @PURPOSE: Main entry point for the initialization script.\n# @COMPLEXITY: 3\n# @POST: auth.db is initialized with the correct schema and seeded permissions.\n# @RELATION: CALLS -> ensure_encryption_key\n# @RELATION: CALLS -> init_db\n# @RELATION: CALLS -> seed_permissions\ndef run_init():\n with belief_scope(\"init_auth_db\"):\n logger.info(\"Initializing authentication database...\")\n try:\n ensure_encryption_key()\n init_db()\n logger.info(\"Authentication database initialized successfully.\")\n\n # Seed permissions\n seed_permissions()\n\n except Exception as e:\n logger.error(f\"Failed to initialize authentication database: {e}\")\n sys.exit(1)\n\n\n# [/DEF:run_init:Function]\n\nif __name__ == \"__main__\":\n run_init()\n\n# [/DEF:InitAuthDbScript:Module]\n" + "anchor_syntax": "region", + "body": "# #region InitAuthDbScript [C:2] [TYPE Module] [SEMANTICS auth]\n#\n# @BRIEF Initializes the auth database and creates the necessary tables.\n# @LAYER: Scripts\n# @RELATION CALLS -> init_db\n# @RELATION CALLS -> ensure_encryption_key\n# @RELATION CALLS -> seed_permissions\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.cot_logger import seed_trace_id\nfrom src.core.database import init_db\nfrom src.core.encryption_key import ensure_encryption_key\nfrom src.core.logger import belief_scope, logger\nfrom src.scripts.seed_permissions import seed_permissions\n\n\n# #region run_init [C:3] [TYPE Function]\n# @BRIEF Main entry point for the initialization script.\n# @POST: auth.db is initialized with the correct schema and seeded permissions.\n# @RELATION CALLS -> ensure_encryption_key\n# @RELATION CALLS -> init_db\n# @RELATION CALLS -> seed_permissions\ndef run_init():\n seed_trace_id()\n with belief_scope(\"init_auth_db\"):\n logger.info(\"Initializing authentication database...\")\n try:\n ensure_encryption_key()\n init_db()\n logger.info(\"Authentication database initialized successfully.\")\n\n # Seed permissions\n seed_permissions()\n\n except Exception as e:\n logger.error(f\"Failed to initialize authentication database: {e}\")\n sys.exit(1)\n\n\n# #endregion run_init\n\nif __name__ == \"__main__\":\n run_init()\n\n# #endregion InitAuthDbScript\n" }, { "contract_id": "run_init", "contract_type": "Function", "file_path": "backend/src/scripts/init_auth_db.py", - "start_line": 27, - "end_line": 50, + "start_line": 24, + "end_line": 47, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "auth.db is initialized with the correct schema and seeded permissions.", "PURPOSE": "Main entry point for the initialization script." }, @@ -72854,8 +56863,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_init:Function]\n# @PURPOSE: Main entry point for the initialization script.\n# @COMPLEXITY: 3\n# @POST: auth.db is initialized with the correct schema and seeded permissions.\n# @RELATION: CALLS -> ensure_encryption_key\n# @RELATION: CALLS -> init_db\n# @RELATION: CALLS -> seed_permissions\ndef run_init():\n with belief_scope(\"init_auth_db\"):\n logger.info(\"Initializing authentication database...\")\n try:\n ensure_encryption_key()\n init_db()\n logger.info(\"Authentication database initialized successfully.\")\n\n # Seed permissions\n seed_permissions()\n\n except Exception as e:\n logger.error(f\"Failed to initialize authentication database: {e}\")\n sys.exit(1)\n\n\n# [/DEF:run_init:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_init [C:3] [TYPE Function]\n# @BRIEF Main entry point for the initialization script.\n# @POST: auth.db is initialized with the correct schema and seeded permissions.\n# @RELATION CALLS -> ensure_encryption_key\n# @RELATION CALLS -> init_db\n# @RELATION CALLS -> seed_permissions\ndef run_init():\n seed_trace_id()\n with belief_scope(\"init_auth_db\"):\n logger.info(\"Initializing authentication database...\")\n try:\n ensure_encryption_key()\n init_db()\n logger.info(\"Authentication database initialized successfully.\")\n\n # Seed permissions\n seed_permissions()\n\n except Exception as e:\n logger.error(f\"Failed to initialize authentication database: {e}\")\n sys.exit(1)\n\n\n# #endregion run_init\n" }, { "contract_id": "MigrateSqliteToPostgresScript", @@ -72866,18 +56875,10 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Script is idempotent for task_records and app_configurations.", "LAYER": "Scripts", - "PURPOSE": "Migrates legacy config and task history from SQLite/file storage to PostgreSQL.", - "SEMANTICS": [ - "migration", - "sqlite", - "postgresql", - "config", - "task_logs", - "task_records" - ] + "PURPOSE": "Migrates legacy config and task history from SQLite/file storage to PostgreSQL." }, "relations": [ { @@ -73012,33 +57013,33 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MigrateSqliteToPostgresScript:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: migration, sqlite, postgresql, config, task_logs, task_records\n# @PURPOSE: Migrates legacy config and task history from SQLite/file storage to PostgreSQL.\n# @LAYER: Scripts\n# @RELATION: READS_FROM -> backend/tasks.db\n# @RELATION: READS_FROM -> backend/config.json\n# @RELATION: WRITES_TO -> postgresql.task_records\n# @RELATION: WRITES_TO -> postgresql.task_logs\n# @RELATION: WRITES_TO -> postgresql.app_configurations\n#\n# @INVARIANT: Script is idempotent for task_records and app_configurations.\n\n# [SECTION: IMPORTS]\nimport argparse\nimport json\nimport os\nimport sqlite3\nfrom pathlib import Path\nfrom typing import Any, Dict, Iterable, Optional\n\nfrom sqlalchemy import create_engine, text\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom src.core.logger import belief_scope\nfrom src.core.cot_logger import MarkerLogger\nlog = MarkerLogger(\"MigrateSqliteToPostgres\")\n\n# [/SECTION]\n\n\n# [DEF:Constants:Section]\nDEFAULT_TARGET_URL = os.getenv(\n \"DATABASE_URL\",\n os.getenv(\n \"POSTGRES_URL\",\n \"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n ),\n)\n# [/DEF:Constants:Section]\n\n\n# [DEF:_json_load_if_needed:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Parses JSON-like values from SQLite TEXT/JSON columns to Python objects.\n# @PRE: value is scalar JSON/text/list/dict or None.\n# @POST: Returns normalized Python object or original scalar value.\ndef _json_load_if_needed(value: Any) -> Any:\n with belief_scope(\"_json_load_if_needed\"):\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return None\n if raw[0] in \"{[\":\n try:\n return json.loads(raw)\n except json.JSONDecodeError:\n return value\n return value\n\n\n# [/DEF:_json_load_if_needed:Function]\n\n\n# [DEF:_find_legacy_config_path:Function]\n# @PURPOSE: Resolves the existing legacy config.json path from candidates.\ndef _find_legacy_config_path(explicit_path: Optional[str]) -> Optional[Path]:\n with belief_scope(\"_find_legacy_config_path\"):\n if explicit_path:\n p = Path(explicit_path)\n return p if p.exists() else None\n\n candidates = [\n Path(\"backend/config.json\"),\n Path(\"config.json\"),\n ]\n for candidate in candidates:\n if candidate.exists():\n return candidate\n return None\n\n\n# [/DEF:_find_legacy_config_path:Function]\n\n\n# [DEF:_connect_sqlite:Function]\n# @PURPOSE: Opens a SQLite connection with row factory.\ndef _connect_sqlite(path: Path) -> sqlite3.Connection:\n with belief_scope(\"_connect_sqlite\"):\n conn = sqlite3.connect(str(path))\n conn.row_factory = sqlite3.Row\n return conn\n\n\n# [/DEF:_connect_sqlite:Function]\n\n\n# [DEF:_ensure_target_schema:Function]\n# @PURPOSE: Ensures required PostgreSQL tables exist before migration.\ndef _ensure_target_schema(engine) -> None:\n with belief_scope(\"_ensure_target_schema\"):\n stmts: Iterable[str] = (\n \"\"\"\n CREATE TABLE IF NOT EXISTS app_configurations (\n id TEXT PRIMARY KEY,\n payload JSONB NOT NULL,\n updated_at TIMESTAMPTZ DEFAULT NOW()\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_records (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n status TEXT NOT NULL,\n environment_id TEXT NULL,\n started_at TIMESTAMPTZ NULL,\n finished_at TIMESTAMPTZ NULL,\n logs JSONB NULL,\n error TEXT NULL,\n result JSONB NULL,\n created_at TIMESTAMPTZ DEFAULT NOW(),\n params JSONB NULL\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_logs (\n id INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL,\n level VARCHAR(16) NOT NULL,\n source VARCHAR(64) NOT NULL DEFAULT 'system',\n message TEXT NOT NULL,\n metadata_json TEXT NULL,\n CONSTRAINT fk_task_logs_task\n FOREIGN KEY(task_id)\n REFERENCES task_records(id)\n ON DELETE CASCADE\n )\n \"\"\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_timestamp ON task_logs (task_id, timestamp)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_level ON task_logs (task_id, level)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_source ON task_logs (task_id, source)\",\n \"\"\"\n DO $$\n BEGIN\n IF EXISTS (\n SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = 'task_logs_id_seq'\n ) THEN\n PERFORM 1;\n ELSE\n CREATE SEQUENCE task_logs_id_seq OWNED BY task_logs.id;\n END IF;\n END $$;\n \"\"\",\n \"ALTER TABLE task_logs ALTER COLUMN id SET DEFAULT nextval('task_logs_id_seq')\",\n )\n with engine.begin() as conn:\n for stmt in stmts:\n conn.execute(text(stmt))\n\n\n# [/DEF:_ensure_target_schema:Function]\n\n\n# [DEF:_migrate_config:Function]\n# @PURPOSE: Migrates legacy config.json into app_configurations(global).\ndef _migrate_config(engine, legacy_config_path: Optional[Path]) -> int:\n with belief_scope(\"_migrate_config\"):\n if legacy_config_path is None:\n log.reason(\"No legacy config.json found, skipping migration\")\n return 0\n\n payload = json.loads(legacy_config_path.read_text(encoding=\"utf-8\"))\n with engine.begin() as conn:\n conn.execute(\n text(\n \"\"\"\n INSERT INTO app_configurations (id, payload, updated_at)\n VALUES ('global', CAST(:payload AS JSONB), NOW())\n ON CONFLICT (id)\n DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()\n \"\"\"\n ),\n {\"payload\": json.dumps(payload, ensure_ascii=True)},\n )\n logger.info(\n \"[_migrate_config][Coherence:OK] Config migrated from %s\",\n legacy_config_path,\n )\n return 1\n\n\n# [/DEF:_migrate_config:Function]\n\n\n# [DEF:_migrate_tasks_and_logs:Function]\n# @PURPOSE: Migrates task_records and task_logs from SQLite into PostgreSQL.\ndef _migrate_tasks_and_logs(engine, sqlite_conn: sqlite3.Connection) -> Dict[str, int]:\n with belief_scope(\"_migrate_tasks_and_logs\"):\n stats = {\n \"task_records_total\": 0,\n \"task_records_inserted\": 0,\n \"task_logs_total\": 0,\n \"task_logs_inserted\": 0,\n }\n\n rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, type, status, environment_id, started_at, finished_at, logs, error, result, created_at, params\n FROM task_records\n ORDER BY created_at ASC\n \"\"\"\n ).fetchall()\n stats[\"task_records_total\"] = len(rows)\n\n with engine.begin() as conn:\n existing_env_ids = {\n row[0]\n for row in conn.execute(text(\"SELECT id FROM environments\")).fetchall()\n }\n for row in rows:\n params_obj = _json_load_if_needed(row[\"params\"])\n result_obj = _json_load_if_needed(row[\"result\"])\n logs_obj = _json_load_if_needed(row[\"logs\"])\n environment_id = row[\"environment_id\"]\n if environment_id and environment_id not in existing_env_ids:\n # Legacy task may reference environments that were not migrated; keep task row and drop FK value.\n environment_id = None\n\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_records (\n id, type, status, environment_id, started_at, finished_at,\n logs, error, result, created_at, params\n ) VALUES (\n :id, :type, :status, :environment_id, :started_at, :finished_at,\n CAST(:logs AS JSONB), :error, CAST(:result AS JSONB), :created_at, CAST(:params AS JSONB)\n )\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"type\": row[\"type\"],\n \"status\": row[\"status\"],\n \"environment_id\": environment_id,\n \"started_at\": row[\"started_at\"],\n \"finished_at\": row[\"finished_at\"],\n \"logs\": json.dumps(logs_obj, ensure_ascii=True)\n if logs_obj is not None\n else None,\n \"error\": row[\"error\"],\n \"result\": json.dumps(result_obj, ensure_ascii=True)\n if result_obj is not None\n else None,\n \"created_at\": row[\"created_at\"],\n \"params\": json.dumps(params_obj, ensure_ascii=True)\n if params_obj is not None\n else None,\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_records_inserted\"] += int(res.rowcount)\n\n log_rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, task_id, timestamp, level, source, message, metadata_json\n FROM task_logs\n ORDER BY id ASC\n \"\"\"\n ).fetchall()\n stats[\"task_logs_total\"] = len(log_rows)\n\n with engine.begin() as conn:\n for row in log_rows:\n # Preserve original IDs to keep migration idempotent.\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_logs (id, task_id, timestamp, level, source, message, metadata_json)\n VALUES (:id, :task_id, :timestamp, :level, :source, :message, :metadata_json)\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"task_id\": row[\"task_id\"],\n \"timestamp\": row[\"timestamp\"],\n \"level\": row[\"level\"],\n \"source\": row[\"source\"] or \"system\",\n \"message\": row[\"message\"],\n \"metadata_json\": row[\"metadata_json\"],\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_logs_inserted\"] += int(res.rowcount)\n\n # Ensure sequence is aligned after explicit id inserts.\n conn.execute(\n text(\n \"\"\"\n SELECT setval(\n 'task_logs_id_seq',\n COALESCE((SELECT MAX(id) FROM task_logs), 1),\n TRUE\n )\n \"\"\"\n )\n )\n\n logger.info(\n \"[_migrate_tasks_and_logs][Coherence:OK] task_records=%s/%s task_logs=%s/%s\",\n stats[\"task_records_inserted\"],\n stats[\"task_records_total\"],\n stats[\"task_logs_inserted\"],\n stats[\"task_logs_total\"],\n )\n return stats\n\n\n# [/DEF:_migrate_tasks_and_logs:Function]\n\n\n# [DEF:run_migration:Function]\n# @PURPOSE: Orchestrates migration from SQLite/file to PostgreSQL.\ndef run_migration(\n sqlite_path: Path, target_url: str, legacy_config_path: Optional[Path]\n) -> Dict[str, int]:\n with belief_scope(\"run_migration\"):\n log.reason(\"Starting migration from SQLite to PostgreSQL\", payload={\"sqlite_path\": str(sqlite_path), \"target_url\": target_url})\n if not sqlite_path.exists():\n raise FileNotFoundError(f\"SQLite source not found: {sqlite_path}\")\n\n sqlite_conn = _connect_sqlite(sqlite_path)\n engine = create_engine(target_url, pool_pre_ping=True)\n try:\n _ensure_target_schema(engine)\n config_upserted = _migrate_config(engine, legacy_config_path)\n stats = _migrate_tasks_and_logs(engine, sqlite_conn)\n stats[\"config_upserted\"] = config_upserted\n return stats\n finally:\n sqlite_conn.close()\n\n\n# [/DEF:run_migration:Function]\n\n\n# [DEF:main:Function]\n# @PURPOSE: CLI entrypoint.\ndef main() -> int:\n with belief_scope(\"main\"):\n parser = argparse.ArgumentParser(\n description=\"Migrate legacy config.json and task logs from SQLite to PostgreSQL.\",\n )\n parser.add_argument(\n \"--sqlite-path\",\n default=\"backend/tasks.db\",\n help=\"Path to source SQLite DB with task_records/task_logs (default: backend/tasks.db).\",\n )\n parser.add_argument(\n \"--target-url\",\n default=DEFAULT_TARGET_URL,\n help=\"Target PostgreSQL SQLAlchemy URL (default: DATABASE_URL/POSTGRES_URL env).\",\n )\n parser.add_argument(\n \"--config-path\",\n default=None,\n help=\"Optional path to legacy config.json (auto-detected when omitted).\",\n )\n\n args = parser.parse_args()\n\n sqlite_path = Path(args.sqlite_path)\n legacy_config_path = _find_legacy_config_path(args.config_path)\n try:\n stats = run_migration(\n sqlite_path=sqlite_path,\n target_url=args.target_url,\n legacy_config_path=legacy_config_path,\n )\n print(\"Migration completed.\")\n print(json.dumps(stats, indent=2))\n return 0\n except (SQLAlchemyError, OSError, sqlite3.Error, ValueError) as e:\n logger.error(\"[main][Coherence:Failed] Migration failed: %s\", e)\n print(f\"Migration failed: {e}\")\n return 1\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n# [/DEF:main:Function]\n\n# [/DEF:MigrateSqliteToPostgresScript:Module]\n" + "anchor_syntax": "region", + "body": "# #region MigrateSqliteToPostgresScript [C:3] [TYPE Module] [SEMANTICS sqlalchemy, sqlite, storage, task, history, config]\n#\n# @BRIEF Migrates legacy config and task history from SQLite/file storage to PostgreSQL.\n# @LAYER: Scripts\n# @RELATION READS_FROM -> backend/tasks.db\n# @RELATION READS_FROM -> backend/config.json\n# @RELATION WRITES_TO -> postgresql.task_records\n# @RELATION WRITES_TO -> postgresql.task_logs\n# @RELATION WRITES_TO -> postgresql.app_configurations\n#\n# @INVARIANT: Script is idempotent for task_records and app_configurations.\n\nimport argparse\nimport json\nimport os\nimport sqlite3\nfrom collections.abc import Iterable\nfrom pathlib import Path\nfrom typing import Any\n\nfrom sqlalchemy import create_engine, text\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.logger import belief_scope, logger\n\n# #region Constants [TYPE Section]\nDEFAULT_TARGET_URL = os.getenv(\n \"DATABASE_URL\",\n os.getenv(\n \"POSTGRES_URL\",\n \"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n ),\n)\n# #endregion Constants\n\n\n# #region _json_load_if_needed [C:3] [TYPE Function]\n# @BRIEF Parses JSON-like values from SQLite TEXT/JSON columns to Python objects.\n# @PRE: value is scalar JSON/text/list/dict or None.\n# @POST: Returns normalized Python object or original scalar value.\ndef _json_load_if_needed(value: Any) -> Any:\n with belief_scope(\"_json_load_if_needed\"):\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return None\n if raw[0] in \"{[\":\n try:\n return json.loads(raw)\n except json.JSONDecodeError:\n return value\n return value\n\n\n# #endregion _json_load_if_needed\n\n\n# #region _find_legacy_config_path [TYPE Function]\n# @BRIEF Resolves the existing legacy config.json path from candidates.\ndef _find_legacy_config_path(explicit_path: str | None) -> Path | None:\n with belief_scope(\"_find_legacy_config_path\"):\n if explicit_path:\n p = Path(explicit_path)\n return p if p.exists() else None\n\n candidates = [\n Path(\"backend/config.json\"),\n Path(\"config.json\"),\n ]\n for candidate in candidates:\n if candidate.exists():\n return candidate\n return None\n\n\n# #endregion _find_legacy_config_path\n\n\n# #region _connect_sqlite [TYPE Function]\n# @BRIEF Opens a SQLite connection with row factory.\ndef _connect_sqlite(path: Path) -> sqlite3.Connection:\n with belief_scope(\"_connect_sqlite\"):\n conn = sqlite3.connect(str(path))\n conn.row_factory = sqlite3.Row\n return conn\n\n\n# #endregion _connect_sqlite\n\n\n# #region _ensure_target_schema [TYPE Function]\n# @BRIEF Ensures required PostgreSQL tables exist before migration.\ndef _ensure_target_schema(engine) -> None:\n with belief_scope(\"_ensure_target_schema\"):\n stmts: Iterable[str] = (\n \"\"\"\n CREATE TABLE IF NOT EXISTS app_configurations (\n id TEXT PRIMARY KEY,\n payload JSONB NOT NULL,\n updated_at TIMESTAMPTZ DEFAULT NOW()\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_records (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n status TEXT NOT NULL,\n environment_id TEXT NULL,\n started_at TIMESTAMPTZ NULL,\n finished_at TIMESTAMPTZ NULL,\n logs JSONB NULL,\n error TEXT NULL,\n result JSONB NULL,\n created_at TIMESTAMPTZ DEFAULT NOW(),\n params JSONB NULL\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_logs (\n id INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL,\n level VARCHAR(16) NOT NULL,\n source VARCHAR(64) NOT NULL DEFAULT 'system',\n message TEXT NOT NULL,\n metadata_json TEXT NULL,\n CONSTRAINT fk_task_logs_task\n FOREIGN KEY(task_id)\n REFERENCES task_records(id)\n ON DELETE CASCADE\n )\n \"\"\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_timestamp ON task_logs (task_id, timestamp)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_level ON task_logs (task_id, level)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_source ON task_logs (task_id, source)\",\n \"\"\"\n DO $$\n BEGIN\n IF EXISTS (\n SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = 'task_logs_id_seq'\n ) THEN\n PERFORM 1;\n ELSE\n CREATE SEQUENCE task_logs_id_seq OWNED BY task_logs.id;\n END IF;\n END $$;\n \"\"\",\n \"ALTER TABLE task_logs ALTER COLUMN id SET DEFAULT nextval('task_logs_id_seq')\",\n )\n with engine.begin() as conn:\n for stmt in stmts:\n conn.execute(text(stmt))\n\n\n# #endregion _ensure_target_schema\n\n\n# #region _migrate_config [TYPE Function]\n# @BRIEF Migrates legacy config.json into app_configurations(global).\ndef _migrate_config(engine, legacy_config_path: Path | None) -> int:\n with belief_scope(\"_migrate_config\"):\n if legacy_config_path is None:\n logger.reason(\n \"No legacy config.json found, skipping\",\n extra={\"src\": \"_migrate_config\"},\n )\n return 0\n\n payload = json.loads(legacy_config_path.read_text(encoding=\"utf-8\"))\n with engine.begin() as conn:\n conn.execute(\n text(\n \"\"\"\n INSERT INTO app_configurations (id, payload, updated_at)\n VALUES ('global', CAST(:payload AS JSONB), NOW())\n ON CONFLICT (id)\n DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()\n \"\"\"\n ),\n {\"payload\": json.dumps(payload, ensure_ascii=True)},\n )\n logger.info(\n \"[_migrate_config][Coherence:OK] Config migrated from %s\",\n legacy_config_path,\n )\n return 1\n\n\n# #endregion _migrate_config\n\n\n# #region _migrate_tasks_and_logs [TYPE Function]\n# @BRIEF Migrates task_records and task_logs from SQLite into PostgreSQL.\ndef _migrate_tasks_and_logs(engine, sqlite_conn: sqlite3.Connection) -> dict[str, int]:\n with belief_scope(\"_migrate_tasks_and_logs\"):\n stats = {\n \"task_records_total\": 0,\n \"task_records_inserted\": 0,\n \"task_logs_total\": 0,\n \"task_logs_inserted\": 0,\n }\n\n rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, type, status, environment_id, started_at, finished_at, logs, error, result, created_at, params\n FROM task_records\n ORDER BY created_at ASC\n \"\"\"\n ).fetchall()\n stats[\"task_records_total\"] = len(rows)\n\n with engine.begin() as conn:\n existing_env_ids = {\n row[0]\n for row in conn.execute(text(\"SELECT id FROM environments\")).fetchall()\n }\n for row in rows:\n params_obj = _json_load_if_needed(row[\"params\"])\n result_obj = _json_load_if_needed(row[\"result\"])\n logs_obj = _json_load_if_needed(row[\"logs\"])\n environment_id = row[\"environment_id\"]\n if environment_id and environment_id not in existing_env_ids:\n # Legacy task may reference environments that were not migrated; keep task row and drop FK value.\n environment_id = None\n\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_records (\n id, type, status, environment_id, started_at, finished_at,\n logs, error, result, created_at, params\n ) VALUES (\n :id, :type, :status, :environment_id, :started_at, :finished_at,\n CAST(:logs AS JSONB), :error, CAST(:result AS JSONB), :created_at, CAST(:params AS JSONB)\n )\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"type\": row[\"type\"],\n \"status\": row[\"status\"],\n \"environment_id\": environment_id,\n \"started_at\": row[\"started_at\"],\n \"finished_at\": row[\"finished_at\"],\n \"logs\": json.dumps(logs_obj, ensure_ascii=True)\n if logs_obj is not None\n else None,\n \"error\": row[\"error\"],\n \"result\": json.dumps(result_obj, ensure_ascii=True)\n if result_obj is not None\n else None,\n \"created_at\": row[\"created_at\"],\n \"params\": json.dumps(params_obj, ensure_ascii=True)\n if params_obj is not None\n else None,\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_records_inserted\"] += int(res.rowcount)\n\n log_rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, task_id, timestamp, level, source, message, metadata_json\n FROM task_logs\n ORDER BY id ASC\n \"\"\"\n ).fetchall()\n stats[\"task_logs_total\"] = len(log_rows)\n\n with engine.begin() as conn:\n for row in log_rows:\n # Preserve original IDs to keep migration idempotent.\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_logs (id, task_id, timestamp, level, source, message, metadata_json)\n VALUES (:id, :task_id, :timestamp, :level, :source, :message, :metadata_json)\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"task_id\": row[\"task_id\"],\n \"timestamp\": row[\"timestamp\"],\n \"level\": row[\"level\"],\n \"source\": row[\"source\"] or \"system\",\n \"message\": row[\"message\"],\n \"metadata_json\": row[\"metadata_json\"],\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_logs_inserted\"] += int(res.rowcount)\n\n # Ensure sequence is aligned after explicit id inserts.\n conn.execute(\n text(\n \"\"\"\n SELECT setval(\n 'task_logs_id_seq',\n COALESCE((SELECT MAX(id) FROM task_logs), 1),\n TRUE\n )\n \"\"\"\n )\n )\n\n logger.info(\n \"[_migrate_tasks_and_logs][Coherence:OK] task_records=%s/%s task_logs=%s/%s\",\n stats[\"task_records_inserted\"],\n stats[\"task_records_total\"],\n stats[\"task_logs_inserted\"],\n stats[\"task_logs_total\"],\n )\n return stats\n\n\n# #endregion _migrate_tasks_and_logs\n\n\n# #region run_migration [TYPE Function]\n# @BRIEF Orchestrates migration from SQLite/file to PostgreSQL.\ndef run_migration(\n sqlite_path: Path, target_url: str, legacy_config_path: Path | None\n) -> dict[str, int]:\n seed_trace_id()\n with belief_scope(\"run_migration\"):\n logger.reason(\n f\"sqlite={sqlite_path} target={target_url}\",\n extra={\"src\": \"run_migration\"},\n )\n if not sqlite_path.exists():\n raise FileNotFoundError(f\"SQLite source not found: {sqlite_path}\")\n\n sqlite_conn = _connect_sqlite(sqlite_path)\n engine = create_engine(target_url, pool_pre_ping=True)\n try:\n _ensure_target_schema(engine)\n config_upserted = _migrate_config(engine, legacy_config_path)\n stats = _migrate_tasks_and_logs(engine, sqlite_conn)\n stats[\"config_upserted\"] = config_upserted\n return stats\n finally:\n sqlite_conn.close()\n\n\n# #endregion run_migration\n\n\n# #region main [TYPE Function]\n# @BRIEF CLI entrypoint.\ndef main() -> int:\n with belief_scope(\"main\"):\n parser = argparse.ArgumentParser(\n description=\"Migrate legacy config.json and task logs from SQLite to PostgreSQL.\",\n )\n parser.add_argument(\n \"--sqlite-path\",\n default=\"backend/tasks.db\",\n help=\"Path to source SQLite DB with task_records/task_logs (default: backend/tasks.db).\",\n )\n parser.add_argument(\n \"--target-url\",\n default=DEFAULT_TARGET_URL,\n help=\"Target PostgreSQL SQLAlchemy URL (default: DATABASE_URL/POSTGRES_URL env).\",\n )\n parser.add_argument(\n \"--config-path\",\n default=None,\n help=\"Optional path to legacy config.json (auto-detected when omitted).\",\n )\n\n args = parser.parse_args()\n\n sqlite_path = Path(args.sqlite_path)\n legacy_config_path = _find_legacy_config_path(args.config_path)\n try:\n stats = run_migration(\n sqlite_path=sqlite_path,\n target_url=args.target_url,\n legacy_config_path=legacy_config_path,\n )\n print(\"Migration completed.\")\n print(json.dumps(stats, indent=2))\n return 0\n except (SQLAlchemyError, OSError, sqlite3.Error, ValueError) as e:\n logger.error(\"[main][Coherence:Failed] Migration failed: %s\", e)\n print(f\"Migration failed: {e}\")\n return 1\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n# #endregion main\n\n# #endregion MigrateSqliteToPostgresScript\n" }, { "contract_id": "Constants", "contract_type": "Section", "file_path": "backend/src/scripts/migrate_sqlite_to_postgres.py", - "start_line": 33, - "end_line": 41, + "start_line": 27, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:Constants:Section]\nDEFAULT_TARGET_URL = os.getenv(\n \"DATABASE_URL\",\n os.getenv(\n \"POSTGRES_URL\",\n \"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n ),\n)\n# [/DEF:Constants:Section]\n" + "anchor_syntax": "region", + "body": "# #region Constants [TYPE Section]\nDEFAULT_TARGET_URL = os.getenv(\n \"DATABASE_URL\",\n os.getenv(\n \"POSTGRES_URL\",\n \"postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools\",\n ),\n)\n# #endregion Constants\n" }, { "contract_id": "_json_load_if_needed", "contract_type": "Function", "file_path": "backend/src/scripts/migrate_sqlite_to_postgres.py", - "start_line": 44, - "end_line": 67, + "start_line": 38, + "end_line": 60, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Returns normalized Python object or original scalar value.", "PRE": "value is scalar JSON/text/list/dict or None.", "PURPOSE": "Parses JSON-like values from SQLite TEXT/JSON columns to Python objects." @@ -73073,15 +57074,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_json_load_if_needed:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Parses JSON-like values from SQLite TEXT/JSON columns to Python objects.\n# @PRE: value is scalar JSON/text/list/dict or None.\n# @POST: Returns normalized Python object or original scalar value.\ndef _json_load_if_needed(value: Any) -> Any:\n with belief_scope(\"_json_load_if_needed\"):\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return None\n if raw[0] in \"{[\":\n try:\n return json.loads(raw)\n except json.JSONDecodeError:\n return value\n return value\n\n\n# [/DEF:_json_load_if_needed:Function]\n" + "anchor_syntax": "region", + "body": "# #region _json_load_if_needed [C:3] [TYPE Function]\n# @BRIEF Parses JSON-like values from SQLite TEXT/JSON columns to Python objects.\n# @PRE: value is scalar JSON/text/list/dict or None.\n# @POST: Returns normalized Python object or original scalar value.\ndef _json_load_if_needed(value: Any) -> Any:\n with belief_scope(\"_json_load_if_needed\"):\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n raw = value.strip()\n if not raw:\n return None\n if raw[0] in \"{[\":\n try:\n return json.loads(raw)\n except json.JSONDecodeError:\n return value\n return value\n\n\n# #endregion _json_load_if_needed\n" }, { "contract_id": "_find_legacy_config_path", "contract_type": "Function", "file_path": "backend/src/scripts/migrate_sqlite_to_postgres.py", - "start_line": 70, - "end_line": 88, + "start_line": 63, + "end_line": 81, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73099,15 +57100,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_find_legacy_config_path:Function]\n# @PURPOSE: Resolves the existing legacy config.json path from candidates.\ndef _find_legacy_config_path(explicit_path: Optional[str]) -> Optional[Path]:\n with belief_scope(\"_find_legacy_config_path\"):\n if explicit_path:\n p = Path(explicit_path)\n return p if p.exists() else None\n\n candidates = [\n Path(\"backend/config.json\"),\n Path(\"config.json\"),\n ]\n for candidate in candidates:\n if candidate.exists():\n return candidate\n return None\n\n\n# [/DEF:_find_legacy_config_path:Function]\n" + "anchor_syntax": "region", + "body": "# #region _find_legacy_config_path [TYPE Function]\n# @BRIEF Resolves the existing legacy config.json path from candidates.\ndef _find_legacy_config_path(explicit_path: str | None) -> Path | None:\n with belief_scope(\"_find_legacy_config_path\"):\n if explicit_path:\n p = Path(explicit_path)\n return p if p.exists() else None\n\n candidates = [\n Path(\"backend/config.json\"),\n Path(\"config.json\"),\n ]\n for candidate in candidates:\n if candidate.exists():\n return candidate\n return None\n\n\n# #endregion _find_legacy_config_path\n" }, { "contract_id": "_connect_sqlite", "contract_type": "Function", "file_path": "backend/src/scripts/migrate_sqlite_to_postgres.py", - "start_line": 91, - "end_line": 100, + "start_line": 84, + "end_line": 93, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73125,15 +57126,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_connect_sqlite:Function]\n# @PURPOSE: Opens a SQLite connection with row factory.\ndef _connect_sqlite(path: Path) -> sqlite3.Connection:\n with belief_scope(\"_connect_sqlite\"):\n conn = sqlite3.connect(str(path))\n conn.row_factory = sqlite3.Row\n return conn\n\n\n# [/DEF:_connect_sqlite:Function]\n" + "anchor_syntax": "region", + "body": "# #region _connect_sqlite [TYPE Function]\n# @BRIEF Opens a SQLite connection with row factory.\ndef _connect_sqlite(path: Path) -> sqlite3.Connection:\n with belief_scope(\"_connect_sqlite\"):\n conn = sqlite3.connect(str(path))\n conn.row_factory = sqlite3.Row\n return conn\n\n\n# #endregion _connect_sqlite\n" }, { "contract_id": "_ensure_target_schema", "contract_type": "Function", "file_path": "backend/src/scripts/migrate_sqlite_to_postgres.py", - "start_line": 103, - "end_line": 167, + "start_line": 96, + "end_line": 160, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73151,15 +57152,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_ensure_target_schema:Function]\n# @PURPOSE: Ensures required PostgreSQL tables exist before migration.\ndef _ensure_target_schema(engine) -> None:\n with belief_scope(\"_ensure_target_schema\"):\n stmts: Iterable[str] = (\n \"\"\"\n CREATE TABLE IF NOT EXISTS app_configurations (\n id TEXT PRIMARY KEY,\n payload JSONB NOT NULL,\n updated_at TIMESTAMPTZ DEFAULT NOW()\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_records (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n status TEXT NOT NULL,\n environment_id TEXT NULL,\n started_at TIMESTAMPTZ NULL,\n finished_at TIMESTAMPTZ NULL,\n logs JSONB NULL,\n error TEXT NULL,\n result JSONB NULL,\n created_at TIMESTAMPTZ DEFAULT NOW(),\n params JSONB NULL\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_logs (\n id INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL,\n level VARCHAR(16) NOT NULL,\n source VARCHAR(64) NOT NULL DEFAULT 'system',\n message TEXT NOT NULL,\n metadata_json TEXT NULL,\n CONSTRAINT fk_task_logs_task\n FOREIGN KEY(task_id)\n REFERENCES task_records(id)\n ON DELETE CASCADE\n )\n \"\"\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_timestamp ON task_logs (task_id, timestamp)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_level ON task_logs (task_id, level)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_source ON task_logs (task_id, source)\",\n \"\"\"\n DO $$\n BEGIN\n IF EXISTS (\n SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = 'task_logs_id_seq'\n ) THEN\n PERFORM 1;\n ELSE\n CREATE SEQUENCE task_logs_id_seq OWNED BY task_logs.id;\n END IF;\n END $$;\n \"\"\",\n \"ALTER TABLE task_logs ALTER COLUMN id SET DEFAULT nextval('task_logs_id_seq')\",\n )\n with engine.begin() as conn:\n for stmt in stmts:\n conn.execute(text(stmt))\n\n\n# [/DEF:_ensure_target_schema:Function]\n" + "anchor_syntax": "region", + "body": "# #region _ensure_target_schema [TYPE Function]\n# @BRIEF Ensures required PostgreSQL tables exist before migration.\ndef _ensure_target_schema(engine) -> None:\n with belief_scope(\"_ensure_target_schema\"):\n stmts: Iterable[str] = (\n \"\"\"\n CREATE TABLE IF NOT EXISTS app_configurations (\n id TEXT PRIMARY KEY,\n payload JSONB NOT NULL,\n updated_at TIMESTAMPTZ DEFAULT NOW()\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_records (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n status TEXT NOT NULL,\n environment_id TEXT NULL,\n started_at TIMESTAMPTZ NULL,\n finished_at TIMESTAMPTZ NULL,\n logs JSONB NULL,\n error TEXT NULL,\n result JSONB NULL,\n created_at TIMESTAMPTZ DEFAULT NOW(),\n params JSONB NULL\n )\n \"\"\",\n \"\"\"\n CREATE TABLE IF NOT EXISTS task_logs (\n id INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL,\n level VARCHAR(16) NOT NULL,\n source VARCHAR(64) NOT NULL DEFAULT 'system',\n message TEXT NOT NULL,\n metadata_json TEXT NULL,\n CONSTRAINT fk_task_logs_task\n FOREIGN KEY(task_id)\n REFERENCES task_records(id)\n ON DELETE CASCADE\n )\n \"\"\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_timestamp ON task_logs (task_id, timestamp)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_level ON task_logs (task_id, level)\",\n \"CREATE INDEX IF NOT EXISTS ix_task_logs_task_source ON task_logs (task_id, source)\",\n \"\"\"\n DO $$\n BEGIN\n IF EXISTS (\n SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = 'task_logs_id_seq'\n ) THEN\n PERFORM 1;\n ELSE\n CREATE SEQUENCE task_logs_id_seq OWNED BY task_logs.id;\n END IF;\n END $$;\n \"\"\",\n \"ALTER TABLE task_logs ALTER COLUMN id SET DEFAULT nextval('task_logs_id_seq')\",\n )\n with engine.begin() as conn:\n for stmt in stmts:\n conn.execute(text(stmt))\n\n\n# #endregion _ensure_target_schema\n" }, { "contract_id": "_migrate_config", "contract_type": "Function", "file_path": "backend/src/scripts/migrate_sqlite_to_postgres.py", - "start_line": 170, - "end_line": 198, + "start_line": 163, + "end_line": 194, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73177,15 +57178,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_migrate_config:Function]\n# @PURPOSE: Migrates legacy config.json into app_configurations(global).\ndef _migrate_config(engine, legacy_config_path: Optional[Path]) -> int:\n with belief_scope(\"_migrate_config\"):\n if legacy_config_path is None:\n log.reason(\"No legacy config.json found, skipping migration\")\n return 0\n\n payload = json.loads(legacy_config_path.read_text(encoding=\"utf-8\"))\n with engine.begin() as conn:\n conn.execute(\n text(\n \"\"\"\n INSERT INTO app_configurations (id, payload, updated_at)\n VALUES ('global', CAST(:payload AS JSONB), NOW())\n ON CONFLICT (id)\n DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()\n \"\"\"\n ),\n {\"payload\": json.dumps(payload, ensure_ascii=True)},\n )\n logger.info(\n \"[_migrate_config][Coherence:OK] Config migrated from %s\",\n legacy_config_path,\n )\n return 1\n\n\n# [/DEF:_migrate_config:Function]\n" + "anchor_syntax": "region", + "body": "# #region _migrate_config [TYPE Function]\n# @BRIEF Migrates legacy config.json into app_configurations(global).\ndef _migrate_config(engine, legacy_config_path: Path | None) -> int:\n with belief_scope(\"_migrate_config\"):\n if legacy_config_path is None:\n logger.reason(\n \"No legacy config.json found, skipping\",\n extra={\"src\": \"_migrate_config\"},\n )\n return 0\n\n payload = json.loads(legacy_config_path.read_text(encoding=\"utf-8\"))\n with engine.begin() as conn:\n conn.execute(\n text(\n \"\"\"\n INSERT INTO app_configurations (id, payload, updated_at)\n VALUES ('global', CAST(:payload AS JSONB), NOW())\n ON CONFLICT (id)\n DO UPDATE SET payload = EXCLUDED.payload, updated_at = NOW()\n \"\"\"\n ),\n {\"payload\": json.dumps(payload, ensure_ascii=True)},\n )\n logger.info(\n \"[_migrate_config][Coherence:OK] Config migrated from %s\",\n legacy_config_path,\n )\n return 1\n\n\n# #endregion _migrate_config\n" }, { "contract_id": "_migrate_tasks_and_logs", "contract_type": "Function", "file_path": "backend/src/scripts/migrate_sqlite_to_postgres.py", - "start_line": 201, - "end_line": 327, + "start_line": 197, + "end_line": 323, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73203,14 +57204,14 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_migrate_tasks_and_logs:Function]\n# @PURPOSE: Migrates task_records and task_logs from SQLite into PostgreSQL.\ndef _migrate_tasks_and_logs(engine, sqlite_conn: sqlite3.Connection) -> Dict[str, int]:\n with belief_scope(\"_migrate_tasks_and_logs\"):\n stats = {\n \"task_records_total\": 0,\n \"task_records_inserted\": 0,\n \"task_logs_total\": 0,\n \"task_logs_inserted\": 0,\n }\n\n rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, type, status, environment_id, started_at, finished_at, logs, error, result, created_at, params\n FROM task_records\n ORDER BY created_at ASC\n \"\"\"\n ).fetchall()\n stats[\"task_records_total\"] = len(rows)\n\n with engine.begin() as conn:\n existing_env_ids = {\n row[0]\n for row in conn.execute(text(\"SELECT id FROM environments\")).fetchall()\n }\n for row in rows:\n params_obj = _json_load_if_needed(row[\"params\"])\n result_obj = _json_load_if_needed(row[\"result\"])\n logs_obj = _json_load_if_needed(row[\"logs\"])\n environment_id = row[\"environment_id\"]\n if environment_id and environment_id not in existing_env_ids:\n # Legacy task may reference environments that were not migrated; keep task row and drop FK value.\n environment_id = None\n\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_records (\n id, type, status, environment_id, started_at, finished_at,\n logs, error, result, created_at, params\n ) VALUES (\n :id, :type, :status, :environment_id, :started_at, :finished_at,\n CAST(:logs AS JSONB), :error, CAST(:result AS JSONB), :created_at, CAST(:params AS JSONB)\n )\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"type\": row[\"type\"],\n \"status\": row[\"status\"],\n \"environment_id\": environment_id,\n \"started_at\": row[\"started_at\"],\n \"finished_at\": row[\"finished_at\"],\n \"logs\": json.dumps(logs_obj, ensure_ascii=True)\n if logs_obj is not None\n else None,\n \"error\": row[\"error\"],\n \"result\": json.dumps(result_obj, ensure_ascii=True)\n if result_obj is not None\n else None,\n \"created_at\": row[\"created_at\"],\n \"params\": json.dumps(params_obj, ensure_ascii=True)\n if params_obj is not None\n else None,\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_records_inserted\"] += int(res.rowcount)\n\n log_rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, task_id, timestamp, level, source, message, metadata_json\n FROM task_logs\n ORDER BY id ASC\n \"\"\"\n ).fetchall()\n stats[\"task_logs_total\"] = len(log_rows)\n\n with engine.begin() as conn:\n for row in log_rows:\n # Preserve original IDs to keep migration idempotent.\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_logs (id, task_id, timestamp, level, source, message, metadata_json)\n VALUES (:id, :task_id, :timestamp, :level, :source, :message, :metadata_json)\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"task_id\": row[\"task_id\"],\n \"timestamp\": row[\"timestamp\"],\n \"level\": row[\"level\"],\n \"source\": row[\"source\"] or \"system\",\n \"message\": row[\"message\"],\n \"metadata_json\": row[\"metadata_json\"],\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_logs_inserted\"] += int(res.rowcount)\n\n # Ensure sequence is aligned after explicit id inserts.\n conn.execute(\n text(\n \"\"\"\n SELECT setval(\n 'task_logs_id_seq',\n COALESCE((SELECT MAX(id) FROM task_logs), 1),\n TRUE\n )\n \"\"\"\n )\n )\n\n logger.info(\n \"[_migrate_tasks_and_logs][Coherence:OK] task_records=%s/%s task_logs=%s/%s\",\n stats[\"task_records_inserted\"],\n stats[\"task_records_total\"],\n stats[\"task_logs_inserted\"],\n stats[\"task_logs_total\"],\n )\n return stats\n\n\n# [/DEF:_migrate_tasks_and_logs:Function]\n" + "anchor_syntax": "region", + "body": "# #region _migrate_tasks_and_logs [TYPE Function]\n# @BRIEF Migrates task_records and task_logs from SQLite into PostgreSQL.\ndef _migrate_tasks_and_logs(engine, sqlite_conn: sqlite3.Connection) -> dict[str, int]:\n with belief_scope(\"_migrate_tasks_and_logs\"):\n stats = {\n \"task_records_total\": 0,\n \"task_records_inserted\": 0,\n \"task_logs_total\": 0,\n \"task_logs_inserted\": 0,\n }\n\n rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, type, status, environment_id, started_at, finished_at, logs, error, result, created_at, params\n FROM task_records\n ORDER BY created_at ASC\n \"\"\"\n ).fetchall()\n stats[\"task_records_total\"] = len(rows)\n\n with engine.begin() as conn:\n existing_env_ids = {\n row[0]\n for row in conn.execute(text(\"SELECT id FROM environments\")).fetchall()\n }\n for row in rows:\n params_obj = _json_load_if_needed(row[\"params\"])\n result_obj = _json_load_if_needed(row[\"result\"])\n logs_obj = _json_load_if_needed(row[\"logs\"])\n environment_id = row[\"environment_id\"]\n if environment_id and environment_id not in existing_env_ids:\n # Legacy task may reference environments that were not migrated; keep task row and drop FK value.\n environment_id = None\n\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_records (\n id, type, status, environment_id, started_at, finished_at,\n logs, error, result, created_at, params\n ) VALUES (\n :id, :type, :status, :environment_id, :started_at, :finished_at,\n CAST(:logs AS JSONB), :error, CAST(:result AS JSONB), :created_at, CAST(:params AS JSONB)\n )\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"type\": row[\"type\"],\n \"status\": row[\"status\"],\n \"environment_id\": environment_id,\n \"started_at\": row[\"started_at\"],\n \"finished_at\": row[\"finished_at\"],\n \"logs\": json.dumps(logs_obj, ensure_ascii=True)\n if logs_obj is not None\n else None,\n \"error\": row[\"error\"],\n \"result\": json.dumps(result_obj, ensure_ascii=True)\n if result_obj is not None\n else None,\n \"created_at\": row[\"created_at\"],\n \"params\": json.dumps(params_obj, ensure_ascii=True)\n if params_obj is not None\n else None,\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_records_inserted\"] += int(res.rowcount)\n\n log_rows = sqlite_conn.execute(\n \"\"\"\n SELECT id, task_id, timestamp, level, source, message, metadata_json\n FROM task_logs\n ORDER BY id ASC\n \"\"\"\n ).fetchall()\n stats[\"task_logs_total\"] = len(log_rows)\n\n with engine.begin() as conn:\n for row in log_rows:\n # Preserve original IDs to keep migration idempotent.\n res = conn.execute(\n text(\n \"\"\"\n INSERT INTO task_logs (id, task_id, timestamp, level, source, message, metadata_json)\n VALUES (:id, :task_id, :timestamp, :level, :source, :message, :metadata_json)\n ON CONFLICT (id) DO NOTHING\n \"\"\"\n ),\n {\n \"id\": row[\"id\"],\n \"task_id\": row[\"task_id\"],\n \"timestamp\": row[\"timestamp\"],\n \"level\": row[\"level\"],\n \"source\": row[\"source\"] or \"system\",\n \"message\": row[\"message\"],\n \"metadata_json\": row[\"metadata_json\"],\n },\n )\n if res.rowcount and res.rowcount > 0:\n stats[\"task_logs_inserted\"] += int(res.rowcount)\n\n # Ensure sequence is aligned after explicit id inserts.\n conn.execute(\n text(\n \"\"\"\n SELECT setval(\n 'task_logs_id_seq',\n COALESCE((SELECT MAX(id) FROM task_logs), 1),\n TRUE\n )\n \"\"\"\n )\n )\n\n logger.info(\n \"[_migrate_tasks_and_logs][Coherence:OK] task_records=%s/%s task_logs=%s/%s\",\n stats[\"task_records_inserted\"],\n stats[\"task_records_total\"],\n stats[\"task_logs_inserted\"],\n stats[\"task_logs_total\"],\n )\n return stats\n\n\n# #endregion _migrate_tasks_and_logs\n" }, { "contract_id": "run_migration", "contract_type": "Function", "file_path": "backend/src/scripts/migrate_sqlite_to_postgres.py", - "start_line": 330, + "start_line": 326, "end_line": 352, "tier": "TIER_1", "complexity": 1, @@ -73229,29 +57230,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_migration:Function]\n# @PURPOSE: Orchestrates migration from SQLite/file to PostgreSQL.\ndef run_migration(\n sqlite_path: Path, target_url: str, legacy_config_path: Optional[Path]\n) -> Dict[str, int]:\n with belief_scope(\"run_migration\"):\n log.reason(\"Starting migration from SQLite to PostgreSQL\", payload={\"sqlite_path\": str(sqlite_path), \"target_url\": target_url})\n if not sqlite_path.exists():\n raise FileNotFoundError(f\"SQLite source not found: {sqlite_path}\")\n\n sqlite_conn = _connect_sqlite(sqlite_path)\n engine = create_engine(target_url, pool_pre_ping=True)\n try:\n _ensure_target_schema(engine)\n config_upserted = _migrate_config(engine, legacy_config_path)\n stats = _migrate_tasks_and_logs(engine, sqlite_conn)\n stats[\"config_upserted\"] = config_upserted\n return stats\n finally:\n sqlite_conn.close()\n\n\n# [/DEF:run_migration:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_migration [TYPE Function]\n# @BRIEF Orchestrates migration from SQLite/file to PostgreSQL.\ndef run_migration(\n sqlite_path: Path, target_url: str, legacy_config_path: Path | None\n) -> dict[str, int]:\n seed_trace_id()\n with belief_scope(\"run_migration\"):\n logger.reason(\n f\"sqlite={sqlite_path} target={target_url}\",\n extra={\"src\": \"run_migration\"},\n )\n if not sqlite_path.exists():\n raise FileNotFoundError(f\"SQLite source not found: {sqlite_path}\")\n\n sqlite_conn = _connect_sqlite(sqlite_path)\n engine = create_engine(target_url, pool_pre_ping=True)\n try:\n _ensure_target_schema(engine)\n config_upserted = _migrate_config(engine, legacy_config_path)\n stats = _migrate_tasks_and_logs(engine, sqlite_conn)\n stats[\"config_upserted\"] = config_upserted\n return stats\n finally:\n sqlite_conn.close()\n\n\n# #endregion run_migration\n" }, { "contract_id": "SeedPermissionsScript", "contract_type": "Module", "file_path": "backend/src/scripts/seed_permissions.py", "start_line": 1, - "end_line": 148, + "end_line": 144, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Safe to run multiple times (idempotent).", "LAYER": "Scripts", - "PURPOSE": "Populates the auth database with initial system permissions.", - "SEMANTICS": [ - "setup", - "database", - "auth", - "permissions", - "seeding" - ] + "PURPOSE": "Populates the auth database with initial system permissions." }, "relations": [ { @@ -73290,19 +57284,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SeedPermissionsScript:Module]\n#\n# @SEMANTICS: setup, database, auth, permissions, seeding\n# @PURPOSE: Populates the auth database with initial system permissions.\n# @COMPLEXITY: 3\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\n# [SECTION: IMPORTS]\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.database import AuthSessionLocal\nfrom src.models.auth import Permission, Role\nfrom src.core.auth.repository import AuthRepository\nfrom src.core.logger import logger, belief_scope\n# [/SECTION]\n\n# [DEF:INITIAL_PERMISSIONS:Constant]\n# @PURPOSE: Canonical bootstrap permission tuples seeded into auth storage.\n# @COMPLEXITY: 3\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# [/DEF:INITIAL_PERMISSIONS:Constant]\n\n\n# [DEF:seed_permissions:Function]\n# @PURPOSE: Inserts missing permissions into the database.\n# @COMPLEXITY: 3\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 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# [/DEF:seed_permissions:Function]\n\nif __name__ == \"__main__\":\n seed_permissions()\n\n# [/DEF:SeedPermissionsScript:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "INITIAL_PERMISSIONS", "contract_type": "Constant", "file_path": "backend/src/scripts/seed_permissions.py", - "start_line": 27, - "end_line": 62, + "start_line": 24, + "end_line": 58, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Canonical bootstrap permission tuples seeded into auth storage." }, "relations": [ @@ -73377,19 +57371,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:INITIAL_PERMISSIONS:Constant]\n# @PURPOSE: Canonical bootstrap permission tuples seeded into auth storage.\n# @COMPLEXITY: 3\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# [/DEF:INITIAL_PERMISSIONS:Constant]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "seed_permissions", "contract_type": "Function", "file_path": "backend/src/scripts/seed_permissions.py", - "start_line": 65, - "end_line": 143, + "start_line": 61, + "end_line": 139, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "All INITIAL_PERMISSIONS exist in the DB.", "PURPOSE": "Inserts missing permissions into the database." }, @@ -73436,30 +57430,22 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:seed_permissions:Function]\n# @PURPOSE: Inserts missing permissions into the database.\n# @COMPLEXITY: 3\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 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# [/DEF:seed_permissions:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SeedSupersetLoadTestScript", "contract_type": "Module", "file_path": "backend/src/scripts/seed_superset_load_test.py", "start_line": 1, - "end_line": 394, + "end_line": 401, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Created chart and dashboard names are globally unique for one script run.", "LAYER": "Scripts", - "PURPOSE": "Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments.", - "SEMANTICS": [ - "superset", - "load-test", - "charts", - "dashboards", - "seed", - "stress" - ] + "PURPOSE": "Creates randomized load-test data in Superset by cloning chart configurations and creating dashboards in target environments." }, "relations": [ { @@ -73522,15 +57508,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:SeedSupersetLoadTestScript:Module]\n#\n# @COMPLEXITY: 3\n# @SEMANTICS: superset, load-test, charts, dashboards, seed, stress\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nimport argparse\nimport json\nimport random\nimport sys\nimport uuid\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\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.logger import belief_scope\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.superset_client import SupersetClient\nlog = MarkerLogger(\"SeedSupersetLoadTest\")\n\n# [/SECTION]\n\n\n# [DEF:_parse_args:Function]\n# @PURPOSE: 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# [/DEF:_parse_args:Function]\n\n\n# [DEF:_extract_result_payload:Function]\n# @PURPOSE: 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# [/DEF:_extract_result_payload:Function]\n\n\n# [DEF:_extract_created_id:Function]\n# @PURPOSE: 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) -> Optional[int]:\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# [/DEF:_extract_created_id:Function]\n\n\n# [DEF:_generate_unique_name:Function]\n# @PURPOSE: 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# [/DEF:_generate_unique_name:Function]\n\n\n# [DEF:_resolve_target_envs:Function]\n# @PURPOSE: 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 log.explore(\"Failed loading environments from config\", payload={\"config_path\": str(config_path)}, error=str(exc))\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# [/DEF:_resolve_target_envs:Function]\n\n\n# [DEF:_build_chart_template_pool:Function]\n# @PURPOSE: 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# [/DEF:_build_chart_template_pool:Function]\n\n\n# [DEF:seed_superset_load_data:Function]\n# @PURPOSE: 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 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 log.reason(\"Loaded chart templates for environment\", payload={\"env_id\": env_id, \"template_count\": len(templates_by_env[env_id])})\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 log.reflect(\"Dry-run dashboard create\", payload={\"env_id\": env_id, \"title\": dashboard_title})\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 log.explore(\"Failed creating dashboard\", payload={\"env_id\": env_id}, error=str(exc))\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 log.reason(f\"Created {index + 1}/{args.charts} charts\")\n except Exception as exc:\n errors += 1\n log.explore(\"Failed creating chart\", payload={\"env_id\": env_id}, error=str(exc))\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# [/DEF:seed_superset_load_data:Function]\n\n\n# [DEF:main:Function]\n# @PURPOSE: 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 with belief_scope(\"seed_superset_load_test.main\"):\n args = _parse_args()\n result = seed_superset_load_data(args)\n log.reflect(\"Seed load test complete\", payload={\"summary\": result})\n\n\n# [/DEF:main:Function]\n\n\nif __name__ == \"__main__\":\n main()\n\n# [/DEF:SeedSupersetLoadTestScript:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "_parse_args", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 32, - "end_line": 73, + "start_line": 25, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73568,15 +57554,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_parse_args:Function]\n# @PURPOSE: 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# [/DEF:_parse_args:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_extract_result_payload", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 76, - "end_line": 87, + "start_line": 69, + "end_line": 80, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73614,15 +57600,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_extract_result_payload:Function]\n# @PURPOSE: 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# [/DEF:_extract_result_payload:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_extract_created_id", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 90, - "end_line": 104, + "start_line": 83, + "end_line": 97, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73660,15 +57646,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_extract_created_id:Function]\n# @PURPOSE: 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) -> Optional[int]:\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# [/DEF:_extract_created_id:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_generate_unique_name", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 107, - "end_line": 144, + "start_line": 100, + "end_line": 137, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73706,15 +57692,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_generate_unique_name:Function]\n# @PURPOSE: 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# [/DEF:_generate_unique_name:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_resolve_target_envs", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 147, - "end_line": 178, + "start_line": 140, + "end_line": 174, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73752,15 +57738,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_target_envs:Function]\n# @PURPOSE: 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 log.explore(\"Failed loading environments from config\", payload={\"config_path\": str(config_path)}, error=str(exc))\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# [/DEF:_resolve_target_envs:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_build_chart_template_pool", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 181, - "end_line": 253, + "start_line": 177, + "end_line": 249, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73798,15 +57784,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_build_chart_template_pool:Function]\n# @PURPOSE: 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# [/DEF:_build_chart_template_pool:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "seed_superset_load_data", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 256, - "end_line": 374, + "start_line": 252, + "end_line": 377, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73854,15 +57840,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:seed_superset_load_data:Function]\n# @PURPOSE: 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 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 log.reason(\"Loaded chart templates for environment\", payload={\"env_id\": env_id, \"template_count\": len(templates_by_env[env_id])})\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 log.reflect(\"Dry-run dashboard create\", payload={\"env_id\": env_id, \"title\": dashboard_title})\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 log.explore(\"Failed creating dashboard\", payload={\"env_id\": env_id}, error=str(exc))\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 log.reason(f\"Created {index + 1}/{args.charts} charts\")\n except Exception as exc:\n errors += 1\n log.explore(\"Failed creating chart\", payload={\"env_id\": env_id}, error=str(exc))\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# [/DEF:seed_superset_load_data:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "main", "contract_type": "Function", "file_path": "backend/src/scripts/seed_superset_load_test.py", - "start_line": 377, - "end_line": 388, + "start_line": 380, + "end_line": 395, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -73900,8 +57886,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:main:Function]\n# @PURPOSE: 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 with belief_scope(\"seed_superset_load_test.main\"):\n args = _parse_args()\n result = seed_superset_load_data(args)\n log.reflect(\"Seed load test complete\", payload={\"summary\": result})\n\n\n# [/DEF:main:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "test_dataset_dashboard_relations_script", @@ -73912,32 +57898,24 @@ "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Tests and inspects dataset-to-dashboard relationship responses from Superset API.", - "SEMANTICS": [ - "scripts", - "test", - "dataset", - "dashboard", - "superset", - "relations" - ] + "COMPLEXITY": 2, + "PURPOSE": "Tests and inspects dataset-to-dashboard relationship responses from Superset API." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_dataset_dashboard_relations_script:Module]\n# @SEMANTICS: scripts, test, dataset, dashboard, superset, relations\n# @PURPOSE: Tests and inspects dataset-to-dashboard relationship responses from Superset API.\n# @COMPLEXITY: 2\n\"\"\"\nScript to test dataset-to-dashboard relationships from Superset API.\n\nUsage:\n cd backend && .venv/bin/python3 src/scripts/test_dataset_dashboard_relations.py\n\"\"\"\n\nimport json\nimport sys\nfrom pathlib import Path\n\n# Add src to path (parent of scripts directory)\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.superset_client import SupersetClient\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import logger\n\n\ndef test_dashboard_dataset_relations():\n \"\"\"Test fetching dataset-to-dashboard relationships.\"\"\"\n \n # Load environment from existing config\n config_manager = ConfigManager()\n environments = config_manager.get_environments()\n \n if not environments:\n logger.error(\"No environments configured!\")\n return\n \n # Use first available environment\n env = environments[0]\n logger.info(f\"Using environment: {env.name} ({env.url})\")\n \n client = SupersetClient(env)\n \n try:\n # Authenticate\n logger.info(\"Authenticating to Superset...\")\n client.authenticate()\n logger.info(\"Authentication successful!\")\n \n # Test dashboard ID 13\n dashboard_id = 13\n logger.info(f\"\\n=== Fetching Dashboard {dashboard_id} ===\")\n dashboard = client.network.request(method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\")\n \n print(\"\\nDashboard structure:\")\n print(f\" ID: {dashboard.get('id')}\")\n print(f\" Title: {dashboard.get('dashboard_title')}\")\n print(f\" Published: {dashboard.get('published')}\")\n \n # Check for slices/charts\n if 'slices' in dashboard:\n logger.info(f\"\\n Found {len(dashboard['slices'])} slices/charts in dashboard\")\n for i, slice_data in enumerate(dashboard['slices'][:5]): # Show first 5\n print(f\" Slice {i+1}:\")\n print(f\" ID: {slice_data.get('slice_id')}\")\n print(f\" Name: {slice_data.get('slice_name')}\")\n # Check for datasource_id\n if 'datasource_id' in slice_data:\n print(f\" Datasource ID: {slice_data['datasource_id']}\")\n if 'datasource_name' in slice_data:\n print(f\" Datasource Name: {slice_data['datasource_name']}\")\n if 'datasource_type' in slice_data:\n print(f\" Datasource Type: {slice_data['datasource_type']}\")\n else:\n logger.warning(\" No 'slices' field found in dashboard response\")\n logger.info(f\" Available fields: {list(dashboard.keys())}\")\n \n # Test dataset ID 26\n dataset_id = 26\n logger.info(f\"\\n=== Fetching Dataset {dataset_id} ===\")\n dataset = client.get_dataset(dataset_id)\n \n print(\"\\nDataset structure:\")\n print(f\" ID: {dataset.get('id')}\")\n print(f\" Table Name: {dataset.get('table_name')}\")\n print(f\" Schema: {dataset.get('schema')}\")\n print(f\" Database: {dataset.get('database', {}).get('database_name', 'Unknown')}\")\n \n # Check for dashboards that use this dataset\n logger.info(f\"\\n=== Finding Dashboards using Dataset {dataset_id} ===\")\n \n # Method: Use Superset's related_objects API\n try:\n logger.info(f\" Using /api/v1/dataset/{dataset_id}/related_objects endpoint...\")\n related_objects = client.network.request(\n method=\"GET\",\n endpoint=f\"/dataset/{dataset_id}/related_objects\"\n )\n \n logger.info(f\" Related objects response type: {type(related_objects)}\")\n logger.info(f\" Related objects keys: {list(related_objects.keys()) if isinstance(related_objects, dict) else 'N/A'}\")\n \n # Check for dashboards in related objects\n if 'dashboards' in related_objects:\n dashboards = related_objects['dashboards']\n logger.info(f\" Found {len(dashboards)} dashboards using this dataset:\")\n \n for dash in dashboards:\n if isinstance(dash, dict):\n logger.info(f\" - Dashboard ID {dash.get('id')}: {dash.get('dashboard_title', dash.get('title', 'Unknown'))}\")\n else:\n logger.info(f\" - Dashboard: {dash}\")\n elif 'result' in related_objects:\n # Some Superset versions use 'result' wrapper\n result = related_objects['result']\n if 'dashboards' in result:\n dashboards = result['dashboards']\n logger.info(f\" Found {len(dashboards)} dashboards using this dataset:\")\n \n for dash in dashboards:\n logger.info(f\" - Dashboard ID {dash.get('id')}: {dash.get('dashboard_title', dash.get('title', 'Unknown'))}\")\n else:\n logger.warning(f\" No 'dashboards' key in result. Keys: {list(result.keys())}\")\n else:\n logger.warning(f\" No 'dashboards' key in response. Available keys: {list(related_objects.keys())}\")\n logger.info(f\" Full related_objects response:\")\n print(json.dumps(related_objects, indent=2, default=str)[:1000])\n \n except Exception as e:\n logger.error(f\" Error fetching related objects: {e}\")\n import traceback\n traceback.print_exc()\n \n # Method 2: Try to use the position_json from dashboard\n logger.info(f\"\\n=== Analyzing Dashboard Position JSON ===\")\n if 'position_json' in dashboard:\n position_data = json.loads(dashboard['position_json'])\n logger.info(f\" Position data type: {type(position_data)}\")\n \n # Look for datasource references\n datasource_ids = set()\n if isinstance(position_data, dict):\n for key, value in position_data.items():\n if 'datasource' in key.lower() or key == 'DASHBOARD_VERSION_KEY':\n logger.debug(f\" Key: {key}, Value type: {type(value)}\")\n elif isinstance(position_data, list):\n logger.info(f\" Position data has {len(position_data)} items\")\n for item in position_data[:3]: # Show first 3\n logger.debug(f\" Item: {type(item)}, keys: {list(item.keys()) if isinstance(item, dict) else 'N/A'}\")\n if isinstance(item, dict):\n if 'datasource_id' in item:\n datasource_ids.add(item['datasource_id'])\n \n if datasource_ids:\n logger.info(f\" Found datasource IDs: {datasource_ids}\")\n \n # Save full response for analysis\n output_file = Path(__file__).parent / \"dataset_dashboard_analysis.json\"\n with open(output_file, 'w') as f:\n json.dump({\n 'dashboard': dashboard,\n 'dataset': dataset\n }, f, indent=2, default=str)\n logger.info(f\"\\nFull response saved to: {output_file}\")\n \n except Exception as e:\n logger.error(f\"Error: {e}\", exc_info=True)\n raise\n\n\nif __name__ == \"__main__\":\n test_dashboard_dataset_relations()\n\n# [/DEF:test_dataset_dashboard_relations_script:Module]\n" + "anchor_syntax": "region", + "body": "# #region test_dataset_dashboard_relations_script [C:2] [TYPE Module] [SEMANTICS pydantic, dashboard, dataset, test, superset]\n# @BRIEF Tests and inspects dataset-to-dashboard relationship responses from Superset API.\n\"\"\"\nScript to test dataset-to-dashboard relationships from Superset API.\n\nUsage:\n cd backend && .venv/bin/python3 src/scripts/test_dataset_dashboard_relations.py\n\"\"\"\n\nimport json\nimport sys\nfrom pathlib import Path\n\n# Add src to path (parent of scripts directory)\nsys.path.append(str(Path(__file__).parent.parent.parent))\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.cot_logger import seed_trace_id\nfrom src.core.logger import logger\nfrom src.core.superset_client import SupersetClient\n\n\ndef test_dashboard_dataset_relations():\n \"\"\"Test fetching dataset-to-dashboard relationships.\"\"\"\n seed_trace_id()\n\n # Load environment from existing config\n config_manager = ConfigManager()\n environments = config_manager.get_environments()\n\n if not environments:\n logger.error(\"No environments configured!\")\n return\n\n # Use first available environment\n env = environments[0]\n logger.info(f\"Using environment: {env.name} ({env.url})\")\n\n client = SupersetClient(env)\n\n try:\n # Authenticate\n logger.info(\"Authenticating to Superset...\")\n client.authenticate()\n logger.info(\"Authentication successful!\")\n\n # Test dashboard ID 13\n dashboard_id = 13\n logger.info(f\"\\n=== Fetching Dashboard {dashboard_id} ===\")\n dashboard = client.network.request(method=\"GET\", endpoint=f\"/dashboard/{dashboard_id}\")\n\n print(\"\\nDashboard structure:\")\n print(f\" ID: {dashboard.get('id')}\")\n print(f\" Title: {dashboard.get('dashboard_title')}\")\n print(f\" Published: {dashboard.get('published')}\")\n\n # Check for slices/charts\n if 'slices' in dashboard:\n logger.info(f\"\\n Found {len(dashboard['slices'])} slices/charts in dashboard\")\n for i, slice_data in enumerate(dashboard['slices'][:5]): # Show first 5\n print(f\" Slice {i+1}:\")\n print(f\" ID: {slice_data.get('slice_id')}\")\n print(f\" Name: {slice_data.get('slice_name')}\")\n # Check for datasource_id\n if 'datasource_id' in slice_data:\n print(f\" Datasource ID: {slice_data['datasource_id']}\")\n if 'datasource_name' in slice_data:\n print(f\" Datasource Name: {slice_data['datasource_name']}\")\n if 'datasource_type' in slice_data:\n print(f\" Datasource Type: {slice_data['datasource_type']}\")\n else:\n logger.warning(\" No 'slices' field found in dashboard response\")\n logger.info(f\" Available fields: {list(dashboard.keys())}\")\n\n # Test dataset ID 26\n dataset_id = 26\n logger.info(f\"\\n=== Fetching Dataset {dataset_id} ===\")\n dataset = client.get_dataset(dataset_id)\n\n print(\"\\nDataset structure:\")\n print(f\" ID: {dataset.get('id')}\")\n print(f\" Table Name: {dataset.get('table_name')}\")\n print(f\" Schema: {dataset.get('schema')}\")\n print(f\" Database: {dataset.get('database', {}).get('database_name', 'Unknown')}\")\n\n # Check for dashboards that use this dataset\n logger.info(f\"\\n=== Finding Dashboards using Dataset {dataset_id} ===\")\n\n # Method: Use Superset's related_objects API\n try:\n logger.info(f\" Using /api/v1/dataset/{dataset_id}/related_objects endpoint...\")\n related_objects = client.network.request(\n method=\"GET\",\n endpoint=f\"/dataset/{dataset_id}/related_objects\"\n )\n\n logger.info(f\" Related objects response type: {type(related_objects)}\")\n logger.info(f\" Related objects keys: {list(related_objects.keys()) if isinstance(related_objects, dict) else 'N/A'}\")\n\n # Check for dashboards in related objects\n if 'dashboards' in related_objects:\n dashboards = related_objects['dashboards']\n logger.info(f\" Found {len(dashboards)} dashboards using this dataset:\")\n\n for dash in dashboards:\n if isinstance(dash, dict):\n logger.info(f\" - Dashboard ID {dash.get('id')}: {dash.get('dashboard_title', dash.get('title', 'Unknown'))}\")\n else:\n logger.info(f\" - Dashboard: {dash}\")\n elif 'result' in related_objects:\n # Some Superset versions use 'result' wrapper\n result = related_objects['result']\n if 'dashboards' in result:\n dashboards = result['dashboards']\n logger.info(f\" Found {len(dashboards)} dashboards using this dataset:\")\n\n for dash in dashboards:\n logger.info(f\" - Dashboard ID {dash.get('id')}: {dash.get('dashboard_title', dash.get('title', 'Unknown'))}\")\n else:\n logger.warning(f\" No 'dashboards' key in result. Keys: {list(result.keys())}\")\n else:\n logger.warning(f\" No 'dashboards' key in response. Available keys: {list(related_objects.keys())}\")\n logger.info(\" Full related_objects response:\")\n print(json.dumps(related_objects, indent=2, default=str)[:1000])\n\n except Exception as e:\n logger.error(f\" Error fetching related objects: {e}\")\n import traceback\n traceback.print_exc()\n\n # Method 2: Try to use the position_json from dashboard\n logger.info(\"\\n=== Analyzing Dashboard Position JSON ===\")\n if 'position_json' in dashboard:\n position_data = json.loads(dashboard['position_json'])\n logger.info(f\" Position data type: {type(position_data)}\")\n\n # Look for datasource references\n datasource_ids = set()\n if isinstance(position_data, dict):\n for key, value in position_data.items():\n if 'datasource' in key.lower() or key == 'DASHBOARD_VERSION_KEY':\n logger.debug(f\" Key: {key}, Value type: {type(value)}\")\n elif isinstance(position_data, list):\n logger.info(f\" Position data has {len(position_data)} items\")\n for item in position_data[:3]: # Show first 3\n logger.debug(f\" Item: {type(item)}, keys: {list(item.keys()) if isinstance(item, dict) else 'N/A'}\")\n if isinstance(item, dict):\n if 'datasource_id' in item:\n datasource_ids.add(item['datasource_id'])\n\n if datasource_ids:\n logger.info(f\" Found datasource IDs: {datasource_ids}\")\n\n # Save full response for analysis\n output_file = Path(__file__).parent / \"dataset_dashboard_analysis.json\"\n with open(output_file, 'w') as f:\n json.dump({\n 'dashboard': dashboard,\n 'dataset': dataset\n }, f, indent=2, default=str)\n logger.info(f\"\\nFull response saved to: {output_file}\")\n\n except Exception as e:\n logger.error(f\"Error: {e}\", exc_info=True)\n raise\n\n\nif __name__ == \"__main__\":\n test_dashboard_dataset_relations()\n\n# #endregion test_dataset_dashboard_relations_script\n" }, { "contract_id": "services", "contract_type": "Module", "file_path": "backend/src/services/__init__.py", "start_line": 1, - "end_line": 18, + "end_line": 17, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "NOTE": "GitService, AuthService, LLMProviderService have circular import issues - import directly when needed", "PURPOSE": "Package initialization for services module" }, @@ -73950,2912 +57928,55 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "# [DEF:services:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Package initialization for services module\n# @NOTE: Only export services that don't cause circular imports\n# @NOTE: GitService, AuthService, LLMProviderService have circular import issues - import directly when needed\n\n# Lazy loading to avoid import issues in tests\n__all__ = ['MappingService', 'ResourceService']\n\ndef __getattr__(name):\n if name == 'MappingService':\n from .mapping_service import MappingService\n return MappingService\n if name == 'ResourceService':\n from .resource_service import ResourceService\n return ResourceService\n raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n# [/DEF:services:Module]\n" - }, - { - "contract_id": "test_encryption_manager", - "contract_type": "Module", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 1, - "end_line": 138, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Encrypt+decrypt roundtrip always returns original plaintext.", - "LAYER": "Domain", - "PURPOSE": "Unit tests for EncryptionManager encrypt/decrypt functionality.", - "SEMANTICS": [ - "encryption", - "security", - "fernet", - "api-keys", - "tests" - ] - }, - "relations": [ - { - "source_id": "test_encryption_manager", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "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 BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_encryption_manager:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: encryption, security, fernet, api-keys, tests\n# @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality.\n# @LAYER: Domain\n# @INVARIANT: Encrypt+decrypt roundtrip always returns original plaintext.\n\nimport sys\nfrom pathlib import Path\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom unittest.mock import patch\nfrom cryptography.fernet import Fernet, InvalidToken\n\n\n# [DEF:TestEncryptionManager:Class]\n# @RELATION: BINDS_TO -> test_encryption_manager\n# @PURPOSE: Validate EncryptionManager encrypt/decrypt roundtrip, uniqueness, and error handling.\n# @PRE: cryptography package installed.\n# @POST: All encrypt/decrypt invariants verified.\nclass TestEncryptionManager:\n \"\"\"Tests for the EncryptionManager class.\"\"\"\n\n def _make_manager(self):\n \"\"\"Construct EncryptionManager directly using Fernet (avoids relative import chain).\"\"\"\n # Re-implement the same logic as EncryptionManager to avoid import issues\n # with the llm_provider module's relative imports\n key = Fernet.generate_key()\n fernet = Fernet(key)\n\n class EncryptionManager:\n def __init__(self):\n self.key = key\n self.fernet = fernet\n def encrypt(self, data: str) -> str:\n return self.fernet.encrypt(data.encode()).decode()\n def decrypt(self, encrypted_data: str) -> str:\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n return EncryptionManager()\n\n # [DEF:test_encrypt_decrypt_roundtrip:Function]\n # @PURPOSE: Encrypt then decrypt returns original plaintext.\n # @PRE: Valid plaintext string.\n # @POST: Decrypted output equals original input.\n def test_encrypt_decrypt_roundtrip(self):\n mgr = self._make_manager()\n original = \"my-secret-api-key-12345\"\n encrypted = mgr.encrypt(original)\n assert encrypted != original\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == original\n # [/DEF:test_encrypt_decrypt_roundtrip:Function]\n\n # [DEF:test_encrypt_produces_different_output:Function]\n # @PURPOSE: Same plaintext produces different ciphertext (Fernet uses random IV).\n # @PRE: Two encrypt calls with same input.\n # @POST: Ciphertexts differ but both decrypt to same value.\n def test_encrypt_produces_different_output(self):\n mgr = self._make_manager()\n ct1 = mgr.encrypt(\"same-key\")\n ct2 = mgr.encrypt(\"same-key\")\n assert ct1 != ct2\n assert mgr.decrypt(ct1) == mgr.decrypt(ct2) == \"same-key\"\n # [/DEF:test_encrypt_produces_different_output:Function]\n\n # [DEF:test_different_inputs_yield_different_ciphertext:Function]\n # @PURPOSE: Different inputs produce different ciphertexts.\n # @PRE: Two different plaintext values.\n # @POST: Encrypted outputs differ.\n def test_different_inputs_yield_different_ciphertext(self):\n mgr = self._make_manager()\n ct1 = mgr.encrypt(\"key-one\")\n ct2 = mgr.encrypt(\"key-two\")\n assert ct1 != ct2\n # [/DEF:test_different_inputs_yield_different_ciphertext:Function]\n\n # [DEF:test_decrypt_invalid_data_raises:Function]\n # @PURPOSE: Decrypting invalid data raises InvalidToken.\n # @PRE: Invalid ciphertext string.\n # @POST: Exception raised.\n def test_decrypt_invalid_data_raises(self):\n mgr = self._make_manager()\n with pytest.raises(Exception):\n mgr.decrypt(\"not-a-valid-fernet-token\")\n # [/DEF:test_decrypt_invalid_data_raises:Function]\n\n # [DEF:test_encrypt_empty_string:Function]\n # @PURPOSE: Encrypting and decrypting an empty string works.\n # @PRE: Empty string input.\n # @POST: Decrypted output equals empty string.\n def test_encrypt_empty_string(self):\n mgr = self._make_manager()\n encrypted = mgr.encrypt(\"\")\n assert encrypted\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == \"\"\n # [/DEF:test_encrypt_empty_string:Function]\n\n # [DEF:test_missing_key_fails_fast:Function]\n # @PURPOSE: Missing ENCRYPTION_KEY must abort initialization instead of using a fallback secret.\n # @PRE: ENCRYPTION_KEY is unset.\n # @POST: RuntimeError raised during EncryptionManager construction.\n def test_missing_key_fails_fast(self):\n from src.services.llm_provider import EncryptionManager\n\n with patch.dict(\"os.environ\", {}, clear=True):\n with pytest.raises(RuntimeError, match=\"ENCRYPTION_KEY must be set\"):\n EncryptionManager()\n # [/DEF:test_missing_key_fails_fast:Function]\n\n # [DEF:test_custom_key_roundtrip:Function]\n # @PURPOSE: Custom Fernet key produces valid roundtrip.\n # @PRE: Generated Fernet key.\n # @POST: Encrypt/decrypt with custom key succeeds.\n def test_custom_key_roundtrip(self):\n custom_key = Fernet.generate_key()\n fernet = Fernet(custom_key)\n\n class CustomManager:\n def __init__(self):\n self.key = custom_key\n self.fernet = fernet\n def encrypt(self, data: str) -> str:\n return self.fernet.encrypt(data.encode()).decode()\n def decrypt(self, encrypted_data: str) -> str:\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n mgr = CustomManager()\n encrypted = mgr.encrypt(\"test-with-custom-key\")\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == \"test-with-custom-key\"\n # [/DEF:test_custom_key_roundtrip:Function]\n\n# [/DEF:TestEncryptionManager:Class]\n# [/DEF:test_encryption_manager:Module]\n" - }, - { - "contract_id": "TestEncryptionManager", - "contract_type": "Class", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 18, - "end_line": 137, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "All encrypt/decrypt invariants verified.", - "PRE": "cryptography package installed.", - "PURPOSE": "Validate EncryptionManager encrypt/decrypt roundtrip, uniqueness, and error handling." - }, - "relations": [ - { - "source_id": "TestEncryptionManager", - "relation_type": "BINDS_TO", - "target_id": "test_encryption_manager", - "target_ref": "test_encryption_manager" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "POST", - "message": "@POST is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestEncryptionManager:Class]\n# @RELATION: BINDS_TO -> test_encryption_manager\n# @PURPOSE: Validate EncryptionManager encrypt/decrypt roundtrip, uniqueness, and error handling.\n# @PRE: cryptography package installed.\n# @POST: All encrypt/decrypt invariants verified.\nclass TestEncryptionManager:\n \"\"\"Tests for the EncryptionManager class.\"\"\"\n\n def _make_manager(self):\n \"\"\"Construct EncryptionManager directly using Fernet (avoids relative import chain).\"\"\"\n # Re-implement the same logic as EncryptionManager to avoid import issues\n # with the llm_provider module's relative imports\n key = Fernet.generate_key()\n fernet = Fernet(key)\n\n class EncryptionManager:\n def __init__(self):\n self.key = key\n self.fernet = fernet\n def encrypt(self, data: str) -> str:\n return self.fernet.encrypt(data.encode()).decode()\n def decrypt(self, encrypted_data: str) -> str:\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n return EncryptionManager()\n\n # [DEF:test_encrypt_decrypt_roundtrip:Function]\n # @PURPOSE: Encrypt then decrypt returns original plaintext.\n # @PRE: Valid plaintext string.\n # @POST: Decrypted output equals original input.\n def test_encrypt_decrypt_roundtrip(self):\n mgr = self._make_manager()\n original = \"my-secret-api-key-12345\"\n encrypted = mgr.encrypt(original)\n assert encrypted != original\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == original\n # [/DEF:test_encrypt_decrypt_roundtrip:Function]\n\n # [DEF:test_encrypt_produces_different_output:Function]\n # @PURPOSE: Same plaintext produces different ciphertext (Fernet uses random IV).\n # @PRE: Two encrypt calls with same input.\n # @POST: Ciphertexts differ but both decrypt to same value.\n def test_encrypt_produces_different_output(self):\n mgr = self._make_manager()\n ct1 = mgr.encrypt(\"same-key\")\n ct2 = mgr.encrypt(\"same-key\")\n assert ct1 != ct2\n assert mgr.decrypt(ct1) == mgr.decrypt(ct2) == \"same-key\"\n # [/DEF:test_encrypt_produces_different_output:Function]\n\n # [DEF:test_different_inputs_yield_different_ciphertext:Function]\n # @PURPOSE: Different inputs produce different ciphertexts.\n # @PRE: Two different plaintext values.\n # @POST: Encrypted outputs differ.\n def test_different_inputs_yield_different_ciphertext(self):\n mgr = self._make_manager()\n ct1 = mgr.encrypt(\"key-one\")\n ct2 = mgr.encrypt(\"key-two\")\n assert ct1 != ct2\n # [/DEF:test_different_inputs_yield_different_ciphertext:Function]\n\n # [DEF:test_decrypt_invalid_data_raises:Function]\n # @PURPOSE: Decrypting invalid data raises InvalidToken.\n # @PRE: Invalid ciphertext string.\n # @POST: Exception raised.\n def test_decrypt_invalid_data_raises(self):\n mgr = self._make_manager()\n with pytest.raises(Exception):\n mgr.decrypt(\"not-a-valid-fernet-token\")\n # [/DEF:test_decrypt_invalid_data_raises:Function]\n\n # [DEF:test_encrypt_empty_string:Function]\n # @PURPOSE: Encrypting and decrypting an empty string works.\n # @PRE: Empty string input.\n # @POST: Decrypted output equals empty string.\n def test_encrypt_empty_string(self):\n mgr = self._make_manager()\n encrypted = mgr.encrypt(\"\")\n assert encrypted\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == \"\"\n # [/DEF:test_encrypt_empty_string:Function]\n\n # [DEF:test_missing_key_fails_fast:Function]\n # @PURPOSE: Missing ENCRYPTION_KEY must abort initialization instead of using a fallback secret.\n # @PRE: ENCRYPTION_KEY is unset.\n # @POST: RuntimeError raised during EncryptionManager construction.\n def test_missing_key_fails_fast(self):\n from src.services.llm_provider import EncryptionManager\n\n with patch.dict(\"os.environ\", {}, clear=True):\n with pytest.raises(RuntimeError, match=\"ENCRYPTION_KEY must be set\"):\n EncryptionManager()\n # [/DEF:test_missing_key_fails_fast:Function]\n\n # [DEF:test_custom_key_roundtrip:Function]\n # @PURPOSE: Custom Fernet key produces valid roundtrip.\n # @PRE: Generated Fernet key.\n # @POST: Encrypt/decrypt with custom key succeeds.\n def test_custom_key_roundtrip(self):\n custom_key = Fernet.generate_key()\n fernet = Fernet(custom_key)\n\n class CustomManager:\n def __init__(self):\n self.key = custom_key\n self.fernet = fernet\n def encrypt(self, data: str) -> str:\n return self.fernet.encrypt(data.encode()).decode()\n def decrypt(self, encrypted_data: str) -> str:\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n mgr = CustomManager()\n encrypted = mgr.encrypt(\"test-with-custom-key\")\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == \"test-with-custom-key\"\n # [/DEF:test_custom_key_roundtrip:Function]\n\n# [/DEF:TestEncryptionManager:Class]\n" - }, - { - "contract_id": "test_encrypt_decrypt_roundtrip", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 44, - "end_line": 55, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Decrypted output equals original input.", - "PRE": "Valid plaintext string.", - "PURPOSE": "Encrypt then decrypt returns original plaintext." - }, - "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": "def", - "body": " # [DEF:test_encrypt_decrypt_roundtrip:Function]\n # @PURPOSE: Encrypt then decrypt returns original plaintext.\n # @PRE: Valid plaintext string.\n # @POST: Decrypted output equals original input.\n def test_encrypt_decrypt_roundtrip(self):\n mgr = self._make_manager()\n original = \"my-secret-api-key-12345\"\n encrypted = mgr.encrypt(original)\n assert encrypted != original\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == original\n # [/DEF:test_encrypt_decrypt_roundtrip:Function]\n" - }, - { - "contract_id": "test_encrypt_produces_different_output", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 57, - "end_line": 67, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Ciphertexts differ but both decrypt to same value.", - "PRE": "Two encrypt calls with same input.", - "PURPOSE": "Same plaintext produces different ciphertext (Fernet uses random IV)." - }, - "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": "def", - "body": " # [DEF:test_encrypt_produces_different_output:Function]\n # @PURPOSE: Same plaintext produces different ciphertext (Fernet uses random IV).\n # @PRE: Two encrypt calls with same input.\n # @POST: Ciphertexts differ but both decrypt to same value.\n def test_encrypt_produces_different_output(self):\n mgr = self._make_manager()\n ct1 = mgr.encrypt(\"same-key\")\n ct2 = mgr.encrypt(\"same-key\")\n assert ct1 != ct2\n assert mgr.decrypt(ct1) == mgr.decrypt(ct2) == \"same-key\"\n # [/DEF:test_encrypt_produces_different_output:Function]\n" - }, - { - "contract_id": "test_different_inputs_yield_different_ciphertext", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 69, - "end_line": 78, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Encrypted outputs differ.", - "PRE": "Two different plaintext values.", - "PURPOSE": "Different inputs produce different ciphertexts." - }, - "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": "def", - "body": " # [DEF:test_different_inputs_yield_different_ciphertext:Function]\n # @PURPOSE: Different inputs produce different ciphertexts.\n # @PRE: Two different plaintext values.\n # @POST: Encrypted outputs differ.\n def test_different_inputs_yield_different_ciphertext(self):\n mgr = self._make_manager()\n ct1 = mgr.encrypt(\"key-one\")\n ct2 = mgr.encrypt(\"key-two\")\n assert ct1 != ct2\n # [/DEF:test_different_inputs_yield_different_ciphertext:Function]\n" - }, - { - "contract_id": "test_decrypt_invalid_data_raises", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 80, - "end_line": 88, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Exception raised.", - "PRE": "Invalid ciphertext string.", - "PURPOSE": "Decrypting invalid data raises InvalidToken." - }, - "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": "def", - "body": " # [DEF:test_decrypt_invalid_data_raises:Function]\n # @PURPOSE: Decrypting invalid data raises InvalidToken.\n # @PRE: Invalid ciphertext string.\n # @POST: Exception raised.\n def test_decrypt_invalid_data_raises(self):\n mgr = self._make_manager()\n with pytest.raises(Exception):\n mgr.decrypt(\"not-a-valid-fernet-token\")\n # [/DEF:test_decrypt_invalid_data_raises:Function]\n" - }, - { - "contract_id": "test_encrypt_empty_string", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 90, - "end_line": 100, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Decrypted output equals empty string.", - "PRE": "Empty string input.", - "PURPOSE": "Encrypting and decrypting an empty string works." - }, - "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": "def", - "body": " # [DEF:test_encrypt_empty_string:Function]\n # @PURPOSE: Encrypting and decrypting an empty string works.\n # @PRE: Empty string input.\n # @POST: Decrypted output equals empty string.\n def test_encrypt_empty_string(self):\n mgr = self._make_manager()\n encrypted = mgr.encrypt(\"\")\n assert encrypted\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == \"\"\n # [/DEF:test_encrypt_empty_string:Function]\n" - }, - { - "contract_id": "test_missing_key_fails_fast", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 102, - "end_line": 112, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "RuntimeError raised during EncryptionManager construction.", - "PRE": "ENCRYPTION_KEY is unset.", - "PURPOSE": "Missing ENCRYPTION_KEY must abort initialization instead of using a fallback secret." - }, - "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": "def", - "body": " # [DEF:test_missing_key_fails_fast:Function]\n # @PURPOSE: Missing ENCRYPTION_KEY must abort initialization instead of using a fallback secret.\n # @PRE: ENCRYPTION_KEY is unset.\n # @POST: RuntimeError raised during EncryptionManager construction.\n def test_missing_key_fails_fast(self):\n from src.services.llm_provider import EncryptionManager\n\n with patch.dict(\"os.environ\", {}, clear=True):\n with pytest.raises(RuntimeError, match=\"ENCRYPTION_KEY must be set\"):\n EncryptionManager()\n # [/DEF:test_missing_key_fails_fast:Function]\n" - }, - { - "contract_id": "test_custom_key_roundtrip", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_encryption_manager.py", - "start_line": 114, - "end_line": 135, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Encrypt/decrypt with custom key succeeds.", - "PRE": "Generated Fernet key.", - "PURPOSE": "Custom Fernet key produces valid roundtrip." - }, - "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": "def", - "body": " # [DEF:test_custom_key_roundtrip:Function]\n # @PURPOSE: Custom Fernet key produces valid roundtrip.\n # @PRE: Generated Fernet key.\n # @POST: Encrypt/decrypt with custom key succeeds.\n def test_custom_key_roundtrip(self):\n custom_key = Fernet.generate_key()\n fernet = Fernet(custom_key)\n\n class CustomManager:\n def __init__(self):\n self.key = custom_key\n self.fernet = fernet\n def encrypt(self, data: str) -> str:\n return self.fernet.encrypt(data.encode()).decode()\n def decrypt(self, encrypted_data: str) -> str:\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n mgr = CustomManager()\n encrypted = mgr.encrypt(\"test-with-custom-key\")\n decrypted = mgr.decrypt(encrypted)\n assert decrypted == \"test-with-custom-key\"\n # [/DEF:test_custom_key_roundtrip:Function]\n" - }, - { - "contract_id": "test_health_service", - "contract_type": "Module", - "file_path": "backend/src/services/__tests__/test_health_service.py", - "start_line": 7, - "end_line": 309, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Unit tests for HealthService aggregation logic." - }, - "relations": [ - { - "source_id": "test_health_service", - "relation_type": "VERIFIES", - "target_id": "src.services.health_service.HealthService", - "target_ref": "[src.services.health_service.HealthService]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_health_service:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for HealthService aggregation logic.\n# @RELATION: VERIFIES ->[src.services.health_service.HealthService]\n\n\n@pytest.mark.asyncio\nasync def test_get_health_summary_aggregation():\n \"\"\"\n @TEST_SCENARIO: Verify that HealthService correctly aggregates the latest record per dashboard.\n \"\"\"\n # Setup: Mock DB session\n db = MagicMock()\n\n now = datetime.utcnow()\n\n # Dashboard 1: Old FAIL, New PASS\n rec1_old = ValidationRecord(\n id=\"rec-old\",\n dashboard_id=\"dash_1\",\n environment_id=\"env_1\",\n status=\"FAIL\",\n timestamp=now - timedelta(hours=1),\n summary=\"Old failure\",\n issues=[],\n )\n rec1_new = ValidationRecord(\n id=\"rec-new\",\n dashboard_id=\"dash_1\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=now,\n summary=\"New pass\",\n issues=[],\n )\n\n # Dashboard 2: Single WARN\n rec2 = ValidationRecord(\n id=\"rec-warn\",\n dashboard_id=\"dash_2\",\n environment_id=\"env_1\",\n status=\"WARN\",\n timestamp=now,\n summary=\"Warning\",\n issues=[],\n )\n\n # Mock the query chain\n # subquery = self.db.query(...).filter(...).group_by(...).subquery()\n # query = self.db.query(ValidationRecord).join(subquery, ...).all()\n\n mock_query = db.query.return_value\n mock_query.filter.return_value = mock_query\n mock_query.group_by.return_value = mock_query\n mock_query.subquery.return_value = MagicMock()\n\n db.query.return_value.join.return_value.all.return_value = [rec1_new, rec2]\n\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=\"env_1\")\n\n assert summary.pass_count == 1\n assert summary.warn_count == 1\n assert summary.fail_count == 0\n assert len(summary.items) == 2\n\n # Verify dash_1 has the latest status (PASS)\n dash_1_item = next(item for item in summary.items if item.dashboard_id == \"dash_1\")\n assert dash_1_item.status == \"PASS\"\n assert dash_1_item.summary == \"New pass\"\n assert dash_1_item.record_id == rec1_new.id\n assert dash_1_item.dashboard_slug == \"dash_1\"\n\n\n@pytest.mark.asyncio\nasync def test_get_health_summary_empty():\n \"\"\"\n @TEST_SCENARIO: Verify behavior with no records.\n \"\"\"\n db = MagicMock()\n db.query.return_value.join.return_value.all.return_value = []\n\n service = HealthService(db)\n summary = await service.get_health_summary(environment_id=\"env_none\")\n\n assert summary.pass_count == 0\n assert len(summary.items) == 0\n\n\n@pytest.mark.asyncio\nasync def test_get_health_summary_resolves_slug_and_title_from_superset():\n db = MagicMock()\n config_manager = MagicMock()\n config_manager.get_environments.return_value = [MagicMock(id=\"env_1\")]\n\n record = ValidationRecord(\n id=\"rec-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n )\n db.query.return_value.join.return_value.all.return_value = [record]\n\n with patch(\"src.services.health_service.SupersetClient\") as mock_client_cls:\n mock_client = MagicMock()\n mock_client.get_dashboards_summary.return_value = [\n {\"id\": 42, \"slug\": \"ops-overview\", \"title\": \"Ops Overview\"}\n ]\n mock_client_cls.return_value = mock_client\n\n service = HealthService(db, config_manager=config_manager)\n summary = await service.get_health_summary(environment_id=\"env_1\")\n\n assert summary.items[0].dashboard_slug == \"ops-overview\"\n assert summary.items[0].dashboard_title == \"Ops Overview\"\n mock_client.get_dashboards_summary.assert_called_once_with()\n\n\n@pytest.mark.anyio\nasync def test_get_health_summary_reuses_dashboard_metadata_cache_across_service_instances():\n HealthService._dashboard_summary_cache.clear()\n\n db = MagicMock()\n config_manager = MagicMock()\n config_manager.get_environments.return_value = [MagicMock(id=\"env_1\")]\n record = ValidationRecord(\n id=\"rec-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n )\n db.query.return_value.join.return_value.all.return_value = [record]\n\n with patch(\"src.services.health_service.SupersetClient\") as mock_client_cls:\n mock_client = MagicMock()\n mock_client.get_dashboards_summary.return_value = [\n {\"id\": 42, \"slug\": \"ops-overview\", \"title\": \"Ops Overview\"}\n ]\n mock_client_cls.return_value = mock_client\n\n first_service = HealthService(db, config_manager=config_manager)\n second_service = HealthService(db, config_manager=config_manager)\n\n first_summary = await first_service.get_health_summary(environment_id=\"env_1\")\n second_summary = await second_service.get_health_summary(environment_id=\"env_1\")\n\n assert first_summary.items[0].dashboard_slug == \"ops-overview\"\n assert second_summary.items[0].dashboard_slug == \"ops-overview\"\n mock_client.get_dashboards_summary.assert_called_once_with()\n HealthService._dashboard_summary_cache.clear()\n\n\n# [DEF:test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify that deleting a validation report also removes dashboard scope and linked tasks.\ndef test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks():\n db = MagicMock()\n config_manager = MagicMock()\n task_manager = MagicMock()\n task_manager.tasks = {\"task-1\": object(), \"task-2\": object(), \"task-3\": object()}\n\n target_record = ValidationRecord(\n id=\"rec-1\",\n task_id=\"task-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n screenshot_path=None,\n )\n older_peer = ValidationRecord(\n id=\"rec-2\",\n task_id=\"task-2\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"FAIL\",\n timestamp=datetime.utcnow() - timedelta(hours=1),\n summary=\"Older\",\n issues=[],\n screenshot_path=None,\n )\n other_environment = ValidationRecord(\n id=\"rec-3\",\n task_id=\"task-3\",\n dashboard_id=\"42\",\n environment_id=\"env_2\",\n status=\"WARN\",\n timestamp=datetime.utcnow(),\n summary=\"Other environment\",\n issues=[],\n screenshot_path=None,\n )\n\n # @RISK: db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test.\n first_query = MagicMock()\n first_query.first.return_value = target_record\n\n peer_query = MagicMock()\n peer_query.filter.return_value = peer_query\n peer_query.all.return_value = [target_record, older_peer]\n\n db.query.side_effect = [first_query, peer_query]\n\n with patch(\"src.services.health_service.TaskCleanupService\") as cleanup_cls:\n cleanup_instance = MagicMock()\n cleanup_cls.return_value = cleanup_instance\n\n service = HealthService(db, config_manager=config_manager)\n deleted = service.delete_validation_report(\"rec-1\", task_manager=task_manager)\n\n assert deleted is True\n assert db.delete.call_count == 2\n db.delete.assert_any_call(target_record)\n db.delete.assert_any_call(older_peer)\n db.commit.assert_called_once()\n cleanup_instance.delete_task_with_logs.assert_any_call(\"task-1\")\n cleanup_instance.delete_task_with_logs.assert_any_call(\"task-2\")\n assert cleanup_instance.delete_task_with_logs.call_count == 2\n assert \"task-1\" not in task_manager.tasks\n assert \"task-2\" not in task_manager.tasks\n assert \"task-3\" in task_manager.tasks\n\n\n# [/DEF:test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks:Function]\n\n\n# [DEF:test_delete_validation_report_returns_false_for_unknown_record:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify delete returns False when validation record does not exist.\ndef test_delete_validation_report_returns_false_for_unknown_record():\n db = MagicMock()\n db.query.return_value.filter.return_value.first.return_value = None\n\n service = HealthService(db, config_manager=MagicMock())\n\n assert service.delete_validation_report(\"missing\") is False\n\n\n# [/DEF:test_delete_validation_report_returns_false_for_unknown_record:Function]\n\n\n# [DEF:test_delete_validation_report_swallows_linked_task_cleanup_failure:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify delete swallows exceptions when cleaning up linked tasks.\ndef test_delete_validation_report_swallows_linked_task_cleanup_failure():\n db = MagicMock()\n config_manager = MagicMock()\n task_manager = MagicMock()\n task_manager.tasks = {\"task-1\": object()}\n\n record = ValidationRecord(\n id=\"rec-1\",\n task_id=\"task-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n screenshot_path=None,\n )\n\n # @RISK: db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test.\n first_query = MagicMock()\n first_query.first.return_value = record\n\n peer_query = MagicMock()\n peer_query.filter.return_value = peer_query\n peer_query.all.return_value = [record]\n\n db.query.side_effect = [first_query, peer_query]\n\n with (\n patch(\"src.services.health_service.TaskCleanupService\") as cleanup_cls,\n patch(\"src.services.health_service.logger\") as mock_logger,\n ):\n cleanup_instance = MagicMock()\n cleanup_instance.delete_task_with_logs.side_effect = RuntimeError(\n \"cleanup exploded\"\n )\n cleanup_cls.return_value = cleanup_instance\n\n service = HealthService(db, config_manager=config_manager)\n deleted = service.delete_validation_report(\"rec-1\", task_manager=task_manager)\n\n assert deleted is True\n db.delete.assert_called_once_with(record)\n db.commit.assert_called_once()\n cleanup_instance.delete_task_with_logs.assert_called_once_with(\"task-1\")\n mock_logger.warning.assert_called_once()\n assert \"task-1\" not in task_manager.tasks\n\n\n# [/DEF:test_delete_validation_report_swallows_linked_task_cleanup_failure:Function]\n# [/DEF:test_health_service:Module]\n" - }, - { - "contract_id": "test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_health_service.py", - "start_line": 165, - "end_line": 238, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify that deleting a validation report also removes dashboard scope and linked tasks.", - "RISK": "db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test." - }, - "relations": [ - { - "source_id": "test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks", - "relation_type": "BINDS_TO", - "target_id": "test_health_service", - "target_ref": "[test_health_service]" - } - ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "RISK", - "message": "@RISK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify that deleting a validation report also removes dashboard scope and linked tasks.\ndef test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks():\n db = MagicMock()\n config_manager = MagicMock()\n task_manager = MagicMock()\n task_manager.tasks = {\"task-1\": object(), \"task-2\": object(), \"task-3\": object()}\n\n target_record = ValidationRecord(\n id=\"rec-1\",\n task_id=\"task-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n screenshot_path=None,\n )\n older_peer = ValidationRecord(\n id=\"rec-2\",\n task_id=\"task-2\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"FAIL\",\n timestamp=datetime.utcnow() - timedelta(hours=1),\n summary=\"Older\",\n issues=[],\n screenshot_path=None,\n )\n other_environment = ValidationRecord(\n id=\"rec-3\",\n task_id=\"task-3\",\n dashboard_id=\"42\",\n environment_id=\"env_2\",\n status=\"WARN\",\n timestamp=datetime.utcnow(),\n summary=\"Other environment\",\n issues=[],\n screenshot_path=None,\n )\n\n # @RISK: db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test.\n first_query = MagicMock()\n first_query.first.return_value = target_record\n\n peer_query = MagicMock()\n peer_query.filter.return_value = peer_query\n peer_query.all.return_value = [target_record, older_peer]\n\n db.query.side_effect = [first_query, peer_query]\n\n with patch(\"src.services.health_service.TaskCleanupService\") as cleanup_cls:\n cleanup_instance = MagicMock()\n cleanup_cls.return_value = cleanup_instance\n\n service = HealthService(db, config_manager=config_manager)\n deleted = service.delete_validation_report(\"rec-1\", task_manager=task_manager)\n\n assert deleted is True\n assert db.delete.call_count == 2\n db.delete.assert_any_call(target_record)\n db.delete.assert_any_call(older_peer)\n db.commit.assert_called_once()\n cleanup_instance.delete_task_with_logs.assert_any_call(\"task-1\")\n cleanup_instance.delete_task_with_logs.assert_any_call(\"task-2\")\n assert cleanup_instance.delete_task_with_logs.call_count == 2\n assert \"task-1\" not in task_manager.tasks\n assert \"task-2\" not in task_manager.tasks\n assert \"task-3\" in task_manager.tasks\n\n\n# [/DEF:test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks:Function]\n" - }, - { - "contract_id": "test_delete_validation_report_returns_false_for_unknown_record", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_health_service.py", - "start_line": 241, - "end_line": 253, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify delete returns False when validation record does not exist." - }, - "relations": [ - { - "source_id": "test_delete_validation_report_returns_false_for_unknown_record", - "relation_type": "BINDS_TO", - "target_id": "test_health_service", - "target_ref": "[test_health_service]" - } - ], - "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": "def", - "body": "# [DEF:test_delete_validation_report_returns_false_for_unknown_record:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify delete returns False when validation record does not exist.\ndef test_delete_validation_report_returns_false_for_unknown_record():\n db = MagicMock()\n db.query.return_value.filter.return_value.first.return_value = None\n\n service = HealthService(db, config_manager=MagicMock())\n\n assert service.delete_validation_report(\"missing\") is False\n\n\n# [/DEF:test_delete_validation_report_returns_false_for_unknown_record:Function]\n" - }, - { - "contract_id": "test_delete_validation_report_swallows_linked_task_cleanup_failure", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_health_service.py", - "start_line": 256, - "end_line": 308, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify delete swallows exceptions when cleaning up linked tasks.", - "RISK": "db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test." - }, - "relations": [ - { - "source_id": "test_delete_validation_report_swallows_linked_task_cleanup_failure", - "relation_type": "BINDS_TO", - "target_id": "test_health_service", - "target_ref": "[test_health_service]" - } - ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "RISK", - "message": "@RISK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_delete_validation_report_swallows_linked_task_cleanup_failure:Function]\n# @RELATION: BINDS_TO ->[test_health_service]\n# @PURPOSE: Verify delete swallows exceptions when cleaning up linked tasks.\ndef test_delete_validation_report_swallows_linked_task_cleanup_failure():\n db = MagicMock()\n config_manager = MagicMock()\n task_manager = MagicMock()\n task_manager.tasks = {\"task-1\": object()}\n\n record = ValidationRecord(\n id=\"rec-1\",\n task_id=\"task-1\",\n dashboard_id=\"42\",\n environment_id=\"env_1\",\n status=\"PASS\",\n timestamp=datetime.utcnow(),\n summary=\"Healthy\",\n issues=[],\n screenshot_path=None,\n )\n\n # @RISK: db.query side_effect chain may not propagate through .filter().first() — verify mock chain setup is correct for this test.\n first_query = MagicMock()\n first_query.first.return_value = record\n\n peer_query = MagicMock()\n peer_query.filter.return_value = peer_query\n peer_query.all.return_value = [record]\n\n db.query.side_effect = [first_query, peer_query]\n\n with (\n patch(\"src.services.health_service.TaskCleanupService\") as cleanup_cls,\n patch(\"src.services.health_service.logger\") as mock_logger,\n ):\n cleanup_instance = MagicMock()\n cleanup_instance.delete_task_with_logs.side_effect = RuntimeError(\n \"cleanup exploded\"\n )\n cleanup_cls.return_value = cleanup_instance\n\n service = HealthService(db, config_manager=config_manager)\n deleted = service.delete_validation_report(\"rec-1\", task_manager=task_manager)\n\n assert deleted is True\n db.delete.assert_called_once_with(record)\n db.commit.assert_called_once()\n cleanup_instance.delete_task_with_logs.assert_called_once_with(\"task-1\")\n mock_logger.warning.assert_called_once()\n assert \"task-1\" not in task_manager.tasks\n\n\n# [/DEF:test_delete_validation_report_swallows_linked_task_cleanup_failure:Function]\n" - }, - { - "contract_id": "test_llm_plugin_persistence", - "contract_type": "Module", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 1, - "end_line": 221, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Regression test for ValidationRecord persistence fields populated from task context." - }, - "relations": [ - { - "source_id": "test_llm_plugin_persistence", - "relation_type": "VERIFIES", - "target_id": "DashboardValidationPlugin:Class", - "target_ref": "[DashboardValidationPlugin:Class]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_llm_plugin_persistence:Module]\n# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context.\n\nimport types\nimport pytest\n\nfrom src.plugins.llm_analysis import plugin as plugin_module\n\n\n# [DEF:_DummyLogger:Class]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests.\n# @INVARIANT: Logging methods are no-ops and must not mutate test state.\nclass _DummyLogger:\n def with_source(self, _source: str):\n return self\n\n def info(self, *_args, **_kwargs):\n return None\n\n def debug(self, *_args, **_kwargs):\n return None\n\n def warning(self, *_args, **_kwargs):\n return None\n\n def error(self, *_args, **_kwargs):\n return None\n\n\n# [/DEF:_DummyLogger:Class]\n\n\n# [DEF:_FakeDBSession:Class]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin.\n# @INVARIANT: add/commit/close provide only persistence signals asserted by this test.\nclass _FakeDBSession:\n def __init__(self):\n self.added = None\n self.committed = False\n self.closed = False\n\n def add(self, obj):\n self.added = obj\n\n def commit(self):\n self.committed = True\n\n def close(self):\n self.closed = True\n\n\n# [/DEF:_FakeDBSession:Class]\n\n\n# [DEF:test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id.\n# @INVARIANT: Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.\n@pytest.mark.asyncio\nasync def test_dashboard_validation_plugin_persists_task_and_environment_ids(\n tmp_path, monkeypatch\n):\n fake_db = _FakeDBSession()\n\n env = types.SimpleNamespace(id=\"env-42\")\n provider = types.SimpleNamespace(\n id=\"provider-1\",\n name=\"Main LLM\",\n provider_type=\"openai\",\n base_url=\"https://example.invalid/v1\",\n default_model=\"gpt-4o\",\n is_active=True,\n )\n\n # [DEF:_FakeProviderService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests.\n # @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic.\n class _FakeProviderService:\n def __init__(self, _db):\n return None\n\n def get_provider(self, _provider_id):\n return provider\n\n def get_decrypted_api_key(self, _provider_id):\n return \"a\" * 32\n\n # [/DEF:_FakeProviderService:Class]\n\n # [DEF:_FakeScreenshotService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects.\n # @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.\n class _FakeScreenshotService:\n def __init__(self, _env):\n return None\n\n async def capture_dashboard(self, _dashboard_id, _screenshot_path):\n return None\n\n # [/DEF:_FakeScreenshotService:Class]\n\n # [DEF:_FakeLLMClient:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions.\n # @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result.\n class _FakeLLMClient:\n \"\"\"Fake LLM client for persistence tests.\n\n Always returns PASS status. FAIL and UNKNOWN persistence branches are NOT\n covered by this fake — see coverage-planner findings.\n \"\"\"\n\n def __init__(self, **_kwargs):\n return None\n\n async def analyze_dashboard(self, *_args, **_kwargs):\n return {\n \"status\": \"PASS\",\n \"summary\": \"Dashboard healthy\",\n \"issues\": [],\n }\n\n # [/DEF:_FakeLLMClient:Class]\n\n # [DEF:_FakeNotificationService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects.\n # @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.\n class _FakeNotificationService:\n def __init__(self, *_args, **_kwargs):\n return None\n\n async def dispatch_report(self, **_kwargs):\n return None\n\n # [/DEF:_FakeNotificationService:Class]\n\n # [DEF:_FakeConfigManager:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path.\n # @INVARIANT: Only storage.root_path and llm fields are safe to access; all other settings fields are absent.\n class _FakeConfigManager:\n def get_environment(self, _env_id):\n return env\n\n def get_config(self):\n return types.SimpleNamespace(\n settings=types.SimpleNamespace(\n storage=types.SimpleNamespace(root_path=str(tmp_path)),\n llm={},\n )\n )\n\n # [/DEF:_FakeConfigManager:Class]\n\n # [DEF:_FakeSupersetClient:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list.\n # @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.\n class _FakeSupersetClient:\n def __init__(self, _env):\n self.network = types.SimpleNamespace(\n request=lambda **_kwargs: {\"result\": []}\n )\n\n # [/DEF:_FakeSupersetClient:Class]\n\n monkeypatch.setattr(plugin_module, \"SessionLocal\", lambda: fake_db)\n monkeypatch.setattr(plugin_module, \"LLMProviderService\", _FakeProviderService)\n monkeypatch.setattr(plugin_module, \"ScreenshotService\", _FakeScreenshotService)\n monkeypatch.setattr(plugin_module, \"LLMClient\", _FakeLLMClient)\n monkeypatch.setattr(plugin_module, \"NotificationService\", _FakeNotificationService)\n monkeypatch.setattr(plugin_module, \"SupersetClient\", _FakeSupersetClient)\n monkeypatch.setattr(\n \"src.dependencies.get_config_manager\", lambda: _FakeConfigManager()\n )\n\n context = types.SimpleNamespace(\n task_id=\"task-999\",\n logger=_DummyLogger(),\n background_tasks=None,\n )\n\n plugin = plugin_module.DashboardValidationPlugin()\n result = await plugin.execute(\n {\n \"dashboard_id\": \"11\",\n \"environment_id\": \"env-42\",\n \"provider_id\": \"provider-1\",\n },\n context=context,\n )\n\n assert result[\"environment_id\"] == \"env-42\"\n assert fake_db.committed is True\n assert fake_db.closed is True\n assert fake_db.added is not None\n assert fake_db.added.task_id == \"task-999\"\n assert fake_db.added.environment_id == \"env-42\"\n\n\n# [/DEF:test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n\n\n# [/DEF:test_llm_plugin_persistence:Module]\n" - }, - { - "contract_id": "_DummyLogger", - "contract_type": "Class", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 12, - "end_line": 34, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "Logging methods are no-ops and must not mutate test state.", - "PURPOSE": "Minimal logger shim for TaskContext-like objects used in tests." - }, - "relations": [ - { - "source_id": "_DummyLogger", - "relation_type": "BINDS_TO", - "target_id": "test_llm_plugin_persistence:Module", - "target_ref": "[test_llm_plugin_persistence:Module]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_DummyLogger:Class]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests.\n# @INVARIANT: Logging methods are no-ops and must not mutate test state.\nclass _DummyLogger:\n def with_source(self, _source: str):\n return self\n\n def info(self, *_args, **_kwargs):\n return None\n\n def debug(self, *_args, **_kwargs):\n return None\n\n def warning(self, *_args, **_kwargs):\n return None\n\n def error(self, *_args, **_kwargs):\n return None\n\n\n# [/DEF:_DummyLogger:Class]\n" - }, - { - "contract_id": "_FakeDBSession", - "contract_type": "Class", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 37, - "end_line": 58, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "INVARIANT": "add/commit/close provide only persistence signals asserted by this test.", - "PURPOSE": "Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin." - }, - "relations": [ - { - "source_id": "_FakeDBSession", - "relation_type": "BINDS_TO", - "target_id": "test_llm_plugin_persistence:Module", - "target_ref": "[test_llm_plugin_persistence:Module]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_FakeDBSession:Class]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin.\n# @INVARIANT: add/commit/close provide only persistence signals asserted by this test.\nclass _FakeDBSession:\n def __init__(self):\n self.added = None\n self.committed = False\n self.closed = False\n\n def add(self, obj):\n self.added = obj\n\n def commit(self):\n self.committed = True\n\n def close(self):\n self.closed = True\n\n\n# [/DEF:_FakeDBSession:Class]\n" - }, - { - "contract_id": "test_dashboard_validation_plugin_persists_task_and_environment_ids", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 61, - "end_line": 218, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "INVARIANT": "Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.", - "PURPOSE": "Ensure db ValidationRecord includes context.task_id and params.environment_id." - }, - "relations": [ - { - "source_id": "test_dashboard_validation_plugin_persists_task_and_environment_ids", - "relation_type": "BINDS_TO", - "target_id": "test_llm_plugin_persistence:Module", - "target_ref": "[test_llm_plugin_persistence:Module]" - }, - { - "source_id": "test_dashboard_validation_plugin_persists_task_and_environment_ids", - "relation_type": "VERIFIES", - "target_id": "DashboardValidationPlugin:Class", - "target_ref": "[DashboardValidationPlugin:Class]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT 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": "def", - "body": "# [DEF:test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]\n# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id.\n# @INVARIANT: Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.\n@pytest.mark.asyncio\nasync def test_dashboard_validation_plugin_persists_task_and_environment_ids(\n tmp_path, monkeypatch\n):\n fake_db = _FakeDBSession()\n\n env = types.SimpleNamespace(id=\"env-42\")\n provider = types.SimpleNamespace(\n id=\"provider-1\",\n name=\"Main LLM\",\n provider_type=\"openai\",\n base_url=\"https://example.invalid/v1\",\n default_model=\"gpt-4o\",\n is_active=True,\n )\n\n # [DEF:_FakeProviderService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests.\n # @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic.\n class _FakeProviderService:\n def __init__(self, _db):\n return None\n\n def get_provider(self, _provider_id):\n return provider\n\n def get_decrypted_api_key(self, _provider_id):\n return \"a\" * 32\n\n # [/DEF:_FakeProviderService:Class]\n\n # [DEF:_FakeScreenshotService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects.\n # @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.\n class _FakeScreenshotService:\n def __init__(self, _env):\n return None\n\n async def capture_dashboard(self, _dashboard_id, _screenshot_path):\n return None\n\n # [/DEF:_FakeScreenshotService:Class]\n\n # [DEF:_FakeLLMClient:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions.\n # @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result.\n class _FakeLLMClient:\n \"\"\"Fake LLM client for persistence tests.\n\n Always returns PASS status. FAIL and UNKNOWN persistence branches are NOT\n covered by this fake — see coverage-planner findings.\n \"\"\"\n\n def __init__(self, **_kwargs):\n return None\n\n async def analyze_dashboard(self, *_args, **_kwargs):\n return {\n \"status\": \"PASS\",\n \"summary\": \"Dashboard healthy\",\n \"issues\": [],\n }\n\n # [/DEF:_FakeLLMClient:Class]\n\n # [DEF:_FakeNotificationService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects.\n # @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.\n class _FakeNotificationService:\n def __init__(self, *_args, **_kwargs):\n return None\n\n async def dispatch_report(self, **_kwargs):\n return None\n\n # [/DEF:_FakeNotificationService:Class]\n\n # [DEF:_FakeConfigManager:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path.\n # @INVARIANT: Only storage.root_path and llm fields are safe to access; all other settings fields are absent.\n class _FakeConfigManager:\n def get_environment(self, _env_id):\n return env\n\n def get_config(self):\n return types.SimpleNamespace(\n settings=types.SimpleNamespace(\n storage=types.SimpleNamespace(root_path=str(tmp_path)),\n llm={},\n )\n )\n\n # [/DEF:_FakeConfigManager:Class]\n\n # [DEF:_FakeSupersetClient:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list.\n # @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.\n class _FakeSupersetClient:\n def __init__(self, _env):\n self.network = types.SimpleNamespace(\n request=lambda **_kwargs: {\"result\": []}\n )\n\n # [/DEF:_FakeSupersetClient:Class]\n\n monkeypatch.setattr(plugin_module, \"SessionLocal\", lambda: fake_db)\n monkeypatch.setattr(plugin_module, \"LLMProviderService\", _FakeProviderService)\n monkeypatch.setattr(plugin_module, \"ScreenshotService\", _FakeScreenshotService)\n monkeypatch.setattr(plugin_module, \"LLMClient\", _FakeLLMClient)\n monkeypatch.setattr(plugin_module, \"NotificationService\", _FakeNotificationService)\n monkeypatch.setattr(plugin_module, \"SupersetClient\", _FakeSupersetClient)\n monkeypatch.setattr(\n \"src.dependencies.get_config_manager\", lambda: _FakeConfigManager()\n )\n\n context = types.SimpleNamespace(\n task_id=\"task-999\",\n logger=_DummyLogger(),\n background_tasks=None,\n )\n\n plugin = plugin_module.DashboardValidationPlugin()\n result = await plugin.execute(\n {\n \"dashboard_id\": \"11\",\n \"environment_id\": \"env-42\",\n \"provider_id\": \"provider-1\",\n },\n context=context,\n )\n\n assert result[\"environment_id\"] == \"env-42\"\n assert fake_db.committed is True\n assert fake_db.closed is True\n assert fake_db.added is not None\n assert fake_db.added.task_id == \"task-999\"\n assert fake_db.added.environment_id == \"env-42\"\n\n\n# [/DEF:test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n" - }, - { - "contract_id": "_FakeProviderService", - "contract_type": "Class", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 83, - "end_line": 98, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "Returns same provider and key regardless of provider_id argument; no lookup logic.", - "PURPOSE": "LLM provider service stub returning deterministic provider and decrypted API key for plugin tests." - }, - "relations": [ - { - "source_id": "_FakeProviderService", - "relation_type": "BINDS_TO", - "target_id": "test_dashboard_validation_plugin_persists_task_and_environment_ids:Function", - "target_ref": "[test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_FakeProviderService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests.\n # @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic.\n class _FakeProviderService:\n def __init__(self, _db):\n return None\n\n def get_provider(self, _provider_id):\n return provider\n\n def get_decrypted_api_key(self, _provider_id):\n return \"a\" * 32\n\n # [/DEF:_FakeProviderService:Class]\n" - }, - { - "contract_id": "_FakeScreenshotService", - "contract_type": "Class", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 100, - "end_line": 112, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.", - "PURPOSE": "Screenshot service stub that accepts capture_dashboard calls without side effects." - }, - "relations": [ - { - "source_id": "_FakeScreenshotService", - "relation_type": "BINDS_TO", - "target_id": "test_dashboard_validation_plugin_persists_task_and_environment_ids:Function", - "target_ref": "[test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_FakeScreenshotService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects.\n # @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.\n class _FakeScreenshotService:\n def __init__(self, _env):\n return None\n\n async def capture_dashboard(self, _dashboard_id, _screenshot_path):\n return None\n\n # [/DEF:_FakeScreenshotService:Class]\n" - }, - { - "contract_id": "_FakeLLMClient", - "contract_type": "Class", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 114, - "end_line": 136, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "INVARIANT": "analyze_dashboard is side-effect free and returns schema-compatible PASS result.", - "PURPOSE": "Deterministic LLM client double returning canonical analysis payload for persistence-path assertions." - }, - "relations": [ - { - "source_id": "_FakeLLMClient", - "relation_type": "BINDS_TO", - "target_id": "test_dashboard_validation_plugin_persists_task_and_environment_ids:Function", - "target_ref": "[test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_FakeLLMClient:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions.\n # @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result.\n class _FakeLLMClient:\n \"\"\"Fake LLM client for persistence tests.\n\n Always returns PASS status. FAIL and UNKNOWN persistence branches are NOT\n covered by this fake — see coverage-planner findings.\n \"\"\"\n\n def __init__(self, **_kwargs):\n return None\n\n async def analyze_dashboard(self, *_args, **_kwargs):\n return {\n \"status\": \"PASS\",\n \"summary\": \"Dashboard healthy\",\n \"issues\": [],\n }\n\n # [/DEF:_FakeLLMClient:Class]\n" - }, - { - "contract_id": "_FakeNotificationService", - "contract_type": "Class", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 138, - "end_line": 150, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.", - "PURPOSE": "Notification service stub that accepts plugin dispatch_report payload without introducing side effects." - }, - "relations": [ - { - "source_id": "_FakeNotificationService", - "relation_type": "BINDS_TO", - "target_id": "test_dashboard_validation_plugin_persists_task_and_environment_ids:Function", - "target_ref": "[test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_FakeNotificationService:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects.\n # @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.\n class _FakeNotificationService:\n def __init__(self, *_args, **_kwargs):\n return None\n\n async def dispatch_report(self, **_kwargs):\n return None\n\n # [/DEF:_FakeNotificationService:Class]\n" - }, - { - "contract_id": "_FakeSupersetClient", - "contract_type": "Class", - "file_path": "backend/src/services/__tests__/test_llm_plugin_persistence.py", - "start_line": 171, - "end_line": 182, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.", - "PURPOSE": "Superset client stub exposing network.request as a lambda that returns empty result list." - }, - "relations": [ - { - "source_id": "_FakeSupersetClient", - "relation_type": "BINDS_TO", - "target_id": "test_dashboard_validation_plugin_persists_task_and_environment_ids:Function", - "target_ref": "[test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Class" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_FakeSupersetClient:Class]\n # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list.\n # @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.\n class _FakeSupersetClient:\n def __init__(self, _env):\n self.network = types.SimpleNamespace(\n request=lambda **_kwargs: {\"result\": []}\n )\n\n # [/DEF:_FakeSupersetClient:Class]\n" - }, - { - "contract_id": "test_llm_prompt_templates", - "contract_type": "Module", - "file_path": "backend/src/services/__tests__/test_llm_prompt_templates.py", - "start_line": 1, - "end_line": 126, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "All required prompt keys remain available after normalization.", - "LAYER": "Domain Tests", - "PURPOSE": "Validate normalization and rendering behavior for configurable LLM prompt templates.", - "SEMANTICS": [ - "tests", - "llm", - "prompts", - "templates", - "settings" - ] - }, - "relations": [ - { - "source_id": "test_llm_prompt_templates", - "relation_type": "DEPENDS_ON", - "target_id": "llm_prompt_templates", - "target_ref": "[llm_prompt_templates]" - } - ], - "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" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_llm_prompt_templates:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, llm, prompts, templates, settings\n# @PURPOSE: Validate normalization and rendering behavior for configurable LLM prompt templates.\n# @LAYER: Domain Tests\n# @RELATION: DEPENDS_ON -> [llm_prompt_templates]\n# @INVARIANT: All required prompt keys remain available after normalization.\n\nfrom src.services.llm_prompt_templates import (\n DEFAULT_LLM_ASSISTANT_SETTINGS,\n DEFAULT_LLM_PROVIDER_BINDINGS,\n DEFAULT_LLM_PROMPTS,\n is_multimodal_model,\n normalize_llm_settings,\n resolve_bound_provider_id,\n render_prompt,\n)\n\n\n# [DEF:test_normalize_llm_settings_adds_default_prompts:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 3\n# @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults.\n# @PRE: Input llm settings do not contain complete prompts object.\n# @POST: Returned structure includes required prompt templates with fallback defaults.\ndef test_normalize_llm_settings_adds_default_prompts():\n normalized = normalize_llm_settings({\"default_provider\": \"x\"})\n\n assert \"prompts\" in normalized\n assert \"provider_bindings\" in normalized\n assert normalized[\"default_provider\"] == \"x\"\n for key in DEFAULT_LLM_PROMPTS:\n assert key in normalized[\"prompts\"]\n assert isinstance(normalized[\"prompts\"][key], str)\n for key in DEFAULT_LLM_PROVIDER_BINDINGS:\n assert key in normalized[\"provider_bindings\"]\n for key in DEFAULT_LLM_ASSISTANT_SETTINGS:\n assert key in normalized\n\n\n# [/DEF:test_normalize_llm_settings_adds_default_prompts:Function]\n\n\n# [DEF:test_normalize_llm_settings_keeps_custom_prompt_values:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 3\n# @PURPOSE: Ensure user-customized prompt values are preserved during normalization.\n# @PRE: Input llm settings contain custom prompt override.\n# @POST: Custom prompt value remains unchanged in normalized output.\ndef test_normalize_llm_settings_keeps_custom_prompt_values():\n custom = \"Doc for {dataset_name} using {columns_json}\"\n normalized = normalize_llm_settings({\"prompts\": {\"documentation_prompt\": custom}})\n\n assert normalized[\"prompts\"][\"documentation_prompt\"] == custom\n\n\n# [/DEF:test_normalize_llm_settings_keeps_custom_prompt_values:Function]\n\n\n# [DEF:test_render_prompt_replaces_known_placeholders:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 3\n# @PURPOSE: Ensure template placeholders are deterministically replaced.\n# @PRE: Template contains placeholders matching provided variables.\n# @POST: Rendered prompt string contains substituted values.\ndef test_render_prompt_replaces_known_placeholders():\n rendered = render_prompt(\n \"Hello {name}, diff={diff}\",\n {\"name\": \"bot\", \"diff\": \"A->B\"},\n )\n\n assert rendered == \"Hello bot, diff=A->B\"\n\n\n# [/DEF:test_render_prompt_replaces_known_placeholders:Function]\n\n\n# [DEF:test_is_multimodal_model_detects_known_vision_models:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 2\n# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names.\ndef test_is_multimodal_model_detects_known_vision_models():\n assert is_multimodal_model(\"gpt-4o\") is True\n assert is_multimodal_model(\"claude-3-5-sonnet\") is True\n assert is_multimodal_model(\"stepfun/step-3.5-flash:free\", \"openrouter\") is False\n assert is_multimodal_model(\"text-only-model\") is False\n\n\n# [/DEF:test_is_multimodal_model_detects_known_vision_models:Function]\n\n\n# [DEF:test_resolve_bound_provider_id_prefers_binding_then_default:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 2\n# @PURPOSE: Verify provider binding resolution priority.\ndef test_resolve_bound_provider_id_prefers_binding_then_default():\n settings = {\n \"default_provider\": \"default-1\",\n \"provider_bindings\": {\"dashboard_validation\": \"vision-1\"},\n }\n assert resolve_bound_provider_id(settings, \"dashboard_validation\") == \"vision-1\"\n assert resolve_bound_provider_id(settings, \"documentation\") == \"default-1\"\n\n\n# [/DEF:test_resolve_bound_provider_id_prefers_binding_then_default:Function]\n\n\n# [DEF:test_normalize_llm_settings_keeps_assistant_planner_settings:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 2\n# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized.\ndef test_normalize_llm_settings_keeps_assistant_planner_settings():\n normalized = normalize_llm_settings(\n {\n \"assistant_planner_provider\": \"provider-a\",\n \"assistant_planner_model\": \"gpt-4.1-mini\",\n }\n )\n assert normalized[\"assistant_planner_provider\"] == \"provider-a\"\n assert normalized[\"assistant_planner_model\"] == \"gpt-4.1-mini\"\n\n\n# [/DEF:test_normalize_llm_settings_keeps_assistant_planner_settings:Function]\n\n\n# [/DEF:test_llm_prompt_templates:Module]\n" - }, - { - "contract_id": "test_normalize_llm_settings_adds_default_prompts", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_prompt_templates.py", - "start_line": 20, - "end_line": 41, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returned structure includes required prompt templates with fallback defaults.", - "PRE": "Input llm settings do not contain complete prompts object.", - "PURPOSE": "Ensure legacy/partial llm settings are expanded with all prompt defaults." - }, - "relations": [ - { - "source_id": "test_normalize_llm_settings_adds_default_prompts", - "relation_type": "BINDS_TO", - "target_id": "test_llm_prompt_templates", - "target_ref": "test_llm_prompt_templates" - } - ], - "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": "def", - "body": "# [DEF:test_normalize_llm_settings_adds_default_prompts:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 3\n# @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults.\n# @PRE: Input llm settings do not contain complete prompts object.\n# @POST: Returned structure includes required prompt templates with fallback defaults.\ndef test_normalize_llm_settings_adds_default_prompts():\n normalized = normalize_llm_settings({\"default_provider\": \"x\"})\n\n assert \"prompts\" in normalized\n assert \"provider_bindings\" in normalized\n assert normalized[\"default_provider\"] == \"x\"\n for key in DEFAULT_LLM_PROMPTS:\n assert key in normalized[\"prompts\"]\n assert isinstance(normalized[\"prompts\"][key], str)\n for key in DEFAULT_LLM_PROVIDER_BINDINGS:\n assert key in normalized[\"provider_bindings\"]\n for key in DEFAULT_LLM_ASSISTANT_SETTINGS:\n assert key in normalized\n\n\n# [/DEF:test_normalize_llm_settings_adds_default_prompts:Function]\n" - }, - { - "contract_id": "test_normalize_llm_settings_keeps_custom_prompt_values", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_prompt_templates.py", - "start_line": 44, - "end_line": 57, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Custom prompt value remains unchanged in normalized output.", - "PRE": "Input llm settings contain custom prompt override.", - "PURPOSE": "Ensure user-customized prompt values are preserved during normalization." - }, - "relations": [ - { - "source_id": "test_normalize_llm_settings_keeps_custom_prompt_values", - "relation_type": "BINDS_TO", - "target_id": "test_llm_prompt_templates", - "target_ref": "test_llm_prompt_templates" - } - ], - "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": "def", - "body": "# [DEF:test_normalize_llm_settings_keeps_custom_prompt_values:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 3\n# @PURPOSE: Ensure user-customized prompt values are preserved during normalization.\n# @PRE: Input llm settings contain custom prompt override.\n# @POST: Custom prompt value remains unchanged in normalized output.\ndef test_normalize_llm_settings_keeps_custom_prompt_values():\n custom = \"Doc for {dataset_name} using {columns_json}\"\n normalized = normalize_llm_settings({\"prompts\": {\"documentation_prompt\": custom}})\n\n assert normalized[\"prompts\"][\"documentation_prompt\"] == custom\n\n\n# [/DEF:test_normalize_llm_settings_keeps_custom_prompt_values:Function]\n" - }, - { - "contract_id": "test_render_prompt_replaces_known_placeholders", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_prompt_templates.py", - "start_line": 60, - "end_line": 75, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Rendered prompt string contains substituted values.", - "PRE": "Template contains placeholders matching provided variables.", - "PURPOSE": "Ensure template placeholders are deterministically replaced." - }, - "relations": [ - { - "source_id": "test_render_prompt_replaces_known_placeholders", - "relation_type": "BINDS_TO", - "target_id": "test_llm_prompt_templates", - "target_ref": "test_llm_prompt_templates" - } - ], - "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": "def", - "body": "# [DEF:test_render_prompt_replaces_known_placeholders:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 3\n# @PURPOSE: Ensure template placeholders are deterministically replaced.\n# @PRE: Template contains placeholders matching provided variables.\n# @POST: Rendered prompt string contains substituted values.\ndef test_render_prompt_replaces_known_placeholders():\n rendered = render_prompt(\n \"Hello {name}, diff={diff}\",\n {\"name\": \"bot\", \"diff\": \"A->B\"},\n )\n\n assert rendered == \"Hello bot, diff=A->B\"\n\n\n# [/DEF:test_render_prompt_replaces_known_placeholders:Function]\n" - }, - { - "contract_id": "test_is_multimodal_model_detects_known_vision_models", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_prompt_templates.py", - "start_line": 78, - "end_line": 89, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Ensure multimodal model detection recognizes common vision-capable model names." - }, - "relations": [ - { - "source_id": "test_is_multimodal_model_detects_known_vision_models", - "relation_type": "BINDS_TO", - "target_id": "test_llm_prompt_templates", - "target_ref": "test_llm_prompt_templates" - } - ], - "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": "def", - "body": "# [DEF:test_is_multimodal_model_detects_known_vision_models:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 2\n# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names.\ndef test_is_multimodal_model_detects_known_vision_models():\n assert is_multimodal_model(\"gpt-4o\") is True\n assert is_multimodal_model(\"claude-3-5-sonnet\") is True\n assert is_multimodal_model(\"stepfun/step-3.5-flash:free\", \"openrouter\") is False\n assert is_multimodal_model(\"text-only-model\") is False\n\n\n# [/DEF:test_is_multimodal_model_detects_known_vision_models:Function]\n" - }, - { - "contract_id": "test_resolve_bound_provider_id_prefers_binding_then_default", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_prompt_templates.py", - "start_line": 92, - "end_line": 105, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Verify provider binding resolution priority." - }, - "relations": [ - { - "source_id": "test_resolve_bound_provider_id_prefers_binding_then_default", - "relation_type": "BINDS_TO", - "target_id": "test_llm_prompt_templates", - "target_ref": "test_llm_prompt_templates" - } - ], - "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": "def", - "body": "# [DEF:test_resolve_bound_provider_id_prefers_binding_then_default:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 2\n# @PURPOSE: Verify provider binding resolution priority.\ndef test_resolve_bound_provider_id_prefers_binding_then_default():\n settings = {\n \"default_provider\": \"default-1\",\n \"provider_bindings\": {\"dashboard_validation\": \"vision-1\"},\n }\n assert resolve_bound_provider_id(settings, \"dashboard_validation\") == \"vision-1\"\n assert resolve_bound_provider_id(settings, \"documentation\") == \"default-1\"\n\n\n# [/DEF:test_resolve_bound_provider_id_prefers_binding_then_default:Function]\n" - }, - { - "contract_id": "test_normalize_llm_settings_keeps_assistant_planner_settings", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_prompt_templates.py", - "start_line": 108, - "end_line": 123, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Ensure assistant planner provider/model fields are preserved and normalized." - }, - "relations": [ - { - "source_id": "test_normalize_llm_settings_keeps_assistant_planner_settings", - "relation_type": "BINDS_TO", - "target_id": "test_llm_prompt_templates", - "target_ref": "test_llm_prompt_templates" - } - ], - "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": "def", - "body": "# [DEF:test_normalize_llm_settings_keeps_assistant_planner_settings:Function]\n# @RELATION: BINDS_TO -> test_llm_prompt_templates\n# @COMPLEXITY: 2\n# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized.\ndef test_normalize_llm_settings_keeps_assistant_planner_settings():\n normalized = normalize_llm_settings(\n {\n \"assistant_planner_provider\": \"provider-a\",\n \"assistant_planner_model\": \"gpt-4.1-mini\",\n }\n )\n assert normalized[\"assistant_planner_provider\"] == \"provider-a\"\n assert normalized[\"assistant_planner_model\"] == \"gpt-4.1-mini\"\n\n\n# [/DEF:test_normalize_llm_settings_keeps_assistant_planner_settings:Function]\n" - }, - { - "contract_id": "test_llm_provider", - "contract_type": "Module", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 1, - "end_line": 6, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Contract testing for LLMProviderService and EncryptionManager", - "SEMANTICS": [ - "tests", - "llm-provider", - "encryption", - "contract" - ] - }, - "relations": [ - { - "source_id": "test_llm_provider", - "relation_type": "VERIFIES", - "target_id": "src.services.llm_provider:Module", - "target_ref": "[src.services.llm_provider:Module]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:test_llm_provider:Module]\n# @RELATION: VERIFIES -> [src.services.llm_provider:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, llm-provider, encryption, contract\n# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager\n# [/DEF:test_llm_provider:Module]\n" - }, - { - "contract_id": "_test_encryption_key_fixture", - "contract_type": "Global", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 17, - "end_line": 21, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Ensure encryption-dependent provider tests run with a valid Fernet key." - }, - "relations": [ - { - "source_id": "_test_encryption_key_fixture", - "relation_type": "DEPENDS_ON", - "target_id": "pytest:Module", - "target_ref": "[pytest:Module]" - } - ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Global'", - "detail": { - "actual_type": "Global", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Global' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Global" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Global' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Global" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_test_encryption_key_fixture:Global]\n# @PURPOSE: Ensure encryption-dependent provider tests run with a valid Fernet key.\n# @RELATION: DEPENDS_ON -> [pytest:Module]\nos.environ.setdefault(\"ENCRYPTION_KEY\", Fernet.generate_key().decode())\n# [/DEF:_test_encryption_key_fixture:Global]\n" - }, - { - "contract_id": "test_encryption_cycle", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 26, - "end_line": 39, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify EncryptionManager round-trip encryption/decryption invariant for non-empty secrets.", - "TEST_EDGE": "empty_string_encryption" - }, - "relations": [ - { - "source_id": "test_encryption_cycle", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "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": "def", - "body": "# [DEF:test_encryption_cycle:Function]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @PURPOSE: Verify EncryptionManager round-trip encryption/decryption invariant for non-empty secrets.\ndef test_encryption_cycle():\n \"\"\"Verify encrypted data can be decrypted back to original string.\"\"\"\n manager = EncryptionManager()\n original = \"secret_api_key_123\"\n encrypted = manager.encrypt(original)\n assert encrypted != original\n assert manager.decrypt(encrypted) == original\n\n\n# @TEST_EDGE: empty_string_encryption\n# [/DEF:test_encryption_cycle:Function]\n" - }, - { - "contract_id": "test_empty_string_encryption", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 42, - "end_line": 53, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify EncryptionManager preserves empty-string payloads through encrypt/decrypt cycle.", - "TEST_EDGE": "decrypt_invalid_data" - }, - "relations": [ - { - "source_id": "test_empty_string_encryption", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "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": "def", - "body": "# [DEF:test_empty_string_encryption:Function]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @PURPOSE: Verify EncryptionManager preserves empty-string payloads through encrypt/decrypt cycle.\ndef test_empty_string_encryption():\n manager = EncryptionManager()\n original = \"\"\n encrypted = manager.encrypt(original)\n assert manager.decrypt(encrypted) == \"\"\n\n\n# @TEST_EDGE: decrypt_invalid_data\n# [/DEF:test_empty_string_encryption:Function]\n" - }, - { - "contract_id": "test_decrypt_invalid_data", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 56, - "end_line": 66, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Ensure decrypt rejects invalid ciphertext input by raising an exception.", - "TEST_FIXTURE": "mock_db_session" - }, - "relations": [ - { - "source_id": "test_decrypt_invalid_data", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "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 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": "def", - "body": "# [DEF:test_decrypt_invalid_data:Function]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @PURPOSE: Ensure decrypt rejects invalid ciphertext input by raising an exception.\ndef test_decrypt_invalid_data():\n manager = EncryptionManager()\n with pytest.raises(Exception):\n manager.decrypt(\"not-encrypted-string\")\n\n\n# @TEST_FIXTURE: mock_db_session\n# [/DEF:test_decrypt_invalid_data:Function]\n" - }, - { - "contract_id": "mock_db", - "contract_type": "Fixture", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 69, - "end_line": 80, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "INVARIANT": "Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced.", - "PURPOSE": "MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests.", - "RISK": "query() returns unconstrained MagicMock — chain beyond query() has no spec protection. Consider create_autospec(Session) for full chain safety." - }, - "relations": [ - { - "source_id": "mock_db", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Fixture'", - "detail": { - "actual_type": "Fixture", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Fixture' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Fixture" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "INVARIANT", - "message": "@INVARIANT is not allowed for contract type 'Fixture'", - "detail": { - "actual_type": "Fixture", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Fixture' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Fixture" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Fixture'", - "detail": { - "actual_type": "Fixture", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Fixture' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Fixture" - } - }, - { - "code": "unknown_tag", - "tag": "RISK", - "message": "@RISK is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Fixture' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Fixture" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:mock_db:Fixture]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests.\n# @INVARIANT: Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced.\n@pytest.fixture\ndef mock_db():\n # @RISK: query() returns unconstrained MagicMock — chain beyond query() has no spec protection. Consider create_autospec(Session) for full chain safety.\n return MagicMock(spec=Session)\n\n\n# [/DEF:mock_db:Fixture]\n" - }, - { - "contract_id": "service", - "contract_type": "Fixture", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 83, - "end_line": 92, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "LLMProviderService fixture wired to mock_db for provider CRUD tests." - }, - "relations": [ - { - "source_id": "service", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Fixture'", - "detail": { - "actual_type": "Fixture", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Fixture' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Fixture" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Fixture'", - "detail": { - "actual_type": "Fixture", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Fixture' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Fixture" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Fixture' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Fixture" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:service:Fixture]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @COMPLEXITY: 1\n# @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests.\n@pytest.fixture\ndef service(mock_db):\n return LLMProviderService(db=mock_db)\n\n\n# [/DEF:service:Fixture]\n" - }, - { - "contract_id": "test_get_all_providers", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 95, - "end_line": 104, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify provider list retrieval issues query/all calls on the backing DB session." - }, - "relations": [ - { - "source_id": "test_get_all_providers", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "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": "def", - "body": "# [DEF:test_get_all_providers:Function]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @PURPOSE: Verify provider list retrieval issues query/all calls on the backing DB session.\ndef test_get_all_providers(service, mock_db):\n service.get_all_providers()\n mock_db.query.assert_called()\n mock_db.query().all.assert_called()\n\n\n# [/DEF:test_get_all_providers:Function]\n" - }, - { - "contract_id": "test_create_provider", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 107, - "end_line": 130, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Ensure provider creation persists entity and stores API key in encrypted form." - }, - "relations": [ - { - "source_id": "test_create_provider", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "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": "def", - "body": "# [DEF:test_create_provider:Function]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @PURPOSE: Ensure provider creation persists entity and stores API key in encrypted form.\ndef test_create_provider(service, mock_db):\n config = LLMProviderConfig(\n provider_type=LLMProviderType.OPENAI,\n name=\"Test OpenAI\",\n base_url=\"https://api.openai.com\",\n api_key=\"sk-test\",\n default_model=\"gpt-4\",\n is_active=True,\n )\n\n provider = service.create_provider(config)\n\n mock_db.add.assert_called()\n mock_db.commit.assert_called()\n # Verify API key was encrypted\n assert provider.api_key != \"sk-test\"\n # Decrypt to verify it matches\n assert EncryptionManager().decrypt(provider.api_key) == \"sk-test\"\n\n\n# [/DEF:test_create_provider:Function]\n" - }, - { - "contract_id": "test_get_decrypted_api_key", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 133, - "end_line": 146, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify service decrypts stored provider API key for an existing provider record." - }, - "relations": [ - { - "source_id": "test_get_decrypted_api_key", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "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": "def", - "body": "# [DEF:test_get_decrypted_api_key:Function]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @PURPOSE: Verify service decrypts stored provider API key for an existing provider record.\ndef test_get_decrypted_api_key(service, mock_db):\n # Setup mock provider\n encrypted_key = EncryptionManager().encrypt(\"secret-value\")\n mock_provider = LLMProvider(id=\"p1\", api_key=encrypted_key)\n mock_db.query().filter().first.return_value = mock_provider\n\n key = service.get_decrypted_api_key(\"p1\")\n assert key == \"secret-value\"\n\n\n# [/DEF:test_get_decrypted_api_key:Function]\n" - }, - { - "contract_id": "test_get_decrypted_api_key_not_found", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 149, - "end_line": 157, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify missing provider lookup returns None instead of attempting decryption." - }, - "relations": [ - { - "source_id": "test_get_decrypted_api_key_not_found", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "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": "def", - "body": "# [DEF:test_get_decrypted_api_key_not_found:Function]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @PURPOSE: Verify missing provider lookup returns None instead of attempting decryption.\ndef test_get_decrypted_api_key_not_found(service, mock_db):\n mock_db.query().filter().first.return_value = None\n assert service.get_decrypted_api_key(\"missing\") is None\n\n\n# [/DEF:test_get_decrypted_api_key_not_found:Function]\n" - }, - { - "contract_id": "test_update_provider_ignores_masked_placeholder_api_key", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_llm_provider.py", - "start_line": 160, - "end_line": 193, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Ensure masked placeholder API keys do not overwrite previously encrypted provider secrets." - }, - "relations": [ - { - "source_id": "test_update_provider_ignores_masked_placeholder_api_key", - "relation_type": "BINDS_TO", - "target_id": "test_llm_provider:Module", - "target_ref": "[test_llm_provider:Module]" - } - ], - "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": "def", - "body": "# [DEF:test_update_provider_ignores_masked_placeholder_api_key:Function]\n# @RELATION: BINDS_TO -> [test_llm_provider:Module]\n# @PURPOSE: Ensure masked placeholder API keys do not overwrite previously encrypted provider secrets.\ndef test_update_provider_ignores_masked_placeholder_api_key(service, mock_db):\n existing_encrypted = EncryptionManager().encrypt(\"secret-value\")\n mock_provider = LLMProvider(\n id=\"p1\",\n provider_type=\"openai\",\n name=\"Existing\",\n base_url=\"https://api.openai.com/v1\",\n api_key=existing_encrypted,\n default_model=\"gpt-4o\",\n is_active=True,\n )\n mock_db.query().filter().first.return_value = mock_provider\n config = LLMProviderConfig(\n id=\"p1\",\n provider_type=LLMProviderType.OPENAI,\n name=\"Existing\",\n base_url=\"https://api.openai.com/v1\",\n api_key=\"********\",\n default_model=\"gpt-4o\",\n is_active=False,\n )\n\n updated = service.update_provider(\"p1\", config)\n\n assert updated is mock_provider\n assert updated.api_key == existing_encrypted\n assert EncryptionManager().decrypt(updated.api_key) == \"secret-value\"\n assert updated.is_active is False\n\n\n# [/DEF:test_update_provider_ignores_masked_placeholder_api_key:Function]\n" - }, - { - "contract_id": "test_rbac_permission_catalog", - "contract_type": "Module", - "file_path": "backend/src/services/__tests__/test_rbac_permission_catalog.py", - "start_line": 1, - "end_line": 144, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Synchronization adds only missing normalized permission pairs.", - "LAYER": "Service Tests", - "PURPOSE": "Verifies RBAC permission catalog discovery and idempotent synchronization behavior.", - "SEMANTICS": [ - "tests", - "rbac", - "permissions", - "catalog", - "discovery", - "sync" - ] - }, - "relations": [ - { - "source_id": "test_rbac_permission_catalog", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "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 BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_rbac_permission_catalog:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, rbac, permissions, catalog, discovery, sync\n# @PURPOSE: Verifies RBAC permission catalog discovery and idempotent synchronization behavior.\n# @LAYER: Service Tests\n# @INVARIANT: Synchronization adds only missing normalized permission pairs.\n\n# [SECTION: IMPORTS]\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock\n\nimport src.services.rbac_permission_catalog as catalog\n# [/SECTION: IMPORTS]\n\n\n# [DEF:test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests:Function]\n# @RELATION: BINDS_TO -> test_rbac_permission_catalog\n# @PURPOSE: Ensures route-scanner extracts has_permission pairs from route files and skips __tests__.\n# @PRE: Temporary route directory contains route and test files.\n# @POST: Returned set includes production route permissions and excludes test-only declarations.\ndef test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests(tmp_path, monkeypatch):\n routes_dir = tmp_path / \"routes\"\n routes_dir.mkdir(parents=True, exist_ok=True)\n\n (routes_dir / \"dashboards.py\").write_text(\n '\\n'.join(\n [\n '_ = Depends(has_permission(\"plugin:migration\", \"READ\"))',\n '_ = Depends(has_permission(\"plugin:migration\", \"EXECUTE\"))',\n '_ = Depends(has_permission(\"tasks\", \"WRITE\"))',\n ]\n ),\n encoding=\"utf-8\",\n )\n\n tests_dir = routes_dir / \"__tests__\"\n tests_dir.mkdir(parents=True, exist_ok=True)\n (tests_dir / \"test_fake.py\").write_text(\n '_ = Depends(has_permission(\"plugin:ignored\", \"READ\"))',\n encoding=\"utf-8\",\n )\n\n monkeypatch.setattr(catalog, \"ROUTES_DIR\", routes_dir)\n\n discovered = catalog._discover_route_permissions()\n\n assert (\"plugin:migration\", \"READ\") in discovered\n assert (\"plugin:migration\", \"EXECUTE\") in discovered\n assert (\"tasks\", \"WRITE\") in discovered\n assert (\"plugin:ignored\", \"READ\") not in discovered\n# [/DEF:test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests:Function]\n\n\n# [DEF:test_discover_declared_permissions_unions_route_and_plugin_permissions:Function]\n# @RELATION: BINDS_TO -> test_rbac_permission_catalog\n# @PURPOSE: Ensures full catalog includes route-level permissions plus dynamic plugin EXECUTE rights.\n# @PRE: Route discovery and plugin loader both return permission sources.\n# @POST: Result set contains union of both sources.\ndef test_discover_declared_permissions_unions_route_and_plugin_permissions(monkeypatch):\n monkeypatch.setattr(\n catalog,\n \"_discover_route_permissions\",\n lambda: {(\"tasks\", \"READ\"), (\"plugin:migration\", \"READ\")},\n )\n\n plugin_loader = MagicMock()\n plugin_loader.get_all_plugin_configs.return_value = [\n SimpleNamespace(id=\"superset-backup\"),\n SimpleNamespace(id=\"llm_dashboard_validation\"),\n ]\n\n discovered = catalog.discover_declared_permissions(plugin_loader=plugin_loader)\n\n assert (\"tasks\", \"READ\") in discovered\n assert (\"plugin:migration\", \"READ\") in discovered\n assert (\"plugin:superset-backup\", \"EXECUTE\") in discovered\n assert (\"plugin:llm_dashboard_validation\", \"EXECUTE\") in discovered\n# [/DEF:test_discover_declared_permissions_unions_route_and_plugin_permissions:Function]\n\n\n# [DEF:test_sync_permission_catalog_inserts_only_missing_normalized_pairs:Function]\n# @RELATION: BINDS_TO -> test_rbac_permission_catalog\n# @PURPOSE: Ensures synchronization inserts only missing pairs and normalizes action/resource tokens.\n# @PRE: DB already contains subset of permissions.\n# @POST: Only missing normalized pairs are inserted and commit is executed once.\ndef test_sync_permission_catalog_inserts_only_missing_normalized_pairs():\n db = MagicMock()\n db.query.return_value.all.return_value = [\n SimpleNamespace(resource=\"tasks\", action=\"READ\"),\n SimpleNamespace(resource=\"plugin:migration\", action=\"EXECUTE\"),\n ]\n\n declared_permissions = {\n (\"tasks\", \"read\"),\n (\"plugin:migration\", \"execute\"),\n (\"plugin:migration\", \"READ\"),\n (\"\", \"WRITE\"),\n (\"plugin:migration\", \"\"),\n }\n\n inserted_count = catalog.sync_permission_catalog(\n db=db,\n declared_permissions=declared_permissions,\n )\n\n assert inserted_count == 1\n assert db.add.call_count == 1\n inserted_permission = db.add.call_args[0][0]\n assert inserted_permission.resource == \"plugin:migration\"\n assert inserted_permission.action == \"READ\"\n db.commit.assert_called_once()\n# [/DEF:test_sync_permission_catalog_inserts_only_missing_normalized_pairs:Function]\n\n\n# [DEF:test_sync_permission_catalog_is_noop_when_all_permissions_exist:Function]\n# @RELATION: BINDS_TO -> test_rbac_permission_catalog\n# @PURPOSE: Ensures synchronization is idempotent when all declared pairs already exist.\n# @PRE: DB contains full declared permission set.\n# @POST: No inserts are added and commit is not called.\ndef test_sync_permission_catalog_is_noop_when_all_permissions_exist():\n db = MagicMock()\n db.query.return_value.all.return_value = [\n SimpleNamespace(resource=\"tasks\", action=\"READ\"),\n SimpleNamespace(resource=\"plugin:migration\", action=\"READ\"),\n ]\n\n declared_permissions = {\n (\"tasks\", \"READ\"),\n (\"plugin:migration\", \"READ\"),\n }\n\n inserted_count = catalog.sync_permission_catalog(\n db=db,\n declared_permissions=declared_permissions,\n )\n\n assert inserted_count == 0\n db.add.assert_not_called()\n db.commit.assert_not_called()\n# [/DEF:test_sync_permission_catalog_is_noop_when_all_permissions_exist:Function]\n\n\n# [/DEF:test_rbac_permission_catalog:Module]\n" - }, - { - "contract_id": "test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_rbac_permission_catalog.py", - "start_line": 17, - "end_line": 52, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returned set includes production route permissions and excludes test-only declarations.", - "PRE": "Temporary route directory contains route and test files.", - "PURPOSE": "Ensures route-scanner extracts has_permission pairs from route files and skips __tests__." - }, - "relations": [ - { - "source_id": "test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests", - "relation_type": "BINDS_TO", - "target_id": "test_rbac_permission_catalog", - "target_ref": "test_rbac_permission_catalog" - } - ], - "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": "def", - "body": "# [DEF:test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests:Function]\n# @RELATION: BINDS_TO -> test_rbac_permission_catalog\n# @PURPOSE: Ensures route-scanner extracts has_permission pairs from route files and skips __tests__.\n# @PRE: Temporary route directory contains route and test files.\n# @POST: Returned set includes production route permissions and excludes test-only declarations.\ndef test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests(tmp_path, monkeypatch):\n routes_dir = tmp_path / \"routes\"\n routes_dir.mkdir(parents=True, exist_ok=True)\n\n (routes_dir / \"dashboards.py\").write_text(\n '\\n'.join(\n [\n '_ = Depends(has_permission(\"plugin:migration\", \"READ\"))',\n '_ = Depends(has_permission(\"plugin:migration\", \"EXECUTE\"))',\n '_ = Depends(has_permission(\"tasks\", \"WRITE\"))',\n ]\n ),\n encoding=\"utf-8\",\n )\n\n tests_dir = routes_dir / \"__tests__\"\n tests_dir.mkdir(parents=True, exist_ok=True)\n (tests_dir / \"test_fake.py\").write_text(\n '_ = Depends(has_permission(\"plugin:ignored\", \"READ\"))',\n encoding=\"utf-8\",\n )\n\n monkeypatch.setattr(catalog, \"ROUTES_DIR\", routes_dir)\n\n discovered = catalog._discover_route_permissions()\n\n assert (\"plugin:migration\", \"READ\") in discovered\n assert (\"plugin:migration\", \"EXECUTE\") in discovered\n assert (\"tasks\", \"WRITE\") in discovered\n assert (\"plugin:ignored\", \"READ\") not in discovered\n# [/DEF:test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests:Function]\n" - }, - { - "contract_id": "test_discover_declared_permissions_unions_route_and_plugin_permissions", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_rbac_permission_catalog.py", - "start_line": 55, - "end_line": 79, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Result set contains union of both sources.", - "PRE": "Route discovery and plugin loader both return permission sources.", - "PURPOSE": "Ensures full catalog includes route-level permissions plus dynamic plugin EXECUTE rights." - }, - "relations": [ - { - "source_id": "test_discover_declared_permissions_unions_route_and_plugin_permissions", - "relation_type": "BINDS_TO", - "target_id": "test_rbac_permission_catalog", - "target_ref": "test_rbac_permission_catalog" - } - ], - "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": "def", - "body": "# [DEF:test_discover_declared_permissions_unions_route_and_plugin_permissions:Function]\n# @RELATION: BINDS_TO -> test_rbac_permission_catalog\n# @PURPOSE: Ensures full catalog includes route-level permissions plus dynamic plugin EXECUTE rights.\n# @PRE: Route discovery and plugin loader both return permission sources.\n# @POST: Result set contains union of both sources.\ndef test_discover_declared_permissions_unions_route_and_plugin_permissions(monkeypatch):\n monkeypatch.setattr(\n catalog,\n \"_discover_route_permissions\",\n lambda: {(\"tasks\", \"READ\"), (\"plugin:migration\", \"READ\")},\n )\n\n plugin_loader = MagicMock()\n plugin_loader.get_all_plugin_configs.return_value = [\n SimpleNamespace(id=\"superset-backup\"),\n SimpleNamespace(id=\"llm_dashboard_validation\"),\n ]\n\n discovered = catalog.discover_declared_permissions(plugin_loader=plugin_loader)\n\n assert (\"tasks\", \"READ\") in discovered\n assert (\"plugin:migration\", \"READ\") in discovered\n assert (\"plugin:superset-backup\", \"EXECUTE\") in discovered\n assert (\"plugin:llm_dashboard_validation\", \"EXECUTE\") in discovered\n# [/DEF:test_discover_declared_permissions_unions_route_and_plugin_permissions:Function]\n" - }, - { - "contract_id": "test_sync_permission_catalog_inserts_only_missing_normalized_pairs", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_rbac_permission_catalog.py", - "start_line": 82, - "end_line": 113, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Only missing normalized pairs are inserted and commit is executed once.", - "PRE": "DB already contains subset of permissions.", - "PURPOSE": "Ensures synchronization inserts only missing pairs and normalizes action/resource tokens." - }, - "relations": [ - { - "source_id": "test_sync_permission_catalog_inserts_only_missing_normalized_pairs", - "relation_type": "BINDS_TO", - "target_id": "test_rbac_permission_catalog", - "target_ref": "test_rbac_permission_catalog" - } - ], - "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": "def", - "body": "# [DEF:test_sync_permission_catalog_inserts_only_missing_normalized_pairs:Function]\n# @RELATION: BINDS_TO -> test_rbac_permission_catalog\n# @PURPOSE: Ensures synchronization inserts only missing pairs and normalizes action/resource tokens.\n# @PRE: DB already contains subset of permissions.\n# @POST: Only missing normalized pairs are inserted and commit is executed once.\ndef test_sync_permission_catalog_inserts_only_missing_normalized_pairs():\n db = MagicMock()\n db.query.return_value.all.return_value = [\n SimpleNamespace(resource=\"tasks\", action=\"READ\"),\n SimpleNamespace(resource=\"plugin:migration\", action=\"EXECUTE\"),\n ]\n\n declared_permissions = {\n (\"tasks\", \"read\"),\n (\"plugin:migration\", \"execute\"),\n (\"plugin:migration\", \"READ\"),\n (\"\", \"WRITE\"),\n (\"plugin:migration\", \"\"),\n }\n\n inserted_count = catalog.sync_permission_catalog(\n db=db,\n declared_permissions=declared_permissions,\n )\n\n assert inserted_count == 1\n assert db.add.call_count == 1\n inserted_permission = db.add.call_args[0][0]\n assert inserted_permission.resource == \"plugin:migration\"\n assert inserted_permission.action == \"READ\"\n db.commit.assert_called_once()\n# [/DEF:test_sync_permission_catalog_inserts_only_missing_normalized_pairs:Function]\n" - }, - { - "contract_id": "test_sync_permission_catalog_is_noop_when_all_permissions_exist", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_rbac_permission_catalog.py", - "start_line": 116, - "end_line": 141, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "No inserts are added and commit is not called.", - "PRE": "DB contains full declared permission set.", - "PURPOSE": "Ensures synchronization is idempotent when all declared pairs already exist." - }, - "relations": [ - { - "source_id": "test_sync_permission_catalog_is_noop_when_all_permissions_exist", - "relation_type": "BINDS_TO", - "target_id": "test_rbac_permission_catalog", - "target_ref": "test_rbac_permission_catalog" - } - ], - "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": "def", - "body": "# [DEF:test_sync_permission_catalog_is_noop_when_all_permissions_exist:Function]\n# @RELATION: BINDS_TO -> test_rbac_permission_catalog\n# @PURPOSE: Ensures synchronization is idempotent when all declared pairs already exist.\n# @PRE: DB contains full declared permission set.\n# @POST: No inserts are added and commit is not called.\ndef test_sync_permission_catalog_is_noop_when_all_permissions_exist():\n db = MagicMock()\n db.query.return_value.all.return_value = [\n SimpleNamespace(resource=\"tasks\", action=\"READ\"),\n SimpleNamespace(resource=\"plugin:migration\", action=\"READ\"),\n ]\n\n declared_permissions = {\n (\"tasks\", \"READ\"),\n (\"plugin:migration\", \"READ\"),\n }\n\n inserted_count = catalog.sync_permission_catalog(\n db=db,\n declared_permissions=declared_permissions,\n )\n\n assert inserted_count == 0\n db.add.assert_not_called()\n db.commit.assert_not_called()\n# [/DEF:test_sync_permission_catalog_is_noop_when_all_permissions_exist:Function]\n" - }, - { - "contract_id": "TestResourceService", - "contract_type": "Module", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 1, - "end_line": 466, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Resource summaries preserve task linkage and status projection behavior.", - "LAYER": "Service", - "PURPOSE": "Unit tests for ResourceService", - "SEMANTICS": [ - "resource-service", - "tests", - "dashboards", - "datasets", - "activity" - ] - }, - "relations": [ - { - "source_id": "TestResourceService", - "relation_type": "VERIFIES", - "target_id": "src.services.resource_service.ResourceService", - "target_ref": "[src.services.resource_service.ResourceService]" - } - ], - "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" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestResourceService:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: resource-service, tests, dashboards, datasets, activity\n# @PURPOSE: Unit tests for ResourceService\n# @LAYER: Service\n# @RELATION: VERIFIES ->[src.services.resource_service.ResourceService]\n# @INVARIANT: Resource summaries preserve task linkage and status projection behavior.\n\nimport pytest\nfrom unittest.mock import MagicMock, patch, AsyncMock\nfrom datetime import datetime, timezone\n\n\n# [DEF:test_get_dashboards_with_status:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @PURPOSE: Validate dashboard enrichment includes git/task status projections.\n# @TEST: get_dashboards_with_status returns dashboards with git and task status\n# @PRE: SupersetClient returns dashboard list\n# @POST: Each dashboard has git_status and last_task fields\n@pytest.mark.asyncio\nasync def test_get_dashboards_with_status():\n with (\n patch(\"src.services.resource_service.SupersetClient\") as mock_client,\n patch(\"src.services.resource_service.GitService\"),\n ):\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Mock Superset response\n mock_client.return_value.get_dashboards_summary.return_value = [\n {\"id\": 1, \"title\": \"Dashboard 1\", \"slug\": \"dash-1\"},\n {\"id\": 2, \"title\": \"Dashboard 2\", \"slug\": \"dash-2\"},\n ]\n\n # Mock tasks\n task_prod_old = MagicMock()\n task_prod_old.id = \"task-123\"\n task_prod_old.plugin_id = \"llm_dashboard_validation\"\n task_prod_old.status = \"SUCCESS\"\n task_prod_old.params = {\"dashboard_id\": \"1\", \"environment_id\": \"prod\"}\n task_prod_old.started_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task_prod_new = MagicMock()\n task_prod_new.id = \"task-124\"\n task_prod_new.plugin_id = \"llm_dashboard_validation\"\n task_prod_new.status = \"TaskStatus.FAILED\"\n task_prod_new.params = {\"dashboard_id\": \"1\", \"environment_id\": \"prod\"}\n task_prod_new.result = {\"status\": \"FAIL\"}\n task_prod_new.started_at = datetime(2024, 1, 1, 12, 0, 0)\n\n task_other_env = MagicMock()\n task_other_env.id = \"task-200\"\n task_other_env.plugin_id = \"llm_dashboard_validation\"\n task_other_env.status = \"SUCCESS\"\n task_other_env.params = {\"dashboard_id\": \"1\", \"environment_id\": \"stage\"}\n task_other_env.started_at = datetime(2024, 1, 1, 13, 0, 0)\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_dashboards_with_status(\n env,\n [task_prod_old, task_prod_new, task_other_env],\n )\n\n assert len(result) == 2\n assert result[0][\"id\"] == 1\n assert \"git_status\" in result[0]\n assert \"last_task\" in result[0]\n assert result[0][\"last_task\"][\"task_id\"] == \"task-124\"\n assert result[0][\"last_task\"][\"status\"] == \"FAILED\"\n assert result[0][\"last_task\"][\"validation_status\"] == \"FAIL\"\n\n\n# [/DEF:test_get_dashboards_with_status:Function]\n\n\n# [DEF:test_get_datasets_with_status:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_datasets_with_status returns datasets with task status\n# @PRE: SupersetClient returns dataset list\n# @POST: Each dataset has last_task field\n# @PURPOSE: Verify ResourceService.get_datasets_with_status returns datasets grouped by validation status.\n@pytest.mark.asyncio\nasync def test_get_datasets_with_status():\n with patch(\"src.services.resource_service.SupersetClient\") as mock_client:\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Mock Superset response\n mock_client.return_value.get_datasets_summary.return_value = [\n {\"id\": 1, \"table_name\": \"users\", \"schema\": \"public\", \"database\": \"app\"},\n {\"id\": 2, \"table_name\": \"orders\", \"schema\": \"public\", \"database\": \"app\"},\n ]\n\n # Mock tasks\n mock_task = MagicMock()\n mock_task.id = \"task-456\"\n mock_task.status = \"RUNNING\"\n mock_task.params = {\"resource_id\": \"dataset-1\"}\n mock_task.created_at = datetime.now()\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_datasets_with_status(env, [mock_task])\n\n assert len(result) == 2\n assert result[0][\"table_name\"] == \"users\"\n assert \"last_task\" in result[0]\n assert result[0][\"last_task\"][\"task_id\"] == \"task-456\"\n assert result[0][\"last_task\"][\"status\"] == \"RUNNING\"\n\n\n# [/DEF:test_get_datasets_with_status:Function]\n\n\n# [DEF:test_get_activity_summary:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_activity_summary returns active count and recent tasks\n# @PRE: tasks list provided\n# @POST: Returns dict with active_count and recent_tasks\n# @PURPOSE: Verify ResourceService.get_activity_summary returns recent task activity.\ndef test_get_activity_summary():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Create mock tasks\n task1 = MagicMock()\n task1.id = \"task-1\"\n task1.status = \"RUNNING\"\n task1.params = {\"resource_name\": \"Dashboard 1\", \"resource_type\": \"dashboard\"}\n task1.created_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task2 = MagicMock()\n task2.id = \"task-2\"\n task2.status = \"SUCCESS\"\n task2.params = {\"resource_name\": \"Dataset 1\", \"resource_type\": \"dataset\"}\n task2.created_at = datetime(2024, 1, 1, 9, 0, 0)\n\n task3 = MagicMock()\n task3.id = \"task-3\"\n task3.status = \"WAITING_INPUT\"\n task3.params = {\"resource_name\": \"Dashboard 2\", \"resource_type\": \"dashboard\"}\n task3.created_at = datetime(2024, 1, 1, 8, 0, 0)\n\n result = service.get_activity_summary([task1, task2, task3])\n\n assert result[\"active_count\"] == 2 # RUNNING + WAITING_INPUT\n assert len(result[\"recent_tasks\"]) == 3\n\n\n# [/DEF:test_get_activity_summary:Function]\n\n\n# [DEF:test_get_git_status_for_dashboard_no_repo:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_git_status_for_dashboard returns None when no repo exists\n# @PRE: GitService returns None for repo\n# @POST: Returns None\n# @PURPOSE: Verify get_git_status_for_dashboard returns None when no repo exists.\ndef test_get_git_status_for_dashboard_no_repo():\n with patch(\"src.services.resource_service.GitService\") as mock_git:\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n mock_git.return_value.get_repo.return_value = None\n\n result = service._get_git_status_for_dashboard(123)\n\n assert result is not None\n assert result[\"sync_status\"] == \"NO_REPO\"\n assert result[\"has_repo\"] is False\n\n\n# [/DEF:test_get_git_status_for_dashboard_no_repo:Function]\n\n\n# [DEF:test_get_last_task_for_resource:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_last_task_for_resource returns most recent task for resource\n# @PRE: tasks list with matching resource_id\n# @POST: Returns task summary with task_id and status\n# @PURPOSE: Verify get_last_task_for_resource returns the most recent task for a given resource.\ndef test_get_last_task_for_resource():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Create mock tasks\n task1 = MagicMock()\n task1.id = \"task-old\"\n task1.status = \"SUCCESS\"\n task1.params = {\"resource_id\": \"dashboard-1\"}\n task1.created_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task2 = MagicMock()\n task2.id = \"task-new\"\n task2.status = \"RUNNING\"\n task2.params = {\"resource_id\": \"dashboard-1\"}\n task2.created_at = datetime(2024, 1, 1, 12, 0, 0)\n\n result = service._get_last_task_for_resource(\"dashboard-1\", [task1, task2])\n\n assert result is not None\n assert result[\"task_id\"] == \"task-new\" # Most recent\n assert result[\"status\"] == \"RUNNING\"\n\n\n# [/DEF:test_get_last_task_for_resource:Function]\n\n\n# [DEF:test_extract_resource_name_from_task:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _extract_resource_name_from_task extracts name from params\n# @PRE: task has resource_name in params\n# @POST: Returns resource name or fallback\n# @PURPOSE: Verify extract_resource_name_from_task correctly parses resource names from task identifiers.\ndef test_extract_resource_name_from_task():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Task with resource_name\n task = MagicMock()\n task.id = \"task-123\"\n task.params = {\"resource_name\": \"My Dashboard\"}\n\n result = service._extract_resource_name_from_task(task)\n assert result == \"My Dashboard\"\n\n # Task without resource_name\n task2 = MagicMock()\n task2.id = \"task-456\"\n task2.params = {}\n\n result2 = service._extract_resource_name_from_task(task2)\n assert \"task-456\" in result2\n\n\n# [/DEF:test_extract_resource_name_from_task:Function]\n\n\n# [DEF:test_get_last_task_for_resource_empty_tasks:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_last_task_for_resource returns None for empty tasks list\n# @PRE: tasks is empty list\n# @POST: Returns None\n# @PURPOSE: Verify get_last_task_for_resource returns None when tasks list is empty.\ndef test_get_last_task_for_resource_empty_tasks():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n result = service._get_last_task_for_resource(\"dashboard-1\", [])\n assert result is None\n\n\n# [/DEF:test_get_last_task_for_resource_empty_tasks:Function]\n\n\n# [DEF:test_get_last_task_for_resource_no_match:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_last_task_for_resource returns None when no tasks match resource_id\n# @PRE: tasks list has no matching resource_id\n# @POST: Returns None\n# @PURPOSE: Verify get_last_task_for_resource returns None when no task matches the resource.\ndef test_get_last_task_for_resource_no_match():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n task = MagicMock()\n task.id = \"task-999\"\n task.status = \"SUCCESS\"\n task.params = {\"resource_id\": \"dashboard-99\"}\n task.created_at = datetime(2024, 1, 1, 10, 0, 0)\n\n result = service._get_last_task_for_resource(\"dashboard-1\", [task])\n assert result is None\n\n\n# [/DEF:test_get_last_task_for_resource_no_match:Function]\n\n\n# [DEF:test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_dashboards_with_status handles mixed naive/aware datetimes without comparison errors.\n# @PRE: Task list includes both timezone-aware and timezone-naive timestamps.\n# @POST: Latest task is selected deterministically and no exception is raised.\n# @PURPOSE: Verify get_dashboards_with_status handles mixed naive and aware datetimes without crashing.\n@pytest.mark.asyncio\nasync def test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes():\n with (\n patch(\"src.services.resource_service.SupersetClient\") as mock_client,\n patch(\"src.services.resource_service.GitService\"),\n ):\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n mock_client.return_value.get_dashboards_summary.return_value = [\n {\"id\": 1, \"title\": \"Dashboard 1\", \"slug\": \"dash-1\"}\n ]\n\n task_naive = MagicMock()\n task_naive.id = \"task-naive\"\n task_naive.plugin_id = \"llm_dashboard_validation\"\n task_naive.status = \"SUCCESS\"\n task_naive.params = {\"dashboard_id\": \"1\", \"environment_id\": \"prod\"}\n task_naive.started_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task_aware = MagicMock()\n task_aware.id = \"task-aware\"\n task_aware.plugin_id = \"llm_dashboard_validation\"\n task_aware.status = \"SUCCESS\"\n task_aware.params = {\"dashboard_id\": \"1\", \"environment_id\": \"prod\"}\n task_aware.started_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_dashboards_with_status(env, [task_naive, task_aware])\n\n assert result[0][\"last_task\"][\"task_id\"] == \"task-aware\"\n\n\n# [/DEF:test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes:Function]\n\n\n# [DEF:test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_dashboards_with_status keeps latest task identity while falling back to older decisive validation status.\n# @PRE: Same dashboard has older WARN and newer UNKNOWN validation tasks.\n# @POST: Returned last_task points to newest task but preserves WARN as last meaningful validation state.\n# @PURPOSE: Verify status ranking prefers decisive validation over newer unknown status.\n@pytest.mark.anyio\nasync def test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown():\n with (\n patch(\"src.services.resource_service.SupersetClient\") as mock_client,\n patch(\"src.services.resource_service.GitService\"),\n ):\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n mock_client.return_value.get_dashboards_summary.return_value = [\n {\"id\": 4, \"title\": \"Dashboard 4\", \"slug\": \"deck\"}\n ]\n\n task_warn = MagicMock()\n task_warn.id = \"task-warn\"\n task_warn.plugin_id = \"llm_dashboard_validation\"\n task_warn.status = \"SUCCESS\"\n task_warn.params = {\"dashboard_id\": \"4\", \"environment_id\": \"prod\"}\n task_warn.result = {\"status\": \"WARN\"}\n task_warn.started_at = datetime(2024, 1, 1, 11, 0, 0)\n\n task_unknown = MagicMock()\n task_unknown.id = \"task-unknown\"\n task_unknown.plugin_id = \"llm_dashboard_validation\"\n task_unknown.status = \"RUNNING\"\n task_unknown.params = {\"dashboard_id\": \"4\", \"environment_id\": \"prod\"}\n task_unknown.result = {\"status\": \"UNKNOWN\"}\n task_unknown.started_at = datetime(2024, 1, 1, 12, 0, 0)\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_dashboards_with_status(\n env, [task_warn, task_unknown]\n )\n\n assert result[0][\"last_task\"][\"task_id\"] == \"task-unknown\"\n assert result[0][\"last_task\"][\"status\"] == \"RUNNING\"\n assert result[0][\"last_task\"][\"validation_status\"] == \"WARN\"\n\n\n# [/DEF:test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown:Function]\n\n\n# [DEF:test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_dashboards_with_status still returns newest UNKNOWN when no decisive validation exists.\n# @PRE: Same dashboard has only UNKNOWN validation tasks.\n# @POST: Returned last_task keeps newest UNKNOWN task.\n# @PURPOSE: Verify fallback to latest unknown status when no decisive history exists.\n@pytest.mark.anyio\nasync def test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history():\n with (\n patch(\"src.services.resource_service.SupersetClient\") as mock_client,\n patch(\"src.services.resource_service.GitService\"),\n ):\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n mock_client.return_value.get_dashboards_summary.return_value = [\n {\"id\": 5, \"title\": \"Dashboard 5\", \"slug\": \"ops\"}\n ]\n\n task_unknown_old = MagicMock()\n task_unknown_old.id = \"task-unknown-old\"\n task_unknown_old.plugin_id = \"llm_dashboard_validation\"\n task_unknown_old.status = \"SUCCESS\"\n task_unknown_old.params = {\"dashboard_id\": \"5\", \"environment_id\": \"prod\"}\n task_unknown_old.result = {\"status\": \"UNKNOWN\"}\n task_unknown_old.started_at = datetime(2024, 1, 1, 11, 0, 0)\n\n task_unknown_new = MagicMock()\n task_unknown_new.id = \"task-unknown-new\"\n task_unknown_new.plugin_id = \"llm_dashboard_validation\"\n task_unknown_new.status = \"SUCCESS\"\n task_unknown_new.params = {\"dashboard_id\": \"5\", \"environment_id\": \"prod\"}\n task_unknown_new.result = {\"status\": \"UNKNOWN\"}\n task_unknown_new.started_at = datetime(2024, 1, 1, 12, 0, 0)\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_dashboards_with_status(\n env, [task_unknown_old, task_unknown_new]\n )\n\n assert result[0][\"last_task\"][\"task_id\"] == \"task-unknown-new\"\n assert result[0][\"last_task\"][\"validation_status\"] == \"UNKNOWN\"\n\n\n# [/DEF:test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history:Function]\n\n\n# [DEF:test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_last_task_for_resource handles mixed naive/aware created_at values.\n# @PRE: Matching tasks include naive and aware created_at timestamps.\n# @POST: Latest task is returned without raising datetime comparison errors.\n# @PURPOSE: Verify get_last_task_for_resource correctly sorts mixed naive and aware created_at timestamps.\ndef test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n task_naive = MagicMock()\n task_naive.id = \"task-old\"\n task_naive.status = \"SUCCESS\"\n task_naive.params = {\"resource_id\": \"dashboard-1\"}\n task_naive.created_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task_aware = MagicMock()\n task_aware.id = \"task-new\"\n task_aware.status = \"RUNNING\"\n task_aware.params = {\"resource_id\": \"dashboard-1\"}\n task_aware.created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)\n\n result = service._get_last_task_for_resource(\n \"dashboard-1\", [task_naive, task_aware]\n )\n\n assert result is not None\n assert result[\"task_id\"] == \"task-new\"\n\n\n# [/DEF:test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at:Function]\n\n\n# [/DEF:TestResourceService:Module]\n" - }, - { - "contract_id": "test_get_dashboards_with_status", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 14, - "end_line": 76, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Each dashboard has git_status and last_task fields", - "PRE": "SupersetClient returns dashboard list", - "PURPOSE": "Validate dashboard enrichment includes git/task status projections.", - "TEST": "get_dashboards_with_status returns dashboards with git and task status" - }, - "relations": [ - { - "source_id": "test_get_dashboards_with_status", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_dashboards_with_status:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @PURPOSE: Validate dashboard enrichment includes git/task status projections.\n# @TEST: get_dashboards_with_status returns dashboards with git and task status\n# @PRE: SupersetClient returns dashboard list\n# @POST: Each dashboard has git_status and last_task fields\n@pytest.mark.asyncio\nasync def test_get_dashboards_with_status():\n with (\n patch(\"src.services.resource_service.SupersetClient\") as mock_client,\n patch(\"src.services.resource_service.GitService\"),\n ):\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Mock Superset response\n mock_client.return_value.get_dashboards_summary.return_value = [\n {\"id\": 1, \"title\": \"Dashboard 1\", \"slug\": \"dash-1\"},\n {\"id\": 2, \"title\": \"Dashboard 2\", \"slug\": \"dash-2\"},\n ]\n\n # Mock tasks\n task_prod_old = MagicMock()\n task_prod_old.id = \"task-123\"\n task_prod_old.plugin_id = \"llm_dashboard_validation\"\n task_prod_old.status = \"SUCCESS\"\n task_prod_old.params = {\"dashboard_id\": \"1\", \"environment_id\": \"prod\"}\n task_prod_old.started_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task_prod_new = MagicMock()\n task_prod_new.id = \"task-124\"\n task_prod_new.plugin_id = \"llm_dashboard_validation\"\n task_prod_new.status = \"TaskStatus.FAILED\"\n task_prod_new.params = {\"dashboard_id\": \"1\", \"environment_id\": \"prod\"}\n task_prod_new.result = {\"status\": \"FAIL\"}\n task_prod_new.started_at = datetime(2024, 1, 1, 12, 0, 0)\n\n task_other_env = MagicMock()\n task_other_env.id = \"task-200\"\n task_other_env.plugin_id = \"llm_dashboard_validation\"\n task_other_env.status = \"SUCCESS\"\n task_other_env.params = {\"dashboard_id\": \"1\", \"environment_id\": \"stage\"}\n task_other_env.started_at = datetime(2024, 1, 1, 13, 0, 0)\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_dashboards_with_status(\n env,\n [task_prod_old, task_prod_new, task_other_env],\n )\n\n assert len(result) == 2\n assert result[0][\"id\"] == 1\n assert \"git_status\" in result[0]\n assert \"last_task\" in result[0]\n assert result[0][\"last_task\"][\"task_id\"] == \"task-124\"\n assert result[0][\"last_task\"][\"status\"] == \"FAILED\"\n assert result[0][\"last_task\"][\"validation_status\"] == \"FAIL\"\n\n\n# [/DEF:test_get_dashboards_with_status:Function]\n" - }, - { - "contract_id": "test_get_datasets_with_status", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 79, - "end_line": 117, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Each dataset has last_task field", - "PRE": "SupersetClient returns dataset list", - "PURPOSE": "Verify ResourceService.get_datasets_with_status returns datasets grouped by validation status.", - "TEST": "get_datasets_with_status returns datasets with task status" - }, - "relations": [ - { - "source_id": "test_get_datasets_with_status", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_datasets_with_status:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_datasets_with_status returns datasets with task status\n# @PRE: SupersetClient returns dataset list\n# @POST: Each dataset has last_task field\n# @PURPOSE: Verify ResourceService.get_datasets_with_status returns datasets grouped by validation status.\n@pytest.mark.asyncio\nasync def test_get_datasets_with_status():\n with patch(\"src.services.resource_service.SupersetClient\") as mock_client:\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Mock Superset response\n mock_client.return_value.get_datasets_summary.return_value = [\n {\"id\": 1, \"table_name\": \"users\", \"schema\": \"public\", \"database\": \"app\"},\n {\"id\": 2, \"table_name\": \"orders\", \"schema\": \"public\", \"database\": \"app\"},\n ]\n\n # Mock tasks\n mock_task = MagicMock()\n mock_task.id = \"task-456\"\n mock_task.status = \"RUNNING\"\n mock_task.params = {\"resource_id\": \"dataset-1\"}\n mock_task.created_at = datetime.now()\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_datasets_with_status(env, [mock_task])\n\n assert len(result) == 2\n assert result[0][\"table_name\"] == \"users\"\n assert \"last_task\" in result[0]\n assert result[0][\"last_task\"][\"task_id\"] == \"task-456\"\n assert result[0][\"last_task\"][\"status\"] == \"RUNNING\"\n\n\n# [/DEF:test_get_datasets_with_status:Function]\n" - }, - { - "contract_id": "test_get_activity_summary", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 120, - "end_line": 156, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returns dict with active_count and recent_tasks", - "PRE": "tasks list provided", - "PURPOSE": "Verify ResourceService.get_activity_summary returns recent task activity.", - "TEST": "get_activity_summary returns active count and recent tasks" - }, - "relations": [ - { - "source_id": "test_get_activity_summary", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_activity_summary:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_activity_summary returns active count and recent tasks\n# @PRE: tasks list provided\n# @POST: Returns dict with active_count and recent_tasks\n# @PURPOSE: Verify ResourceService.get_activity_summary returns recent task activity.\ndef test_get_activity_summary():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Create mock tasks\n task1 = MagicMock()\n task1.id = \"task-1\"\n task1.status = \"RUNNING\"\n task1.params = {\"resource_name\": \"Dashboard 1\", \"resource_type\": \"dashboard\"}\n task1.created_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task2 = MagicMock()\n task2.id = \"task-2\"\n task2.status = \"SUCCESS\"\n task2.params = {\"resource_name\": \"Dataset 1\", \"resource_type\": \"dataset\"}\n task2.created_at = datetime(2024, 1, 1, 9, 0, 0)\n\n task3 = MagicMock()\n task3.id = \"task-3\"\n task3.status = \"WAITING_INPUT\"\n task3.params = {\"resource_name\": \"Dashboard 2\", \"resource_type\": \"dashboard\"}\n task3.created_at = datetime(2024, 1, 1, 8, 0, 0)\n\n result = service.get_activity_summary([task1, task2, task3])\n\n assert result[\"active_count\"] == 2 # RUNNING + WAITING_INPUT\n assert len(result[\"recent_tasks\"]) == 3\n\n\n# [/DEF:test_get_activity_summary:Function]\n" - }, - { - "contract_id": "test_get_git_status_for_dashboard_no_repo", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 159, - "end_line": 179, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returns None", - "PRE": "GitService returns None for repo", - "PURPOSE": "Verify get_git_status_for_dashboard returns None when no repo exists.", - "TEST": "_get_git_status_for_dashboard returns None when no repo exists" - }, - "relations": [ - { - "source_id": "test_get_git_status_for_dashboard_no_repo", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_git_status_for_dashboard_no_repo:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_git_status_for_dashboard returns None when no repo exists\n# @PRE: GitService returns None for repo\n# @POST: Returns None\n# @PURPOSE: Verify get_git_status_for_dashboard returns None when no repo exists.\ndef test_get_git_status_for_dashboard_no_repo():\n with patch(\"src.services.resource_service.GitService\") as mock_git:\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n mock_git.return_value.get_repo.return_value = None\n\n result = service._get_git_status_for_dashboard(123)\n\n assert result is not None\n assert result[\"sync_status\"] == \"NO_REPO\"\n assert result[\"has_repo\"] is False\n\n\n# [/DEF:test_get_git_status_for_dashboard_no_repo:Function]\n" - }, - { - "contract_id": "test_get_last_task_for_resource", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 182, - "end_line": 213, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returns task summary with task_id and status", - "PRE": "tasks list with matching resource_id", - "PURPOSE": "Verify get_last_task_for_resource returns the most recent task for a given resource.", - "TEST": "_get_last_task_for_resource returns most recent task for resource" - }, - "relations": [ - { - "source_id": "test_get_last_task_for_resource", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_last_task_for_resource:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_last_task_for_resource returns most recent task for resource\n# @PRE: tasks list with matching resource_id\n# @POST: Returns task summary with task_id and status\n# @PURPOSE: Verify get_last_task_for_resource returns the most recent task for a given resource.\ndef test_get_last_task_for_resource():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Create mock tasks\n task1 = MagicMock()\n task1.id = \"task-old\"\n task1.status = \"SUCCESS\"\n task1.params = {\"resource_id\": \"dashboard-1\"}\n task1.created_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task2 = MagicMock()\n task2.id = \"task-new\"\n task2.status = \"RUNNING\"\n task2.params = {\"resource_id\": \"dashboard-1\"}\n task2.created_at = datetime(2024, 1, 1, 12, 0, 0)\n\n result = service._get_last_task_for_resource(\"dashboard-1\", [task1, task2])\n\n assert result is not None\n assert result[\"task_id\"] == \"task-new\" # Most recent\n assert result[\"status\"] == \"RUNNING\"\n\n\n# [/DEF:test_get_last_task_for_resource:Function]\n" - }, - { - "contract_id": "test_extract_resource_name_from_task", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 216, - "end_line": 244, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returns resource name or fallback", - "PRE": "task has resource_name in params", - "PURPOSE": "Verify extract_resource_name_from_task correctly parses resource names from task identifiers.", - "TEST": "_extract_resource_name_from_task extracts name from params" - }, - "relations": [ - { - "source_id": "test_extract_resource_name_from_task", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_extract_resource_name_from_task:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _extract_resource_name_from_task extracts name from params\n# @PRE: task has resource_name in params\n# @POST: Returns resource name or fallback\n# @PURPOSE: Verify extract_resource_name_from_task correctly parses resource names from task identifiers.\ndef test_extract_resource_name_from_task():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n # Task with resource_name\n task = MagicMock()\n task.id = \"task-123\"\n task.params = {\"resource_name\": \"My Dashboard\"}\n\n result = service._extract_resource_name_from_task(task)\n assert result == \"My Dashboard\"\n\n # Task without resource_name\n task2 = MagicMock()\n task2.id = \"task-456\"\n task2.params = {}\n\n result2 = service._extract_resource_name_from_task(task2)\n assert \"task-456\" in result2\n\n\n# [/DEF:test_extract_resource_name_from_task:Function]\n" - }, - { - "contract_id": "test_get_last_task_for_resource_empty_tasks", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 247, - "end_line": 262, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returns None", - "PRE": "tasks is empty list", - "PURPOSE": "Verify get_last_task_for_resource returns None when tasks list is empty.", - "TEST": "_get_last_task_for_resource returns None for empty tasks list" - }, - "relations": [ - { - "source_id": "test_get_last_task_for_resource_empty_tasks", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_last_task_for_resource_empty_tasks:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_last_task_for_resource returns None for empty tasks list\n# @PRE: tasks is empty list\n# @POST: Returns None\n# @PURPOSE: Verify get_last_task_for_resource returns None when tasks list is empty.\ndef test_get_last_task_for_resource_empty_tasks():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n result = service._get_last_task_for_resource(\"dashboard-1\", [])\n assert result is None\n\n\n# [/DEF:test_get_last_task_for_resource_empty_tasks:Function]\n" - }, - { - "contract_id": "test_get_last_task_for_resource_no_match", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 265, - "end_line": 286, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returns None", - "PRE": "tasks list has no matching resource_id", - "PURPOSE": "Verify get_last_task_for_resource returns None when no task matches the resource.", - "TEST": "_get_last_task_for_resource returns None when no tasks match resource_id" - }, - "relations": [ - { - "source_id": "test_get_last_task_for_resource_no_match", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_last_task_for_resource_no_match:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_last_task_for_resource returns None when no tasks match resource_id\n# @PRE: tasks list has no matching resource_id\n# @POST: Returns None\n# @PURPOSE: Verify get_last_task_for_resource returns None when no task matches the resource.\ndef test_get_last_task_for_resource_no_match():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n task = MagicMock()\n task.id = \"task-999\"\n task.status = \"SUCCESS\"\n task.params = {\"resource_id\": \"dashboard-99\"}\n task.created_at = datetime(2024, 1, 1, 10, 0, 0)\n\n result = service._get_last_task_for_resource(\"dashboard-1\", [task])\n assert result is None\n\n\n# [/DEF:test_get_last_task_for_resource_no_match:Function]\n" - }, - { - "contract_id": "test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 289, - "end_line": 330, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Latest task is selected deterministically and no exception is raised.", - "PRE": "Task list includes both timezone-aware and timezone-naive timestamps.", - "PURPOSE": "Verify get_dashboards_with_status handles mixed naive and aware datetimes without crashing.", - "TEST": "get_dashboards_with_status handles mixed naive/aware datetimes without comparison errors." - }, - "relations": [ - { - "source_id": "test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_dashboards_with_status handles mixed naive/aware datetimes without comparison errors.\n# @PRE: Task list includes both timezone-aware and timezone-naive timestamps.\n# @POST: Latest task is selected deterministically and no exception is raised.\n# @PURPOSE: Verify get_dashboards_with_status handles mixed naive and aware datetimes without crashing.\n@pytest.mark.asyncio\nasync def test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes():\n with (\n patch(\"src.services.resource_service.SupersetClient\") as mock_client,\n patch(\"src.services.resource_service.GitService\"),\n ):\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n mock_client.return_value.get_dashboards_summary.return_value = [\n {\"id\": 1, \"title\": \"Dashboard 1\", \"slug\": \"dash-1\"}\n ]\n\n task_naive = MagicMock()\n task_naive.id = \"task-naive\"\n task_naive.plugin_id = \"llm_dashboard_validation\"\n task_naive.status = \"SUCCESS\"\n task_naive.params = {\"dashboard_id\": \"1\", \"environment_id\": \"prod\"}\n task_naive.started_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task_aware = MagicMock()\n task_aware.id = \"task-aware\"\n task_aware.plugin_id = \"llm_dashboard_validation\"\n task_aware.status = \"SUCCESS\"\n task_aware.params = {\"dashboard_id\": \"1\", \"environment_id\": \"prod\"}\n task_aware.started_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_dashboards_with_status(env, [task_naive, task_aware])\n\n assert result[0][\"last_task\"][\"task_id\"] == \"task-aware\"\n\n\n# [/DEF:test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes:Function]\n" - }, - { - "contract_id": "test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 333, - "end_line": 380, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returned last_task points to newest task but preserves WARN as last meaningful validation state.", - "PRE": "Same dashboard has older WARN and newer UNKNOWN validation tasks.", - "PURPOSE": "Verify status ranking prefers decisive validation over newer unknown status.", - "TEST": "get_dashboards_with_status keeps latest task identity while falling back to older decisive validation status." - }, - "relations": [ - { - "source_id": "test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_dashboards_with_status keeps latest task identity while falling back to older decisive validation status.\n# @PRE: Same dashboard has older WARN and newer UNKNOWN validation tasks.\n# @POST: Returned last_task points to newest task but preserves WARN as last meaningful validation state.\n# @PURPOSE: Verify status ranking prefers decisive validation over newer unknown status.\n@pytest.mark.anyio\nasync def test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown():\n with (\n patch(\"src.services.resource_service.SupersetClient\") as mock_client,\n patch(\"src.services.resource_service.GitService\"),\n ):\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n mock_client.return_value.get_dashboards_summary.return_value = [\n {\"id\": 4, \"title\": \"Dashboard 4\", \"slug\": \"deck\"}\n ]\n\n task_warn = MagicMock()\n task_warn.id = \"task-warn\"\n task_warn.plugin_id = \"llm_dashboard_validation\"\n task_warn.status = \"SUCCESS\"\n task_warn.params = {\"dashboard_id\": \"4\", \"environment_id\": \"prod\"}\n task_warn.result = {\"status\": \"WARN\"}\n task_warn.started_at = datetime(2024, 1, 1, 11, 0, 0)\n\n task_unknown = MagicMock()\n task_unknown.id = \"task-unknown\"\n task_unknown.plugin_id = \"llm_dashboard_validation\"\n task_unknown.status = \"RUNNING\"\n task_unknown.params = {\"dashboard_id\": \"4\", \"environment_id\": \"prod\"}\n task_unknown.result = {\"status\": \"UNKNOWN\"}\n task_unknown.started_at = datetime(2024, 1, 1, 12, 0, 0)\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_dashboards_with_status(\n env, [task_warn, task_unknown]\n )\n\n assert result[0][\"last_task\"][\"task_id\"] == \"task-unknown\"\n assert result[0][\"last_task\"][\"status\"] == \"RUNNING\"\n assert result[0][\"last_task\"][\"validation_status\"] == \"WARN\"\n\n\n# [/DEF:test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown:Function]\n" - }, - { - "contract_id": "test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 383, - "end_line": 429, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Returned last_task keeps newest UNKNOWN task.", - "PRE": "Same dashboard has only UNKNOWN validation tasks.", - "PURPOSE": "Verify fallback to latest unknown status when no decisive history exists.", - "TEST": "get_dashboards_with_status still returns newest UNKNOWN when no decisive validation exists." - }, - "relations": [ - { - "source_id": "test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: get_dashboards_with_status still returns newest UNKNOWN when no decisive validation exists.\n# @PRE: Same dashboard has only UNKNOWN validation tasks.\n# @POST: Returned last_task keeps newest UNKNOWN task.\n# @PURPOSE: Verify fallback to latest unknown status when no decisive history exists.\n@pytest.mark.anyio\nasync def test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history():\n with (\n patch(\"src.services.resource_service.SupersetClient\") as mock_client,\n patch(\"src.services.resource_service.GitService\"),\n ):\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n mock_client.return_value.get_dashboards_summary.return_value = [\n {\"id\": 5, \"title\": \"Dashboard 5\", \"slug\": \"ops\"}\n ]\n\n task_unknown_old = MagicMock()\n task_unknown_old.id = \"task-unknown-old\"\n task_unknown_old.plugin_id = \"llm_dashboard_validation\"\n task_unknown_old.status = \"SUCCESS\"\n task_unknown_old.params = {\"dashboard_id\": \"5\", \"environment_id\": \"prod\"}\n task_unknown_old.result = {\"status\": \"UNKNOWN\"}\n task_unknown_old.started_at = datetime(2024, 1, 1, 11, 0, 0)\n\n task_unknown_new = MagicMock()\n task_unknown_new.id = \"task-unknown-new\"\n task_unknown_new.plugin_id = \"llm_dashboard_validation\"\n task_unknown_new.status = \"SUCCESS\"\n task_unknown_new.params = {\"dashboard_id\": \"5\", \"environment_id\": \"prod\"}\n task_unknown_new.result = {\"status\": \"UNKNOWN\"}\n task_unknown_new.started_at = datetime(2024, 1, 1, 12, 0, 0)\n\n env = MagicMock()\n env.id = \"prod\"\n\n result = await service.get_dashboards_with_status(\n env, [task_unknown_old, task_unknown_new]\n )\n\n assert result[0][\"last_task\"][\"task_id\"] == \"task-unknown-new\"\n assert result[0][\"last_task\"][\"validation_status\"] == \"UNKNOWN\"\n\n\n# [/DEF:test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history:Function]\n" - }, - { - "contract_id": "test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at", - "contract_type": "Function", - "file_path": "backend/src/services/__tests__/test_resource_service.py", - "start_line": 432, - "end_line": 463, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Latest task is returned without raising datetime comparison errors.", - "PRE": "Matching tasks include naive and aware created_at timestamps.", - "PURPOSE": "Verify get_last_task_for_resource correctly sorts mixed naive and aware created_at timestamps.", - "TEST": "_get_last_task_for_resource handles mixed naive/aware created_at values." - }, - "relations": [ - { - "source_id": "test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at", - "relation_type": "BINDS_TO", - "target_id": "TestResourceService", - "target_ref": "[TestResourceService]" - } - ], - "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": "unknown_tag", - "tag": "TEST", - "message": "@TEST is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "# [DEF:test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at:Function]\n# @RELATION: BINDS_TO ->[TestResourceService]\n# @TEST: _get_last_task_for_resource handles mixed naive/aware created_at values.\n# @PRE: Matching tasks include naive and aware created_at timestamps.\n# @POST: Latest task is returned without raising datetime comparison errors.\n# @PURPOSE: Verify get_last_task_for_resource correctly sorts mixed naive and aware created_at timestamps.\ndef test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at():\n from src.services.resource_service import ResourceService\n\n service = ResourceService()\n\n task_naive = MagicMock()\n task_naive.id = \"task-old\"\n task_naive.status = \"SUCCESS\"\n task_naive.params = {\"resource_id\": \"dashboard-1\"}\n task_naive.created_at = datetime(2024, 1, 1, 10, 0, 0)\n\n task_aware = MagicMock()\n task_aware.id = \"task-new\"\n task_aware.status = \"RUNNING\"\n task_aware.params = {\"resource_id\": \"dashboard-1\"}\n task_aware.created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)\n\n result = service._get_last_task_for_resource(\n \"dashboard-1\", [task_naive, task_aware]\n )\n\n assert result is not None\n assert result[\"task_id\"] == \"task-new\"\n\n\n# [/DEF:test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at:Function]\n" + "anchor_syntax": "region", + "body": "# #region services [C:2] [TYPE Module] [SEMANTICS lazy, export, package, circular-import]\n# @BRIEF Package initialization for services module\n# @NOTE: Only export services that don't cause circular imports\n# @NOTE: GitService, AuthService, LLMProviderService have circular import issues - import directly when needed\n\n# Lazy loading to avoid import issues in tests\n__all__ = ['MappingService', 'ResourceService']\n\ndef __getattr__(name):\n if name == 'MappingService':\n from .mapping_service import MappingService\n return MappingService\n if name == 'ResourceService':\n from .resource_service import ResourceService\n return ResourceService\n raise AttributeError(f\"module {__name__!r} has no attribute {name!r}\")\n# #endregion services\n" }, { "contract_id": "auth_service", "contract_type": "Module", "file_path": "backend/src/services/auth_service.py", "start_line": 1, - "end_line": 149, + "end_line": 143, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "[Credentials | ADFSClaims] -> [UserEntity | SessionToken]", "INVARIANT": "Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles.", "LAYER": "Domain", "POST": "User identity verified and session tokens issued according to role scopes.", "PRE": "Core auth models and security utilities available.", "PURPOSE": "Orchestrates credential authentication and ADFS JIT user provisioning.", - "SEMANTICS": [ - "auth", - "service", - "business-logic", - "login", - "jwt", - "adfs", - "jit-provisioning" - ], "SIDE_EFFECT": "Writes last login timestamps and JIT-provisions external users." }, "relations": [ { "source_id": "auth_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AuthRepository", "target_ref": "[AuthRepository]" }, { "source_id": "auth_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "verify_password", "target_ref": "[verify_password]" }, { "source_id": "auth_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "create_access_token", "target_ref": "[create_access_token]" }, { "source_id": "auth_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "User", "target_ref": "[User]" }, { "source_id": "auth_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "Role", "target_ref": "[Role]" } @@ -76878,2584 +57999,96 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:auth_service:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: auth, service, business-logic, login, jwt, adfs, jit-provisioning\n# @PURPOSE: Orchestrates credential authentication and ADFS JIT user provisioning.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[AuthRepository]\n# @RELATION: [DEPENDS_ON] ->[verify_password]\n# @RELATION: [DEPENDS_ON] ->[create_access_token]\n# @RELATION: [DEPENDS_ON] ->[User]\n# @RELATION: [DEPENDS_ON] ->[Role]\n# @INVARIANT: Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles.\n# @PRE: Core auth models and security utilities available.\n# @POST: User identity verified and session tokens issued according to role scopes.\n# @SIDE_EFFECT: Writes last login timestamps and JIT-provisions external users.\n# @DATA_CONTRACT: [Credentials | ADFSClaims] -> [UserEntity | SessionToken]\n\nfrom typing import Dict, Any, Optional, List\nfrom datetime import datetime\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.repository import AuthRepository\nfrom ..core.auth.security import verify_password\nfrom ..core.auth.jwt import create_access_token\nfrom ..core.auth.logger import log_security_event\nfrom ..models.auth import User, Role\nfrom ..core.logger import belief_scope\n\n\n# [DEF:AuthService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Provides high-level authentication services.\n# @RELATION: [DEPENDS_ON] ->[AuthRepository]\n# @RELATION: [DEPENDS_ON] ->[User]\n# @RELATION: [DEPENDS_ON] ->[Role]\nclass AuthService:\n # [DEF:AuthService_init:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST: self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT: Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT: Input(Session) -> Model(AuthRepository)\n # @PARAM: db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # [/DEF:AuthService_init:Function]\n\n # [DEF:AuthService.authenticate_user:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE: username and password are non-empty credential inputs.\n # @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT: Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT: Input(str username, str password) -> Output(User | None)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [CALLS] ->[verify_password]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @PARAM: username (str) - The username.\n # @PARAM: password (str) - The plain password.\n # @RETURN: Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> Optional[User]:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # [/DEF:AuthService.authenticate_user:Function]\n\n # [DEF:AuthService.create_session:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE: user is a valid User entity containing username and iterable roles with role.name values.\n # @POST: Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT: Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT: Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION: [CALLS] ->[create_access_token]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user (User) - The authenticated user.\n # @RETURN: Dict[str, str] - Session data.\n def create_session(self, user: User) -> Dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # [/DEF:AuthService.create_session:Function]\n\n # [DEF:AuthService.provision_adfs_user:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT: May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT: Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN: User - The provisioned user.\n def provision_adfs_user(self, user_info: Dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # [/DEF:AuthService.provision_adfs_user:Function]\n\n\n# [/DEF:AuthService:Class]\n\n# [/DEF:auth_service:Module]\n" + "anchor_syntax": "region", + "body": "# #region auth_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, credential, session, jwt]\n# @BRIEF Orchestrates credential authentication and ADFS JIT user provisioning.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [verify_password]\n# @RELATION DEPENDS_ON -> [create_access_token]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\n# @INVARIANT: Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles.\n# @PRE: Core auth models and security utilities available.\n# @POST: User identity verified and session tokens issued according to role scopes.\n# @SIDE_EFFECT: Writes last login timestamps and JIT-provisions external users.\n# @DATA_CONTRACT: [Credentials | ADFSClaims] -> [UserEntity | SessionToken]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.jwt import create_access_token\nfrom ..core.auth.logger import log_security_event\nfrom ..core.auth.repository import AuthRepository\nfrom ..core.auth.security import verify_password\nfrom ..core.logger import belief_scope\nfrom ..models.auth import User\n\n\n# #region AuthService [C:3] [TYPE Class]\n# @BRIEF Provides high-level authentication services.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\nclass AuthService:\n # region AuthService_init [TYPE Function]\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST: self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT: Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT: Input(Session) -> Model(AuthRepository)\n # @PARAM: db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # endregion AuthService_init\n\n # region AuthService.authenticate_user [TYPE Function]\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE: username and password are non-empty credential inputs.\n # @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT: Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT: Input(str username, str password) -> Output(User | None)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [CALLS] ->[verify_password]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @PARAM: username (str) - The username.\n # @PARAM: password (str) - The plain password.\n # @RETURN: Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> User | None:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.authenticate_user\n\n # region AuthService.create_session [TYPE Function]\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE: user is a valid User entity containing username and iterable roles with role.name values.\n # @POST: Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT: Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT: Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION: [CALLS] ->[create_access_token]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user (User) - The authenticated user.\n # @RETURN: Dict[str, str] - Session data.\n def create_session(self, user: User) -> dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # endregion AuthService.create_session\n\n # region AuthService.provision_adfs_user [TYPE Function]\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT: May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT: Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN: User - The provisioned user.\n def provision_adfs_user(self, user_info: dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.provision_adfs_user\n\n\n# #endregion AuthService\n\n# #endregion auth_service\n" }, { "contract_id": "AuthService", "contract_type": "Class", "file_path": "backend/src/services/auth_service.py", - "start_line": 29, - "end_line": 147, + "start_line": 28, + "end_line": 141, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Provides high-level authentication services." }, "relations": [ { "source_id": "AuthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AuthRepository", "target_ref": "[AuthRepository]" }, { "source_id": "AuthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "User", "target_ref": "[User]" }, { "source_id": "AuthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "Role", "target_ref": "[Role]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:AuthService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Provides high-level authentication services.\n# @RELATION: [DEPENDS_ON] ->[AuthRepository]\n# @RELATION: [DEPENDS_ON] ->[User]\n# @RELATION: [DEPENDS_ON] ->[Role]\nclass AuthService:\n # [DEF:AuthService_init:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST: self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT: Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT: Input(Session) -> Model(AuthRepository)\n # @PARAM: db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # [/DEF:AuthService_init:Function]\n\n # [DEF:AuthService.authenticate_user:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE: username and password are non-empty credential inputs.\n # @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT: Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT: Input(str username, str password) -> Output(User | None)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [CALLS] ->[verify_password]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @PARAM: username (str) - The username.\n # @PARAM: password (str) - The plain password.\n # @RETURN: Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> Optional[User]:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # [/DEF:AuthService.authenticate_user:Function]\n\n # [DEF:AuthService.create_session:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE: user is a valid User entity containing username and iterable roles with role.name values.\n # @POST: Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT: Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT: Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION: [CALLS] ->[create_access_token]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user (User) - The authenticated user.\n # @RETURN: Dict[str, str] - Session data.\n def create_session(self, user: User) -> Dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # [/DEF:AuthService.create_session:Function]\n\n # [DEF:AuthService.provision_adfs_user:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT: May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT: Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN: User - The provisioned user.\n def provision_adfs_user(self, user_info: Dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # [/DEF:AuthService.provision_adfs_user:Function]\n\n\n# [/DEF:AuthService:Class]\n" - }, - { - "contract_id": "AuthService_init", - "contract_type": "Function", - "file_path": "backend/src/services/auth_service.py", - "start_line": 36, - "end_line": 48, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "DATA_CONTRACT": "Input(Session) -> Model(AuthRepository)", - "PARAM": "db (Session) - SQLAlchemy session.", - "POST": "self.repo is initialized and ready for auth user/role CRUD operations.", - "PRE": "db is a valid SQLAlchemy Session instance bound to the auth persistence context.", - "PURPOSE": "Initializes the authentication service with repository access over an active DB session.", - "SIDE_EFFECT": "Allocates AuthRepository and binds it to the provided Session." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AuthService_init:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST: self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT: Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT: Input(Session) -> Model(AuthRepository)\n # @PARAM: db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # [/DEF:AuthService_init:Function]\n" - }, - { - "contract_id": "AuthService.authenticate_user", - "contract_type": "Function", - "file_path": "backend/src/services/auth_service.py", - "start_line": 50, - "end_line": 79, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input(str username, str password) -> Output(User | None)", - "PARAM": "password (str) - The plain password.", - "POST": "Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.", - "PRE": "username and password are non-empty credential inputs.", - "PURPOSE": "Validates credentials and account state for local username/password authentication.", - "RETURN": "Optional[User] - The authenticated user or None.", - "SIDE_EFFECT": "Persists last_login update for successful authentications via repository." - }, - "relations": [ - { - "source_id": "AuthService.authenticate_user", - "relation_type": "[DEPENDS_ON]", - "target_id": "AuthRepository", - "target_ref": "[AuthRepository]" - }, - { - "source_id": "AuthService.authenticate_user", - "relation_type": "[CALLS]", - "target_id": "verify_password", - "target_ref": "[verify_password]" - }, - { - "source_id": "AuthService.authenticate_user", - "relation_type": "[DEPENDS_ON]", - "target_id": "User", - "target_ref": "[User]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "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]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AuthService.authenticate_user:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE: username and password are non-empty credential inputs.\n # @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT: Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT: Input(str username, str password) -> Output(User | None)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [CALLS] ->[verify_password]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @PARAM: username (str) - The username.\n # @PARAM: password (str) - The plain password.\n # @RETURN: Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> Optional[User]:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # [/DEF:AuthService.authenticate_user:Function]\n" - }, - { - "contract_id": "AuthService.create_session", - "contract_type": "Function", - "file_path": "backend/src/services/auth_service.py", - "start_line": 81, - "end_line": 101, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input(User) -> Output(Dict[str, str]{access_token, token_type})", - "PARAM": "user (User) - The authenticated user.", - "POST": "Returns session dict with non-empty access_token and token_type='bearer'.", - "PRE": "user is a valid User entity containing username and iterable roles with role.name values.", - "PURPOSE": "Issues an access token payload for an already authenticated user.", - "RETURN": "Dict[str, str] - Session data.", - "SIDE_EFFECT": "Generates signed JWT via auth JWT provider." - }, - "relations": [ - { - "source_id": "AuthService.create_session", - "relation_type": "[CALLS]", - "target_id": "create_access_token", - "target_ref": "[create_access_token]" - }, - { - "source_id": "AuthService.create_session", - "relation_type": "[DEPENDS_ON]", - "target_id": "User", - "target_ref": "[User]" - }, - { - "source_id": "AuthService.create_session", - "relation_type": "[DEPENDS_ON]", - "target_id": "Role", - "target_ref": "[Role]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AuthService.create_session:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE: user is a valid User entity containing username and iterable roles with role.name values.\n # @POST: Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT: Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT: Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION: [CALLS] ->[create_access_token]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user (User) - The authenticated user.\n # @RETURN: Dict[str, str] - Session data.\n def create_session(self, user: User) -> Dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # [/DEF:AuthService.create_session:Function]\n" - }, - { - "contract_id": "AuthService.provision_adfs_user", - "contract_type": "Function", - "file_path": "backend/src/services/auth_service.py", - "start_line": 103, - "end_line": 144, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)", - "PARAM": "user_info (Dict[str, Any]) - Claims from ADFS token.", - "POST": "Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.", - "PRE": "user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.", - "PURPOSE": "Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.", - "RETURN": "User - The provisioned user.", - "SIDE_EFFECT": "May insert new User, mutate user.roles, commit transaction, and refresh ORM state." - }, - "relations": [ - { - "source_id": "AuthService.provision_adfs_user", - "relation_type": "[DEPENDS_ON]", - "target_id": "AuthRepository", - "target_ref": "[AuthRepository]" - }, - { - "source_id": "AuthService.provision_adfs_user", - "relation_type": "[DEPENDS_ON]", - "target_id": "User", - "target_ref": "[User]" - }, - { - "source_id": "AuthService.provision_adfs_user", - "relation_type": "[DEPENDS_ON]", - "target_id": "Role", - "target_ref": "[Role]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:AuthService.provision_adfs_user:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT: May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT: Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN: User - The provisioned user.\n def provision_adfs_user(self, user_info: Dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # [/DEF:AuthService.provision_adfs_user:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region AuthService [C:3] [TYPE Class]\n# @BRIEF Provides high-level authentication services.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\nclass AuthService:\n # region AuthService_init [TYPE Function]\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST: self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT: Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT: Input(Session) -> Model(AuthRepository)\n # @PARAM: db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # endregion AuthService_init\n\n # region AuthService.authenticate_user [TYPE Function]\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE: username and password are non-empty credential inputs.\n # @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT: Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT: Input(str username, str password) -> Output(User | None)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [CALLS] ->[verify_password]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @PARAM: username (str) - The username.\n # @PARAM: password (str) - The plain password.\n # @RETURN: Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> User | None:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.authenticate_user\n\n # region AuthService.create_session [TYPE Function]\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE: user is a valid User entity containing username and iterable roles with role.name values.\n # @POST: Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT: Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT: Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION: [CALLS] ->[create_access_token]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user (User) - The authenticated user.\n # @RETURN: Dict[str, str] - Session data.\n def create_session(self, user: User) -> dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # endregion AuthService.create_session\n\n # region AuthService.provision_adfs_user [TYPE Function]\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT: May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT: Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION: [DEPENDS_ON] ->[AuthRepository]\n # @RELATION: [DEPENDS_ON] ->[User]\n # @RELATION: [DEPENDS_ON] ->[Role]\n # @PARAM: user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN: User - The provisioned user.\n def provision_adfs_user(self, user_info: dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.provision_adfs_user\n\n\n# #endregion AuthService\n" }, { "contract_id": "CleanReleaseContracts", "contract_type": "Module", "file_path": "backend/src/services/clean_release/__init__.py", "start_line": 1, - "end_line": 20, + "end_line": 19, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", "PURPOSE": "Publish the canonical semantic root for the clean-release backend service cluster." }, "relations": [ { "source_id": "CleanReleaseContracts", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ComplianceOrchestrator", "target_ref": "[ComplianceOrchestrator]" }, { "source_id": "CleanReleaseContracts", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ManifestBuilder", "target_ref": "[ManifestBuilder]" }, { "source_id": "CleanReleaseContracts", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "PolicyEngine", "target_ref": "[PolicyEngine]" }, { "source_id": "CleanReleaseContracts", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "RepositoryRelations", "target_ref": "[RepositoryRelations]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseContracts:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: 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# [/DEF:CleanReleaseContracts:Module]\n" - }, - { - "contract_id": "TestAuditService", - "contract_type": "Module", - "file_path": "backend/src/services/clean_release/__tests__/test_audit_service.py", - "start_line": 1, - "end_line": 56, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Infra", - "PURPOSE": "Validate audit hooks emit expected log patterns for clean release lifecycle.", - "SEMANTICS": [ - "tests", - "clean-release", - "audit", - "logging" - ] - }, - "relations": [ - { - "source_id": "TestAuditService", - "relation_type": "[DEPENDS_ON]", - "target_id": "AuditService", - "target_ref": "[AuditService]" - } - ], - "schema_warnings": [ - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestAuditService:Module]\n# @RELATION: [DEPENDS_ON] ->[AuditService]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, clean-release, audit, logging\n# @PURPOSE: Validate audit hooks emit expected log patterns for clean release lifecycle.\n# @LAYER: Infra\n\nfrom unittest.mock import patch\nfrom src.services.clean_release.audit_service import (\n audit_preparation,\n audit_check_run,\n audit_report,\n)\n\n\n@patch(\"src.services.clean_release.audit_service.logger\")\n# [DEF:test_audit_preparation:Function]\n# @RELATION: BINDS_TO -> TestAuditService\n# @PURPOSE: Verify audit preparation stage correctly initializes and validates candidate state.\ndef test_audit_preparation(mock_logger):\n audit_preparation(\"cand-1\", \"PREPARED\")\n mock_logger.info.assert_called_with(\n \"[REASON] clean-release preparation candidate=cand-1 status=PREPARED\"\n )\n\n\n# [/DEF:test_audit_preparation:Function]\n\n\n@patch(\"src.services.clean_release.audit_service.logger\")\n# [DEF:test_audit_check_run:Function]\n# @RELATION: BINDS_TO -> TestAuditService\n# @PURPOSE: Verify audit check run executes all checks and collects results.\ndef test_audit_check_run(mock_logger):\n audit_check_run(\"check-1\", \"COMPLIANT\")\n mock_logger.info.assert_called_with(\n \"[REFLECT] clean-release check_run=check-1 final_status=COMPLIANT\"\n )\n\n\n# [/DEF:test_audit_check_run:Function]\n\n\n@patch(\"src.services.clean_release.audit_service.logger\")\n# [DEF:test_audit_report:Function]\n# @RELATION: BINDS_TO -> TestAuditService\n# @PURPOSE: Verify audit report generation aggregates check results into a structured report.\ndef test_audit_report(mock_logger):\n audit_report(\"rep-1\", \"cand-1\")\n mock_logger.info.assert_called_with(\n \"[EXPLORE] clean-release report_id=rep-1 candidate=cand-1\"\n )\n\n\n# [/DEF:test_audit_report:Function]\n# [/DEF:TestAuditService:Module]\n" - }, - { - "contract_id": "test_audit_preparation", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_audit_service.py", - "start_line": 17, - "end_line": 27, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify audit preparation stage correctly initializes and validates candidate state." - }, - "relations": [ - { - "source_id": "test_audit_preparation", - "relation_type": "BINDS_TO", - "target_id": "TestAuditService", - "target_ref": "TestAuditService" - } - ], - "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": "def", - "body": "# [DEF:test_audit_preparation:Function]\n# @RELATION: BINDS_TO -> TestAuditService\n# @PURPOSE: Verify audit preparation stage correctly initializes and validates candidate state.\ndef test_audit_preparation(mock_logger):\n audit_preparation(\"cand-1\", \"PREPARED\")\n mock_logger.info.assert_called_with(\n \"[REASON] clean-release preparation candidate=cand-1 status=PREPARED\"\n )\n\n\n# [/DEF:test_audit_preparation:Function]\n" - }, - { - "contract_id": "test_audit_check_run", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_audit_service.py", - "start_line": 31, - "end_line": 41, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify audit check run executes all checks and collects results." - }, - "relations": [ - { - "source_id": "test_audit_check_run", - "relation_type": "BINDS_TO", - "target_id": "TestAuditService", - "target_ref": "TestAuditService" - } - ], - "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": "def", - "body": "# [DEF:test_audit_check_run:Function]\n# @RELATION: BINDS_TO -> TestAuditService\n# @PURPOSE: Verify audit check run executes all checks and collects results.\ndef test_audit_check_run(mock_logger):\n audit_check_run(\"check-1\", \"COMPLIANT\")\n mock_logger.info.assert_called_with(\n \"[REFLECT] clean-release check_run=check-1 final_status=COMPLIANT\"\n )\n\n\n# [/DEF:test_audit_check_run:Function]\n" - }, - { - "contract_id": "test_audit_report", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_audit_service.py", - "start_line": 45, - "end_line": 55, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify audit report generation aggregates check results into a structured report." - }, - "relations": [ - { - "source_id": "test_audit_report", - "relation_type": "BINDS_TO", - "target_id": "TestAuditService", - "target_ref": "TestAuditService" - } - ], - "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": "def", - "body": "# [DEF:test_audit_report:Function]\n# @RELATION: BINDS_TO -> TestAuditService\n# @PURPOSE: Verify audit report generation aggregates check results into a structured report.\ndef test_audit_report(mock_logger):\n audit_report(\"rep-1\", \"cand-1\")\n mock_logger.info.assert_called_with(\n \"[EXPLORE] clean-release report_id=rep-1 candidate=cand-1\"\n )\n\n\n# [/DEF:test_audit_report:Function]\n" - }, - { - "contract_id": "TestComplianceOrchestrator", - "contract_type": "Module", - "file_path": "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py", - "start_line": 1, - "end_line": 168, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Failed mandatory stage forces BLOCKED terminal status.", - "LAYER": "Domain", - "PURPOSE": "Validate compliance orchestrator stage transitions and final status derivation.", - "SEMANTICS": [ - "tests", - "clean-release", - "orchestrator", - "stage-state-machine" - ] - }, - "relations": [ - { - "source_id": "TestComplianceOrchestrator", - "relation_type": "[DEPENDS_ON]", - "target_id": "ComplianceOrchestrator", - "target_ref": "[ComplianceOrchestrator]" - } - ], - "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 [DEPENDS_ON] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[DEPENDS_ON]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestComplianceOrchestrator:Module]\n# @RELATION: [DEPENDS_ON] ->[ComplianceOrchestrator]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, clean-release, orchestrator, stage-state-machine\n# @PURPOSE: Validate compliance orchestrator stage transitions and final status derivation.\n# @LAYER: Domain\n# @INVARIANT: Failed mandatory stage forces BLOCKED terminal status.\n\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom src.models.clean_release import (\n CheckFinalStatus,\n CheckStageName,\n CheckStageResult,\n CheckStageStatus,\n)\nfrom src.services.clean_release.compliance_orchestrator import (\n CleanComplianceOrchestrator,\n)\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:test_orchestrator_stage_failure_blocks_release:Function]\n# @RELATION: BINDS_TO -> TestComplianceOrchestrator\n# @PURPOSE: Verify mandatory stage failure forces BLOCKED final status.\ndef test_orchestrator_stage_failure_blocks_release():\n repository = CleanReleaseRepository()\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n triggered_by=\"tester\",\n execution_mode=\"tui\",\n )\n run = orchestrator.execute_stages(\n run,\n forced_results=[\n CheckStageResult(\n stage=CheckStageName.DATA_PURITY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.INTERNAL_SOURCES_ONLY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.NO_EXTERNAL_ENDPOINTS,\n status=CheckStageStatus.FAIL,\n details=\"external\",\n ),\n CheckStageResult(\n stage=CheckStageName.MANIFEST_CONSISTENCY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n ],\n )\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == CheckFinalStatus.BLOCKED\n\n\n# [/DEF:test_orchestrator_stage_failure_blocks_release:Function]\n\n\n# [DEF:test_orchestrator_compliant_candidate:Function]\n# @RELATION: BINDS_TO -> TestComplianceOrchestrator\n# @PURPOSE: Verify happy path where all mandatory stages pass yields COMPLIANT.\ndef test_orchestrator_compliant_candidate():\n repository = CleanReleaseRepository()\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n triggered_by=\"tester\",\n execution_mode=\"tui\",\n )\n run = orchestrator.execute_stages(\n run,\n forced_results=[\n CheckStageResult(\n stage=CheckStageName.DATA_PURITY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.INTERNAL_SOURCES_ONLY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.NO_EXTERNAL_ENDPOINTS,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.MANIFEST_CONSISTENCY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n ],\n )\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == CheckFinalStatus.COMPLIANT\n\n\n# [/DEF:test_orchestrator_compliant_candidate:Function]\n\n\n# [DEF:test_orchestrator_missing_stage_result:Function]\n# @RELATION: BINDS_TO -> TestComplianceOrchestrator\n# @PURPOSE: Verify incomplete mandatory stage set cannot end as COMPLIANT and results in FAILED.\ndef test_orchestrator_missing_stage_result():\n repository = CleanReleaseRepository()\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\"cand-1\", \"pol-1\", \"tester\", \"tui\")\n run = orchestrator.execute_stages(\n run,\n forced_results=[\n CheckStageResult(\n stage=CheckStageName.DATA_PURITY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n )\n ],\n )\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == CheckFinalStatus.FAILED\n\n\n# [/DEF:test_orchestrator_missing_stage_result:Function]\n\n\n# [DEF:test_orchestrator_report_generation_error:Function]\n# @RELATION: BINDS_TO -> TestComplianceOrchestrator\n# @PURPOSE: Verify downstream report errors do not mutate orchestrator final status.\ndef test_orchestrator_report_generation_error():\n repository = CleanReleaseRepository()\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\"cand-1\", \"pol-1\", \"tester\", \"tui\")\n run = orchestrator.finalize_run(run)\n assert run.final_status == CheckFinalStatus.FAILED\n\n with patch.object(\n ComplianceReportBuilder,\n \"build_report_payload\",\n side_effect=ValueError(\"Report error\"),\n ):\n builder = ComplianceReportBuilder(repository)\n with pytest.raises(ValueError, match=\"Report error\"):\n builder.build_report_payload(run, [])\n\n assert run.final_status == CheckFinalStatus.FAILED\n\n\n# [/DEF:test_orchestrator_report_generation_error:Function]\n# [/DEF:TestComplianceOrchestrator:Module]\n" - }, - { - "contract_id": "test_orchestrator_stage_failure_blocks_release", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py", - "start_line": 26, - "end_line": 69, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify mandatory stage failure forces BLOCKED final status." - }, - "relations": [ - { - "source_id": "test_orchestrator_stage_failure_blocks_release", - "relation_type": "BINDS_TO", - "target_id": "TestComplianceOrchestrator", - "target_ref": "TestComplianceOrchestrator" - } - ], - "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": "def", - "body": "# [DEF:test_orchestrator_stage_failure_blocks_release:Function]\n# @RELATION: BINDS_TO -> TestComplianceOrchestrator\n# @PURPOSE: Verify mandatory stage failure forces BLOCKED final status.\ndef test_orchestrator_stage_failure_blocks_release():\n repository = CleanReleaseRepository()\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n triggered_by=\"tester\",\n execution_mode=\"tui\",\n )\n run = orchestrator.execute_stages(\n run,\n forced_results=[\n CheckStageResult(\n stage=CheckStageName.DATA_PURITY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.INTERNAL_SOURCES_ONLY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.NO_EXTERNAL_ENDPOINTS,\n status=CheckStageStatus.FAIL,\n details=\"external\",\n ),\n CheckStageResult(\n stage=CheckStageName.MANIFEST_CONSISTENCY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n ],\n )\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == CheckFinalStatus.BLOCKED\n\n\n# [/DEF:test_orchestrator_stage_failure_blocks_release:Function]\n" - }, - { - "contract_id": "test_orchestrator_compliant_candidate", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py", - "start_line": 72, - "end_line": 115, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify happy path where all mandatory stages pass yields COMPLIANT." - }, - "relations": [ - { - "source_id": "test_orchestrator_compliant_candidate", - "relation_type": "BINDS_TO", - "target_id": "TestComplianceOrchestrator", - "target_ref": "TestComplianceOrchestrator" - } - ], - "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": "def", - "body": "# [DEF:test_orchestrator_compliant_candidate:Function]\n# @RELATION: BINDS_TO -> TestComplianceOrchestrator\n# @PURPOSE: Verify happy path where all mandatory stages pass yields COMPLIANT.\ndef test_orchestrator_compliant_candidate():\n repository = CleanReleaseRepository()\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n triggered_by=\"tester\",\n execution_mode=\"tui\",\n )\n run = orchestrator.execute_stages(\n run,\n forced_results=[\n CheckStageResult(\n stage=CheckStageName.DATA_PURITY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.INTERNAL_SOURCES_ONLY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.NO_EXTERNAL_ENDPOINTS,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n CheckStageResult(\n stage=CheckStageName.MANIFEST_CONSISTENCY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n ),\n ],\n )\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == CheckFinalStatus.COMPLIANT\n\n\n# [/DEF:test_orchestrator_compliant_candidate:Function]\n" - }, - { - "contract_id": "test_orchestrator_missing_stage_result", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py", - "start_line": 118, - "end_line": 141, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify incomplete mandatory stage set cannot end as COMPLIANT and results in FAILED." - }, - "relations": [ - { - "source_id": "test_orchestrator_missing_stage_result", - "relation_type": "BINDS_TO", - "target_id": "TestComplianceOrchestrator", - "target_ref": "TestComplianceOrchestrator" - } - ], - "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": "def", - "body": "# [DEF:test_orchestrator_missing_stage_result:Function]\n# @RELATION: BINDS_TO -> TestComplianceOrchestrator\n# @PURPOSE: Verify incomplete mandatory stage set cannot end as COMPLIANT and results in FAILED.\ndef test_orchestrator_missing_stage_result():\n repository = CleanReleaseRepository()\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\"cand-1\", \"pol-1\", \"tester\", \"tui\")\n run = orchestrator.execute_stages(\n run,\n forced_results=[\n CheckStageResult(\n stage=CheckStageName.DATA_PURITY,\n status=CheckStageStatus.PASS,\n details=\"ok\",\n )\n ],\n )\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == CheckFinalStatus.FAILED\n\n\n# [/DEF:test_orchestrator_missing_stage_result:Function]\n" - }, - { - "contract_id": "test_orchestrator_report_generation_error", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py", - "start_line": 144, - "end_line": 167, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify downstream report errors do not mutate orchestrator final status." - }, - "relations": [ - { - "source_id": "test_orchestrator_report_generation_error", - "relation_type": "BINDS_TO", - "target_id": "TestComplianceOrchestrator", - "target_ref": "TestComplianceOrchestrator" - } - ], - "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": "def", - "body": "# [DEF:test_orchestrator_report_generation_error:Function]\n# @RELATION: BINDS_TO -> TestComplianceOrchestrator\n# @PURPOSE: Verify downstream report errors do not mutate orchestrator final status.\ndef test_orchestrator_report_generation_error():\n repository = CleanReleaseRepository()\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\"cand-1\", \"pol-1\", \"tester\", \"tui\")\n run = orchestrator.finalize_run(run)\n assert run.final_status == CheckFinalStatus.FAILED\n\n with patch.object(\n ComplianceReportBuilder,\n \"build_report_payload\",\n side_effect=ValueError(\"Report error\"),\n ):\n builder = ComplianceReportBuilder(repository)\n with pytest.raises(ValueError, match=\"Report error\"):\n builder.build_report_payload(run, [])\n\n assert run.final_status == CheckFinalStatus.FAILED\n\n\n# [/DEF:test_orchestrator_report_generation_error:Function]\n" - }, - { - "contract_id": "TestManifestBuilder", - "contract_type": "Module", - "file_path": "backend/src/services/clean_release/__tests__/test_manifest_builder.py", - "start_line": 1, - "end_line": 58, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": "5", - "DATA_CONTRACT": "TestInput -> TestOutput", - "INVARIANT": "Same input artifacts produce identical deterministic hash.", - "LAYER": "Domain", - "POST": "All test assertions pass", - "PRE": "Test fixtures are properly initialized", - "PURPOSE": "Validate deterministic manifest generation behavior for US1.", - "SEMANTICS": [ - "tests", - "clean-release", - "manifest", - "deterministic" - ], - "SIDE_EFFECT": "None - test isolation" - }, - "relations": [ - { - "source_id": "TestManifestBuilder", - "relation_type": "[DEPENDS_ON]", - "target_id": "ManifestBuilder", - "target_ref": "[ManifestBuilder]" - } - ], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestManifestBuilder:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: tests, clean-release, manifest, deterministic\n# @PURPOSE: Validate deterministic manifest generation behavior for US1.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[ManifestBuilder]\n# @INVARIANT: Same input artifacts produce identical deterministic hash.\n# @PRE: Test fixtures are properly initialized\n# @POST: All test assertions pass\n# @SIDE_EFFECT: None - test isolation\n# @DATA_CONTRACT: TestInput -> TestOutput\n\nfrom src.services.clean_release.manifest_builder import build_distribution_manifest\n\n\n# [DEF:test_manifest_deterministic_hash_for_same_input:Function]\n# @RELATION: BINDS_TO -> TestManifestBuilder\n# @PURPOSE: Ensure hash is stable for same candidate/policy/artifact input.\n# @PRE: Same input lists are passed twice.\n# @POST: Hash and summary remain identical.\ndef test_manifest_deterministic_hash_for_same_input():\n artifacts = [\n {\n \"path\": \"a.yaml\",\n \"category\": \"system-init\",\n \"classification\": \"required-system\",\n \"reason\": \"required\",\n },\n {\n \"path\": \"b.yaml\",\n \"category\": \"test-data\",\n \"classification\": \"excluded-prohibited\",\n \"reason\": \"prohibited\",\n },\n ]\n\n manifest1 = build_distribution_manifest(\n manifest_id=\"m1\",\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n generated_by=\"tester\",\n artifacts=artifacts,\n )\n manifest2 = build_distribution_manifest(\n manifest_id=\"m2\",\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n generated_by=\"tester\",\n artifacts=artifacts,\n )\n\n assert manifest1.deterministic_hash == manifest2.deterministic_hash\n assert manifest1.summary.included_count == manifest2.summary.included_count\n assert manifest1.summary.excluded_count == manifest2.summary.excluded_count\n\n\n# [/DEF:test_manifest_deterministic_hash_for_same_input:Function]\n# [/DEF:TestManifestBuilder:Module]\n" - }, - { - "contract_id": "test_manifest_deterministic_hash_for_same_input", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_manifest_builder.py", - "start_line": 16, - "end_line": 57, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "POST": "Hash and summary remain identical.", - "PRE": "Same input lists are passed twice.", - "PURPOSE": "Ensure hash is stable for same candidate/policy/artifact input." - }, - "relations": [ - { - "source_id": "test_manifest_deterministic_hash_for_same_input", - "relation_type": "BINDS_TO", - "target_id": "TestManifestBuilder", - "target_ref": "TestManifestBuilder" - } - ], - "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": "def", - "body": "# [DEF:test_manifest_deterministic_hash_for_same_input:Function]\n# @RELATION: BINDS_TO -> TestManifestBuilder\n# @PURPOSE: Ensure hash is stable for same candidate/policy/artifact input.\n# @PRE: Same input lists are passed twice.\n# @POST: Hash and summary remain identical.\ndef test_manifest_deterministic_hash_for_same_input():\n artifacts = [\n {\n \"path\": \"a.yaml\",\n \"category\": \"system-init\",\n \"classification\": \"required-system\",\n \"reason\": \"required\",\n },\n {\n \"path\": \"b.yaml\",\n \"category\": \"test-data\",\n \"classification\": \"excluded-prohibited\",\n \"reason\": \"prohibited\",\n },\n ]\n\n manifest1 = build_distribution_manifest(\n manifest_id=\"m1\",\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n generated_by=\"tester\",\n artifacts=artifacts,\n )\n manifest2 = build_distribution_manifest(\n manifest_id=\"m2\",\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n generated_by=\"tester\",\n artifacts=artifacts,\n )\n\n assert manifest1.deterministic_hash == manifest2.deterministic_hash\n assert manifest1.summary.included_count == manifest2.summary.included_count\n assert manifest1.summary.excluded_count == manifest2.summary.excluded_count\n\n\n# [/DEF:test_manifest_deterministic_hash_for_same_input:Function]\n" - }, - { - "contract_id": "TestPolicyEngine", - "contract_type": "Module", - "file_path": "backend/src/services/clean_release/__tests__/test_policy_engine.py", - "start_line": 1, - "end_line": 4, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Contract testing for CleanPolicyEngine" - }, - "relations": [ - { - "source_id": "TestPolicyEngine", - "relation_type": "[DEPENDS_ON]", - "target_id": "PolicyEngine", - "target_ref": "[PolicyEngine]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestPolicyEngine:Module]\n# @RELATION: [DEPENDS_ON] ->[PolicyEngine]\n# @PURPOSE: Contract testing for CleanPolicyEngine\n# [/DEF:TestPolicyEngine:Module]\n" - }, - { - "contract_id": "test_policy_valid", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_policy_engine.py", - "start_line": 51, - "end_line": 63, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify policy validation passes when all required fields are present and valid.", - "TEST_EDGE": "missing_registry_ref" - }, - "relations": [ - { - "source_id": "test_policy_valid", - "relation_type": "BINDS_TO", - "target_id": "TestPolicyEngine", - "target_ref": "TestPolicyEngine" - } - ], - "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": "def", - "body": "# [DEF:test_policy_valid:Function]\n# @RELATION: BINDS_TO -> TestPolicyEngine\n# @PURPOSE: Verify policy validation passes when all required fields are present and valid.\ndef test_policy_valid(enterprise_clean_setup):\n policy, registry = enterprise_clean_setup\n engine = CleanPolicyEngine(policy, registry)\n result = engine.validate_policy()\n assert result.ok is True\n assert not result.blocking_reasons\n\n\n# @TEST_EDGE: missing_registry_ref\n# [/DEF:test_policy_valid:Function]\n" - }, - { - "contract_id": "test_missing_registry_ref", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_policy_engine.py", - "start_line": 66, - "end_line": 79, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify policy validation fails when registry_ref is missing.", - "TEST_EDGE": "conflicting_registry" - }, - "relations": [ - { - "source_id": "test_missing_registry_ref", - "relation_type": "BINDS_TO", - "target_id": "TestPolicyEngine", - "target_ref": "TestPolicyEngine" - } - ], - "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": "def", - "body": "# [DEF:test_missing_registry_ref:Function]\n# @RELATION: BINDS_TO -> TestPolicyEngine\n# @PURPOSE: Verify policy validation fails when registry_ref is missing.\ndef test_missing_registry_ref(enterprise_clean_setup):\n policy, registry = enterprise_clean_setup\n policy.internal_source_registry_ref = \" \"\n engine = CleanPolicyEngine(policy, registry)\n result = engine.validate_policy()\n assert result.ok is False\n assert \"Policy missing internal_source_registry_ref\" in result.blocking_reasons\n\n\n# @TEST_EDGE: conflicting_registry\n# [/DEF:test_missing_registry_ref:Function]\n" - }, - { - "contract_id": "test_conflicting_registry", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_policy_engine.py", - "start_line": 82, - "end_line": 98, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify policy engine rejects conflicting registry references.", - "TEST_INVARIANT": "deterministic_classification" - }, - "relations": [ - { - "source_id": "test_conflicting_registry", - "relation_type": "BINDS_TO", - "target_id": "TestPolicyEngine", - "target_ref": "TestPolicyEngine" - } - ], - "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": "def", - "body": "# [DEF:test_conflicting_registry:Function]\n# @RELATION: BINDS_TO -> TestPolicyEngine\n# @PURPOSE: Verify policy engine rejects conflicting registry references.\ndef test_conflicting_registry(enterprise_clean_setup):\n policy, registry = enterprise_clean_setup\n registry.registry_id = \"WRONG-REG\"\n engine = CleanPolicyEngine(policy, registry)\n result = engine.validate_policy()\n assert result.ok is False\n assert (\n \"Policy registry ref does not match provided registry\"\n in result.blocking_reasons\n )\n\n\n# @TEST_INVARIANT: deterministic_classification\n# [/DEF:test_conflicting_registry:Function]\n" - }, - { - "contract_id": "test_classify_artifact", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_policy_engine.py", - "start_line": 101, - "end_line": 123, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify policy engine correctly classifies artifacts based on source and type." - }, - "relations": [ - { - "source_id": "test_classify_artifact", - "relation_type": "BINDS_TO", - "target_id": "TestPolicyEngine", - "target_ref": "TestPolicyEngine" - } - ], - "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": "def", - "body": "# [DEF:test_classify_artifact:Function]\n# @RELATION: BINDS_TO -> TestPolicyEngine\n# @PURPOSE: Verify policy engine correctly classifies artifacts based on source and type.\ndef test_classify_artifact(enterprise_clean_setup):\n policy, registry = enterprise_clean_setup\n engine = CleanPolicyEngine(policy, registry)\n\n # Required\n assert (\n engine.classify_artifact({\"category\": \"core\", \"path\": \"p1\"})\n == \"required-system\"\n )\n # Prohibited\n assert (\n engine.classify_artifact({\"category\": \"demo\", \"path\": \"p2\"})\n == \"excluded-prohibited\"\n )\n # Allowed\n assert engine.classify_artifact({\"category\": \"others\", \"path\": \"p3\"}) == \"allowed\"\n\n\n# @TEST_EDGE: external_endpoint\n# [/DEF:test_classify_artifact:Function]\n" - }, - { - "contract_id": "test_validate_resource_source", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_policy_engine.py", - "start_line": 126, - "end_line": 144, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify validate_resource_source correctly validates or rejects resource source identifiers." - }, - "relations": [ - { - "source_id": "test_validate_resource_source", - "relation_type": "BINDS_TO", - "target_id": "TestPolicyEngine", - "target_ref": "TestPolicyEngine" - } - ], - "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": "def", - "body": "# [DEF:test_validate_resource_source:Function]\n# @RELATION: BINDS_TO -> TestPolicyEngine\n# @PURPOSE: Verify validate_resource_source correctly validates or rejects resource source identifiers.\ndef test_validate_resource_source(enterprise_clean_setup):\n policy, registry = enterprise_clean_setup\n engine = CleanPolicyEngine(policy, registry)\n\n # Internal (OK)\n res_ok = engine.validate_resource_source(\"internal.com\")\n assert res_ok.ok is True\n\n # External (Blocked)\n res_fail = engine.validate_resource_source(\"external.evil\")\n assert res_fail.ok is False\n assert res_fail.violation[\"category\"] == \"external-source\"\n assert res_fail.violation[\"blocked_release\"] is True\n\n\n# [/DEF:test_validate_resource_source:Function]\n" - }, - { - "contract_id": "test_evaluate_candidate", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_policy_engine.py", - "start_line": 147, - "end_line": 172, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify policy engine evaluates release candidates against configured policies." - }, - "relations": [ - { - "source_id": "test_evaluate_candidate", - "relation_type": "BINDS_TO", - "target_id": "TestPolicyEngine", - "target_ref": "TestPolicyEngine" - } - ], - "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": "def", - "body": "# [DEF:test_evaluate_candidate:Function]\n# @RELATION: BINDS_TO -> TestPolicyEngine\n# @PURPOSE: Verify policy engine evaluates release candidates against configured policies.\ndef test_evaluate_candidate(enterprise_clean_setup):\n policy, registry = enterprise_clean_setup\n engine = CleanPolicyEngine(policy, registry)\n\n artifacts = [\n {\"path\": \"core.js\", \"category\": \"core\"},\n {\"path\": \"demo.sql\", \"category\": \"demo\"},\n ]\n sources = [\"internal.com\", \"google.com\"]\n\n classified, violations = engine.evaluate_candidate(artifacts, sources)\n\n assert len(classified) == 2\n assert classified[0][\"classification\"] == \"required-system\"\n assert classified[1][\"classification\"] == \"excluded-prohibited\"\n\n # 1 violation for demo artifact + 1 for google.com source\n assert len(violations) == 2\n assert violations[0][\"category\"] == \"data-purity\"\n assert violations[1][\"category\"] == \"external-source\"\n\n\n# [/DEF:test_evaluate_candidate:Function]\n" - }, - { - "contract_id": "TestPreparationService", - "contract_type": "Module", - "file_path": "backend/src/services/clean_release/__tests__/test_preparation_service.py", - "start_line": 1, - "end_line": 229, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Candidate preparation always persists manifest and candidate status deterministically.", - "LAYER": "Domain", - "PURPOSE": "Validate release candidate preparation flow, including policy evaluation and manifest persisting.", - "SEMANTICS": [ - "tests", - "clean-release", - "preparation", - "flow" - ] - }, - "relations": [ - { - "source_id": "TestPreparationService", - "relation_type": "[DEPENDS_ON]", - "target_id": "PreparationService", - "target_ref": "[PreparationService]" - } - ], - "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 [DEPENDS_ON] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[DEPENDS_ON]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestPreparationService:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, clean-release, preparation, flow\n# @PURPOSE: Validate release candidate preparation flow, including policy evaluation and manifest persisting.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[PreparationService]\n# @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically.\n\nimport pytest\nfrom unittest.mock import MagicMock, patch\nfrom datetime import datetime, timezone\n\nfrom src.models.clean_release import (\n CleanProfilePolicy,\n ResourceSourceRegistry,\n ResourceSourceEntry,\n ReleaseCandidate,\n ReleaseCandidateStatus,\n ProfileType,\n DistributionManifest,\n)\nfrom src.services.clean_release.preparation_service import prepare_candidate\n\n\n# [DEF:_mock_policy:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Build a valid clean profile policy fixture for preparation tests.\ndef _mock_policy() -> CleanProfilePolicy:\n return CleanProfilePolicy(\n policy_id=\"pol-1\",\n policy_version=\"1.0.0\",\n active=True,\n prohibited_artifact_categories=[\"prohibited\"],\n required_system_categories=[\"system\"],\n external_source_forbidden=True,\n internal_source_registry_ref=\"reg-1\",\n effective_from=datetime.now(timezone.utc),\n profile=ProfileType.ENTERPRISE_CLEAN,\n )\n\n\n# [/DEF:_mock_policy:Function]\n\n\n# [DEF:_mock_registry:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Build an internal-only source registry fixture for preparation tests.\ndef _mock_registry() -> ResourceSourceRegistry:\n return ResourceSourceRegistry(\n registry_id=\"reg-1\",\n name=\"Reg\",\n entries=[\n ResourceSourceEntry(\n source_id=\"s1\",\n host=\"nexus.internal\",\n protocol=\"https\",\n purpose=\"pkg\",\n enabled=True,\n )\n ],\n updated_at=datetime.now(timezone.utc),\n updated_by=\"tester\",\n )\n\n\n# [/DEF:_mock_registry:Function]\n\n\n# [DEF:_mock_candidate:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Build a draft release candidate fixture with provided identifier.\ndef _mock_candidate(candidate_id: str) -> ReleaseCandidate:\n return ReleaseCandidate(\n candidate_id=candidate_id,\n version=\"1.0.0\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n created_at=datetime.now(timezone.utc),\n status=ReleaseCandidateStatus.DRAFT,\n created_by=\"tester\",\n source_snapshot_ref=\"v1.0.0-snapshot\",\n )\n\n\n# [/DEF:_mock_candidate:Function]\n\n\n# [DEF:test_prepare_candidate_success:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Verify candidate transitions to PREPARED when evaluation returns no violations.\n# @TEST_CONTRACT: [valid_candidate + active_policy + internal_sources + no_violations] -> [status=PREPARED, manifest_persisted, candidate_saved]\n# @TEST_SCENARIO: [prepare_success] -> [prepared status and persistence side effects are produced]\n# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON\n# @TEST_EDGE: [external_fail] -> [none; dependency interactions mocked and successful]\n# @TEST_INVARIANT: [prepared_flow_persists_state] -> VERIFIED_BY: [prepare_success]\ndef test_prepare_candidate_success():\n # Setup\n repository = MagicMock()\n candidate_id = \"cand-1\"\n candidate = _mock_candidate(candidate_id)\n repository.get_candidate.return_value = candidate\n repository.get_active_policy.return_value = _mock_policy()\n repository.get_registry.return_value = _mock_registry()\n\n artifacts = [{\"path\": \"file1.txt\", \"category\": \"system\"}]\n sources = [\"nexus.internal\"]\n\n # Execute\n with patch(\n \"src.services.clean_release.preparation_service.CleanPolicyEngine\"\n ) as MockEngine:\n mock_engine_instance = MockEngine.return_value\n mock_engine_instance.validate_policy.return_value.ok = True\n mock_engine_instance.evaluate_candidate.return_value = (\n [\n {\n \"path\": \"file1.txt\",\n \"category\": \"system\",\n \"classification\": \"required-system\",\n \"reason\": \"system-core\",\n }\n ],\n [],\n )\n\n result = prepare_candidate(\n repository, candidate_id, artifacts, sources, \"operator-1\"\n )\n\n # Verify\n assert result[\"status\"] == ReleaseCandidateStatus.PREPARED.value\n assert candidate.status == ReleaseCandidateStatus.PREPARED\n repository.save_manifest.assert_called_once()\n repository.save_candidate.assert_called_with(candidate)\n\n\n# [/DEF:test_prepare_candidate_success:Function]\n\n\n# [DEF:test_prepare_candidate_with_violations:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Verify candidate transitions to BLOCKED when evaluation returns blocking violations.\n# @TEST_CONTRACT: [valid_candidate + active_policy + evaluation_with_violations] -> [status=BLOCKED, violations_exposed]\n# @TEST_SCENARIO: [prepare_blocked_due_to_policy] -> [blocked status and violation list are produced]\n# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON\n# @TEST_EDGE: [external_fail] -> [none; dependency interactions mocked and successful]\n# @TEST_INVARIANT: [blocked_flow_reports_violations] -> VERIFIED_BY: [prepare_blocked_due_to_policy]\ndef test_prepare_candidate_with_violations():\n # Setup\n repository = MagicMock()\n candidate_id = \"cand-1\"\n candidate = _mock_candidate(candidate_id)\n repository.get_candidate.return_value = candidate\n repository.get_active_policy.return_value = _mock_policy()\n repository.get_registry.return_value = _mock_registry()\n\n artifacts = [{\"path\": \"bad.txt\", \"category\": \"prohibited\"}]\n sources = []\n\n # Execute\n with patch(\n \"src.services.clean_release.preparation_service.CleanPolicyEngine\"\n ) as MockEngine:\n mock_engine_instance = MockEngine.return_value\n mock_engine_instance.validate_policy.return_value.ok = True\n mock_engine_instance.evaluate_candidate.return_value = (\n [\n {\n \"path\": \"bad.txt\",\n \"category\": \"prohibited\",\n \"classification\": \"excluded-prohibited\",\n \"reason\": \"test-data\",\n }\n ],\n [{\"category\": \"data-purity\", \"blocked_release\": True}],\n )\n\n result = prepare_candidate(\n repository, candidate_id, artifacts, sources, \"operator-1\"\n )\n\n # Verify\n assert result[\"status\"] == ReleaseCandidateStatus.BLOCKED.value\n assert candidate.status == ReleaseCandidateStatus.BLOCKED\n assert len(result[\"violations\"]) == 1\n\n\n# [/DEF:test_prepare_candidate_with_violations:Function]\n\n\n# [DEF:test_prepare_candidate_not_found:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Verify preparation raises ValueError when candidate does not exist.\n# @TEST_CONTRACT: [missing_candidate] -> [ValueError('Candidate not found')]\n# @TEST_SCENARIO: [prepare_missing_candidate] -> [raises candidate not found error]\n# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON\n# @TEST_EDGE: [missing_field] -> [candidate lookup returns None]\n# @TEST_INVARIANT: [missing_candidate_is_rejected] -> VERIFIED_BY: [prepare_missing_candidate]\ndef test_prepare_candidate_not_found():\n repository = MagicMock()\n repository.get_candidate.return_value = None\n\n with pytest.raises(ValueError, match=\"Candidate not found\"):\n prepare_candidate(repository, \"non-existent\", [], [], \"op\")\n\n\n# [/DEF:test_prepare_candidate_not_found:Function]\n\n\n# [DEF:test_prepare_candidate_no_active_policy:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Verify preparation raises ValueError when no active policy is available.\n# @TEST_CONTRACT: [candidate_present + missing_active_policy] -> [ValueError('Active clean policy not found')]\n# @TEST_SCENARIO: [prepare_missing_policy] -> [raises active policy missing error]\n# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON\n# @TEST_EDGE: [invalid_type] -> [policy dependency resolves to None]\n# @TEST_INVARIANT: [active_policy_required] -> VERIFIED_BY: [prepare_missing_policy]\ndef test_prepare_candidate_no_active_policy():\n repository = MagicMock()\n repository.get_candidate.return_value = _mock_candidate(\"cand-1\")\n repository.get_active_policy.return_value = None\n\n with pytest.raises(ValueError, match=\"Active clean policy not found\"):\n prepare_candidate(repository, \"cand-1\", [], [], \"op\")\n\n\n# [/DEF:test_prepare_candidate_no_active_policy:Function]\n\n\n# [/DEF:TestPreparationService:Module]\n" - }, - { - "contract_id": "_mock_policy", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_preparation_service.py", - "start_line": 25, - "end_line": 42, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Build a valid clean profile policy fixture for preparation tests." - }, - "relations": [ - { - "source_id": "_mock_policy", - "relation_type": "BINDS_TO", - "target_id": "TestPreparationService", - "target_ref": "TestPreparationService" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_mock_policy:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Build a valid clean profile policy fixture for preparation tests.\ndef _mock_policy() -> CleanProfilePolicy:\n return CleanProfilePolicy(\n policy_id=\"pol-1\",\n policy_version=\"1.0.0\",\n active=True,\n prohibited_artifact_categories=[\"prohibited\"],\n required_system_categories=[\"system\"],\n external_source_forbidden=True,\n internal_source_registry_ref=\"reg-1\",\n effective_from=datetime.now(timezone.utc),\n profile=ProfileType.ENTERPRISE_CLEAN,\n )\n\n\n# [/DEF:_mock_policy:Function]\n" - }, - { - "contract_id": "_mock_registry", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_preparation_service.py", - "start_line": 45, - "end_line": 66, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Build an internal-only source registry fixture for preparation tests." - }, - "relations": [ - { - "source_id": "_mock_registry", - "relation_type": "BINDS_TO", - "target_id": "TestPreparationService", - "target_ref": "TestPreparationService" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_mock_registry:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Build an internal-only source registry fixture for preparation tests.\ndef _mock_registry() -> ResourceSourceRegistry:\n return ResourceSourceRegistry(\n registry_id=\"reg-1\",\n name=\"Reg\",\n entries=[\n ResourceSourceEntry(\n source_id=\"s1\",\n host=\"nexus.internal\",\n protocol=\"https\",\n purpose=\"pkg\",\n enabled=True,\n )\n ],\n updated_at=datetime.now(timezone.utc),\n updated_by=\"tester\",\n )\n\n\n# [/DEF:_mock_registry:Function]\n" - }, - { - "contract_id": "_mock_candidate", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_preparation_service.py", - "start_line": 69, - "end_line": 84, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Build a draft release candidate fixture with provided identifier." - }, - "relations": [ - { - "source_id": "_mock_candidate", - "relation_type": "BINDS_TO", - "target_id": "TestPreparationService", - "target_ref": "TestPreparationService" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_mock_candidate:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Build a draft release candidate fixture with provided identifier.\ndef _mock_candidate(candidate_id: str) -> ReleaseCandidate:\n return ReleaseCandidate(\n candidate_id=candidate_id,\n version=\"1.0.0\",\n profile=ProfileType.ENTERPRISE_CLEAN,\n created_at=datetime.now(timezone.utc),\n status=ReleaseCandidateStatus.DRAFT,\n created_by=\"tester\",\n source_snapshot_ref=\"v1.0.0-snapshot\",\n )\n\n\n# [/DEF:_mock_candidate:Function]\n" - }, - { - "contract_id": "test_prepare_candidate_success", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_preparation_service.py", - "start_line": 87, - "end_line": 136, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify candidate transitions to PREPARED when evaluation returns no violations.", - "TEST_CONTRACT": "[valid_candidate + active_policy + internal_sources + no_violations] -> [status=PREPARED, manifest_persisted, candidate_saved]", - "TEST_EDGE": "[external_fail] -> [none; dependency interactions mocked and successful]", - "TEST_FIXTURE": "[INLINE_MOCKS] -> INLINE_JSON", - "TEST_INVARIANT": "[prepared_flow_persists_state] -> VERIFIED_BY: [prepare_success]", - "TEST_SCENARIO": "[prepare_success] -> [prepared status and persistence side effects are produced]" - }, - "relations": [ - { - "source_id": "test_prepare_candidate_success", - "relation_type": "BINDS_TO", - "target_id": "TestPreparationService", - "target_ref": "TestPreparationService" - } - ], - "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 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": "def", - "body": "# [DEF:test_prepare_candidate_success:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Verify candidate transitions to PREPARED when evaluation returns no violations.\n# @TEST_CONTRACT: [valid_candidate + active_policy + internal_sources + no_violations] -> [status=PREPARED, manifest_persisted, candidate_saved]\n# @TEST_SCENARIO: [prepare_success] -> [prepared status and persistence side effects are produced]\n# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON\n# @TEST_EDGE: [external_fail] -> [none; dependency interactions mocked and successful]\n# @TEST_INVARIANT: [prepared_flow_persists_state] -> VERIFIED_BY: [prepare_success]\ndef test_prepare_candidate_success():\n # Setup\n repository = MagicMock()\n candidate_id = \"cand-1\"\n candidate = _mock_candidate(candidate_id)\n repository.get_candidate.return_value = candidate\n repository.get_active_policy.return_value = _mock_policy()\n repository.get_registry.return_value = _mock_registry()\n\n artifacts = [{\"path\": \"file1.txt\", \"category\": \"system\"}]\n sources = [\"nexus.internal\"]\n\n # Execute\n with patch(\n \"src.services.clean_release.preparation_service.CleanPolicyEngine\"\n ) as MockEngine:\n mock_engine_instance = MockEngine.return_value\n mock_engine_instance.validate_policy.return_value.ok = True\n mock_engine_instance.evaluate_candidate.return_value = (\n [\n {\n \"path\": \"file1.txt\",\n \"category\": \"system\",\n \"classification\": \"required-system\",\n \"reason\": \"system-core\",\n }\n ],\n [],\n )\n\n result = prepare_candidate(\n repository, candidate_id, artifacts, sources, \"operator-1\"\n )\n\n # Verify\n assert result[\"status\"] == ReleaseCandidateStatus.PREPARED.value\n assert candidate.status == ReleaseCandidateStatus.PREPARED\n repository.save_manifest.assert_called_once()\n repository.save_candidate.assert_called_with(candidate)\n\n\n# [/DEF:test_prepare_candidate_success:Function]\n" - }, - { - "contract_id": "test_prepare_candidate_with_violations", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_preparation_service.py", - "start_line": 139, - "end_line": 187, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify candidate transitions to BLOCKED when evaluation returns blocking violations.", - "TEST_CONTRACT": "[valid_candidate + active_policy + evaluation_with_violations] -> [status=BLOCKED, violations_exposed]", - "TEST_EDGE": "[external_fail] -> [none; dependency interactions mocked and successful]", - "TEST_FIXTURE": "[INLINE_MOCKS] -> INLINE_JSON", - "TEST_INVARIANT": "[blocked_flow_reports_violations] -> VERIFIED_BY: [prepare_blocked_due_to_policy]", - "TEST_SCENARIO": "[prepare_blocked_due_to_policy] -> [blocked status and violation list are produced]" - }, - "relations": [ - { - "source_id": "test_prepare_candidate_with_violations", - "relation_type": "BINDS_TO", - "target_id": "TestPreparationService", - "target_ref": "TestPreparationService" - } - ], - "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 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": "def", - "body": "# [DEF:test_prepare_candidate_with_violations:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Verify candidate transitions to BLOCKED when evaluation returns blocking violations.\n# @TEST_CONTRACT: [valid_candidate + active_policy + evaluation_with_violations] -> [status=BLOCKED, violations_exposed]\n# @TEST_SCENARIO: [prepare_blocked_due_to_policy] -> [blocked status and violation list are produced]\n# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON\n# @TEST_EDGE: [external_fail] -> [none; dependency interactions mocked and successful]\n# @TEST_INVARIANT: [blocked_flow_reports_violations] -> VERIFIED_BY: [prepare_blocked_due_to_policy]\ndef test_prepare_candidate_with_violations():\n # Setup\n repository = MagicMock()\n candidate_id = \"cand-1\"\n candidate = _mock_candidate(candidate_id)\n repository.get_candidate.return_value = candidate\n repository.get_active_policy.return_value = _mock_policy()\n repository.get_registry.return_value = _mock_registry()\n\n artifacts = [{\"path\": \"bad.txt\", \"category\": \"prohibited\"}]\n sources = []\n\n # Execute\n with patch(\n \"src.services.clean_release.preparation_service.CleanPolicyEngine\"\n ) as MockEngine:\n mock_engine_instance = MockEngine.return_value\n mock_engine_instance.validate_policy.return_value.ok = True\n mock_engine_instance.evaluate_candidate.return_value = (\n [\n {\n \"path\": \"bad.txt\",\n \"category\": \"prohibited\",\n \"classification\": \"excluded-prohibited\",\n \"reason\": \"test-data\",\n }\n ],\n [{\"category\": \"data-purity\", \"blocked_release\": True}],\n )\n\n result = prepare_candidate(\n repository, candidate_id, artifacts, sources, \"operator-1\"\n )\n\n # Verify\n assert result[\"status\"] == ReleaseCandidateStatus.BLOCKED.value\n assert candidate.status == ReleaseCandidateStatus.BLOCKED\n assert len(result[\"violations\"]) == 1\n\n\n# [/DEF:test_prepare_candidate_with_violations:Function]\n" - }, - { - "contract_id": "test_prepare_candidate_not_found", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_preparation_service.py", - "start_line": 190, - "end_line": 206, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify preparation raises ValueError when candidate does not exist.", - "TEST_CONTRACT": "[missing_candidate] -> [ValueError('Candidate not found')]", - "TEST_EDGE": "[missing_field] -> [candidate lookup returns None]", - "TEST_FIXTURE": "[INLINE_MOCKS] -> INLINE_JSON", - "TEST_INVARIANT": "[missing_candidate_is_rejected] -> VERIFIED_BY: [prepare_missing_candidate]", - "TEST_SCENARIO": "[prepare_missing_candidate] -> [raises candidate not found error]" - }, - "relations": [ - { - "source_id": "test_prepare_candidate_not_found", - "relation_type": "BINDS_TO", - "target_id": "TestPreparationService", - "target_ref": "TestPreparationService" - } - ], - "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 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": "def", - "body": "# [DEF:test_prepare_candidate_not_found:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Verify preparation raises ValueError when candidate does not exist.\n# @TEST_CONTRACT: [missing_candidate] -> [ValueError('Candidate not found')]\n# @TEST_SCENARIO: [prepare_missing_candidate] -> [raises candidate not found error]\n# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON\n# @TEST_EDGE: [missing_field] -> [candidate lookup returns None]\n# @TEST_INVARIANT: [missing_candidate_is_rejected] -> VERIFIED_BY: [prepare_missing_candidate]\ndef test_prepare_candidate_not_found():\n repository = MagicMock()\n repository.get_candidate.return_value = None\n\n with pytest.raises(ValueError, match=\"Candidate not found\"):\n prepare_candidate(repository, \"non-existent\", [], [], \"op\")\n\n\n# [/DEF:test_prepare_candidate_not_found:Function]\n" - }, - { - "contract_id": "test_prepare_candidate_no_active_policy", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_preparation_service.py", - "start_line": 209, - "end_line": 226, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify preparation raises ValueError when no active policy is available.", - "TEST_CONTRACT": "[candidate_present + missing_active_policy] -> [ValueError('Active clean policy not found')]", - "TEST_EDGE": "[invalid_type] -> [policy dependency resolves to None]", - "TEST_FIXTURE": "[INLINE_MOCKS] -> INLINE_JSON", - "TEST_INVARIANT": "[active_policy_required] -> VERIFIED_BY: [prepare_missing_policy]", - "TEST_SCENARIO": "[prepare_missing_policy] -> [raises active policy missing error]" - }, - "relations": [ - { - "source_id": "test_prepare_candidate_no_active_policy", - "relation_type": "BINDS_TO", - "target_id": "TestPreparationService", - "target_ref": "TestPreparationService" - } - ], - "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 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": "def", - "body": "# [DEF:test_prepare_candidate_no_active_policy:Function]\n# @RELATION: BINDS_TO -> TestPreparationService\n# @PURPOSE: Verify preparation raises ValueError when no active policy is available.\n# @TEST_CONTRACT: [candidate_present + missing_active_policy] -> [ValueError('Active clean policy not found')]\n# @TEST_SCENARIO: [prepare_missing_policy] -> [raises active policy missing error]\n# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON\n# @TEST_EDGE: [invalid_type] -> [policy dependency resolves to None]\n# @TEST_INVARIANT: [active_policy_required] -> VERIFIED_BY: [prepare_missing_policy]\ndef test_prepare_candidate_no_active_policy():\n repository = MagicMock()\n repository.get_candidate.return_value = _mock_candidate(\"cand-1\")\n repository.get_active_policy.return_value = None\n\n with pytest.raises(ValueError, match=\"Active clean policy not found\"):\n prepare_candidate(repository, \"cand-1\", [], [], \"op\")\n\n\n# [/DEF:test_prepare_candidate_no_active_policy:Function]\n" - }, - { - "contract_id": "TestReportBuilder", - "contract_type": "Module", - "file_path": "backend/src/services/clean_release/__tests__/test_report_builder.py", - "start_line": 1, - "end_line": 130, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "blocked run requires at least one blocking violation.", - "LAYER": "Domain", - "PURPOSE": "Validate compliance report builder counter integrity and blocked-run constraints.", - "SEMANTICS": [ - "tests", - "clean-release", - "report-builder", - "counters" - ] - }, - "relations": [ - { - "source_id": "TestReportBuilder", - "relation_type": "[DEPENDS_ON]", - "target_id": "ReportBuilder", - "target_ref": "[ReportBuilder]" - } - ], - "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 [DEPENDS_ON] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[DEPENDS_ON]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestReportBuilder:Module]\n# @RELATION: [DEPENDS_ON] ->[ReportBuilder]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, clean-release, report-builder, counters\n# @PURPOSE: Validate compliance report builder counter integrity and blocked-run constraints.\n# @LAYER: Domain\n# @INVARIANT: blocked run requires at least one blocking violation.\n\nfrom datetime import datetime, timezone\n\nimport pytest\n\nfrom src.models.clean_release import (\n CheckFinalStatus,\n ComplianceCheckRun,\n ComplianceViolation,\n ExecutionMode,\n ViolationCategory,\n ViolationSeverity,\n)\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 -> TestReportBuilder\n# @PURPOSE: Build terminal/non-terminal run fixtures for report builder tests.\ndef _terminal_run(status: CheckFinalStatus) -> ComplianceCheckRun:\n return ComplianceCheckRun(\n check_run_id=\"check-1\",\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n started_at=datetime.now(timezone.utc),\n finished_at=datetime.now(timezone.utc),\n final_status=status,\n triggered_by=\"tester\",\n execution_mode=ExecutionMode.TUI,\n checks=[],\n )\n\n\n# [/DEF:_terminal_run:Function]\n\n\n# [DEF:_blocking_violation:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Build a blocking violation fixture for blocked report scenarios.\ndef _blocking_violation() -> ComplianceViolation:\n return ComplianceViolation(\n violation_id=\"viol-1\",\n check_run_id=\"check-1\",\n category=ViolationCategory.EXTERNAL_SOURCE,\n severity=ViolationSeverity.CRITICAL,\n location=\"pypi.org\",\n remediation=\"replace\",\n blocked_release=True,\n detected_at=datetime.now(timezone.utc),\n )\n\n\n# [/DEF:_blocking_violation:Function]\n\n\n# [DEF:test_report_builder_blocked_requires_blocking_violations:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Verify BLOCKED run requires at least one blocking violation.\ndef test_report_builder_blocked_requires_blocking_violations():\n builder = ComplianceReportBuilder(CleanReleaseRepository())\n run = _terminal_run(CheckFinalStatus.BLOCKED)\n\n with pytest.raises(ValueError):\n builder.build_report_payload(run, [])\n\n\n# [/DEF:test_report_builder_blocked_requires_blocking_violations:Function]\n\n\n# [DEF:test_report_builder_blocked_with_two_violations:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Verify report builder generates conformant payload for a BLOCKED run with violations.\ndef test_report_builder_blocked_with_two_violations():\n builder = ComplianceReportBuilder(CleanReleaseRepository())\n run = _terminal_run(CheckFinalStatus.BLOCKED)\n v1 = _blocking_violation()\n v2 = _blocking_violation()\n v2.violation_id = \"viol-2\"\n v2.category = ViolationCategory.DATA_PURITY\n\n report = builder.build_report_payload(run, [v1, v2])\n\n assert report.check_run_id == run.check_run_id\n assert report.candidate_id == run.candidate_id\n assert report.final_status == CheckFinalStatus.BLOCKED\n assert report.violations_count == 2\n assert report.blocking_violations_count == 2\n\n\n# [/DEF:test_report_builder_blocked_with_two_violations:Function]\n\n\n# [DEF:test_report_builder_counter_consistency:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Verify violations counters remain consistent for blocking payload.\ndef test_report_builder_counter_consistency():\n builder = ComplianceReportBuilder(CleanReleaseRepository())\n run = _terminal_run(CheckFinalStatus.BLOCKED)\n report = builder.build_report_payload(run, [_blocking_violation()])\n\n assert report.violations_count == 1\n assert report.blocking_violations_count == 1\n\n\n# [/DEF:test_report_builder_counter_consistency:Function]\n\n\n# [DEF:test_missing_operator_summary:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Validate non-terminal run prevents operator summary/report generation.\ndef test_missing_operator_summary():\n builder = ComplianceReportBuilder(CleanReleaseRepository())\n run = _terminal_run(CheckFinalStatus.RUNNING)\n\n with pytest.raises(ValueError) as exc:\n builder.build_report_payload(run, [])\n\n assert \"Cannot build report for non-terminal run\" in str(exc.value)\n\n\n# [/DEF:test_missing_operator_summary:Function]\n# [/DEF:TestReportBuilder:Module]\n" - }, - { - "contract_id": "_terminal_run", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_report_builder.py", - "start_line": 25, - "end_line": 42, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Build terminal/non-terminal run fixtures for report builder tests." - }, - "relations": [ - { - "source_id": "_terminal_run", - "relation_type": "BINDS_TO", - "target_id": "TestReportBuilder", - "target_ref": "TestReportBuilder" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_terminal_run:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Build terminal/non-terminal run fixtures for report builder tests.\ndef _terminal_run(status: CheckFinalStatus) -> ComplianceCheckRun:\n return ComplianceCheckRun(\n check_run_id=\"check-1\",\n candidate_id=\"2026.03.03-rc1\",\n policy_id=\"policy-enterprise-clean-v1\",\n started_at=datetime.now(timezone.utc),\n finished_at=datetime.now(timezone.utc),\n final_status=status,\n triggered_by=\"tester\",\n execution_mode=ExecutionMode.TUI,\n checks=[],\n )\n\n\n# [/DEF:_terminal_run:Function]\n" - }, - { - "contract_id": "_blocking_violation", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_report_builder.py", - "start_line": 45, - "end_line": 61, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Build a blocking violation fixture for blocked report scenarios." - }, - "relations": [ - { - "source_id": "_blocking_violation", - "relation_type": "BINDS_TO", - "target_id": "TestReportBuilder", - "target_ref": "TestReportBuilder" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_blocking_violation:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Build a blocking violation fixture for blocked report scenarios.\ndef _blocking_violation() -> ComplianceViolation:\n return ComplianceViolation(\n violation_id=\"viol-1\",\n check_run_id=\"check-1\",\n category=ViolationCategory.EXTERNAL_SOURCE,\n severity=ViolationSeverity.CRITICAL,\n location=\"pypi.org\",\n remediation=\"replace\",\n blocked_release=True,\n detected_at=datetime.now(timezone.utc),\n )\n\n\n# [/DEF:_blocking_violation:Function]\n" - }, - { - "contract_id": "test_report_builder_blocked_requires_blocking_violations", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_report_builder.py", - "start_line": 64, - "end_line": 75, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify BLOCKED run requires at least one blocking violation." - }, - "relations": [ - { - "source_id": "test_report_builder_blocked_requires_blocking_violations", - "relation_type": "BINDS_TO", - "target_id": "TestReportBuilder", - "target_ref": "TestReportBuilder" - } - ], - "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": "def", - "body": "# [DEF:test_report_builder_blocked_requires_blocking_violations:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Verify BLOCKED run requires at least one blocking violation.\ndef test_report_builder_blocked_requires_blocking_violations():\n builder = ComplianceReportBuilder(CleanReleaseRepository())\n run = _terminal_run(CheckFinalStatus.BLOCKED)\n\n with pytest.raises(ValueError):\n builder.build_report_payload(run, [])\n\n\n# [/DEF:test_report_builder_blocked_requires_blocking_violations:Function]\n" - }, - { - "contract_id": "test_report_builder_blocked_with_two_violations", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_report_builder.py", - "start_line": 78, - "end_line": 98, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify report builder generates conformant payload for a BLOCKED run with violations." - }, - "relations": [ - { - "source_id": "test_report_builder_blocked_with_two_violations", - "relation_type": "BINDS_TO", - "target_id": "TestReportBuilder", - "target_ref": "TestReportBuilder" - } - ], - "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": "def", - "body": "# [DEF:test_report_builder_blocked_with_two_violations:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Verify report builder generates conformant payload for a BLOCKED run with violations.\ndef test_report_builder_blocked_with_two_violations():\n builder = ComplianceReportBuilder(CleanReleaseRepository())\n run = _terminal_run(CheckFinalStatus.BLOCKED)\n v1 = _blocking_violation()\n v2 = _blocking_violation()\n v2.violation_id = \"viol-2\"\n v2.category = ViolationCategory.DATA_PURITY\n\n report = builder.build_report_payload(run, [v1, v2])\n\n assert report.check_run_id == run.check_run_id\n assert report.candidate_id == run.candidate_id\n assert report.final_status == CheckFinalStatus.BLOCKED\n assert report.violations_count == 2\n assert report.blocking_violations_count == 2\n\n\n# [/DEF:test_report_builder_blocked_with_two_violations:Function]\n" - }, - { - "contract_id": "test_report_builder_counter_consistency", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_report_builder.py", - "start_line": 101, - "end_line": 113, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify violations counters remain consistent for blocking payload." - }, - "relations": [ - { - "source_id": "test_report_builder_counter_consistency", - "relation_type": "BINDS_TO", - "target_id": "TestReportBuilder", - "target_ref": "TestReportBuilder" - } - ], - "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": "def", - "body": "# [DEF:test_report_builder_counter_consistency:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Verify violations counters remain consistent for blocking payload.\ndef test_report_builder_counter_consistency():\n builder = ComplianceReportBuilder(CleanReleaseRepository())\n run = _terminal_run(CheckFinalStatus.BLOCKED)\n report = builder.build_report_payload(run, [_blocking_violation()])\n\n assert report.violations_count == 1\n assert report.blocking_violations_count == 1\n\n\n# [/DEF:test_report_builder_counter_consistency:Function]\n" - }, - { - "contract_id": "test_missing_operator_summary", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_report_builder.py", - "start_line": 116, - "end_line": 129, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Validate non-terminal run prevents operator summary/report generation." - }, - "relations": [ - { - "source_id": "test_missing_operator_summary", - "relation_type": "BINDS_TO", - "target_id": "TestReportBuilder", - "target_ref": "TestReportBuilder" - } - ], - "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": "def", - "body": "# [DEF:test_missing_operator_summary:Function]\n# @RELATION: BINDS_TO -> TestReportBuilder\n# @PURPOSE: Validate non-terminal run prevents operator summary/report generation.\ndef test_missing_operator_summary():\n builder = ComplianceReportBuilder(CleanReleaseRepository())\n run = _terminal_run(CheckFinalStatus.RUNNING)\n\n with pytest.raises(ValueError) as exc:\n builder.build_report_payload(run, [])\n\n assert \"Cannot build report for non-terminal run\" in str(exc.value)\n\n\n# [/DEF:test_missing_operator_summary:Function]\n" - }, - { - "contract_id": "TestSourceIsolation", - "contract_type": "Module", - "file_path": "backend/src/services/clean_release/__tests__/test_source_isolation.py", - "start_line": 1, - "end_line": 76, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "External endpoints always produce blocking violations.", - "LAYER": "Domain", - "PURPOSE": "Verify internal source registry validation behavior.", - "SEMANTICS": [ - "tests", - "clean-release", - "source-isolation", - "internal-only" - ] - }, - "relations": [ - { - "source_id": "TestSourceIsolation", - "relation_type": "[DEPENDS_ON]", - "target_id": "SourceIsolation", - "target_ref": "[SourceIsolation]" - } - ], - "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 [DEPENDS_ON] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[DEPENDS_ON]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestSourceIsolation:Module]\n# @RELATION: [DEPENDS_ON] ->[SourceIsolation]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, clean-release, source-isolation, internal-only\n# @PURPOSE: Verify internal source registry validation behavior.\n# @LAYER: Domain\n# @INVARIANT: External endpoints always produce blocking violations.\n\nfrom datetime import datetime, timezone\n\nfrom src.models.clean_release import ResourceSourceEntry, ResourceSourceRegistry\nfrom src.services.clean_release.source_isolation import validate_internal_sources\n\n\n# [DEF:_registry:Function]\n# @RELATION: BINDS_TO -> TestSourceIsolation\ndef _registry() -> ResourceSourceRegistry:\n return ResourceSourceRegistry(\n registry_id=\"registry-internal-v1\",\n name=\"Internal Sources\",\n entries=[\n ResourceSourceEntry(\n source_id=\"src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n ),\n ResourceSourceEntry(\n source_id=\"src-2\",\n host=\"pypi.intra.company.local\",\n protocol=\"https\",\n purpose=\"package-mirror\",\n enabled=True,\n ),\n ],\n updated_at=datetime.now(timezone.utc),\n updated_by=\"tester\",\n status=\"active\",\n )\n\n\n# [/DEF:_registry:Function]\n\n\n# [DEF:test_validate_internal_sources_all_internal_ok:Function]\n# @RELATION: BINDS_TO -> TestSourceIsolation\n# @PURPOSE: Verify validate_internal_sources passes when all sources are internal and allowed.\ndef test_validate_internal_sources_all_internal_ok():\n result = validate_internal_sources(\n registry=_registry(),\n endpoints=[\"repo.intra.company.local\", \"pypi.intra.company.local\"],\n )\n assert result[\"ok\"] is True\n assert result[\"violations\"] == []\n\n\n# [/DEF:test_validate_internal_sources_all_internal_ok:Function]\n\n\n# [DEF:test_validate_internal_sources_external_blocked:Function]\n# @RELATION: BINDS_TO -> TestSourceIsolation\n# @PURPOSE: Verify validate_internal_sources blocks external sources when policy requires internal-only.\ndef test_validate_internal_sources_external_blocked():\n result = validate_internal_sources(\n registry=_registry(),\n endpoints=[\"repo.intra.company.local\", \"pypi.org\"],\n )\n assert result[\"ok\"] is False\n assert len(result[\"violations\"]) == 1\n assert result[\"violations\"][0][\"category\"] == \"external-source\"\n assert result[\"violations\"][0][\"blocked_release\"] is True\n\n\n# [/DEF:test_validate_internal_sources_external_blocked:Function]\n# [/DEF:TestSourceIsolation:Module]\n" - }, - { - "contract_id": "_registry", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_source_isolation.py", - "start_line": 15, - "end_line": 43, - "tier": "TIER_1", - "complexity": 1, - "metadata": {}, - "relations": [ - { - "source_id": "_registry", - "relation_type": "BINDS_TO", - "target_id": "TestSourceIsolation", - "target_ref": "TestSourceIsolation" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:_registry:Function]\n# @RELATION: BINDS_TO -> TestSourceIsolation\ndef _registry() -> ResourceSourceRegistry:\n return ResourceSourceRegistry(\n registry_id=\"registry-internal-v1\",\n name=\"Internal Sources\",\n entries=[\n ResourceSourceEntry(\n source_id=\"src-1\",\n host=\"repo.intra.company.local\",\n protocol=\"https\",\n purpose=\"artifact-repo\",\n enabled=True,\n ),\n ResourceSourceEntry(\n source_id=\"src-2\",\n host=\"pypi.intra.company.local\",\n protocol=\"https\",\n purpose=\"package-mirror\",\n enabled=True,\n ),\n ],\n updated_at=datetime.now(timezone.utc),\n updated_by=\"tester\",\n status=\"active\",\n )\n\n\n# [/DEF:_registry:Function]\n" - }, - { - "contract_id": "test_validate_internal_sources_all_internal_ok", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_source_isolation.py", - "start_line": 46, - "end_line": 58, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify validate_internal_sources passes when all sources are internal and allowed." - }, - "relations": [ - { - "source_id": "test_validate_internal_sources_all_internal_ok", - "relation_type": "BINDS_TO", - "target_id": "TestSourceIsolation", - "target_ref": "TestSourceIsolation" - } - ], - "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": "def", - "body": "# [DEF:test_validate_internal_sources_all_internal_ok:Function]\n# @RELATION: BINDS_TO -> TestSourceIsolation\n# @PURPOSE: Verify validate_internal_sources passes when all sources are internal and allowed.\ndef test_validate_internal_sources_all_internal_ok():\n result = validate_internal_sources(\n registry=_registry(),\n endpoints=[\"repo.intra.company.local\", \"pypi.intra.company.local\"],\n )\n assert result[\"ok\"] is True\n assert result[\"violations\"] == []\n\n\n# [/DEF:test_validate_internal_sources_all_internal_ok:Function]\n" - }, - { - "contract_id": "test_validate_internal_sources_external_blocked", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_source_isolation.py", - "start_line": 61, - "end_line": 75, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify validate_internal_sources blocks external sources when policy requires internal-only." - }, - "relations": [ - { - "source_id": "test_validate_internal_sources_external_blocked", - "relation_type": "BINDS_TO", - "target_id": "TestSourceIsolation", - "target_ref": "TestSourceIsolation" - } - ], - "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": "def", - "body": "# [DEF:test_validate_internal_sources_external_blocked:Function]\n# @RELATION: BINDS_TO -> TestSourceIsolation\n# @PURPOSE: Verify validate_internal_sources blocks external sources when policy requires internal-only.\ndef test_validate_internal_sources_external_blocked():\n result = validate_internal_sources(\n registry=_registry(),\n endpoints=[\"repo.intra.company.local\", \"pypi.org\"],\n )\n assert result[\"ok\"] is False\n assert len(result[\"violations\"]) == 1\n assert result[\"violations\"][0][\"category\"] == \"external-source\"\n assert result[\"violations\"][0][\"blocked_release\"] is True\n\n\n# [/DEF:test_validate_internal_sources_external_blocked:Function]\n" - }, - { - "contract_id": "TestStages", - "contract_type": "Module", - "file_path": "backend/src/services/clean_release/__tests__/test_stages.py", - "start_line": 1, - "end_line": 74, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Domain", - "PURPOSE": "Validate final status derivation logic from stage results.", - "SEMANTICS": [ - "tests", - "clean-release", - "compliance", - "stages" - ] - }, - "relations": [ - { - "source_id": "TestStages", - "relation_type": "[DEPENDS_ON]", - "target_id": "ComplianceStages", - "target_ref": "[ComplianceStages]" - } - ], - "schema_warnings": [ - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TestStages:Module]\n# @RELATION: [DEPENDS_ON] ->[ComplianceStages]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, clean-release, compliance, stages\n# @PURPOSE: Validate final status derivation logic from stage results.\n# @LAYER: Domain\n\nfrom src.models.clean_release import (\n CheckFinalStatus,\n CheckStageName,\n CheckStageResult,\n CheckStageStatus,\n)\nfrom src.services.clean_release.stages import derive_final_status, MANDATORY_STAGE_ORDER\n\n\n# [DEF:test_derive_final_status_compliant:Function]\n# @RELATION: BINDS_TO -> TestStages\n# @PURPOSE: Verify derive_final_status returns compliant when all stages pass.\ndef test_derive_final_status_compliant():\n results = [\n CheckStageResult(stage=s, status=CheckStageStatus.PASS, details=\"ok\")\n for s in MANDATORY_STAGE_ORDER\n ]\n assert derive_final_status(results) == CheckFinalStatus.COMPLIANT\n\n\n# [/DEF:test_derive_final_status_compliant:Function]\n\n\n# [DEF:test_derive_final_status_blocked:Function]\n# @RELATION: BINDS_TO -> TestStages\n# @PURPOSE: Verify derive_final_status returns blocked when any stage fails.\ndef test_derive_final_status_blocked():\n results = [\n CheckStageResult(stage=s, status=CheckStageStatus.PASS, details=\"ok\")\n for s in MANDATORY_STAGE_ORDER\n ]\n results[1].status = CheckStageStatus.FAIL\n assert derive_final_status(results) == CheckFinalStatus.BLOCKED\n\n\n# [/DEF:test_derive_final_status_blocked:Function]\n\n\n# [DEF:test_derive_final_status_failed_missing:Function]\n# @RELATION: BINDS_TO -> TestStages\n# @PURPOSE: Verify derive_final_status returns failed when required stages are missing.\ndef test_derive_final_status_failed_missing():\n results = [\n CheckStageResult(\n stage=MANDATORY_STAGE_ORDER[0], status=CheckStageStatus.PASS, details=\"ok\"\n )\n ]\n assert derive_final_status(results) == CheckFinalStatus.FAILED\n\n\n# [/DEF:test_derive_final_status_failed_missing:Function]\n\n\n# [DEF:test_derive_final_status_failed_skipped:Function]\n# @RELATION: BINDS_TO -> TestStages\n# @PURPOSE: Verify derive_final_status returns failed when critical stages are skipped.\ndef test_derive_final_status_failed_skipped():\n results = [\n CheckStageResult(stage=s, status=CheckStageStatus.PASS, details=\"ok\")\n for s in MANDATORY_STAGE_ORDER\n ]\n results[2].status = CheckStageStatus.SKIPPED\n assert derive_final_status(results) == CheckFinalStatus.FAILED\n\n\n# [/DEF:test_derive_final_status_failed_skipped:Function]\n# [/DEF:TestStages:Module]\n" - }, - { - "contract_id": "test_derive_final_status_compliant", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_stages.py", - "start_line": 17, - "end_line": 28, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify derive_final_status returns compliant when all stages pass." - }, - "relations": [ - { - "source_id": "test_derive_final_status_compliant", - "relation_type": "BINDS_TO", - "target_id": "TestStages", - "target_ref": "TestStages" - } - ], - "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": "def", - "body": "# [DEF:test_derive_final_status_compliant:Function]\n# @RELATION: BINDS_TO -> TestStages\n# @PURPOSE: Verify derive_final_status returns compliant when all stages pass.\ndef test_derive_final_status_compliant():\n results = [\n CheckStageResult(stage=s, status=CheckStageStatus.PASS, details=\"ok\")\n for s in MANDATORY_STAGE_ORDER\n ]\n assert derive_final_status(results) == CheckFinalStatus.COMPLIANT\n\n\n# [/DEF:test_derive_final_status_compliant:Function]\n" - }, - { - "contract_id": "test_derive_final_status_blocked", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_stages.py", - "start_line": 31, - "end_line": 43, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify derive_final_status returns blocked when any stage fails." - }, - "relations": [ - { - "source_id": "test_derive_final_status_blocked", - "relation_type": "BINDS_TO", - "target_id": "TestStages", - "target_ref": "TestStages" - } - ], - "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": "def", - "body": "# [DEF:test_derive_final_status_blocked:Function]\n# @RELATION: BINDS_TO -> TestStages\n# @PURPOSE: Verify derive_final_status returns blocked when any stage fails.\ndef test_derive_final_status_blocked():\n results = [\n CheckStageResult(stage=s, status=CheckStageStatus.PASS, details=\"ok\")\n for s in MANDATORY_STAGE_ORDER\n ]\n results[1].status = CheckStageStatus.FAIL\n assert derive_final_status(results) == CheckFinalStatus.BLOCKED\n\n\n# [/DEF:test_derive_final_status_blocked:Function]\n" - }, - { - "contract_id": "test_derive_final_status_failed_missing", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_stages.py", - "start_line": 46, - "end_line": 58, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify derive_final_status returns failed when required stages are missing." - }, - "relations": [ - { - "source_id": "test_derive_final_status_failed_missing", - "relation_type": "BINDS_TO", - "target_id": "TestStages", - "target_ref": "TestStages" - } - ], - "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": "def", - "body": "# [DEF:test_derive_final_status_failed_missing:Function]\n# @RELATION: BINDS_TO -> TestStages\n# @PURPOSE: Verify derive_final_status returns failed when required stages are missing.\ndef test_derive_final_status_failed_missing():\n results = [\n CheckStageResult(\n stage=MANDATORY_STAGE_ORDER[0], status=CheckStageStatus.PASS, details=\"ok\"\n )\n ]\n assert derive_final_status(results) == CheckFinalStatus.FAILED\n\n\n# [/DEF:test_derive_final_status_failed_missing:Function]\n" - }, - { - "contract_id": "test_derive_final_status_failed_skipped", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/__tests__/test_stages.py", - "start_line": 61, - "end_line": 73, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify derive_final_status returns failed when critical stages are skipped." - }, - "relations": [ - { - "source_id": "test_derive_final_status_failed_skipped", - "relation_type": "BINDS_TO", - "target_id": "TestStages", - "target_ref": "TestStages" - } - ], - "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": "def", - "body": "# [DEF:test_derive_final_status_failed_skipped:Function]\n# @RELATION: BINDS_TO -> TestStages\n# @PURPOSE: Verify derive_final_status returns failed when critical stages are skipped.\ndef test_derive_final_status_failed_skipped():\n results = [\n CheckStageResult(stage=s, status=CheckStageStatus.PASS, details=\"ok\")\n for s in MANDATORY_STAGE_ORDER\n ]\n results[2].status = CheckStageStatus.SKIPPED\n assert derive_final_status(results) == CheckFinalStatus.FAILED\n\n\n# [/DEF:test_derive_final_status_failed_skipped:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "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": "_get_or_init_decisions_store", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 25, - "end_line": 39, + "start_line": 22, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -79493,15 +58126,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_or_init_decisions_store:Function]\n# @PURPOSE: 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 setattr(repository, \"approval_decisions\", decisions)\n return decisions\n\n\n# [/DEF:_get_or_init_decisions_store:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_latest_decision_for_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 42, - "end_line": 60, + "start_line": 39, + "end_line": 57, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -79539,15 +58172,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_latest_decision_for_candidate:Function]\n# @PURPOSE: 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=timezone.utc),\n reverse=True,\n )[0]\n\n\n# [/DEF:_latest_decision_for_candidate:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_resolve_candidate_and_report", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 63, - "end_line": 87, + "start_line": 60, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -79585,15 +58218,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_resolve_candidate_and_report:Function]\n# @PURPOSE: 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# [/DEF:_resolve_candidate_and_report:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "approve_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 90, - "end_line": 163, + "start_line": 87, + "end_line": 160, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -79631,15 +58264,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:approve_candidate:Function]\n# @PURPOSE: 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\"[REASON] 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: # noqa: BLE001\n logger.explore(\n f\"[EXPLORE] 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(timezone.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\"[REFLECT] Approval persisted candidate_id={candidate_id} decision_id={decision.id}\"\n )\n return decision\n\n\n# [/DEF:approve_candidate:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "reject_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/approval_service.py", - "start_line": 166, - "end_line": 211, + "start_line": 163, + "end_line": 208, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -79677,15 +58310,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:reject_candidate:Function]\n# @PURPOSE: 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\"[REASON] 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(timezone.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\"[REFLECT] Rejection persisted candidate_id={candidate_id} decision_id={decision.id}\"\n )\n return decision\n\n\n# [/DEF:reject_candidate:Function]\n" + "anchor_syntax": "region", + "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": "load_bootstrap_artifacts", "contract_type": "Function", "file_path": "backend/src/services/clean_release/artifact_catalog_loader.py", - "start_line": 18, - "end_line": 105, + "start_line": 15, + "end_line": 102, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -79723,15 +58356,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:load_bootstrap_artifacts:Function]\n# @PURPOSE: 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# [/DEF:load_bootstrap_artifacts:Function]\n" + "anchor_syntax": "region", + "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": "_validate_artifacts", "contract_type": "Function", "file_path": "backend/src/services/clean_release/candidate_service.py", - "start_line": 22, - "end_line": 47, + "start_line": 21, + "end_line": 46, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -79769,32 +58402,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_validate_artifacts:Function]\n# @PURPOSE: 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# [/DEF:_validate_artifacts:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ComplianceExecutionService", "contract_type": "Module", "file_path": "backend/src/services/clean_release/compliance_execution_service.py", "start_line": 1, - "end_line": 233, + "end_line": 230, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult]", "INVARIANT": "A run binds to exactly one candidate/manifest/policy/registry snapshot set.", "LAYER": "Domain", "POST": "Candidate-scoped compliance runs persist stage evidence, terminal status, and immutable report artifacts when execution succeeds.", "PRE": "Repository adapters, trusted policy resolution, and deterministic stage implementations are available for the run request.", "PURPOSE": "Create and execute compliance runs with trusted snapshots, deterministic stages, violations and immutable report persistence.", - "SEMANTICS": [ - "clean-release", - "compliance", - "execution", - "stages", - "immutable-evidence" - ], "SIDE_EFFECT": "Persists runs, stage results, violations, and reports through repository adapters and audit helpers." }, "relations": [ @@ -79843,15 +58469,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceExecutionService:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: clean-release, compliance, execution, stages, immutable-evidence\n# @PURPOSE: Create and execute compliance runs with trusted snapshots, deterministic stages, violations and immutable report persistence.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> RepositoryRelations\n# @RELATION: DEPENDS_ON -> PolicyResolutionService\n# @RELATION: DEPENDS_ON -> ComplianceStages\n# @RELATION: DEPENDS_ON -> ReportBuilder\n# @PRE: Repository adapters, trusted policy resolution, and deterministic stage implementations are available for the run request.\n# @POST: Candidate-scoped compliance runs persist stage evidence, terminal status, and immutable report artifacts when execution succeeds.\n# @SIDE_EFFECT: Persists runs, stage results, violations, and reports through repository adapters and audit helpers.\n# @DATA_CONTRACT: Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult]\n# @INVARIANT: A run binds to exactly one candidate/manifest/policy/registry snapshot set.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\nfrom typing import Any, Iterable, List, Optional, cast\nfrom uuid import uuid4\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.clean_release import (\n ComplianceReport,\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n DistributionManifest,\n)\nfrom .audit_service import audit_check_run, audit_report, audit_violation\nfrom .enums import ComplianceDecision, RunStatus\nfrom .exceptions import ComplianceRunError, PolicyResolutionError\nfrom .policy_resolution_service import resolve_trusted_policy_snapshots\nfrom .report_builder import ComplianceReportBuilder\nfrom .repository import CleanReleaseRepository\nfrom .stages import build_default_stages, derive_final_status\nfrom .stages.base import ComplianceStage, ComplianceStageContext, build_stage_run_record\n\n\nbelief_logger = cast(Any, logger)\n\n\n# [DEF:ComplianceExecutionResult:Class]\n# @PURPOSE: Return envelope for compliance execution with run/report and persisted stage artifacts.\n@dataclass\nclass ComplianceExecutionResult:\n run: ComplianceRun\n report: Optional[ComplianceReport]\n stage_runs: List[ComplianceStageRun]\n violations: List[ComplianceViolation]\n\n\n# [/DEF:ComplianceExecutionResult:Class]\n\n\n# [DEF:ComplianceExecutionService:Class]\n# @PURPOSE: Execute clean-release compliance lifecycle over trusted snapshots and immutable evidence.\n# @PRE: Database session active, candidate registered\n# @POST: Returns ComplianceReport with pass/fail status and violation details\n# @SIDE_EFFECT: Updates compliance status in database, logs violations\n# @DATA_CONTRACT: ComplianceCheckResult, ComplianceReport, Violation\nclass ComplianceExecutionService:\n TASK_PLUGIN_ID = \"clean-release-compliance\"\n\n def __init__(\n self,\n *,\n repository: CleanReleaseRepository,\n config_manager,\n stages: Optional[Iterable[ComplianceStage]] = None,\n ):\n self.repository = repository\n self.config_manager = config_manager\n self.stages = list(stages) if stages is not None else build_default_stages()\n self.report_builder = ComplianceReportBuilder(repository)\n\n # [DEF:_resolve_manifest:Function]\n # @PURPOSE: Resolve explicit manifest or fallback to latest candidate manifest.\n # @PRE: candidate exists.\n # @POST: Returns manifest snapshot or raises ComplianceRunError.\n def _resolve_manifest(\n self, candidate_id: str, manifest_id: Optional[str]\n ) -> DistributionManifest:\n with belief_scope(\"ComplianceExecutionService._resolve_manifest\"):\n manifest_id_value = cast(Optional[str], manifest_id)\n if manifest_id_value is not None and manifest_id_value != \"\":\n manifest = self.repository.get_manifest(manifest_id_value)\n if manifest is None:\n raise ComplianceRunError(\n f\"manifest '{manifest_id_value}' not found\"\n )\n if str(getattr(manifest, \"candidate_id\")) != candidate_id:\n raise ComplianceRunError(\"manifest does not belong to candidate\")\n return manifest\n\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n if not manifests:\n raise ComplianceRunError(f\"candidate '{candidate_id}' has no manifest\")\n return sorted(\n manifests, key=lambda item: item.manifest_version, reverse=True\n )[0]\n\n # [/DEF:_resolve_manifest:Function]\n\n # [DEF:_persist_stage_run:Function]\n # @PURPOSE: Persist stage run if repository supports stage records.\n # @POST: Stage run is persisted when adapter is available, otherwise no-op.\n def _persist_stage_run(self, stage_run: ComplianceStageRun) -> None:\n self.repository.save_stage_run(stage_run)\n\n # [/DEF:_persist_stage_run:Function]\n\n # [DEF:_persist_violations:Function]\n # @PURPOSE: Persist stage violations via repository adapters.\n # @POST: Violations are appended to repository evidence store.\n def _persist_violations(self, violations: List[ComplianceViolation]) -> None:\n for violation in violations:\n self.repository.save_violation(violation)\n\n # [/DEF:_persist_violations:Function]\n\n # [DEF:execute_run:Function]\n # @PURPOSE: Execute compliance run stages and finalize immutable report on terminal success.\n # @PRE: candidate exists and trusted policy/registry snapshots are resolvable.\n # @POST: Run and evidence are persisted; report exists for SUCCEEDED runs.\n def execute_run(\n self,\n *,\n candidate_id: str,\n requested_by: str,\n manifest_id: Optional[str] = None,\n ) -> ComplianceExecutionResult:\n with belief_scope(\"ComplianceExecutionService.execute_run\"):\n belief_logger.reason(\n f\"Starting compliance execution candidate_id={candidate_id}\"\n )\n\n candidate = self.repository.get_candidate(candidate_id)\n if candidate is None:\n raise ComplianceRunError(f\"candidate '{candidate_id}' not found\")\n\n manifest = self._resolve_manifest(candidate_id, manifest_id)\n\n try:\n policy_snapshot, registry_snapshot = resolve_trusted_policy_snapshots(\n config_manager=self.config_manager,\n repository=self.repository,\n )\n except PolicyResolutionError as exc:\n raise ComplianceRunError(str(exc)) from exc\n\n run = ComplianceRun(\n id=f\"run-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=str(getattr(manifest, \"id\")),\n manifest_digest=str(getattr(manifest, \"manifest_digest\")),\n policy_snapshot_id=str(getattr(policy_snapshot, \"id\")),\n registry_snapshot_id=str(getattr(registry_snapshot, \"id\")),\n requested_by=requested_by,\n requested_at=datetime.now(timezone.utc),\n started_at=datetime.now(timezone.utc),\n status=RunStatus.RUNNING.value,\n )\n self.repository.save_check_run(run)\n\n stage_runs: List[ComplianceStageRun] = []\n violations: List[ComplianceViolation] = []\n report: Optional[ComplianceReport] = None\n\n context = ComplianceStageContext(\n run=run,\n candidate=candidate,\n manifest=manifest,\n policy=policy_snapshot,\n registry=registry_snapshot,\n )\n\n try:\n for stage in self.stages:\n started = datetime.now(timezone.utc)\n result = stage.execute(context)\n finished = datetime.now(timezone.utc)\n\n stage_run = build_stage_run_record(\n run_id=str(getattr(run, \"id\")),\n stage_name=stage.stage_name,\n result=result,\n started_at=started,\n finished_at=finished,\n )\n self._persist_stage_run(stage_run)\n stage_runs.append(stage_run)\n\n if result.violations:\n self._persist_violations(result.violations)\n violations.extend(result.violations)\n\n setattr(run, \"final_status\", derive_final_status(stage_runs).value)\n setattr(run, \"status\", RunStatus.SUCCEEDED.value)\n setattr(run, \"finished_at\", datetime.now(timezone.utc))\n self.repository.save_check_run(run)\n\n report = self.report_builder.build_report_payload(run, violations)\n report = self.report_builder.persist_report(report)\n setattr(run, \"report_id\", str(getattr(report, \"id\")))\n self.repository.save_check_run(run)\n belief_logger.reflect(\n f\"[REFLECT] Compliance run completed run_id={getattr(run, 'id')} final_status={getattr(run, 'final_status', None)}\"\n )\n except Exception as exc: # noqa: BLE001\n setattr(run, \"status\", RunStatus.FAILED.value)\n setattr(run, \"final_status\", ComplianceDecision.ERROR.value)\n setattr(run, \"failure_reason\", str(exc))\n setattr(run, \"finished_at\", datetime.now(timezone.utc))\n self.repository.save_check_run(run)\n belief_logger.explore(\n f\"[EXPLORE] Compliance run failed run_id={getattr(run, 'id')}: {exc}\"\n )\n\n return ComplianceExecutionResult(\n run=run,\n report=report,\n stage_runs=stage_runs,\n violations=violations,\n )\n\n # [/DEF:execute_run:Function]\n\n\n# [/DEF:ComplianceExecutionService:Class]\n\n# [/DEF:ComplianceExecutionService:Module]\n" + "anchor_syntax": "region", + "body": "# #region ComplianceExecutionService [C:5] [TYPE Module] [SEMANTICS clean-release, execution, compliance, report, stage]\n# @BRIEF Create and execute compliance runs with trusted snapshots, deterministic stages, violations and immutable report persistence.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> RepositoryRelations\n# @RELATION DEPENDS_ON -> PolicyResolutionService\n# @RELATION DEPENDS_ON -> ComplianceStages\n# @RELATION DEPENDS_ON -> ReportBuilder\n# @PRE: Repository adapters, trusted policy resolution, and deterministic stage implementations are available for the run request.\n# @POST: Candidate-scoped compliance runs persist stage evidence, terminal status, and immutable report artifacts when execution succeeds.\n# @SIDE_EFFECT: Persists runs, stage results, violations, and reports through repository adapters and audit helpers.\n# @DATA_CONTRACT: Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult]\n# @INVARIANT: A run binds to exactly one candidate/manifest/policy/registry snapshot set.\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom dataclasses import dataclass\nfrom datetime import UTC, datetime\nfrom typing import Any, cast\nfrom uuid import uuid4\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.clean_release import (\n ComplianceReport,\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n DistributionManifest,\n)\nfrom .enums import ComplianceDecision, RunStatus\nfrom .exceptions import ComplianceRunError, PolicyResolutionError\nfrom .policy_resolution_service import resolve_trusted_policy_snapshots\nfrom .report_builder import ComplianceReportBuilder\nfrom .repository import CleanReleaseRepository\nfrom .stages import build_default_stages, derive_final_status\nfrom .stages.base import ComplianceStage, ComplianceStageContext, build_stage_run_record\n\nbelief_logger = cast(Any, logger)\n\n\n# #region ComplianceExecutionResult [TYPE Class]\n# @BRIEF Return envelope for compliance execution with run/report and persisted stage artifacts.\n@dataclass\nclass ComplianceExecutionResult:\n run: ComplianceRun\n report: ComplianceReport | None\n stage_runs: list[ComplianceStageRun]\n violations: list[ComplianceViolation]\n\n\n# #endregion ComplianceExecutionResult\n\n\n# #region ComplianceExecutionService [TYPE Class]\n# @BRIEF Execute clean-release compliance lifecycle over trusted snapshots and immutable evidence.\n# @PRE: Database session active, candidate registered\n# @POST: Returns ComplianceReport with pass/fail status and violation details\n# @SIDE_EFFECT: Updates compliance status in database, logs violations\n# @DATA_CONTRACT: ComplianceCheckResult, ComplianceReport, Violation\nclass ComplianceExecutionService:\n TASK_PLUGIN_ID = \"clean-release-compliance\"\n\n def __init__(\n self,\n *,\n repository: CleanReleaseRepository,\n config_manager,\n stages: Iterable[ComplianceStage] | None = None,\n ):\n self.repository = repository\n self.config_manager = config_manager\n self.stages = list(stages) if stages is not None else build_default_stages()\n self.report_builder = ComplianceReportBuilder(repository)\n\n # region _resolve_manifest [TYPE Function]\n # @PURPOSE: Resolve explicit manifest or fallback to latest candidate manifest.\n # @PRE: candidate exists.\n # @POST: Returns manifest snapshot or raises ComplianceRunError.\n def _resolve_manifest(\n self, candidate_id: str, manifest_id: str | None\n ) -> DistributionManifest:\n with belief_scope(\"ComplianceExecutionService._resolve_manifest\"):\n manifest_id_value = cast(str | None, manifest_id)\n if manifest_id_value is not None and manifest_id_value != \"\":\n manifest = self.repository.get_manifest(manifest_id_value)\n if manifest is None:\n raise ComplianceRunError(\n f\"manifest '{manifest_id_value}' not found\"\n )\n if str(manifest.candidate_id) != candidate_id:\n raise ComplianceRunError(\"manifest does not belong to candidate\")\n return manifest\n\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n if not manifests:\n raise ComplianceRunError(f\"candidate '{candidate_id}' has no manifest\")\n return sorted(\n manifests, key=lambda item: item.manifest_version, reverse=True\n )[0]\n\n # endregion _resolve_manifest\n\n # region _persist_stage_run [TYPE Function]\n # @PURPOSE: Persist stage run if repository supports stage records.\n # @POST: Stage run is persisted when adapter is available, otherwise no-op.\n def _persist_stage_run(self, stage_run: ComplianceStageRun) -> None:\n self.repository.save_stage_run(stage_run)\n\n # endregion _persist_stage_run\n\n # region _persist_violations [TYPE Function]\n # @PURPOSE: Persist stage violations via repository adapters.\n # @POST: Violations are appended to repository evidence store.\n def _persist_violations(self, violations: list[ComplianceViolation]) -> None:\n for violation in violations:\n self.repository.save_violation(violation)\n\n # endregion _persist_violations\n\n # region execute_run [TYPE Function]\n # @PURPOSE: Execute compliance run stages and finalize immutable report on terminal success.\n # @PRE: candidate exists and trusted policy/registry snapshots are resolvable.\n # @POST: Run and evidence are persisted; report exists for SUCCEEDED runs.\n def execute_run(\n self,\n *,\n candidate_id: str,\n requested_by: str,\n manifest_id: str | None = None,\n ) -> ComplianceExecutionResult:\n with belief_scope(\"ComplianceExecutionService.execute_run\"):\n belief_logger.reason(\n f\"Starting compliance execution candidate_id={candidate_id}\"\n )\n\n candidate = self.repository.get_candidate(candidate_id)\n if candidate is None:\n raise ComplianceRunError(f\"candidate '{candidate_id}' not found\")\n\n manifest = self._resolve_manifest(candidate_id, manifest_id)\n\n try:\n policy_snapshot, registry_snapshot = resolve_trusted_policy_snapshots(\n config_manager=self.config_manager,\n repository=self.repository,\n )\n except PolicyResolutionError as exc:\n raise ComplianceRunError(str(exc)) from exc\n\n run = ComplianceRun(\n id=f\"run-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=str(manifest.id),\n manifest_digest=str(manifest.manifest_digest),\n policy_snapshot_id=str(policy_snapshot.id),\n registry_snapshot_id=str(registry_snapshot.id),\n requested_by=requested_by,\n requested_at=datetime.now(UTC),\n started_at=datetime.now(UTC),\n status=RunStatus.RUNNING.value,\n )\n self.repository.save_check_run(run)\n\n stage_runs: list[ComplianceStageRun] = []\n violations: list[ComplianceViolation] = []\n report: ComplianceReport | None = None\n\n context = ComplianceStageContext(\n run=run,\n candidate=candidate,\n manifest=manifest,\n policy=policy_snapshot,\n registry=registry_snapshot,\n )\n\n try:\n for stage in self.stages:\n started = datetime.now(UTC)\n result = stage.execute(context)\n finished = datetime.now(UTC)\n\n stage_run = build_stage_run_record(\n run_id=str(run.id),\n stage_name=stage.stage_name,\n result=result,\n started_at=started,\n finished_at=finished,\n )\n self._persist_stage_run(stage_run)\n stage_runs.append(stage_run)\n\n if result.violations:\n self._persist_violations(result.violations)\n violations.extend(result.violations)\n\n run.final_status = derive_final_status(stage_runs).value\n run.status = RunStatus.SUCCEEDED.value\n run.finished_at = datetime.now(UTC)\n self.repository.save_check_run(run)\n\n report = self.report_builder.build_report_payload(run, violations)\n report = self.report_builder.persist_report(report)\n run.report_id = str(report.id)\n self.repository.save_check_run(run)\n belief_logger.reflect(\n f\"Compliance run completed run_id={run.id} final_status={getattr(run, 'final_status', None)}\"\n )\n except Exception as exc:\n run.status = RunStatus.FAILED.value\n run.final_status = ComplianceDecision.ERROR.value\n run.failure_reason = str(exc)\n run.finished_at = datetime.now(UTC)\n self.repository.save_check_run(run)\n belief_logger.explore(\n f\"Compliance run failed run_id={run.id}: {exc}\"\n )\n\n return ComplianceExecutionResult(\n run=run,\n report=report,\n stage_runs=stage_runs,\n violations=violations,\n )\n\n # endregion execute_run\n\n\n# #endregion ComplianceExecutionService\n\n# #endregion ComplianceExecutionService\n" }, { "contract_id": "ComplianceExecutionResult", "contract_type": "Class", "file_path": "backend/src/services/clean_release/compliance_execution_service.py", - "start_line": 44, - "end_line": 54, + "start_line": 41, + "end_line": 51, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -79869,195 +58495,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceExecutionResult:Class]\n# @PURPOSE: Return envelope for compliance execution with run/report and persisted stage artifacts.\n@dataclass\nclass ComplianceExecutionResult:\n run: ComplianceRun\n report: Optional[ComplianceReport]\n stage_runs: List[ComplianceStageRun]\n violations: List[ComplianceViolation]\n\n\n# [/DEF:ComplianceExecutionResult:Class]\n" - }, - { - "contract_id": "_resolve_manifest", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/compliance_execution_service.py", - "start_line": 78, - "end_line": 104, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns manifest snapshot or raises ComplianceRunError.", - "PRE": "candidate exists.", - "PURPOSE": "Resolve explicit manifest or fallback to latest candidate manifest." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_resolve_manifest:Function]\n # @PURPOSE: Resolve explicit manifest or fallback to latest candidate manifest.\n # @PRE: candidate exists.\n # @POST: Returns manifest snapshot or raises ComplianceRunError.\n def _resolve_manifest(\n self, candidate_id: str, manifest_id: Optional[str]\n ) -> DistributionManifest:\n with belief_scope(\"ComplianceExecutionService._resolve_manifest\"):\n manifest_id_value = cast(Optional[str], manifest_id)\n if manifest_id_value is not None and manifest_id_value != \"\":\n manifest = self.repository.get_manifest(manifest_id_value)\n if manifest is None:\n raise ComplianceRunError(\n f\"manifest '{manifest_id_value}' not found\"\n )\n if str(getattr(manifest, \"candidate_id\")) != candidate_id:\n raise ComplianceRunError(\"manifest does not belong to candidate\")\n return manifest\n\n manifests = self.repository.get_manifests_by_candidate(candidate_id)\n if not manifests:\n raise ComplianceRunError(f\"candidate '{candidate_id}' has no manifest\")\n return sorted(\n manifests, key=lambda item: item.manifest_version, reverse=True\n )[0]\n\n # [/DEF:_resolve_manifest:Function]\n" - }, - { - "contract_id": "_persist_stage_run", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/compliance_execution_service.py", - "start_line": 106, - "end_line": 112, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Stage run is persisted when adapter is available, otherwise no-op.", - "PURPOSE": "Persist stage run if repository supports stage records." - }, - "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": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_persist_stage_run:Function]\n # @PURPOSE: Persist stage run if repository supports stage records.\n # @POST: Stage run is persisted when adapter is available, otherwise no-op.\n def _persist_stage_run(self, stage_run: ComplianceStageRun) -> None:\n self.repository.save_stage_run(stage_run)\n\n # [/DEF:_persist_stage_run:Function]\n" - }, - { - "contract_id": "_persist_violations", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/compliance_execution_service.py", - "start_line": 114, - "end_line": 121, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Violations are appended to repository evidence store.", - "PURPOSE": "Persist stage violations via repository adapters." - }, - "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": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_persist_violations:Function]\n # @PURPOSE: Persist stage violations via repository adapters.\n # @POST: Violations are appended to repository evidence store.\n def _persist_violations(self, violations: List[ComplianceViolation]) -> None:\n for violation in violations:\n self.repository.save_violation(violation)\n\n # [/DEF:_persist_violations:Function]\n" - }, - { - "contract_id": "execute_run", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/compliance_execution_service.py", - "start_line": 123, - "end_line": 228, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Run and evidence are persisted; report exists for SUCCEEDED runs.", - "PRE": "candidate exists and trusted policy/registry snapshots are resolvable.", - "PURPOSE": "Execute compliance run stages and finalize immutable report on terminal success." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:execute_run:Function]\n # @PURPOSE: Execute compliance run stages and finalize immutable report on terminal success.\n # @PRE: candidate exists and trusted policy/registry snapshots are resolvable.\n # @POST: Run and evidence are persisted; report exists for SUCCEEDED runs.\n def execute_run(\n self,\n *,\n candidate_id: str,\n requested_by: str,\n manifest_id: Optional[str] = None,\n ) -> ComplianceExecutionResult:\n with belief_scope(\"ComplianceExecutionService.execute_run\"):\n belief_logger.reason(\n f\"Starting compliance execution candidate_id={candidate_id}\"\n )\n\n candidate = self.repository.get_candidate(candidate_id)\n if candidate is None:\n raise ComplianceRunError(f\"candidate '{candidate_id}' not found\")\n\n manifest = self._resolve_manifest(candidate_id, manifest_id)\n\n try:\n policy_snapshot, registry_snapshot = resolve_trusted_policy_snapshots(\n config_manager=self.config_manager,\n repository=self.repository,\n )\n except PolicyResolutionError as exc:\n raise ComplianceRunError(str(exc)) from exc\n\n run = ComplianceRun(\n id=f\"run-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=str(getattr(manifest, \"id\")),\n manifest_digest=str(getattr(manifest, \"manifest_digest\")),\n policy_snapshot_id=str(getattr(policy_snapshot, \"id\")),\n registry_snapshot_id=str(getattr(registry_snapshot, \"id\")),\n requested_by=requested_by,\n requested_at=datetime.now(timezone.utc),\n started_at=datetime.now(timezone.utc),\n status=RunStatus.RUNNING.value,\n )\n self.repository.save_check_run(run)\n\n stage_runs: List[ComplianceStageRun] = []\n violations: List[ComplianceViolation] = []\n report: Optional[ComplianceReport] = None\n\n context = ComplianceStageContext(\n run=run,\n candidate=candidate,\n manifest=manifest,\n policy=policy_snapshot,\n registry=registry_snapshot,\n )\n\n try:\n for stage in self.stages:\n started = datetime.now(timezone.utc)\n result = stage.execute(context)\n finished = datetime.now(timezone.utc)\n\n stage_run = build_stage_run_record(\n run_id=str(getattr(run, \"id\")),\n stage_name=stage.stage_name,\n result=result,\n started_at=started,\n finished_at=finished,\n )\n self._persist_stage_run(stage_run)\n stage_runs.append(stage_run)\n\n if result.violations:\n self._persist_violations(result.violations)\n violations.extend(result.violations)\n\n setattr(run, \"final_status\", derive_final_status(stage_runs).value)\n setattr(run, \"status\", RunStatus.SUCCEEDED.value)\n setattr(run, \"finished_at\", datetime.now(timezone.utc))\n self.repository.save_check_run(run)\n\n report = self.report_builder.build_report_payload(run, violations)\n report = self.report_builder.persist_report(report)\n setattr(run, \"report_id\", str(getattr(report, \"id\")))\n self.repository.save_check_run(run)\n belief_logger.reflect(\n f\"[REFLECT] Compliance run completed run_id={getattr(run, 'id')} final_status={getattr(run, 'final_status', None)}\"\n )\n except Exception as exc: # noqa: BLE001\n setattr(run, \"status\", RunStatus.FAILED.value)\n setattr(run, \"final_status\", ComplianceDecision.ERROR.value)\n setattr(run, \"failure_reason\", str(exc))\n setattr(run, \"finished_at\", datetime.now(timezone.utc))\n self.repository.save_check_run(run)\n belief_logger.explore(\n f\"[EXPLORE] Compliance run failed run_id={getattr(run, 'id')}: {exc}\"\n )\n\n return ComplianceExecutionResult(\n run=run,\n report=report,\n stage_runs=stage_runs,\n violations=violations,\n )\n\n # [/DEF:execute_run:Function]\n" + "anchor_syntax": "region", + "body": "# #region ComplianceExecutionResult [TYPE Class]\n# @BRIEF Return envelope for compliance execution with run/report and persisted stage artifacts.\n@dataclass\nclass ComplianceExecutionResult:\n run: ComplianceRun\n report: ComplianceReport | None\n stage_runs: list[ComplianceStageRun]\n violations: list[ComplianceViolation]\n\n\n# #endregion ComplianceExecutionResult\n" }, { "contract_id": "ComplianceOrchestrator", "contract_type": "Module", "file_path": "backend/src/services/clean_release/compliance_orchestrator.py", "start_line": 1, - "end_line": 275, + "end_line": 266, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Manifest -> ComplianceReport", "INVARIANT": "COMPLIANT is impossible when any mandatory stage fails.", "LAYER": "Domain", "POST": "OrchestrationResult with compliance status", "PRE": "ManifestService and PolicyEngine are available", "PURPOSE": "Execute mandatory clean compliance stages and produce final COMPLIANT/BLOCKED/FAILED outcome.", - "SEMANTICS": [ - "clean-release", - "orchestrator", - "compliance-gate", - "stages" - ], "SIDE_EFFECT": "Triggers compliance checks; may modify manifest state", "TEST_CONTRACT": "ComplianceCheckRun -> ComplianceCheckRun", "TEST_EDGE": "report_generation_error -> Downstream reporting failure does not alter orchestrator status derivation contract", @@ -80067,19 +58523,19 @@ "relations": [ { "source_id": "ComplianceOrchestrator", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ComplianceStages", "target_ref": "[ComplianceStages]" }, { "source_id": "ComplianceOrchestrator", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "RepositoryRelations", "target_ref": "[RepositoryRelations]" }, { "source_id": "ComplianceOrchestrator", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" } @@ -80164,71 +58620,17 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceOrchestrator:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: clean-release, orchestrator, compliance-gate, stages\n# @PURPOSE: Execute mandatory clean compliance stages and produce final COMPLIANT/BLOCKED/FAILED outcome.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[ComplianceStages]\n# @RELATION: [DEPENDS_ON] ->[RepositoryRelations]\n# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels]\n# @INVARIANT: COMPLIANT is impossible when any mandatory stage fails.\n# @TEST_CONTRACT: ComplianceCheckRun -> ComplianceCheckRun\n# @TEST_FIXTURE: compliant_candidate -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json\n# @TEST_EDGE: stage_failure_blocks_release -> Mandatory stage returns FAIL and final status becomes BLOCKED\n# @TEST_EDGE: missing_stage_result -> Finalization with incomplete/empty mandatory stage set must not produce COMPLIANT\n# @TEST_EDGE: report_generation_error -> Downstream reporting failure does not alter orchestrator status derivation contract\n# @TEST_INVARIANT: compliant_requires_all_mandatory_pass -> VERIFIED_BY: [stage_failure_blocks_release]\n# @PRE: ManifestService and PolicyEngine are available\n# @POST: OrchestrationResult with compliance status\n# @SIDE_EFFECT: Triggers compliance checks; may modify manifest state\n# @DATA_CONTRACT: Manifest -> ComplianceReport\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\nfrom typing import List, Optional\nfrom uuid import uuid4\n\nfrom .enums import (\n RunStatus,\n ComplianceDecision,\n ComplianceStageName,\n ViolationCategory,\n ViolationSeverity,\n)\nfrom ...models.clean_release import (\n ComplianceRun,\n ComplianceStageRun,\n ComplianceViolation,\n CheckFinalStatus,\n)\nfrom .policy_engine import CleanPolicyEngine\nfrom .repository import CleanReleaseRepository\nfrom .stages import derive_final_status\nfrom ...core.logger import belief_scope, logger\n\n\n# [DEF:CleanComplianceOrchestrator:Class]\n# @PURPOSE: Coordinate clean-release compliance verification stages.\nclass CleanComplianceOrchestrator:\n # [DEF:__init__:Function]\n # @PURPOSE: Bind repository dependency used for orchestrator persistence and lookups.\n # @PRE: repository is a valid CleanReleaseRepository instance with required methods.\n # @POST: self.repository is assigned and used by all orchestration steps.\n # @SIDE_EFFECT: Stores repository reference on orchestrator instance.\n # @DATA_CONTRACT: Input -> CleanReleaseRepository, Output -> None\n def __init__(self, repository: CleanReleaseRepository):\n with belief_scope(\"CleanComplianceOrchestrator.__init__\"):\n self.repository = repository\n\n # [/DEF:__init__:Function]\n\n # [DEF:start_check_run:Function]\n # @PURPOSE: Initiate a new compliance run session.\n # @PRE: candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.\n # @POST: Returns initialized ComplianceRun in RUNNING state persisted in repository.\n # @SIDE_EFFECT: Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run.\n # @DATA_CONTRACT: Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun\n def start_check_run(\n self,\n candidate_id: str,\n policy_id: str,\n requested_by: str | None = None,\n manifest_id: str | None = None,\n **legacy_kwargs,\n ) -> ComplianceRun:\n with belief_scope(\"start_check_run\"):\n actor = requested_by or legacy_kwargs.get(\"triggered_by\") or \"system\"\n execution_mode = (\n str(legacy_kwargs.get(\"execution_mode\") or \"\").strip().lower()\n )\n manifest_id_value = manifest_id\n\n if manifest_id_value and str(manifest_id_value).strip().lower() in {\n \"tui\",\n \"api\",\n \"scheduler\",\n }:\n logger.reason(\n \"Detected legacy positional execution_mode passed through manifest_id slot\",\n extra={\n \"candidate_id\": candidate_id,\n \"execution_mode\": manifest_id_value,\n },\n )\n execution_mode = str(manifest_id_value).strip().lower()\n manifest_id_value = None\n\n manifest = (\n self.repository.get_manifest(manifest_id_value)\n if manifest_id_value\n else None\n )\n policy = self.repository.get_policy(policy_id)\n\n if manifest_id_value and manifest is None:\n logger.explore(\n \"Manifest lookup missed during run start; rejecting explicit manifest contract\",\n extra={\n \"candidate_id\": candidate_id,\n \"manifest_id\": manifest_id_value,\n },\n )\n raise ValueError(\"Manifest or Policy not found\")\n\n if policy is None:\n logger.explore(\n \"Policy lookup missed during run start; using compatibility placeholder snapshot\",\n extra={\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"execution_mode\": execution_mode or \"unspecified\",\n },\n )\n\n manifest_id_value = manifest_id_value or f\"manifest-{candidate_id}\"\n manifest_digest = getattr(manifest, \"manifest_digest\", \"pending\")\n registry_snapshot_id = (\n getattr(policy, \"registry_snapshot_id\", None)\n or getattr(policy, \"internal_source_registry_ref\", None)\n or \"pending\"\n )\n\n check_run = ComplianceRun(\n id=f\"check-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=manifest_id_value,\n manifest_digest=manifest_digest,\n policy_snapshot_id=policy_id,\n registry_snapshot_id=registry_snapshot_id,\n requested_by=actor,\n requested_at=datetime.now(timezone.utc),\n started_at=datetime.now(timezone.utc),\n status=RunStatus.RUNNING,\n )\n logger.reflect(\n \"Initialized compliance run with compatibility-safe dependency placeholders\",\n extra={\n \"run_id\": check_run.id,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n },\n )\n return self.repository.save_check_run(check_run)\n\n # [/DEF:start_check_run:Function]\n\n # [DEF:execute_stages:Function]\n # @PURPOSE: Execute or accept compliance stage outcomes and set intermediate/final check-run status fields.\n # @PRE: check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.\n # @POST: Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.\n # @SIDE_EFFECT: Reads candidate/policy/registry/manifest and persists updated check_run.\n # @DATA_CONTRACT: Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun\n def execute_stages(\n self,\n check_run: ComplianceRun,\n forced_results: Optional[List[ComplianceStageRun]] = None,\n ) -> ComplianceRun:\n with belief_scope(\"execute_stages\"):\n if forced_results is not None:\n for index, result in enumerate(forced_results, start=1):\n if isinstance(result, ComplianceStageRun):\n stage_run = result\n else:\n status_value = getattr(result, \"status\", None)\n if status_value == \"PASS\":\n decision = ComplianceDecision.PASSED.value\n elif status_value == \"FAIL\":\n decision = ComplianceDecision.BLOCKED.value\n else:\n decision = ComplianceDecision.ERROR.value\n stage_run = ComplianceStageRun(\n id=f\"{check_run.id}-stage-{index}\",\n run_id=check_run.id,\n stage_name=result.stage.value,\n status=result.status.value,\n decision=decision,\n details_json={\"details\": result.details},\n )\n self.repository.stage_runs[stage_run.id] = stage_run\n\n check_run.final_status = derive_final_status(forced_results).value\n check_run.status = RunStatus.SUCCEEDED\n return self.repository.save_check_run(check_run)\n\n candidate = self.repository.get_candidate(check_run.candidate_id)\n policy = self.repository.get_policy(check_run.policy_snapshot_id)\n registry = self.repository.get_registry(check_run.registry_snapshot_id)\n manifest = self.repository.get_manifest(check_run.manifest_id)\n\n if not candidate or not policy or not registry or not manifest:\n check_run.status = RunStatus.FAILED\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n summary = manifest.content_json.get(\"summary\", {})\n purity_ok = summary.get(\"prohibited_detected_count\", 0) == 0\n check_run.final_status = (\n ComplianceDecision.PASSED.value\n if purity_ok\n else ComplianceDecision.BLOCKED.value\n )\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(timezone.utc)\n\n return self.repository.save_check_run(check_run)\n\n # [/DEF:execute_stages:Function]\n\n # [DEF:finalize_run:Function]\n # @PURPOSE: Finalize run status based on cumulative stage results.\n # @PRE: check_run was started and may already contain a derived final_status from stage execution.\n # @POST: Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.\n # @SIDE_EFFECT: Mutates check_run terminal fields and persists via repository.save_check_run.\n # @DATA_CONTRACT: Input -> ComplianceRun, Output -> ComplianceRun\n def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun:\n with belief_scope(\"finalize_run\"):\n if check_run.status == RunStatus.FAILED:\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n if not check_run.final_status:\n stage_results = [\n stage_run\n for stage_run in self.repository.stage_runs.values()\n if stage_run.run_id == check_run.id\n ]\n derived = derive_final_status(stage_results)\n check_run.final_status = derived.value\n\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n # [/DEF:finalize_run:Function]\n\n\n# [/DEF:CleanComplianceOrchestrator:Class]\n\n\n# [DEF:run_check_legacy:Function]\n# @PURPOSE: Legacy wrapper for compatibility with previous orchestrator call style.\n# @PRE: repository and identifiers are valid and resolvable by orchestrator dependencies.\n# @POST: Returns finalized ComplianceRun produced by orchestrator start->execute->finalize sequence.\n# @SIDE_EFFECT: Reads/writes compliance entities through repository during orchestrator calls.\n# @DATA_CONTRACT: Input -> (repository:CleanReleaseRepository, candidate_id:str, policy_id:str, requested_by:str, manifest_id:str), Output -> ComplianceRun\ndef run_check_legacy(\n repository: CleanReleaseRepository,\n candidate_id: str,\n policy_id: str,\n requested_by: str,\n manifest_id: str,\n) -> ComplianceRun:\n with belief_scope(\"run_check_legacy\"):\n orchestrator = CleanComplianceOrchestrator(repository)\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=requested_by,\n manifest_id=manifest_id,\n )\n run = orchestrator.execute_stages(run)\n return orchestrator.finalize_run(run)\n\n\n# [/DEF:run_check_legacy:Function]\n# [/DEF:ComplianceOrchestrator:Module]\n" + "anchor_syntax": "region", + "body": "# #region ComplianceOrchestrator [C:5] [TYPE Module] [SEMANTICS clean-release, compliance, orchestration, stage]\n# @BRIEF Execute mandatory clean compliance stages and produce final COMPLIANT/BLOCKED/FAILED outcome.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [ComplianceStages]\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @INVARIANT: COMPLIANT is impossible when any mandatory stage fails.\n# @TEST_CONTRACT: ComplianceCheckRun -> ComplianceCheckRun\n# @TEST_FIXTURE: compliant_candidate -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json\n# @TEST_EDGE: stage_failure_blocks_release -> Mandatory stage returns FAIL and final status becomes BLOCKED\n# @TEST_EDGE: missing_stage_result -> Finalization with incomplete/empty mandatory stage set must not produce COMPLIANT\n# @TEST_EDGE: report_generation_error -> Downstream reporting failure does not alter orchestrator status derivation contract\n# @TEST_INVARIANT: compliant_requires_all_mandatory_pass -> VERIFIED_BY: [stage_failure_blocks_release]\n# @PRE: ManifestService and PolicyEngine are available\n# @POST: OrchestrationResult with compliance status\n# @SIDE_EFFECT: Triggers compliance checks; may modify manifest state\n# @DATA_CONTRACT: Manifest -> ComplianceReport\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 (\n ComplianceRun,\n ComplianceStageRun,\n)\nfrom .enums import (\n ComplianceDecision,\n RunStatus,\n)\nfrom .repository import CleanReleaseRepository\nfrom .stages import derive_final_status\n\n\n# #region CleanComplianceOrchestrator [TYPE Class]\n# @BRIEF Coordinate clean-release compliance verification stages.\nclass CleanComplianceOrchestrator:\n # region __init__ [TYPE Function]\n # @PURPOSE: Bind repository dependency used for orchestrator persistence and lookups.\n # @PRE: repository is a valid CleanReleaseRepository instance with required methods.\n # @POST: self.repository is assigned and used by all orchestration steps.\n # @SIDE_EFFECT: Stores repository reference on orchestrator instance.\n # @DATA_CONTRACT: Input -> CleanReleaseRepository, Output -> None\n def __init__(self, repository: CleanReleaseRepository):\n with belief_scope(\"CleanComplianceOrchestrator.__init__\"):\n self.repository = repository\n\n # endregion __init__\n\n # region start_check_run [TYPE Function]\n # @PURPOSE: Initiate a new compliance run session.\n # @PRE: candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.\n # @POST: Returns initialized ComplianceRun in RUNNING state persisted in repository.\n # @SIDE_EFFECT: Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run.\n # @DATA_CONTRACT: Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun\n def start_check_run(\n self,\n candidate_id: str,\n policy_id: str,\n requested_by: str | None = None,\n manifest_id: str | None = None,\n **legacy_kwargs,\n ) -> ComplianceRun:\n with belief_scope(\"start_check_run\"):\n actor = requested_by or legacy_kwargs.get(\"triggered_by\") or \"system\"\n execution_mode = (\n str(legacy_kwargs.get(\"execution_mode\") or \"\").strip().lower()\n )\n manifest_id_value = manifest_id\n\n if manifest_id_value and str(manifest_id_value).strip().lower() in {\n \"tui\",\n \"api\",\n \"scheduler\",\n }:\n logger.reason(\n \"Detected legacy positional execution_mode passed through manifest_id slot\",\n extra={\n \"candidate_id\": candidate_id,\n \"execution_mode\": manifest_id_value,\n },\n )\n execution_mode = str(manifest_id_value).strip().lower()\n manifest_id_value = None\n\n manifest = (\n self.repository.get_manifest(manifest_id_value)\n if manifest_id_value\n else None\n )\n policy = self.repository.get_policy(policy_id)\n\n if manifest_id_value and manifest is None:\n logger.explore(\n \"Manifest lookup missed during run start; rejecting explicit manifest contract\",\n extra={\n \"candidate_id\": candidate_id,\n \"manifest_id\": manifest_id_value,\n },\n )\n raise ValueError(\"Manifest or Policy not found\")\n\n if policy is None:\n logger.explore(\n \"Policy lookup missed during run start; using compatibility placeholder snapshot\",\n extra={\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"execution_mode\": execution_mode or \"unspecified\",\n },\n )\n\n manifest_id_value = manifest_id_value or f\"manifest-{candidate_id}\"\n manifest_digest = getattr(manifest, \"manifest_digest\", \"pending\")\n registry_snapshot_id = (\n getattr(policy, \"registry_snapshot_id\", None)\n or getattr(policy, \"internal_source_registry_ref\", None)\n or \"pending\"\n )\n\n check_run = ComplianceRun(\n id=f\"check-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=manifest_id_value,\n manifest_digest=manifest_digest,\n policy_snapshot_id=policy_id,\n registry_snapshot_id=registry_snapshot_id,\n requested_by=actor,\n requested_at=datetime.now(UTC),\n started_at=datetime.now(UTC),\n status=RunStatus.RUNNING,\n )\n logger.reflect(\n \"Initialized compliance run with compatibility-safe dependency placeholders\",\n extra={\n \"run_id\": check_run.id,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n },\n )\n return self.repository.save_check_run(check_run)\n\n # endregion start_check_run\n\n # region execute_stages [TYPE Function]\n # @PURPOSE: Execute or accept compliance stage outcomes and set intermediate/final check-run status fields.\n # @PRE: check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.\n # @POST: Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.\n # @SIDE_EFFECT: Reads candidate/policy/registry/manifest and persists updated check_run.\n # @DATA_CONTRACT: Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun\n def execute_stages(\n self,\n check_run: ComplianceRun,\n forced_results: list[ComplianceStageRun] | None = None,\n ) -> ComplianceRun:\n with belief_scope(\"execute_stages\"):\n if forced_results is not None:\n for index, result in enumerate(forced_results, start=1):\n if isinstance(result, ComplianceStageRun):\n stage_run = result\n else:\n status_value = getattr(result, \"status\", None)\n if status_value == \"PASS\":\n decision = ComplianceDecision.PASSED.value\n elif status_value == \"FAIL\":\n decision = ComplianceDecision.BLOCKED.value\n else:\n decision = ComplianceDecision.ERROR.value\n stage_run = ComplianceStageRun(\n id=f\"{check_run.id}-stage-{index}\",\n run_id=check_run.id,\n stage_name=result.stage.value,\n status=result.status.value,\n decision=decision,\n details_json={\"details\": result.details},\n )\n self.repository.stage_runs[stage_run.id] = stage_run\n\n check_run.final_status = derive_final_status(forced_results).value\n check_run.status = RunStatus.SUCCEEDED\n return self.repository.save_check_run(check_run)\n\n candidate = self.repository.get_candidate(check_run.candidate_id)\n policy = self.repository.get_policy(check_run.policy_snapshot_id)\n registry = self.repository.get_registry(check_run.registry_snapshot_id)\n manifest = self.repository.get_manifest(check_run.manifest_id)\n\n if not candidate or not policy or not registry or not manifest:\n check_run.status = RunStatus.FAILED\n check_run.finished_at = datetime.now(UTC)\n return self.repository.save_check_run(check_run)\n\n summary = manifest.content_json.get(\"summary\", {})\n purity_ok = summary.get(\"prohibited_detected_count\", 0) == 0\n check_run.final_status = (\n ComplianceDecision.PASSED.value\n if purity_ok\n else ComplianceDecision.BLOCKED.value\n )\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(UTC)\n\n return self.repository.save_check_run(check_run)\n\n # endregion execute_stages\n\n # region finalize_run [TYPE Function]\n # @PURPOSE: Finalize run status based on cumulative stage results.\n # @PRE: check_run was started and may already contain a derived final_status from stage execution.\n # @POST: Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.\n # @SIDE_EFFECT: Mutates check_run terminal fields and persists via repository.save_check_run.\n # @DATA_CONTRACT: Input -> ComplianceRun, Output -> ComplianceRun\n def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun:\n with belief_scope(\"finalize_run\"):\n if check_run.status == RunStatus.FAILED:\n check_run.finished_at = datetime.now(UTC)\n return self.repository.save_check_run(check_run)\n\n if not check_run.final_status:\n stage_results = [\n stage_run\n for stage_run in self.repository.stage_runs.values()\n if stage_run.run_id == check_run.id\n ]\n derived = derive_final_status(stage_results)\n check_run.final_status = derived.value\n\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(UTC)\n return self.repository.save_check_run(check_run)\n\n # endregion finalize_run\n\n\n# #endregion CleanComplianceOrchestrator\n\n\n# #region run_check_legacy [TYPE Function]\n# @BRIEF Legacy wrapper for compatibility with previous orchestrator call style.\n# @PRE: repository and identifiers are valid and resolvable by orchestrator dependencies.\n# @POST: Returns finalized ComplianceRun produced by orchestrator start->execute->finalize sequence.\n# @SIDE_EFFECT: Reads/writes compliance entities through repository during orchestrator calls.\n# @DATA_CONTRACT: Input -> (repository:CleanReleaseRepository, candidate_id:str, policy_id:str, requested_by:str, manifest_id:str), Output -> ComplianceRun\ndef run_check_legacy(\n repository: CleanReleaseRepository,\n candidate_id: str,\n policy_id: str,\n requested_by: str,\n manifest_id: str,\n) -> ComplianceRun:\n with belief_scope(\"run_check_legacy\"):\n orchestrator = CleanComplianceOrchestrator(repository)\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=requested_by,\n manifest_id=manifest_id,\n )\n run = orchestrator.execute_stages(run)\n return orchestrator.finalize_run(run)\n\n\n# #endregion run_check_legacy\n# #endregion ComplianceOrchestrator\n" }, { "contract_id": "CleanComplianceOrchestrator", "contract_type": "Class", "file_path": "backend/src/services/clean_release/compliance_orchestrator.py", - "start_line": 46, - "end_line": 246, + "start_line": 37, + "end_line": 237, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -80246,213 +58648,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanComplianceOrchestrator:Class]\n# @PURPOSE: Coordinate clean-release compliance verification stages.\nclass CleanComplianceOrchestrator:\n # [DEF:__init__:Function]\n # @PURPOSE: Bind repository dependency used for orchestrator persistence and lookups.\n # @PRE: repository is a valid CleanReleaseRepository instance with required methods.\n # @POST: self.repository is assigned and used by all orchestration steps.\n # @SIDE_EFFECT: Stores repository reference on orchestrator instance.\n # @DATA_CONTRACT: Input -> CleanReleaseRepository, Output -> None\n def __init__(self, repository: CleanReleaseRepository):\n with belief_scope(\"CleanComplianceOrchestrator.__init__\"):\n self.repository = repository\n\n # [/DEF:__init__:Function]\n\n # [DEF:start_check_run:Function]\n # @PURPOSE: Initiate a new compliance run session.\n # @PRE: candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.\n # @POST: Returns initialized ComplianceRun in RUNNING state persisted in repository.\n # @SIDE_EFFECT: Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run.\n # @DATA_CONTRACT: Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun\n def start_check_run(\n self,\n candidate_id: str,\n policy_id: str,\n requested_by: str | None = None,\n manifest_id: str | None = None,\n **legacy_kwargs,\n ) -> ComplianceRun:\n with belief_scope(\"start_check_run\"):\n actor = requested_by or legacy_kwargs.get(\"triggered_by\") or \"system\"\n execution_mode = (\n str(legacy_kwargs.get(\"execution_mode\") or \"\").strip().lower()\n )\n manifest_id_value = manifest_id\n\n if manifest_id_value and str(manifest_id_value).strip().lower() in {\n \"tui\",\n \"api\",\n \"scheduler\",\n }:\n logger.reason(\n \"Detected legacy positional execution_mode passed through manifest_id slot\",\n extra={\n \"candidate_id\": candidate_id,\n \"execution_mode\": manifest_id_value,\n },\n )\n execution_mode = str(manifest_id_value).strip().lower()\n manifest_id_value = None\n\n manifest = (\n self.repository.get_manifest(manifest_id_value)\n if manifest_id_value\n else None\n )\n policy = self.repository.get_policy(policy_id)\n\n if manifest_id_value and manifest is None:\n logger.explore(\n \"Manifest lookup missed during run start; rejecting explicit manifest contract\",\n extra={\n \"candidate_id\": candidate_id,\n \"manifest_id\": manifest_id_value,\n },\n )\n raise ValueError(\"Manifest or Policy not found\")\n\n if policy is None:\n logger.explore(\n \"Policy lookup missed during run start; using compatibility placeholder snapshot\",\n extra={\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"execution_mode\": execution_mode or \"unspecified\",\n },\n )\n\n manifest_id_value = manifest_id_value or f\"manifest-{candidate_id}\"\n manifest_digest = getattr(manifest, \"manifest_digest\", \"pending\")\n registry_snapshot_id = (\n getattr(policy, \"registry_snapshot_id\", None)\n or getattr(policy, \"internal_source_registry_ref\", None)\n or \"pending\"\n )\n\n check_run = ComplianceRun(\n id=f\"check-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=manifest_id_value,\n manifest_digest=manifest_digest,\n policy_snapshot_id=policy_id,\n registry_snapshot_id=registry_snapshot_id,\n requested_by=actor,\n requested_at=datetime.now(timezone.utc),\n started_at=datetime.now(timezone.utc),\n status=RunStatus.RUNNING,\n )\n logger.reflect(\n \"Initialized compliance run with compatibility-safe dependency placeholders\",\n extra={\n \"run_id\": check_run.id,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n },\n )\n return self.repository.save_check_run(check_run)\n\n # [/DEF:start_check_run:Function]\n\n # [DEF:execute_stages:Function]\n # @PURPOSE: Execute or accept compliance stage outcomes and set intermediate/final check-run status fields.\n # @PRE: check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.\n # @POST: Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.\n # @SIDE_EFFECT: Reads candidate/policy/registry/manifest and persists updated check_run.\n # @DATA_CONTRACT: Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun\n def execute_stages(\n self,\n check_run: ComplianceRun,\n forced_results: Optional[List[ComplianceStageRun]] = None,\n ) -> ComplianceRun:\n with belief_scope(\"execute_stages\"):\n if forced_results is not None:\n for index, result in enumerate(forced_results, start=1):\n if isinstance(result, ComplianceStageRun):\n stage_run = result\n else:\n status_value = getattr(result, \"status\", None)\n if status_value == \"PASS\":\n decision = ComplianceDecision.PASSED.value\n elif status_value == \"FAIL\":\n decision = ComplianceDecision.BLOCKED.value\n else:\n decision = ComplianceDecision.ERROR.value\n stage_run = ComplianceStageRun(\n id=f\"{check_run.id}-stage-{index}\",\n run_id=check_run.id,\n stage_name=result.stage.value,\n status=result.status.value,\n decision=decision,\n details_json={\"details\": result.details},\n )\n self.repository.stage_runs[stage_run.id] = stage_run\n\n check_run.final_status = derive_final_status(forced_results).value\n check_run.status = RunStatus.SUCCEEDED\n return self.repository.save_check_run(check_run)\n\n candidate = self.repository.get_candidate(check_run.candidate_id)\n policy = self.repository.get_policy(check_run.policy_snapshot_id)\n registry = self.repository.get_registry(check_run.registry_snapshot_id)\n manifest = self.repository.get_manifest(check_run.manifest_id)\n\n if not candidate or not policy or not registry or not manifest:\n check_run.status = RunStatus.FAILED\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n summary = manifest.content_json.get(\"summary\", {})\n purity_ok = summary.get(\"prohibited_detected_count\", 0) == 0\n check_run.final_status = (\n ComplianceDecision.PASSED.value\n if purity_ok\n else ComplianceDecision.BLOCKED.value\n )\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(timezone.utc)\n\n return self.repository.save_check_run(check_run)\n\n # [/DEF:execute_stages:Function]\n\n # [DEF:finalize_run:Function]\n # @PURPOSE: Finalize run status based on cumulative stage results.\n # @PRE: check_run was started and may already contain a derived final_status from stage execution.\n # @POST: Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.\n # @SIDE_EFFECT: Mutates check_run terminal fields and persists via repository.save_check_run.\n # @DATA_CONTRACT: Input -> ComplianceRun, Output -> ComplianceRun\n def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun:\n with belief_scope(\"finalize_run\"):\n if check_run.status == RunStatus.FAILED:\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n if not check_run.final_status:\n stage_results = [\n stage_run\n for stage_run in self.repository.stage_runs.values()\n if stage_run.run_id == check_run.id\n ]\n derived = derive_final_status(stage_results)\n check_run.final_status = derived.value\n\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n # [/DEF:finalize_run:Function]\n\n\n# [/DEF:CleanComplianceOrchestrator:Class]\n" - }, - { - "contract_id": "start_check_run", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/compliance_orchestrator.py", - "start_line": 61, - "end_line": 154, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "DATA_CONTRACT": "Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun", - "POST": "Returns initialized ComplianceRun in RUNNING state persisted in repository.", - "PRE": "candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.", - "PURPOSE": "Initiate a new compliance run session.", - "SIDE_EFFECT": "Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:start_check_run:Function]\n # @PURPOSE: Initiate a new compliance run session.\n # @PRE: candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.\n # @POST: Returns initialized ComplianceRun in RUNNING state persisted in repository.\n # @SIDE_EFFECT: Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run.\n # @DATA_CONTRACT: Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun\n def start_check_run(\n self,\n candidate_id: str,\n policy_id: str,\n requested_by: str | None = None,\n manifest_id: str | None = None,\n **legacy_kwargs,\n ) -> ComplianceRun:\n with belief_scope(\"start_check_run\"):\n actor = requested_by or legacy_kwargs.get(\"triggered_by\") or \"system\"\n execution_mode = (\n str(legacy_kwargs.get(\"execution_mode\") or \"\").strip().lower()\n )\n manifest_id_value = manifest_id\n\n if manifest_id_value and str(manifest_id_value).strip().lower() in {\n \"tui\",\n \"api\",\n \"scheduler\",\n }:\n logger.reason(\n \"Detected legacy positional execution_mode passed through manifest_id slot\",\n extra={\n \"candidate_id\": candidate_id,\n \"execution_mode\": manifest_id_value,\n },\n )\n execution_mode = str(manifest_id_value).strip().lower()\n manifest_id_value = None\n\n manifest = (\n self.repository.get_manifest(manifest_id_value)\n if manifest_id_value\n else None\n )\n policy = self.repository.get_policy(policy_id)\n\n if manifest_id_value and manifest is None:\n logger.explore(\n \"Manifest lookup missed during run start; rejecting explicit manifest contract\",\n extra={\n \"candidate_id\": candidate_id,\n \"manifest_id\": manifest_id_value,\n },\n )\n raise ValueError(\"Manifest or Policy not found\")\n\n if policy is None:\n logger.explore(\n \"Policy lookup missed during run start; using compatibility placeholder snapshot\",\n extra={\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"execution_mode\": execution_mode or \"unspecified\",\n },\n )\n\n manifest_id_value = manifest_id_value or f\"manifest-{candidate_id}\"\n manifest_digest = getattr(manifest, \"manifest_digest\", \"pending\")\n registry_snapshot_id = (\n getattr(policy, \"registry_snapshot_id\", None)\n or getattr(policy, \"internal_source_registry_ref\", None)\n or \"pending\"\n )\n\n check_run = ComplianceRun(\n id=f\"check-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=manifest_id_value,\n manifest_digest=manifest_digest,\n policy_snapshot_id=policy_id,\n registry_snapshot_id=registry_snapshot_id,\n requested_by=actor,\n requested_at=datetime.now(timezone.utc),\n started_at=datetime.now(timezone.utc),\n status=RunStatus.RUNNING,\n )\n logger.reflect(\n \"Initialized compliance run with compatibility-safe dependency placeholders\",\n extra={\n \"run_id\": check_run.id,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n },\n )\n return self.repository.save_check_run(check_run)\n\n # [/DEF:start_check_run:Function]\n" - }, - { - "contract_id": "execute_stages", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/compliance_orchestrator.py", - "start_line": 156, - "end_line": 216, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "DATA_CONTRACT": "Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun", - "POST": "Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.", - "PRE": "check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.", - "PURPOSE": "Execute or accept compliance stage outcomes and set intermediate/final check-run status fields.", - "SIDE_EFFECT": "Reads candidate/policy/registry/manifest and persists updated check_run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:execute_stages:Function]\n # @PURPOSE: Execute or accept compliance stage outcomes and set intermediate/final check-run status fields.\n # @PRE: check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.\n # @POST: Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.\n # @SIDE_EFFECT: Reads candidate/policy/registry/manifest and persists updated check_run.\n # @DATA_CONTRACT: Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun\n def execute_stages(\n self,\n check_run: ComplianceRun,\n forced_results: Optional[List[ComplianceStageRun]] = None,\n ) -> ComplianceRun:\n with belief_scope(\"execute_stages\"):\n if forced_results is not None:\n for index, result in enumerate(forced_results, start=1):\n if isinstance(result, ComplianceStageRun):\n stage_run = result\n else:\n status_value = getattr(result, \"status\", None)\n if status_value == \"PASS\":\n decision = ComplianceDecision.PASSED.value\n elif status_value == \"FAIL\":\n decision = ComplianceDecision.BLOCKED.value\n else:\n decision = ComplianceDecision.ERROR.value\n stage_run = ComplianceStageRun(\n id=f\"{check_run.id}-stage-{index}\",\n run_id=check_run.id,\n stage_name=result.stage.value,\n status=result.status.value,\n decision=decision,\n details_json={\"details\": result.details},\n )\n self.repository.stage_runs[stage_run.id] = stage_run\n\n check_run.final_status = derive_final_status(forced_results).value\n check_run.status = RunStatus.SUCCEEDED\n return self.repository.save_check_run(check_run)\n\n candidate = self.repository.get_candidate(check_run.candidate_id)\n policy = self.repository.get_policy(check_run.policy_snapshot_id)\n registry = self.repository.get_registry(check_run.registry_snapshot_id)\n manifest = self.repository.get_manifest(check_run.manifest_id)\n\n if not candidate or not policy or not registry or not manifest:\n check_run.status = RunStatus.FAILED\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n summary = manifest.content_json.get(\"summary\", {})\n purity_ok = summary.get(\"prohibited_detected_count\", 0) == 0\n check_run.final_status = (\n ComplianceDecision.PASSED.value\n if purity_ok\n else ComplianceDecision.BLOCKED.value\n )\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(timezone.utc)\n\n return self.repository.save_check_run(check_run)\n\n # [/DEF:execute_stages:Function]\n" - }, - { - "contract_id": "finalize_run", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/compliance_orchestrator.py", - "start_line": 218, - "end_line": 243, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "DATA_CONTRACT": "Input -> ComplianceRun, Output -> ComplianceRun", - "POST": "Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.", - "PRE": "check_run was started and may already contain a derived final_status from stage execution.", - "PURPOSE": "Finalize run status based on cumulative stage results.", - "SIDE_EFFECT": "Mutates check_run terminal fields and persists via repository.save_check_run." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:finalize_run:Function]\n # @PURPOSE: Finalize run status based on cumulative stage results.\n # @PRE: check_run was started and may already contain a derived final_status from stage execution.\n # @POST: Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.\n # @SIDE_EFFECT: Mutates check_run terminal fields and persists via repository.save_check_run.\n # @DATA_CONTRACT: Input -> ComplianceRun, Output -> ComplianceRun\n def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun:\n with belief_scope(\"finalize_run\"):\n if check_run.status == RunStatus.FAILED:\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n if not check_run.final_status:\n stage_results = [\n stage_run\n for stage_run in self.repository.stage_runs.values()\n if stage_run.run_id == check_run.id\n ]\n derived = derive_final_status(stage_results)\n check_run.final_status = derived.value\n\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(timezone.utc)\n return self.repository.save_check_run(check_run)\n\n # [/DEF:finalize_run:Function]\n" + "anchor_syntax": "region", + "body": "# #region CleanComplianceOrchestrator [TYPE Class]\n# @BRIEF Coordinate clean-release compliance verification stages.\nclass CleanComplianceOrchestrator:\n # region __init__ [TYPE Function]\n # @PURPOSE: Bind repository dependency used for orchestrator persistence and lookups.\n # @PRE: repository is a valid CleanReleaseRepository instance with required methods.\n # @POST: self.repository is assigned and used by all orchestration steps.\n # @SIDE_EFFECT: Stores repository reference on orchestrator instance.\n # @DATA_CONTRACT: Input -> CleanReleaseRepository, Output -> None\n def __init__(self, repository: CleanReleaseRepository):\n with belief_scope(\"CleanComplianceOrchestrator.__init__\"):\n self.repository = repository\n\n # endregion __init__\n\n # region start_check_run [TYPE Function]\n # @PURPOSE: Initiate a new compliance run session.\n # @PRE: candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.\n # @POST: Returns initialized ComplianceRun in RUNNING state persisted in repository.\n # @SIDE_EFFECT: Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run.\n # @DATA_CONTRACT: Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun\n def start_check_run(\n self,\n candidate_id: str,\n policy_id: str,\n requested_by: str | None = None,\n manifest_id: str | None = None,\n **legacy_kwargs,\n ) -> ComplianceRun:\n with belief_scope(\"start_check_run\"):\n actor = requested_by or legacy_kwargs.get(\"triggered_by\") or \"system\"\n execution_mode = (\n str(legacy_kwargs.get(\"execution_mode\") or \"\").strip().lower()\n )\n manifest_id_value = manifest_id\n\n if manifest_id_value and str(manifest_id_value).strip().lower() in {\n \"tui\",\n \"api\",\n \"scheduler\",\n }:\n logger.reason(\n \"Detected legacy positional execution_mode passed through manifest_id slot\",\n extra={\n \"candidate_id\": candidate_id,\n \"execution_mode\": manifest_id_value,\n },\n )\n execution_mode = str(manifest_id_value).strip().lower()\n manifest_id_value = None\n\n manifest = (\n self.repository.get_manifest(manifest_id_value)\n if manifest_id_value\n else None\n )\n policy = self.repository.get_policy(policy_id)\n\n if manifest_id_value and manifest is None:\n logger.explore(\n \"Manifest lookup missed during run start; rejecting explicit manifest contract\",\n extra={\n \"candidate_id\": candidate_id,\n \"manifest_id\": manifest_id_value,\n },\n )\n raise ValueError(\"Manifest or Policy not found\")\n\n if policy is None:\n logger.explore(\n \"Policy lookup missed during run start; using compatibility placeholder snapshot\",\n extra={\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"execution_mode\": execution_mode or \"unspecified\",\n },\n )\n\n manifest_id_value = manifest_id_value or f\"manifest-{candidate_id}\"\n manifest_digest = getattr(manifest, \"manifest_digest\", \"pending\")\n registry_snapshot_id = (\n getattr(policy, \"registry_snapshot_id\", None)\n or getattr(policy, \"internal_source_registry_ref\", None)\n or \"pending\"\n )\n\n check_run = ComplianceRun(\n id=f\"check-{uuid4()}\",\n candidate_id=candidate_id,\n manifest_id=manifest_id_value,\n manifest_digest=manifest_digest,\n policy_snapshot_id=policy_id,\n registry_snapshot_id=registry_snapshot_id,\n requested_by=actor,\n requested_at=datetime.now(UTC),\n started_at=datetime.now(UTC),\n status=RunStatus.RUNNING,\n )\n logger.reflect(\n \"Initialized compliance run with compatibility-safe dependency placeholders\",\n extra={\n \"run_id\": check_run.id,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n },\n )\n return self.repository.save_check_run(check_run)\n\n # endregion start_check_run\n\n # region execute_stages [TYPE Function]\n # @PURPOSE: Execute or accept compliance stage outcomes and set intermediate/final check-run status fields.\n # @PRE: check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.\n # @POST: Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.\n # @SIDE_EFFECT: Reads candidate/policy/registry/manifest and persists updated check_run.\n # @DATA_CONTRACT: Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun\n def execute_stages(\n self,\n check_run: ComplianceRun,\n forced_results: list[ComplianceStageRun] | None = None,\n ) -> ComplianceRun:\n with belief_scope(\"execute_stages\"):\n if forced_results is not None:\n for index, result in enumerate(forced_results, start=1):\n if isinstance(result, ComplianceStageRun):\n stage_run = result\n else:\n status_value = getattr(result, \"status\", None)\n if status_value == \"PASS\":\n decision = ComplianceDecision.PASSED.value\n elif status_value == \"FAIL\":\n decision = ComplianceDecision.BLOCKED.value\n else:\n decision = ComplianceDecision.ERROR.value\n stage_run = ComplianceStageRun(\n id=f\"{check_run.id}-stage-{index}\",\n run_id=check_run.id,\n stage_name=result.stage.value,\n status=result.status.value,\n decision=decision,\n details_json={\"details\": result.details},\n )\n self.repository.stage_runs[stage_run.id] = stage_run\n\n check_run.final_status = derive_final_status(forced_results).value\n check_run.status = RunStatus.SUCCEEDED\n return self.repository.save_check_run(check_run)\n\n candidate = self.repository.get_candidate(check_run.candidate_id)\n policy = self.repository.get_policy(check_run.policy_snapshot_id)\n registry = self.repository.get_registry(check_run.registry_snapshot_id)\n manifest = self.repository.get_manifest(check_run.manifest_id)\n\n if not candidate or not policy or not registry or not manifest:\n check_run.status = RunStatus.FAILED\n check_run.finished_at = datetime.now(UTC)\n return self.repository.save_check_run(check_run)\n\n summary = manifest.content_json.get(\"summary\", {})\n purity_ok = summary.get(\"prohibited_detected_count\", 0) == 0\n check_run.final_status = (\n ComplianceDecision.PASSED.value\n if purity_ok\n else ComplianceDecision.BLOCKED.value\n )\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(UTC)\n\n return self.repository.save_check_run(check_run)\n\n # endregion execute_stages\n\n # region finalize_run [TYPE Function]\n # @PURPOSE: Finalize run status based on cumulative stage results.\n # @PRE: check_run was started and may already contain a derived final_status from stage execution.\n # @POST: Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.\n # @SIDE_EFFECT: Mutates check_run terminal fields and persists via repository.save_check_run.\n # @DATA_CONTRACT: Input -> ComplianceRun, Output -> ComplianceRun\n def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun:\n with belief_scope(\"finalize_run\"):\n if check_run.status == RunStatus.FAILED:\n check_run.finished_at = datetime.now(UTC)\n return self.repository.save_check_run(check_run)\n\n if not check_run.final_status:\n stage_results = [\n stage_run\n for stage_run in self.repository.stage_runs.values()\n if stage_run.run_id == check_run.id\n ]\n derived = derive_final_status(stage_results)\n check_run.final_status = derived.value\n\n check_run.status = RunStatus.SUCCEEDED\n check_run.finished_at = datetime.now(UTC)\n return self.repository.save_check_run(check_run)\n\n # endregion finalize_run\n\n\n# #endregion CleanComplianceOrchestrator\n" }, { "contract_id": "run_check_legacy", "contract_type": "Function", "file_path": "backend/src/services/clean_release/compliance_orchestrator.py", - "start_line": 249, - "end_line": 274, + "start_line": 240, + "end_line": 265, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -80510,34 +58714,27 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:run_check_legacy:Function]\n# @PURPOSE: Legacy wrapper for compatibility with previous orchestrator call style.\n# @PRE: repository and identifiers are valid and resolvable by orchestrator dependencies.\n# @POST: Returns finalized ComplianceRun produced by orchestrator start->execute->finalize sequence.\n# @SIDE_EFFECT: Reads/writes compliance entities through repository during orchestrator calls.\n# @DATA_CONTRACT: Input -> (repository:CleanReleaseRepository, candidate_id:str, policy_id:str, requested_by:str, manifest_id:str), Output -> ComplianceRun\ndef run_check_legacy(\n repository: CleanReleaseRepository,\n candidate_id: str,\n policy_id: str,\n requested_by: str,\n manifest_id: str,\n) -> ComplianceRun:\n with belief_scope(\"run_check_legacy\"):\n orchestrator = CleanComplianceOrchestrator(repository)\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=requested_by,\n manifest_id=manifest_id,\n )\n run = orchestrator.execute_stages(run)\n return orchestrator.finalize_run(run)\n\n\n# [/DEF:run_check_legacy:Function]\n" + "anchor_syntax": "region", + "body": "# #region run_check_legacy [TYPE Function]\n# @BRIEF Legacy wrapper for compatibility with previous orchestrator call style.\n# @PRE: repository and identifiers are valid and resolvable by orchestrator dependencies.\n# @POST: Returns finalized ComplianceRun produced by orchestrator start->execute->finalize sequence.\n# @SIDE_EFFECT: Reads/writes compliance entities through repository during orchestrator calls.\n# @DATA_CONTRACT: Input -> (repository:CleanReleaseRepository, candidate_id:str, policy_id:str, requested_by:str, manifest_id:str), Output -> ComplianceRun\ndef run_check_legacy(\n repository: CleanReleaseRepository,\n candidate_id: str,\n policy_id: str,\n requested_by: str,\n manifest_id: str,\n) -> ComplianceRun:\n with belief_scope(\"run_check_legacy\"):\n orchestrator = CleanComplianceOrchestrator(repository)\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=requested_by,\n manifest_id=manifest_id,\n )\n run = orchestrator.execute_stages(run)\n return orchestrator.finalize_run(run)\n\n\n# #endregion run_check_legacy\n" }, { "contract_id": "DemoDataService", "contract_type": "Module", "file_path": "backend/src/services/clean_release/demo_data_service.py", "start_line": 1, - "end_line": 56, + "end_line": 54, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Demo and real namespaces must never collide for generated physical identifiers.", "LAYER": "Domain", - "PURPOSE": "Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes.", - "SEMANTICS": [ - "clean-release", - "demo-mode", - "namespace", - "isolation", - "repository" - ] + "PURPOSE": "Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes." }, "relations": [ { "source_id": "DemoDataService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "RepositoryRelations", "target_ref": "[RepositoryRelations]" } @@ -80551,35 +58748,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:DemoDataService:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, demo-mode, namespace, isolation, repository\n# @PURPOSE: 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# [DEF:resolve_namespace:Function]\n# @PURPOSE: 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# [/DEF:resolve_namespace:Function]\n\n\n# [DEF:build_namespaced_id:Function]\n# @PURPOSE: 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# [/DEF:build_namespaced_id:Function]\n\n\n# [DEF:create_isolated_repository:Function]\n# @PURPOSE: 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 setattr(repository, \"namespace\", namespace)\n return repository\n\n\n# [/DEF:create_isolated_repository:Function]\n\n# [/DEF:DemoDataService:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "resolve_namespace", "contract_type": "Function", "file_path": "backend/src/services/clean_release/demo_data_service.py", - "start_line": 14, - "end_line": 25, + "start_line": 12, + "end_line": 23, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -80617,15 +58796,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:resolve_namespace:Function]\n# @PURPOSE: 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# [/DEF:resolve_namespace:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "build_namespaced_id", "contract_type": "Function", "file_path": "backend/src/services/clean_release/demo_data_service.py", - "start_line": 28, - "end_line": 40, + "start_line": 26, + "end_line": 38, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -80663,15 +58842,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_namespaced_id:Function]\n# @PURPOSE: 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# [/DEF:build_namespaced_id:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "create_isolated_repository", "contract_type": "Function", "file_path": "backend/src/services/clean_release/demo_data_service.py", - "start_line": 43, - "end_line": 54, + "start_line": 41, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -80709,19 +58888,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:create_isolated_repository:Function]\n# @PURPOSE: 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 setattr(repository, \"namespace\", namespace)\n return repository\n\n\n# [/DEF:create_isolated_repository:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "clean_release_dto", "contract_type": "Module", "file_path": "backend/src/services/clean_release/dto.py", "start_line": 1, - "end_line": 86, + "end_line": 88, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Application", "PURPOSE": "Data Transfer Objects for clean release compliance subsystem." }, @@ -80734,8 +58913,8 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:clean_release_dto:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Data Transfer Objects for clean release compliance subsystem.\n# @LAYER: Application\n# @RELATION: DEPENDS_ON -> pydantic\n\nfrom datetime import datetime\nfrom typing import List, Optional, Dict, Any\nfrom pydantic import BaseModel, Field\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus, ComplianceDecision\n\nclass CandidateDTO(BaseModel):\n \"\"\"DTO for ReleaseCandidate.\"\"\"\n id: str\n version: str\n source_snapshot_ref: str\n build_id: Optional[str] = None\n created_at: datetime\n created_by: str\n status: CandidateStatus\n\nclass ArtifactDTO(BaseModel):\n \"\"\"DTO for CandidateArtifact.\"\"\"\n id: str\n candidate_id: str\n path: str\n sha256: str\n size: int\n detected_category: Optional[str] = None\n declared_category: Optional[str] = None\n source_uri: Optional[str] = None\n source_host: Optional[str] = None\n metadata: Dict[str, Any] = Field(default_factory=dict)\n\nclass ManifestDTO(BaseModel):\n \"\"\"DTO for DistributionManifest.\"\"\"\n id: str\n candidate_id: str\n manifest_version: int\n manifest_digest: str\n artifacts_digest: str\n created_at: datetime\n created_by: str\n source_snapshot_ref: str\n content_json: Dict[str, Any]\n\nclass ComplianceRunDTO(BaseModel):\n \"\"\"DTO for ComplianceRun status tracking.\"\"\"\n run_id: str\n candidate_id: str\n status: RunStatus\n final_status: Optional[ComplianceDecision] = None\n report_id: Optional[str] = None\n task_id: Optional[str] = None\n\nclass ReportDTO(BaseModel):\n \"\"\"Compact report view.\"\"\"\n report_id: str\n candidate_id: str\n final_status: ComplianceDecision\n policy_version: str\n manifest_digest: str\n violation_count: int\n generated_at: datetime\n\nclass CandidateOverviewDTO(BaseModel):\n \"\"\"Read model for candidate overview.\"\"\"\n candidate_id: str\n version: str\n source_snapshot_ref: str\n status: CandidateStatus\n latest_manifest_id: Optional[str] = None\n latest_manifest_digest: Optional[str] = None\n latest_run_id: Optional[str] = None\n latest_run_status: Optional[RunStatus] = None\n latest_report_id: Optional[str] = None\n latest_report_final_status: Optional[ComplianceDecision] = None\n latest_policy_snapshot_id: Optional[str] = None\n latest_policy_version: Optional[str] = None\n latest_registry_snapshot_id: Optional[str] = None\n latest_registry_version: Optional[str] = None\n latest_approval_decision: Optional[str] = None\n latest_publication_id: Optional[str] = None\n latest_publication_status: Optional[str] = None\n\n# [/DEF:clean_release_dto:Module]\n" + "anchor_syntax": "region", + "body": "# #region clean_release_dto [C:3] [TYPE Module] [SEMANTICS pydantic, clean-release, dto, schema, transfer]\n# @BRIEF Data Transfer Objects for clean release compliance subsystem.\n# @LAYER: Application\n# @RELATION DEPENDS_ON -> pydantic\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision, RunStatus\n\n\nclass CandidateDTO(BaseModel):\n \"\"\"DTO for ReleaseCandidate.\"\"\"\n id: str\n version: str\n source_snapshot_ref: str\n build_id: str | None = None\n created_at: datetime\n created_by: str\n status: CandidateStatus\n\nclass ArtifactDTO(BaseModel):\n \"\"\"DTO for CandidateArtifact.\"\"\"\n id: str\n candidate_id: str\n path: str\n sha256: str\n size: int\n detected_category: str | None = None\n declared_category: str | None = None\n source_uri: str | None = None\n source_host: str | None = None\n metadata: dict[str, Any] = Field(default_factory=dict)\n\nclass ManifestDTO(BaseModel):\n \"\"\"DTO for DistributionManifest.\"\"\"\n id: str\n candidate_id: str\n manifest_version: int\n manifest_digest: str\n artifacts_digest: str\n created_at: datetime\n created_by: str\n source_snapshot_ref: str\n content_json: dict[str, Any]\n\nclass ComplianceRunDTO(BaseModel):\n \"\"\"DTO for ComplianceRun status tracking.\"\"\"\n run_id: str\n candidate_id: str\n status: RunStatus\n final_status: ComplianceDecision | None = None\n report_id: str | None = None\n task_id: str | None = None\n\nclass ReportDTO(BaseModel):\n \"\"\"Compact report view.\"\"\"\n report_id: str\n candidate_id: str\n final_status: ComplianceDecision\n policy_version: str\n manifest_digest: str\n violation_count: int\n generated_at: datetime\n\nclass CandidateOverviewDTO(BaseModel):\n \"\"\"Read model for candidate overview.\"\"\"\n candidate_id: str\n version: str\n source_snapshot_ref: str\n status: CandidateStatus\n latest_manifest_id: str | None = None\n latest_manifest_digest: str | None = None\n latest_run_id: str | None = None\n latest_run_status: RunStatus | None = None\n latest_report_id: str | None = None\n latest_report_final_status: ComplianceDecision | None = None\n latest_policy_snapshot_id: str | None = None\n latest_policy_version: str | None = None\n latest_registry_snapshot_id: str | None = None\n latest_registry_version: str | None = None\n latest_approval_decision: str | None = None\n latest_publication_id: str | None = None\n latest_publication_status: str | None = None\n\n# #endregion clean_release_dto\n" }, { "contract_id": "clean_release_enums", @@ -80746,7 +58925,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", "PURPOSE": "Canonical enums for clean release lifecycle and compliance." }, @@ -80759,19 +58938,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:clean_release_enums:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Canonical enums for clean release lifecycle and compliance.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> enum\n\nfrom enum import Enum\n\nclass CandidateStatus(str, Enum):\n \"\"\"Lifecycle states for a ReleaseCandidate.\"\"\"\n DRAFT = \"DRAFT\"\n PREPARED = \"PREPARED\"\n MANIFEST_BUILT = \"MANIFEST_BUILT\"\n CHECK_PENDING = \"CHECK_PENDING\"\n CHECK_RUNNING = \"CHECK_RUNNING\"\n CHECK_PASSED = \"CHECK_PASSED\"\n CHECK_BLOCKED = \"CHECK_BLOCKED\"\n CHECK_ERROR = \"CHECK_ERROR\"\n APPROVED = \"APPROVED\"\n PUBLISHED = \"PUBLISHED\"\n REVOKED = \"REVOKED\"\n\nclass RunStatus(str, Enum):\n \"\"\"Execution status for a ComplianceRun.\"\"\"\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n SUCCEEDED = \"SUCCEEDED\"\n FAILED = \"FAILED\"\n CANCELLED = \"CANCELLED\"\n\nclass ComplianceDecision(str, Enum):\n \"\"\"Final compliance result for a run or stage.\"\"\"\n PASSED = \"PASSED\"\n BLOCKED = \"BLOCKED\"\n ERROR = \"ERROR\"\n\nclass ApprovalDecisionType(str, Enum):\n \"\"\"Types of approval decisions.\"\"\"\n APPROVED = \"APPROVED\"\n REJECTED = \"REJECTED\"\n\nclass PublicationStatus(str, Enum):\n \"\"\"Status of a publication record.\"\"\"\n ACTIVE = \"ACTIVE\"\n REVOKED = \"REVOKED\"\n\nclass ComplianceStageName(str, Enum):\n \"\"\"Canonical names for compliance stages.\"\"\"\n DATA_PURITY = \"DATA_PURITY\"\n INTERNAL_SOURCES_ONLY = \"INTERNAL_SOURCES_ONLY\"\n NO_EXTERNAL_ENDPOINTS = \"NO_EXTERNAL_ENDPOINTS\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n\nclass ClassificationType(str, Enum):\n \"\"\"Classification types for artifacts.\"\"\"\n REQUIRED_SYSTEM = \"required-system\"\n ALLOWED = \"allowed\"\n EXCLUDED_PROHIBITED = \"excluded-prohibited\"\n\nclass ViolationSeverity(str, Enum):\n \"\"\"Severity levels for compliance violations.\"\"\"\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n\nclass ViolationCategory(str, Enum):\n \"\"\"Categories for compliance violations.\"\"\"\n DATA_PURITY = \"DATA_PURITY\"\n SOURCE_ISOLATION = \"SOURCE_ISOLATION\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n EXTERNAL_ENDPOINT = \"EXTERNAL_ENDPOINT\"\n\n# [/DEF:clean_release_enums:Module]\n" + "anchor_syntax": "region", + "body": "# #region clean_release_enums [C:3] [TYPE Module] [SEMANTICS clean-release, enum, lifecycle, status, compliance]\n# @BRIEF Canonical enums for clean release lifecycle and compliance.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> enum\n\nfrom enum import Enum\n\n\nclass CandidateStatus(str, Enum):\n \"\"\"Lifecycle states for a ReleaseCandidate.\"\"\"\n DRAFT = \"DRAFT\"\n PREPARED = \"PREPARED\"\n MANIFEST_BUILT = \"MANIFEST_BUILT\"\n CHECK_PENDING = \"CHECK_PENDING\"\n CHECK_RUNNING = \"CHECK_RUNNING\"\n CHECK_PASSED = \"CHECK_PASSED\"\n CHECK_BLOCKED = \"CHECK_BLOCKED\"\n CHECK_ERROR = \"CHECK_ERROR\"\n APPROVED = \"APPROVED\"\n PUBLISHED = \"PUBLISHED\"\n REVOKED = \"REVOKED\"\n\nclass RunStatus(str, Enum):\n \"\"\"Execution status for a ComplianceRun.\"\"\"\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n SUCCEEDED = \"SUCCEEDED\"\n FAILED = \"FAILED\"\n CANCELLED = \"CANCELLED\"\n\nclass ComplianceDecision(str, Enum):\n \"\"\"Final compliance result for a run or stage.\"\"\"\n PASSED = \"PASSED\"\n BLOCKED = \"BLOCKED\"\n ERROR = \"ERROR\"\n\nclass ApprovalDecisionType(str, Enum):\n \"\"\"Types of approval decisions.\"\"\"\n APPROVED = \"APPROVED\"\n REJECTED = \"REJECTED\"\n\nclass PublicationStatus(str, Enum):\n \"\"\"Status of a publication record.\"\"\"\n ACTIVE = \"ACTIVE\"\n REVOKED = \"REVOKED\"\n\nclass ComplianceStageName(str, Enum):\n \"\"\"Canonical names for compliance stages.\"\"\"\n DATA_PURITY = \"DATA_PURITY\"\n INTERNAL_SOURCES_ONLY = \"INTERNAL_SOURCES_ONLY\"\n NO_EXTERNAL_ENDPOINTS = \"NO_EXTERNAL_ENDPOINTS\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n\nclass ClassificationType(str, Enum):\n \"\"\"Classification types for artifacts.\"\"\"\n REQUIRED_SYSTEM = \"required-system\"\n ALLOWED = \"allowed\"\n EXCLUDED_PROHIBITED = \"excluded-prohibited\"\n\nclass ViolationSeverity(str, Enum):\n \"\"\"Severity levels for compliance violations.\"\"\"\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n\nclass ViolationCategory(str, Enum):\n \"\"\"Categories for compliance violations.\"\"\"\n DATA_PURITY = \"DATA_PURITY\"\n SOURCE_ISOLATION = \"SOURCE_ISOLATION\"\n MANIFEST_CONSISTENCY = \"MANIFEST_CONSISTENCY\"\n EXTERNAL_ENDPOINT = \"EXTERNAL_ENDPOINT\"\n\n# #endregion clean_release_enums\n" }, { "contract_id": "clean_release_exceptions", "contract_type": "Module", "file_path": "backend/src/services/clean_release/exceptions.py", "start_line": 1, - "end_line": 39, + "end_line": 38, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", "PURPOSE": "Domain exceptions for clean release compliance subsystem." }, @@ -80784,19 +58963,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:clean_release_exceptions:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Domain exceptions for clean release compliance subsystem.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> Exception\n\nclass CleanReleaseError(Exception):\n \"\"\"Base exception for clean release subsystem.\"\"\"\n pass\n\nclass CandidateNotFoundError(CleanReleaseError):\n \"\"\"Raised when a release candidate is not found.\"\"\"\n pass\n\nclass IllegalTransitionError(CleanReleaseError):\n \"\"\"Raised when a forbidden lifecycle transition is attempted.\"\"\"\n pass\n\nclass ManifestImmutableError(CleanReleaseError):\n \"\"\"Raised when an attempt is made to mutate an existing manifest.\"\"\"\n pass\n\nclass PolicyResolutionError(CleanReleaseError):\n \"\"\"Raised when trusted policy or registry cannot be resolved.\"\"\"\n pass\n\nclass ComplianceRunError(CleanReleaseError):\n \"\"\"Raised when a compliance run fails or is invalid.\"\"\"\n pass\n\nclass ApprovalGateError(CleanReleaseError):\n \"\"\"Raised when approval requirements are not met.\"\"\"\n pass\n\nclass PublicationGateError(CleanReleaseError):\n \"\"\"Raised when publication requirements are not met.\"\"\"\n pass\n\n# [/DEF:clean_release_exceptions:Module]\n" + "anchor_syntax": "region", + "body": "# #region clean_release_exceptions [C:3] [TYPE Module] [SEMANTICS clean-release, exception, domain, error]\n# @BRIEF Domain exceptions for clean release compliance subsystem.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> Exception\n\nclass CleanReleaseError(Exception):\n \"\"\"Base exception for clean release subsystem.\"\"\"\n pass\n\nclass CandidateNotFoundError(CleanReleaseError):\n \"\"\"Raised when a release candidate is not found.\"\"\"\n pass\n\nclass IllegalTransitionError(CleanReleaseError):\n \"\"\"Raised when a forbidden lifecycle transition is attempted.\"\"\"\n pass\n\nclass ManifestImmutableError(CleanReleaseError):\n \"\"\"Raised when an attempt is made to mutate an existing manifest.\"\"\"\n pass\n\nclass PolicyResolutionError(CleanReleaseError):\n \"\"\"Raised when trusted policy or registry cannot be resolved.\"\"\"\n pass\n\nclass ComplianceRunError(CleanReleaseError):\n \"\"\"Raised when a compliance run fails or is invalid.\"\"\"\n pass\n\nclass ApprovalGateError(CleanReleaseError):\n \"\"\"Raised when approval requirements are not met.\"\"\"\n pass\n\nclass PublicationGateError(CleanReleaseError):\n \"\"\"Raised when publication requirements are not met.\"\"\"\n pass\n\n# #endregion clean_release_exceptions\n" }, { "contract_id": "clean_release_facade", "contract_type": "Module", "file_path": "backend/src/services/clean_release/facade.py", "start_line": 1, - "end_line": 123, + "end_line": 116, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Application", "PURPOSE": "Unified entry point for clean release operations." }, @@ -80809,33 +58988,27 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:clean_release_facade:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Unified entry point for clean release operations.\n# @LAYER: Application\n# @RELATION: DEPENDS_ON -> ComplianceOrchestrator\n\nfrom typing import List, Optional\nfrom src.services.clean_release.repositories import (\n CandidateRepository, ArtifactRepository, ManifestRepository,\n PolicyRepository, ComplianceRepository, ReportRepository,\n ApprovalRepository, PublicationRepository, AuditRepository\n)\nfrom src.services.clean_release.dto import (\n CandidateDTO, ArtifactDTO, ManifestDTO, ComplianceRunDTO,\n ReportDTO, CandidateOverviewDTO\n)\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus, ComplianceDecision\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom src.core.logger import belief_scope\nfrom src.core.config_manager import ConfigManager\n\nclass CleanReleaseFacade:\n \"\"\"\n @PURPOSE: Orchestrates repositories and services to provide a clean API for UI/CLI.\n \"\"\"\n def __init__(\n self,\n candidate_repo: CandidateRepository,\n artifact_repo: ArtifactRepository,\n manifest_repo: ManifestRepository,\n policy_repo: PolicyRepository,\n compliance_repo: ComplianceRepository,\n report_repo: ReportRepository,\n approval_repo: ApprovalRepository,\n publication_repo: PublicationRepository,\n audit_repo: AuditRepository,\n config_manager: ConfigManager\n ):\n self.candidate_repo = candidate_repo\n self.artifact_repo = artifact_repo\n self.manifest_repo = manifest_repo\n self.policy_repo = policy_repo\n self.compliance_repo = compliance_repo\n self.report_repo = report_repo\n self.approval_repo = approval_repo\n self.publication_repo = publication_repo\n self.audit_repo = audit_repo\n self.config_manager = config_manager\n\n def resolve_active_policy_snapshot(self) -> Optional[CleanPolicySnapshot]:\n \"\"\"\n @PURPOSE: Resolve the active policy snapshot based on ConfigManager.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.resolve_active_policy_snapshot\"):\n config = self.config_manager.get_config()\n policy_id = config.settings.clean_release.active_policy_id\n if not policy_id:\n return None\n return self.policy_repo.get_policy_snapshot(policy_id)\n\n def resolve_active_registry_snapshot(self) -> Optional[SourceRegistrySnapshot]:\n \"\"\"\n @PURPOSE: Resolve the active registry snapshot based on ConfigManager.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.resolve_active_registry_snapshot\"):\n config = self.config_manager.get_config()\n registry_id = config.settings.clean_release.active_registry_id\n if not registry_id:\n return None\n return self.policy_repo.get_registry_snapshot(registry_id)\n\n def get_candidate_overview(self, candidate_id: str) -> Optional[CandidateOverviewDTO]:\n \"\"\"\n @PURPOSE: Build a comprehensive overview for a candidate.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.get_candidate_overview\"):\n candidate = self.candidate_repo.get_by_id(candidate_id)\n if not candidate:\n return None\n\n manifest = self.manifest_repo.get_latest_for_candidate(candidate_id)\n runs = self.compliance_repo.list_runs_by_candidate(candidate_id)\n latest_run = runs[-1] if runs else None\n \n report = None\n if latest_run:\n report = self.report_repo.get_by_run(latest_run.id)\n\n approval = self.approval_repo.get_latest_for_candidate(candidate_id)\n publication = self.publication_repo.get_latest_for_candidate(candidate_id)\n \n active_policy = self.resolve_active_policy_snapshot()\n active_registry = self.resolve_active_registry_snapshot()\n\n return CandidateOverviewDTO(\n candidate_id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n status=CandidateStatus(candidate.status),\n latest_manifest_id=manifest.id if manifest else None,\n latest_manifest_digest=manifest.manifest_digest if manifest else None,\n latest_run_id=latest_run.id if latest_run else None,\n latest_run_status=RunStatus(latest_run.status) if latest_run else None,\n latest_report_id=report.id if report else None,\n latest_report_final_status=ComplianceDecision(report.final_status) if report else None,\n latest_policy_snapshot_id=active_policy.id if active_policy else None,\n latest_policy_version=active_policy.policy_version if active_policy else None,\n latest_registry_snapshot_id=active_registry.id if active_registry else None,\n latest_registry_version=active_registry.registry_version if active_registry else None,\n latest_approval_decision=approval.decision if approval else None,\n latest_publication_id=publication.id if publication else None,\n latest_publication_status=publication.status if publication else None\n )\n\n def list_candidates(self) -> List[CandidateOverviewDTO]:\n \"\"\"\n @PURPOSE: List all candidates with their current status.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.list_candidates\"):\n candidates = self.candidate_repo.list_all()\n return [self.get_candidate_overview(c.id) for c in candidates]\n\n# [/DEF:clean_release_facade:Module]\n" + "anchor_syntax": "region", + "body": "# #region clean_release_facade [C:3] [TYPE Module] [SEMANTICS clean-release, facade, orchestration, repository, crud]\n# @BRIEF Unified entry point for clean release operations.\n# @LAYER: Application\n# @RELATION DEPENDS_ON -> ComplianceOrchestrator\n\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom src.services.clean_release.dto import CandidateOverviewDTO\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision, RunStatus\nfrom src.services.clean_release.repositories import ApprovalRepository, ArtifactRepository, AuditRepository, CandidateRepository, ComplianceRepository, ManifestRepository, PolicyRepository, PublicationRepository, ReportRepository\n\n\nclass CleanReleaseFacade:\n \"\"\"\n @PURPOSE: Orchestrates repositories and services to provide a clean API for UI/CLI.\n \"\"\"\n def __init__(\n self,\n candidate_repo: CandidateRepository,\n artifact_repo: ArtifactRepository,\n manifest_repo: ManifestRepository,\n policy_repo: PolicyRepository,\n compliance_repo: ComplianceRepository,\n report_repo: ReportRepository,\n approval_repo: ApprovalRepository,\n publication_repo: PublicationRepository,\n audit_repo: AuditRepository,\n config_manager: ConfigManager\n ):\n self.candidate_repo = candidate_repo\n self.artifact_repo = artifact_repo\n self.manifest_repo = manifest_repo\n self.policy_repo = policy_repo\n self.compliance_repo = compliance_repo\n self.report_repo = report_repo\n self.approval_repo = approval_repo\n self.publication_repo = publication_repo\n self.audit_repo = audit_repo\n self.config_manager = config_manager\n\n def resolve_active_policy_snapshot(self) -> CleanPolicySnapshot | None:\n \"\"\"\n @PURPOSE: Resolve the active policy snapshot based on ConfigManager.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.resolve_active_policy_snapshot\"):\n config = self.config_manager.get_config()\n policy_id = config.settings.clean_release.active_policy_id\n if not policy_id:\n return None\n return self.policy_repo.get_policy_snapshot(policy_id)\n\n def resolve_active_registry_snapshot(self) -> SourceRegistrySnapshot | None:\n \"\"\"\n @PURPOSE: Resolve the active registry snapshot based on ConfigManager.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.resolve_active_registry_snapshot\"):\n config = self.config_manager.get_config()\n registry_id = config.settings.clean_release.active_registry_id\n if not registry_id:\n return None\n return self.policy_repo.get_registry_snapshot(registry_id)\n\n def get_candidate_overview(self, candidate_id: str) -> CandidateOverviewDTO | None:\n \"\"\"\n @PURPOSE: Build a comprehensive overview for a candidate.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.get_candidate_overview\"):\n candidate = self.candidate_repo.get_by_id(candidate_id)\n if not candidate:\n return None\n\n manifest = self.manifest_repo.get_latest_for_candidate(candidate_id)\n runs = self.compliance_repo.list_runs_by_candidate(candidate_id)\n latest_run = runs[-1] if runs else None\n\n report = None\n if latest_run:\n report = self.report_repo.get_by_run(latest_run.id)\n\n approval = self.approval_repo.get_latest_for_candidate(candidate_id)\n publication = self.publication_repo.get_latest_for_candidate(candidate_id)\n\n active_policy = self.resolve_active_policy_snapshot()\n active_registry = self.resolve_active_registry_snapshot()\n\n return CandidateOverviewDTO(\n candidate_id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n status=CandidateStatus(candidate.status),\n latest_manifest_id=manifest.id if manifest else None,\n latest_manifest_digest=manifest.manifest_digest if manifest else None,\n latest_run_id=latest_run.id if latest_run else None,\n latest_run_status=RunStatus(latest_run.status) if latest_run else None,\n latest_report_id=report.id if report else None,\n latest_report_final_status=ComplianceDecision(report.final_status) if report else None,\n latest_policy_snapshot_id=active_policy.id if active_policy else None,\n latest_policy_version=active_policy.policy_version if active_policy else None,\n latest_registry_snapshot_id=active_registry.id if active_registry else None,\n latest_registry_version=active_registry.registry_version if active_registry else None,\n latest_approval_decision=approval.decision if approval else None,\n latest_publication_id=publication.id if publication else None,\n latest_publication_status=publication.status if publication else None\n )\n\n def list_candidates(self) -> list[CandidateOverviewDTO]:\n \"\"\"\n @PURPOSE: List all candidates with their current status.\n \"\"\"\n with belief_scope(\"CleanReleaseFacade.list_candidates\"):\n candidates = self.candidate_repo.list_all()\n return [self.get_candidate_overview(c.id) for c in candidates]\n\n# #endregion clean_release_facade\n" }, { "contract_id": "ManifestBuilder", "contract_type": "Module", "file_path": "backend/src/services/clean_release/manifest_builder.py", "start_line": 1, - "end_line": 135, + "end_line": 134, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Equal semantic artifact sets produce identical deterministic hash values.", "LAYER": "Domain", - "PURPOSE": "Build deterministic distribution manifest from classified artifact input.", - "SEMANTICS": [ - "clean-release", - "manifest", - "deterministic-hash", - "summary" - ] + "PURPOSE": "Build deterministic distribution manifest from classified artifact input." }, "relations": [ { "source_id": "ManifestBuilder", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" } @@ -80849,35 +59022,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ManifestBuilder:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, manifest, deterministic-hash, summary\n# @PURPOSE: 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 datetime import datetime, timezone\nfrom typing import Iterable, List, Dict, 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# [DEF:build_distribution_manifest:Function]\n# @PURPOSE: 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(timezone.utc),\n generated_by=generated_by,\n items=items,\n summary=summary,\n deterministic_hash=deterministic_hash,\n )\n\n\n# [/DEF:build_distribution_manifest:Function]\n\n\n# [DEF:build_manifest:Function]\n# @PURPOSE: 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# [/DEF:build_manifest:Function]\n# [/DEF:ManifestBuilder:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "build_distribution_manifest", "contract_type": "Function", "file_path": "backend/src/services/clean_release/manifest_builder.py", - "start_line": 57, - "end_line": 111, + "start_line": 56, + "end_line": 110, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -80915,50 +59070,43 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_distribution_manifest:Function]\n# @PURPOSE: 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(timezone.utc),\n generated_by=generated_by,\n items=items,\n summary=summary,\n deterministic_hash=deterministic_hash,\n )\n\n\n# [/DEF:build_distribution_manifest:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ManifestService", "contract_type": "Module", "file_path": "backend/src/services/clean_release/manifest_service.py", "start_line": 1, - "end_line": 97, + "end_line": 95, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Manifest -> ManifestRecord; Candidate -> ManifestRecord", "INVARIANT": "Existing manifests are never mutated.", "LAYER": "Domain", "POST": "New immutable manifest is persisted with incremented version and deterministic digest.", "PRE": "Candidate exists and is PREPARED or MANIFEST_BUILT; artifacts are present.", "PURPOSE": "Build immutable distribution manifests with deterministic digest and version increment.", - "SEMANTICS": [ - "clean-release", - "manifest", - "versioning", - "immutability", - "lifecycle" - ], "SIDE_EFFECT": "May modify manifest state during processing" }, "relations": [ { "source_id": "ManifestService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "RepositoryRelations", "target_ref": "[RepositoryRelations]" }, { "source_id": "ManifestService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ManifestBuilder", "target_ref": "[ManifestBuilder]" }, { "source_id": "ManifestService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" } @@ -80981,71 +59129,17 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ManifestService:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: clean-release, manifest, versioning, immutability, lifecycle\n# @PURPOSE: Build immutable distribution manifests with deterministic digest and version increment.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[RepositoryRelations]\n# @RELATION: [DEPENDS_ON] ->[ManifestBuilder]\n# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels]\n# @PRE: Candidate exists and is PREPARED or MANIFEST_BUILT; artifacts are present.\n# @POST: New immutable manifest is persisted with incremented version and deterministic digest.\n# @INVARIANT: Existing manifests are never mutated.\n# @SIDE_EFFECT: May modify manifest state during processing\n# @DATA_CONTRACT: Manifest -> ManifestRecord; Candidate -> ManifestRecord\n\nfrom __future__ import annotations\n\nfrom typing import Any, Dict, List\n\nfrom ...models.clean_release import DistributionManifest\nfrom .enums import CandidateStatus\nfrom .manifest_builder import build_distribution_manifest\nfrom .repository import CleanReleaseRepository\n\n\n# [DEF:build_manifest_snapshot:Function]\n# @PURPOSE: Create a new immutable manifest version for a candidate.\n# @PRE: Candidate is prepared, artifacts are available, candidate_id is valid.\n# @POST: Returns persisted DistributionManifest with monotonically incremented version.\ndef build_manifest_snapshot(\n repository: CleanReleaseRepository,\n candidate_id: str,\n created_by: str,\n policy_id: str = \"policy-default\",\n) -> DistributionManifest:\n if not candidate_id or not candidate_id.strip():\n raise ValueError(\"candidate_id 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 candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise ValueError(f\"candidate '{candidate_id}' not found\")\n\n if candidate.status not in {\n CandidateStatus.PREPARED.value,\n CandidateStatus.MANIFEST_BUILT.value,\n }:\n raise ValueError(\n \"candidate must be PREPARED or MANIFEST_BUILT to build manifest\"\n )\n\n artifacts = repository.get_artifacts_by_candidate(candidate_id)\n if not artifacts:\n raise ValueError(\"candidate artifacts are required to build manifest\")\n\n existing = repository.get_manifests_by_candidate(candidate_id)\n for manifest in existing:\n if not manifest.immutable:\n raise ValueError(\"existing manifest immutability invariant violated\")\n\n next_version = max((m.manifest_version for m in existing), default=0) + 1\n manifest_id = f\"manifest-{candidate_id}-v{next_version}\"\n\n classified_artifacts: List[Dict[str, Any]] = [\n {\n \"path\": artifact.path,\n \"category\": artifact.detected_category or \"generic\",\n \"classification\": \"allowed\",\n \"reason\": \"artifact import\",\n \"checksum\": artifact.sha256,\n }\n for artifact in artifacts\n ]\n\n manifest = build_distribution_manifest(\n manifest_id=manifest_id,\n candidate_id=candidate_id,\n policy_id=policy_id,\n generated_by=created_by,\n artifacts=classified_artifacts,\n )\n manifest.manifest_version = next_version\n manifest.source_snapshot_ref = candidate.source_snapshot_ref\n manifest.artifacts_digest = manifest.manifest_digest\n manifest.immutable = True\n repository.save_manifest(manifest)\n\n if candidate.status == CandidateStatus.PREPARED.value:\n candidate.transition_to(CandidateStatus.MANIFEST_BUILT)\n repository.save_candidate(candidate)\n\n return manifest\n\n\n# [/DEF:build_manifest_snapshot:Function]\n\n# [/DEF:ManifestService:Module]\n" + "anchor_syntax": "region", + "body": "# #region ManifestService [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, verify, digest]\n# @BRIEF Build immutable distribution manifests with deterministic digest and version increment.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @RELATION DEPENDS_ON -> [ManifestBuilder]\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @PRE: Candidate exists and is PREPARED or MANIFEST_BUILT; artifacts are present.\n# @POST: New immutable manifest is persisted with incremented version and deterministic digest.\n# @INVARIANT: Existing manifests are never mutated.\n# @SIDE_EFFECT: May modify manifest state during processing\n# @DATA_CONTRACT: Manifest -> ManifestRecord; Candidate -> ManifestRecord\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom ...models.clean_release import DistributionManifest\nfrom .enums import CandidateStatus\nfrom .manifest_builder import build_distribution_manifest\nfrom .repository import CleanReleaseRepository\n\n\n# #region build_manifest_snapshot [TYPE Function]\n# @BRIEF Create a new immutable manifest version for a candidate.\n# @PRE: Candidate is prepared, artifacts are available, candidate_id is valid.\n# @POST: Returns persisted DistributionManifest with monotonically incremented version.\ndef build_manifest_snapshot(\n repository: CleanReleaseRepository,\n candidate_id: str,\n created_by: str,\n policy_id: str = \"policy-default\",\n) -> DistributionManifest:\n if not candidate_id or not candidate_id.strip():\n raise ValueError(\"candidate_id 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 candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise ValueError(f\"candidate '{candidate_id}' not found\")\n\n if candidate.status not in {\n CandidateStatus.PREPARED.value,\n CandidateStatus.MANIFEST_BUILT.value,\n }:\n raise ValueError(\n \"candidate must be PREPARED or MANIFEST_BUILT to build manifest\"\n )\n\n artifacts = repository.get_artifacts_by_candidate(candidate_id)\n if not artifacts:\n raise ValueError(\"candidate artifacts are required to build manifest\")\n\n existing = repository.get_manifests_by_candidate(candidate_id)\n for manifest in existing:\n if not manifest.immutable:\n raise ValueError(\"existing manifest immutability invariant violated\")\n\n next_version = max((m.manifest_version for m in existing), default=0) + 1\n manifest_id = f\"manifest-{candidate_id}-v{next_version}\"\n\n classified_artifacts: list[dict[str, Any]] = [\n {\n \"path\": artifact.path,\n \"category\": artifact.detected_category or \"generic\",\n \"classification\": \"allowed\",\n \"reason\": \"artifact import\",\n \"checksum\": artifact.sha256,\n }\n for artifact in artifacts\n ]\n\n manifest = build_distribution_manifest(\n manifest_id=manifest_id,\n candidate_id=candidate_id,\n policy_id=policy_id,\n generated_by=created_by,\n artifacts=classified_artifacts,\n )\n manifest.manifest_version = next_version\n manifest.source_snapshot_ref = candidate.source_snapshot_ref\n manifest.artifacts_digest = manifest.manifest_digest\n manifest.immutable = True\n repository.save_manifest(manifest)\n\n if candidate.status == CandidateStatus.PREPARED.value:\n candidate.transition_to(CandidateStatus.MANIFEST_BUILT)\n repository.save_candidate(candidate)\n\n return manifest\n\n\n# #endregion build_manifest_snapshot\n\n# #endregion ManifestService\n" }, { "contract_id": "build_manifest_snapshot", "contract_type": "Function", "file_path": "backend/src/services/clean_release/manifest_service.py", - "start_line": 25, - "end_line": 95, + "start_line": 23, + "end_line": 93, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81083,19 +59177,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_manifest_snapshot:Function]\n# @PURPOSE: Create a new immutable manifest version for a candidate.\n# @PRE: Candidate is prepared, artifacts are available, candidate_id is valid.\n# @POST: Returns persisted DistributionManifest with monotonically incremented version.\ndef build_manifest_snapshot(\n repository: CleanReleaseRepository,\n candidate_id: str,\n created_by: str,\n policy_id: str = \"policy-default\",\n) -> DistributionManifest:\n if not candidate_id or not candidate_id.strip():\n raise ValueError(\"candidate_id 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 candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise ValueError(f\"candidate '{candidate_id}' not found\")\n\n if candidate.status not in {\n CandidateStatus.PREPARED.value,\n CandidateStatus.MANIFEST_BUILT.value,\n }:\n raise ValueError(\n \"candidate must be PREPARED or MANIFEST_BUILT to build manifest\"\n )\n\n artifacts = repository.get_artifacts_by_candidate(candidate_id)\n if not artifacts:\n raise ValueError(\"candidate artifacts are required to build manifest\")\n\n existing = repository.get_manifests_by_candidate(candidate_id)\n for manifest in existing:\n if not manifest.immutable:\n raise ValueError(\"existing manifest immutability invariant violated\")\n\n next_version = max((m.manifest_version for m in existing), default=0) + 1\n manifest_id = f\"manifest-{candidate_id}-v{next_version}\"\n\n classified_artifacts: List[Dict[str, Any]] = [\n {\n \"path\": artifact.path,\n \"category\": artifact.detected_category or \"generic\",\n \"classification\": \"allowed\",\n \"reason\": \"artifact import\",\n \"checksum\": artifact.sha256,\n }\n for artifact in artifacts\n ]\n\n manifest = build_distribution_manifest(\n manifest_id=manifest_id,\n candidate_id=candidate_id,\n policy_id=policy_id,\n generated_by=created_by,\n artifacts=classified_artifacts,\n )\n manifest.manifest_version = next_version\n manifest.source_snapshot_ref = candidate.source_snapshot_ref\n manifest.artifacts_digest = manifest.manifest_digest\n manifest.immutable = True\n repository.save_manifest(manifest)\n\n if candidate.status == CandidateStatus.PREPARED.value:\n candidate.transition_to(CandidateStatus.MANIFEST_BUILT)\n repository.save_candidate(candidate)\n\n return manifest\n\n\n# [/DEF:build_manifest_snapshot:Function]\n" + "anchor_syntax": "region", + "body": "# #region build_manifest_snapshot [TYPE Function]\n# @BRIEF Create a new immutable manifest version for a candidate.\n# @PRE: Candidate is prepared, artifacts are available, candidate_id is valid.\n# @POST: Returns persisted DistributionManifest with monotonically incremented version.\ndef build_manifest_snapshot(\n repository: CleanReleaseRepository,\n candidate_id: str,\n created_by: str,\n policy_id: str = \"policy-default\",\n) -> DistributionManifest:\n if not candidate_id or not candidate_id.strip():\n raise ValueError(\"candidate_id 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 candidate = repository.get_candidate(candidate_id)\n if candidate is None:\n raise ValueError(f\"candidate '{candidate_id}' not found\")\n\n if candidate.status not in {\n CandidateStatus.PREPARED.value,\n CandidateStatus.MANIFEST_BUILT.value,\n }:\n raise ValueError(\n \"candidate must be PREPARED or MANIFEST_BUILT to build manifest\"\n )\n\n artifacts = repository.get_artifacts_by_candidate(candidate_id)\n if not artifacts:\n raise ValueError(\"candidate artifacts are required to build manifest\")\n\n existing = repository.get_manifests_by_candidate(candidate_id)\n for manifest in existing:\n if not manifest.immutable:\n raise ValueError(\"existing manifest immutability invariant violated\")\n\n next_version = max((m.manifest_version for m in existing), default=0) + 1\n manifest_id = f\"manifest-{candidate_id}-v{next_version}\"\n\n classified_artifacts: list[dict[str, Any]] = [\n {\n \"path\": artifact.path,\n \"category\": artifact.detected_category or \"generic\",\n \"classification\": \"allowed\",\n \"reason\": \"artifact import\",\n \"checksum\": artifact.sha256,\n }\n for artifact in artifacts\n ]\n\n manifest = build_distribution_manifest(\n manifest_id=manifest_id,\n candidate_id=candidate_id,\n policy_id=policy_id,\n generated_by=created_by,\n artifacts=classified_artifacts,\n )\n manifest.manifest_version = next_version\n manifest.source_snapshot_ref = candidate.source_snapshot_ref\n manifest.artifacts_digest = manifest.manifest_digest\n manifest.immutable = True\n repository.save_manifest(manifest)\n\n if candidate.status == CandidateStatus.PREPARED.value:\n candidate.transition_to(CandidateStatus.MANIFEST_BUILT)\n repository.save_candidate(candidate)\n\n return manifest\n\n\n# #endregion build_manifest_snapshot\n" }, { "contract_id": "clean_release_mappers", "contract_type": "Module", "file_path": "backend/src/services/clean_release/mappers.py", "start_line": 1, - "end_line": 68, + "end_line": 56, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Application", "PURPOSE": "Map between domain entities (SQLAlchemy models) and DTOs." }, @@ -81108,43 +59202,37 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:clean_release_mappers:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Map between domain entities (SQLAlchemy models) and DTOs.\n# @LAYER: Application\n# @RELATION: DEPENDS_ON -> clean_release_dto\n\nfrom typing import List\nfrom src.models.clean_release import (\n ReleaseCandidate, DistributionManifest, ComplianceRun,\n ComplianceStageRun, ComplianceViolation, ComplianceReport,\n CleanPolicySnapshot, SourceRegistrySnapshot, ApprovalDecision,\n PublicationRecord\n)\nfrom src.services.clean_release.dto import (\n CandidateDTO, ArtifactDTO, ManifestDTO, ComplianceRunDTO,\n ReportDTO\n)\nfrom src.services.clean_release.enums import (\n CandidateStatus, RunStatus, ComplianceDecision,\n ViolationSeverity, ViolationCategory\n)\n\ndef map_candidate_to_dto(candidate: ReleaseCandidate) -> CandidateDTO:\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n build_id=candidate.build_id,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status)\n )\n\ndef map_manifest_to_dto(manifest: DistributionManifest) -> ManifestDTO:\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json or {}\n )\n\ndef map_run_to_dto(run: ComplianceRun) -> ComplianceRunDTO:\n return ComplianceRunDTO(\n run_id=run.id,\n candidate_id=run.candidate_id,\n status=RunStatus(run.status),\n final_status=ComplianceDecision(run.final_status) if run.final_status else None,\n task_id=run.task_id\n )\n\ndef map_report_to_dto(report: ComplianceReport) -> ReportDTO:\n # Note: ReportDTO in dto.py is a compact view\n return ReportDTO(\n report_id=report.id,\n candidate_id=report.candidate_id,\n final_status=ComplianceDecision(report.final_status),\n policy_version=\"unknown\", # Would need to resolve from run/snapshot\n manifest_digest=\"unknown\", # Would need to resolve from run/manifest\n violation_count=0, # Would need to resolve from violations\n generated_at=report.generated_at\n )\n\n# [/DEF:clean_release_mappers:Module]\n" + "anchor_syntax": "region", + "body": "# #region clean_release_mappers [C:3] [TYPE Module] [SEMANTICS clean-release, mapper, dto, entity, transform]\n# @BRIEF Map between domain entities (SQLAlchemy models) and DTOs.\n# @LAYER: Application\n# @RELATION DEPENDS_ON -> clean_release_dto\n\nfrom src.models.clean_release import ComplianceReport, ComplianceRun, DistributionManifest, ReleaseCandidate\nfrom src.services.clean_release.dto import CandidateDTO, ComplianceRunDTO, ManifestDTO, ReportDTO\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision, RunStatus\n\n\ndef map_candidate_to_dto(candidate: ReleaseCandidate) -> CandidateDTO:\n return CandidateDTO(\n id=candidate.id,\n version=candidate.version,\n source_snapshot_ref=candidate.source_snapshot_ref,\n build_id=candidate.build_id,\n created_at=candidate.created_at,\n created_by=candidate.created_by,\n status=CandidateStatus(candidate.status)\n )\n\ndef map_manifest_to_dto(manifest: DistributionManifest) -> ManifestDTO:\n return ManifestDTO(\n id=manifest.id,\n candidate_id=manifest.candidate_id,\n manifest_version=manifest.manifest_version,\n manifest_digest=manifest.manifest_digest,\n artifacts_digest=manifest.artifacts_digest,\n created_at=manifest.created_at,\n created_by=manifest.created_by,\n source_snapshot_ref=manifest.source_snapshot_ref,\n content_json=manifest.content_json or {}\n )\n\ndef map_run_to_dto(run: ComplianceRun) -> ComplianceRunDTO:\n return ComplianceRunDTO(\n run_id=run.id,\n candidate_id=run.candidate_id,\n status=RunStatus(run.status),\n final_status=ComplianceDecision(run.final_status) if run.final_status else None,\n task_id=run.task_id\n )\n\ndef map_report_to_dto(report: ComplianceReport) -> ReportDTO:\n # Note: ReportDTO in dto.py is a compact view\n return ReportDTO(\n report_id=report.id,\n candidate_id=report.candidate_id,\n final_status=ComplianceDecision(report.final_status),\n policy_version=\"unknown\", # Would need to resolve from run/snapshot\n manifest_digest=\"unknown\", # Would need to resolve from run/manifest\n violation_count=0, # Would need to resolve from violations\n generated_at=report.generated_at\n )\n\n# #endregion clean_release_mappers\n" }, { "contract_id": "PolicyEngine", "contract_type": "Module", "file_path": "backend/src/services/clean_release/policy_engine.py", "start_line": 1, - "end_line": 236, + "end_line": 234, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Candidate -> PolicyDecision", "INVARIANT": "Enterprise-clean policy always treats non-registry sources as violations.", "LAYER": "Domain", "POST": "PolicyDecision returned with approval status", "PRE": "PolicyRepository is accessible", "PURPOSE": "Evaluate artifact/source policies for enterprise clean profile with deterministic outcomes.", - "SEMANTICS": [ - "clean-release", - "policy", - "classification", - "source-isolation" - ], "SIDE_EFFECT": "Read-only policy evaluation; no state changes" }, "relations": [ { "source_id": "PolicyEngine", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" }, { "source_id": "PolicyEngine", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LoggerModule", "target_ref": "[LoggerModule]" } @@ -81167,53 +59255,17 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:PolicyEngine:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: clean-release, policy, classification, source-isolation\n# @PURPOSE: Evaluate artifact/source policies for enterprise clean profile with deterministic outcomes.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels]\n# @RELATION: [DEPENDS_ON] ->[LoggerModule]\n# @INVARIANT: Enterprise-clean policy always treats non-registry sources as violations.\n# @DATA_CONTRACT: Candidate -> PolicyDecision\n# @PRE: PolicyRepository is accessible\n# @POST: PolicyDecision returned with approval status\n# @SIDE_EFFECT: Read-only policy evaluation; no state changes\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Dict, Iterable, List, Tuple\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.clean_release import (\n CleanPolicySnapshot,\n SourceRegistrySnapshot,\n CleanProfilePolicy,\n ResourceSourceRegistry,\n)\n\n\n@dataclass\nclass PolicyValidationResult:\n ok: bool\n blocking_reasons: List[str]\n\n\n@dataclass\nclass SourceValidationResult:\n ok: bool\n violation: Dict | None\n\n\n# [DEF:CleanPolicyEngine:Class]\n# @PRE: Active policy exists and is internally consistent.\n# @POST: Deterministic classification and source validation are available.\n# @TEST_CONTRACT: CandidateEvaluationInput -> PolicyValidationResult|SourceValidationResult\n# @TEST_SCENARIO: policy_valid -> Enterprise clean policy with matching registry returns ok=True\n# @TEST_FIXTURE: policy_enterprise_clean -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json\n# @TEST_EDGE: missing_registry_ref -> policy has empty registry_snapshot_id\n# @TEST_EDGE: conflicting_registry -> policy registry ref does not match registry id\n# @TEST_EDGE: external_endpoint -> endpoint not present in enabled internal registry entries\n# @TEST_INVARIANT: deterministic_classification -> VERIFIED_BY: [policy_valid]\nclass CleanPolicyEngine:\n def __init__(\n self,\n policy: CleanPolicySnapshot | CleanProfilePolicy,\n registry: SourceRegistrySnapshot | ResourceSourceRegistry,\n ):\n self.policy = policy\n self.registry = registry\n\n def validate_policy(self) -> PolicyValidationResult:\n with belief_scope(\"clean_policy_engine.validate_policy\"):\n logger.reason(\n \"Validating enterprise-clean policy and internal registry consistency\"\n )\n reasons: List[str] = []\n\n registry_ref = (\n getattr(self.policy, \"registry_snapshot_id\", None)\n or getattr(self.policy, \"internal_source_registry_ref\", \"\")\n or \"\"\n )\n if not str(registry_ref).strip():\n reasons.append(\"Policy missing internal_source_registry_ref\")\n\n content = dict(getattr(self.policy, \"content_json\", None) or {})\n if not content:\n content = {\n \"profile\": getattr(\n getattr(self.policy, \"profile\", None),\n \"value\",\n getattr(self.policy, \"profile\", \"standard\"),\n ),\n \"prohibited_artifact_categories\": list(\n getattr(self.policy, \"prohibited_artifact_categories\", []) or []\n ),\n \"required_system_categories\": list(\n getattr(self.policy, \"required_system_categories\", []) or []\n ),\n \"external_source_forbidden\": getattr(\n self.policy, \"external_source_forbidden\", False\n ),\n }\n\n profile = content.get(\"profile\", \"standard\")\n\n if profile == \"enterprise-clean\":\n if not content.get(\"prohibited_artifact_categories\"):\n reasons.append(\n \"Enterprise policy requires prohibited artifact categories\"\n )\n if not content.get(\"external_source_forbidden\"):\n reasons.append(\n \"Enterprise policy requires external_source_forbidden=true\"\n )\n\n registry_id = getattr(self.registry, \"id\", None) or getattr(\n self.registry, \"registry_id\", None\n )\n if registry_id != registry_ref:\n reasons.append(\"Policy registry ref does not match provided registry\")\n\n allowed_hosts = getattr(self.registry, \"allowed_hosts\", None)\n if allowed_hosts is None:\n entries = getattr(self.registry, \"entries\", []) or []\n allowed_hosts = [\n entry.host for entry in entries if getattr(entry, \"enabled\", True)\n ]\n\n if not allowed_hosts:\n reasons.append(\"Registry must contain allowed hosts\")\n\n logger.reflect(\n f\"Policy validation completed. blocking_reasons={len(reasons)}\"\n )\n return PolicyValidationResult(\n ok=len(reasons) == 0, blocking_reasons=reasons\n )\n\n def classify_artifact(self, artifact: Dict) -> str:\n category = (artifact.get(\"category\") or \"\").strip()\n content = dict(getattr(self.policy, \"content_json\", None) or {})\n if not content:\n content = {\n \"required_system_categories\": list(\n getattr(self.policy, \"required_system_categories\", []) or []\n ),\n \"prohibited_artifact_categories\": list(\n getattr(self.policy, \"prohibited_artifact_categories\", []) or []\n ),\n }\n\n required = content.get(\"required_system_categories\", [])\n prohibited = content.get(\"prohibited_artifact_categories\", [])\n\n if category in required:\n logger.reason(\n f\"Artifact category '{category}' classified as required-system\"\n )\n return \"required-system\"\n if category in prohibited:\n logger.reason(\n f\"Artifact category '{category}' classified as excluded-prohibited\"\n )\n return \"excluded-prohibited\"\n logger.reflect(f\"Artifact category '{category}' classified as allowed\")\n return \"allowed\"\n\n def validate_resource_source(self, endpoint: str) -> SourceValidationResult:\n with belief_scope(\"clean_policy_engine.validate_resource_source\"):\n if not endpoint:\n logger.explore(\n \"Empty endpoint detected; treating as blocking external-source violation\"\n )\n return SourceValidationResult(\n ok=False,\n violation={\n \"category\": \"external-source\",\n \"location\": \"\",\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n },\n )\n\n allowed_hosts = getattr(self.registry, \"allowed_hosts\", None)\n if allowed_hosts is None:\n entries = getattr(self.registry, \"entries\", []) or []\n allowed_hosts = [\n entry.host for entry in entries if getattr(entry, \"enabled\", True)\n ]\n allowed_hosts = set(allowed_hosts or [])\n normalized = endpoint.strip().lower()\n\n if normalized in allowed_hosts:\n logger.reason(\n f\"Endpoint '{normalized}' is present in internal allowlist\"\n )\n return SourceValidationResult(ok=True, violation=None)\n\n logger.explore(f\"Endpoint '{endpoint}' is outside internal allowlist\")\n return SourceValidationResult(\n ok=False,\n violation={\n \"category\": \"external-source\",\n \"location\": endpoint,\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n },\n )\n\n def evaluate_candidate(\n self, artifacts: Iterable[Dict], sources: Iterable[str]\n ) -> Tuple[List[Dict], List[Dict]]:\n with belief_scope(\"clean_policy_engine.evaluate_candidate\"):\n logger.reason(\n \"Evaluating candidate artifacts and resource sources against enterprise policy\"\n )\n classified: List[Dict] = []\n violations: List[Dict] = []\n\n for artifact in artifacts:\n classification = self.classify_artifact(artifact)\n enriched = dict(artifact)\n enriched[\"classification\"] = classification\n if classification == \"excluded-prohibited\":\n violations.append(\n {\n \"category\": \"data-purity\",\n \"location\": artifact.get(\"path\", \"\"),\n \"remediation\": \"Remove prohibited content\",\n \"blocked_release\": True,\n }\n )\n classified.append(enriched)\n\n for source in sources:\n source_result = self.validate_resource_source(source)\n if not source_result.ok and source_result.violation:\n violations.append(source_result.violation)\n\n logger.reflect(\n f\"Candidate evaluation finished. artifacts={len(classified)} violations={len(violations)}\"\n )\n return classified, violations\n\n\n# [/DEF:CleanPolicyEngine:Class]\n# [/DEF:PolicyEngine:Module]\n" + "anchor_syntax": "region", + "body": "# #region PolicyEngine [C:5] [TYPE Module] [SEMANTICS clean-release, policy, validate, profile, artifact]\n# @BRIEF Evaluate artifact/source policies for enterprise clean profile with deterministic outcomes.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT: Enterprise-clean policy always treats non-registry sources as violations.\n# @DATA_CONTRACT: Candidate -> PolicyDecision\n# @PRE: PolicyRepository is accessible\n# @POST: PolicyDecision returned with approval status\n# @SIDE_EFFECT: Read-only policy evaluation; no state changes\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom dataclasses import dataclass\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.clean_release import (\n CleanPolicySnapshot,\n CleanProfilePolicy,\n ResourceSourceRegistry,\n SourceRegistrySnapshot,\n)\n\n\n@dataclass\nclass PolicyValidationResult:\n ok: bool\n blocking_reasons: list[str]\n\n\n@dataclass\nclass SourceValidationResult:\n ok: bool\n violation: dict | None\n\n\n# #region CleanPolicyEngine [TYPE Class]\n# @PRE: Active policy exists and is internally consistent.\n# @POST: Deterministic classification and source validation are available.\n# @TEST_CONTRACT: CandidateEvaluationInput -> PolicyValidationResult|SourceValidationResult\n# @TEST_SCENARIO: policy_valid -> Enterprise clean policy with matching registry returns ok=True\n# @TEST_FIXTURE: policy_enterprise_clean -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json\n# @TEST_EDGE: missing_registry_ref -> policy has empty registry_snapshot_id\n# @TEST_EDGE: conflicting_registry -> policy registry ref does not match registry id\n# @TEST_EDGE: external_endpoint -> endpoint not present in enabled internal registry entries\n# @TEST_INVARIANT: deterministic_classification -> VERIFIED_BY: [policy_valid]\nclass CleanPolicyEngine:\n def __init__(\n self,\n policy: CleanPolicySnapshot | CleanProfilePolicy,\n registry: SourceRegistrySnapshot | ResourceSourceRegistry,\n ):\n self.policy = policy\n self.registry = registry\n\n def validate_policy(self) -> PolicyValidationResult:\n with belief_scope(\"clean_policy_engine.validate_policy\"):\n logger.reason(\n \"Validating enterprise-clean policy and internal registry consistency\"\n )\n reasons: list[str] = []\n\n registry_ref = (\n getattr(self.policy, \"registry_snapshot_id\", None)\n or getattr(self.policy, \"internal_source_registry_ref\", \"\")\n or \"\"\n )\n if not str(registry_ref).strip():\n reasons.append(\"Policy missing internal_source_registry_ref\")\n\n content = dict(getattr(self.policy, \"content_json\", None) or {})\n if not content:\n content = {\n \"profile\": getattr(\n getattr(self.policy, \"profile\", None),\n \"value\",\n getattr(self.policy, \"profile\", \"standard\"),\n ),\n \"prohibited_artifact_categories\": list(\n getattr(self.policy, \"prohibited_artifact_categories\", []) or []\n ),\n \"required_system_categories\": list(\n getattr(self.policy, \"required_system_categories\", []) or []\n ),\n \"external_source_forbidden\": getattr(\n self.policy, \"external_source_forbidden\", False\n ),\n }\n\n profile = content.get(\"profile\", \"standard\")\n\n if profile == \"enterprise-clean\":\n if not content.get(\"prohibited_artifact_categories\"):\n reasons.append(\n \"Enterprise policy requires prohibited artifact categories\"\n )\n if not content.get(\"external_source_forbidden\"):\n reasons.append(\n \"Enterprise policy requires external_source_forbidden=true\"\n )\n\n registry_id = getattr(self.registry, \"id\", None) or getattr(\n self.registry, \"registry_id\", None\n )\n if registry_id != registry_ref:\n reasons.append(\"Policy registry ref does not match provided registry\")\n\n allowed_hosts = getattr(self.registry, \"allowed_hosts\", None)\n if allowed_hosts is None:\n entries = getattr(self.registry, \"entries\", []) or []\n allowed_hosts = [\n entry.host for entry in entries if getattr(entry, \"enabled\", True)\n ]\n\n if not allowed_hosts:\n reasons.append(\"Registry must contain allowed hosts\")\n\n logger.reflect(\n f\"Policy validation completed. blocking_reasons={len(reasons)}\"\n )\n return PolicyValidationResult(\n ok=len(reasons) == 0, blocking_reasons=reasons\n )\n\n def classify_artifact(self, artifact: dict) -> str:\n category = (artifact.get(\"category\") or \"\").strip()\n content = dict(getattr(self.policy, \"content_json\", None) or {})\n if not content:\n content = {\n \"required_system_categories\": list(\n getattr(self.policy, \"required_system_categories\", []) or []\n ),\n \"prohibited_artifact_categories\": list(\n getattr(self.policy, \"prohibited_artifact_categories\", []) or []\n ),\n }\n\n required = content.get(\"required_system_categories\", [])\n prohibited = content.get(\"prohibited_artifact_categories\", [])\n\n if category in required:\n logger.reason(\n f\"Artifact category '{category}' classified as required-system\"\n )\n return \"required-system\"\n if category in prohibited:\n logger.reason(\n f\"Artifact category '{category}' classified as excluded-prohibited\"\n )\n return \"excluded-prohibited\"\n logger.reflect(f\"Artifact category '{category}' classified as allowed\")\n return \"allowed\"\n\n def validate_resource_source(self, endpoint: str) -> SourceValidationResult:\n with belief_scope(\"clean_policy_engine.validate_resource_source\"):\n if not endpoint:\n logger.explore(\n \"Empty endpoint detected; treating as blocking external-source violation\"\n )\n return SourceValidationResult(\n ok=False,\n violation={\n \"category\": \"external-source\",\n \"location\": \"\",\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n },\n )\n\n allowed_hosts = getattr(self.registry, \"allowed_hosts\", None)\n if allowed_hosts is None:\n entries = getattr(self.registry, \"entries\", []) or []\n allowed_hosts = [\n entry.host for entry in entries if getattr(entry, \"enabled\", True)\n ]\n allowed_hosts = set(allowed_hosts or [])\n normalized = endpoint.strip().lower()\n\n if normalized in allowed_hosts:\n logger.reason(\n f\"Endpoint '{normalized}' is present in internal allowlist\"\n )\n return SourceValidationResult(ok=True, violation=None)\n\n logger.explore(f\"Endpoint '{endpoint}' is outside internal allowlist\")\n return SourceValidationResult(\n ok=False,\n violation={\n \"category\": \"external-source\",\n \"location\": endpoint,\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n },\n )\n\n def evaluate_candidate(\n self, artifacts: Iterable[dict], sources: Iterable[str]\n ) -> tuple[list[dict], list[dict]]:\n with belief_scope(\"clean_policy_engine.evaluate_candidate\"):\n logger.reason(\n \"Evaluating candidate artifacts and resource sources against enterprise policy\"\n )\n classified: list[dict] = []\n violations: list[dict] = []\n\n for artifact in artifacts:\n classification = self.classify_artifact(artifact)\n enriched = dict(artifact)\n enriched[\"classification\"] = classification\n if classification == \"excluded-prohibited\":\n violations.append(\n {\n \"category\": \"data-purity\",\n \"location\": artifact.get(\"path\", \"\"),\n \"remediation\": \"Remove prohibited content\",\n \"blocked_release\": True,\n }\n )\n classified.append(enriched)\n\n for source in sources:\n source_result = self.validate_resource_source(source)\n if not source_result.ok and source_result.violation:\n violations.append(source_result.violation)\n\n logger.reflect(\n f\"Candidate evaluation finished. artifacts={len(classified)} violations={len(violations)}\"\n )\n return classified, violations\n\n\n# #endregion CleanPolicyEngine\n# #endregion PolicyEngine\n" }, { "contract_id": "CleanPolicyEngine", "contract_type": "Class", "file_path": "backend/src/services/clean_release/policy_engine.py", - "start_line": 40, - "end_line": 235, + "start_line": 38, + "end_line": 233, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81350,50 +59402,43 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanPolicyEngine:Class]\n# @PRE: Active policy exists and is internally consistent.\n# @POST: Deterministic classification and source validation are available.\n# @TEST_CONTRACT: CandidateEvaluationInput -> PolicyValidationResult|SourceValidationResult\n# @TEST_SCENARIO: policy_valid -> Enterprise clean policy with matching registry returns ok=True\n# @TEST_FIXTURE: policy_enterprise_clean -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json\n# @TEST_EDGE: missing_registry_ref -> policy has empty registry_snapshot_id\n# @TEST_EDGE: conflicting_registry -> policy registry ref does not match registry id\n# @TEST_EDGE: external_endpoint -> endpoint not present in enabled internal registry entries\n# @TEST_INVARIANT: deterministic_classification -> VERIFIED_BY: [policy_valid]\nclass CleanPolicyEngine:\n def __init__(\n self,\n policy: CleanPolicySnapshot | CleanProfilePolicy,\n registry: SourceRegistrySnapshot | ResourceSourceRegistry,\n ):\n self.policy = policy\n self.registry = registry\n\n def validate_policy(self) -> PolicyValidationResult:\n with belief_scope(\"clean_policy_engine.validate_policy\"):\n logger.reason(\n \"Validating enterprise-clean policy and internal registry consistency\"\n )\n reasons: List[str] = []\n\n registry_ref = (\n getattr(self.policy, \"registry_snapshot_id\", None)\n or getattr(self.policy, \"internal_source_registry_ref\", \"\")\n or \"\"\n )\n if not str(registry_ref).strip():\n reasons.append(\"Policy missing internal_source_registry_ref\")\n\n content = dict(getattr(self.policy, \"content_json\", None) or {})\n if not content:\n content = {\n \"profile\": getattr(\n getattr(self.policy, \"profile\", None),\n \"value\",\n getattr(self.policy, \"profile\", \"standard\"),\n ),\n \"prohibited_artifact_categories\": list(\n getattr(self.policy, \"prohibited_artifact_categories\", []) or []\n ),\n \"required_system_categories\": list(\n getattr(self.policy, \"required_system_categories\", []) or []\n ),\n \"external_source_forbidden\": getattr(\n self.policy, \"external_source_forbidden\", False\n ),\n }\n\n profile = content.get(\"profile\", \"standard\")\n\n if profile == \"enterprise-clean\":\n if not content.get(\"prohibited_artifact_categories\"):\n reasons.append(\n \"Enterprise policy requires prohibited artifact categories\"\n )\n if not content.get(\"external_source_forbidden\"):\n reasons.append(\n \"Enterprise policy requires external_source_forbidden=true\"\n )\n\n registry_id = getattr(self.registry, \"id\", None) or getattr(\n self.registry, \"registry_id\", None\n )\n if registry_id != registry_ref:\n reasons.append(\"Policy registry ref does not match provided registry\")\n\n allowed_hosts = getattr(self.registry, \"allowed_hosts\", None)\n if allowed_hosts is None:\n entries = getattr(self.registry, \"entries\", []) or []\n allowed_hosts = [\n entry.host for entry in entries if getattr(entry, \"enabled\", True)\n ]\n\n if not allowed_hosts:\n reasons.append(\"Registry must contain allowed hosts\")\n\n logger.reflect(\n f\"Policy validation completed. blocking_reasons={len(reasons)}\"\n )\n return PolicyValidationResult(\n ok=len(reasons) == 0, blocking_reasons=reasons\n )\n\n def classify_artifact(self, artifact: Dict) -> str:\n category = (artifact.get(\"category\") or \"\").strip()\n content = dict(getattr(self.policy, \"content_json\", None) or {})\n if not content:\n content = {\n \"required_system_categories\": list(\n getattr(self.policy, \"required_system_categories\", []) or []\n ),\n \"prohibited_artifact_categories\": list(\n getattr(self.policy, \"prohibited_artifact_categories\", []) or []\n ),\n }\n\n required = content.get(\"required_system_categories\", [])\n prohibited = content.get(\"prohibited_artifact_categories\", [])\n\n if category in required:\n logger.reason(\n f\"Artifact category '{category}' classified as required-system\"\n )\n return \"required-system\"\n if category in prohibited:\n logger.reason(\n f\"Artifact category '{category}' classified as excluded-prohibited\"\n )\n return \"excluded-prohibited\"\n logger.reflect(f\"Artifact category '{category}' classified as allowed\")\n return \"allowed\"\n\n def validate_resource_source(self, endpoint: str) -> SourceValidationResult:\n with belief_scope(\"clean_policy_engine.validate_resource_source\"):\n if not endpoint:\n logger.explore(\n \"Empty endpoint detected; treating as blocking external-source violation\"\n )\n return SourceValidationResult(\n ok=False,\n violation={\n \"category\": \"external-source\",\n \"location\": \"\",\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n },\n )\n\n allowed_hosts = getattr(self.registry, \"allowed_hosts\", None)\n if allowed_hosts is None:\n entries = getattr(self.registry, \"entries\", []) or []\n allowed_hosts = [\n entry.host for entry in entries if getattr(entry, \"enabled\", True)\n ]\n allowed_hosts = set(allowed_hosts or [])\n normalized = endpoint.strip().lower()\n\n if normalized in allowed_hosts:\n logger.reason(\n f\"Endpoint '{normalized}' is present in internal allowlist\"\n )\n return SourceValidationResult(ok=True, violation=None)\n\n logger.explore(f\"Endpoint '{endpoint}' is outside internal allowlist\")\n return SourceValidationResult(\n ok=False,\n violation={\n \"category\": \"external-source\",\n \"location\": endpoint,\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n },\n )\n\n def evaluate_candidate(\n self, artifacts: Iterable[Dict], sources: Iterable[str]\n ) -> Tuple[List[Dict], List[Dict]]:\n with belief_scope(\"clean_policy_engine.evaluate_candidate\"):\n logger.reason(\n \"Evaluating candidate artifacts and resource sources against enterprise policy\"\n )\n classified: List[Dict] = []\n violations: List[Dict] = []\n\n for artifact in artifacts:\n classification = self.classify_artifact(artifact)\n enriched = dict(artifact)\n enriched[\"classification\"] = classification\n if classification == \"excluded-prohibited\":\n violations.append(\n {\n \"category\": \"data-purity\",\n \"location\": artifact.get(\"path\", \"\"),\n \"remediation\": \"Remove prohibited content\",\n \"blocked_release\": True,\n }\n )\n classified.append(enriched)\n\n for source in sources:\n source_result = self.validate_resource_source(source)\n if not source_result.ok and source_result.violation:\n violations.append(source_result.violation)\n\n logger.reflect(\n f\"Candidate evaluation finished. artifacts={len(classified)} violations={len(violations)}\"\n )\n return classified, violations\n\n\n# [/DEF:CleanPolicyEngine:Class]\n" + "anchor_syntax": "region", + "body": "# #region CleanPolicyEngine [TYPE Class]\n# @PRE: Active policy exists and is internally consistent.\n# @POST: Deterministic classification and source validation are available.\n# @TEST_CONTRACT: CandidateEvaluationInput -> PolicyValidationResult|SourceValidationResult\n# @TEST_SCENARIO: policy_valid -> Enterprise clean policy with matching registry returns ok=True\n# @TEST_FIXTURE: policy_enterprise_clean -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json\n# @TEST_EDGE: missing_registry_ref -> policy has empty registry_snapshot_id\n# @TEST_EDGE: conflicting_registry -> policy registry ref does not match registry id\n# @TEST_EDGE: external_endpoint -> endpoint not present in enabled internal registry entries\n# @TEST_INVARIANT: deterministic_classification -> VERIFIED_BY: [policy_valid]\nclass CleanPolicyEngine:\n def __init__(\n self,\n policy: CleanPolicySnapshot | CleanProfilePolicy,\n registry: SourceRegistrySnapshot | ResourceSourceRegistry,\n ):\n self.policy = policy\n self.registry = registry\n\n def validate_policy(self) -> PolicyValidationResult:\n with belief_scope(\"clean_policy_engine.validate_policy\"):\n logger.reason(\n \"Validating enterprise-clean policy and internal registry consistency\"\n )\n reasons: list[str] = []\n\n registry_ref = (\n getattr(self.policy, \"registry_snapshot_id\", None)\n or getattr(self.policy, \"internal_source_registry_ref\", \"\")\n or \"\"\n )\n if not str(registry_ref).strip():\n reasons.append(\"Policy missing internal_source_registry_ref\")\n\n content = dict(getattr(self.policy, \"content_json\", None) or {})\n if not content:\n content = {\n \"profile\": getattr(\n getattr(self.policy, \"profile\", None),\n \"value\",\n getattr(self.policy, \"profile\", \"standard\"),\n ),\n \"prohibited_artifact_categories\": list(\n getattr(self.policy, \"prohibited_artifact_categories\", []) or []\n ),\n \"required_system_categories\": list(\n getattr(self.policy, \"required_system_categories\", []) or []\n ),\n \"external_source_forbidden\": getattr(\n self.policy, \"external_source_forbidden\", False\n ),\n }\n\n profile = content.get(\"profile\", \"standard\")\n\n if profile == \"enterprise-clean\":\n if not content.get(\"prohibited_artifact_categories\"):\n reasons.append(\n \"Enterprise policy requires prohibited artifact categories\"\n )\n if not content.get(\"external_source_forbidden\"):\n reasons.append(\n \"Enterprise policy requires external_source_forbidden=true\"\n )\n\n registry_id = getattr(self.registry, \"id\", None) or getattr(\n self.registry, \"registry_id\", None\n )\n if registry_id != registry_ref:\n reasons.append(\"Policy registry ref does not match provided registry\")\n\n allowed_hosts = getattr(self.registry, \"allowed_hosts\", None)\n if allowed_hosts is None:\n entries = getattr(self.registry, \"entries\", []) or []\n allowed_hosts = [\n entry.host for entry in entries if getattr(entry, \"enabled\", True)\n ]\n\n if not allowed_hosts:\n reasons.append(\"Registry must contain allowed hosts\")\n\n logger.reflect(\n f\"Policy validation completed. blocking_reasons={len(reasons)}\"\n )\n return PolicyValidationResult(\n ok=len(reasons) == 0, blocking_reasons=reasons\n )\n\n def classify_artifact(self, artifact: dict) -> str:\n category = (artifact.get(\"category\") or \"\").strip()\n content = dict(getattr(self.policy, \"content_json\", None) or {})\n if not content:\n content = {\n \"required_system_categories\": list(\n getattr(self.policy, \"required_system_categories\", []) or []\n ),\n \"prohibited_artifact_categories\": list(\n getattr(self.policy, \"prohibited_artifact_categories\", []) or []\n ),\n }\n\n required = content.get(\"required_system_categories\", [])\n prohibited = content.get(\"prohibited_artifact_categories\", [])\n\n if category in required:\n logger.reason(\n f\"Artifact category '{category}' classified as required-system\"\n )\n return \"required-system\"\n if category in prohibited:\n logger.reason(\n f\"Artifact category '{category}' classified as excluded-prohibited\"\n )\n return \"excluded-prohibited\"\n logger.reflect(f\"Artifact category '{category}' classified as allowed\")\n return \"allowed\"\n\n def validate_resource_source(self, endpoint: str) -> SourceValidationResult:\n with belief_scope(\"clean_policy_engine.validate_resource_source\"):\n if not endpoint:\n logger.explore(\n \"Empty endpoint detected; treating as blocking external-source violation\"\n )\n return SourceValidationResult(\n ok=False,\n violation={\n \"category\": \"external-source\",\n \"location\": \"\",\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n },\n )\n\n allowed_hosts = getattr(self.registry, \"allowed_hosts\", None)\n if allowed_hosts is None:\n entries = getattr(self.registry, \"entries\", []) or []\n allowed_hosts = [\n entry.host for entry in entries if getattr(entry, \"enabled\", True)\n ]\n allowed_hosts = set(allowed_hosts or [])\n normalized = endpoint.strip().lower()\n\n if normalized in allowed_hosts:\n logger.reason(\n f\"Endpoint '{normalized}' is present in internal allowlist\"\n )\n return SourceValidationResult(ok=True, violation=None)\n\n logger.explore(f\"Endpoint '{endpoint}' is outside internal allowlist\")\n return SourceValidationResult(\n ok=False,\n violation={\n \"category\": \"external-source\",\n \"location\": endpoint,\n \"remediation\": \"Replace with approved internal server\",\n \"blocked_release\": True,\n },\n )\n\n def evaluate_candidate(\n self, artifacts: Iterable[dict], sources: Iterable[str]\n ) -> tuple[list[dict], list[dict]]:\n with belief_scope(\"clean_policy_engine.evaluate_candidate\"):\n logger.reason(\n \"Evaluating candidate artifacts and resource sources against enterprise policy\"\n )\n classified: list[dict] = []\n violations: list[dict] = []\n\n for artifact in artifacts:\n classification = self.classify_artifact(artifact)\n enriched = dict(artifact)\n enriched[\"classification\"] = classification\n if classification == \"excluded-prohibited\":\n violations.append(\n {\n \"category\": \"data-purity\",\n \"location\": artifact.get(\"path\", \"\"),\n \"remediation\": \"Remove prohibited content\",\n \"blocked_release\": True,\n }\n )\n classified.append(enriched)\n\n for source in sources:\n source_result = self.validate_resource_source(source)\n if not source_result.ok and source_result.violation:\n violations.append(source_result.violation)\n\n logger.reflect(\n f\"Candidate evaluation finished. artifacts={len(classified)} violations={len(violations)}\"\n )\n return classified, violations\n\n\n# #endregion CleanPolicyEngine\n" }, { "contract_id": "PolicyResolutionService", "contract_type": "Module", "file_path": "backend/src/services/clean_release/policy_resolution_service.py", "start_line": 1, - "end_line": 82, + "end_line": 78, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "PolicyRequest -> ResolutionResult", "INVARIANT": "Trusted snapshot resolution is based only on ConfigManager active identifiers.", "LAYER": "Domain", "POST": "ResolutionResult with matched policies", "PRE": "PolicyRepository and Manifest are available", "PURPOSE": "Resolve trusted policy and registry snapshots from ConfigManager without runtime overrides.", - "SEMANTICS": [ - "clean-release", - "policy", - "registry", - "trusted-resolution", - "immutable-snapshots" - ], "SIDE_EFFECT": "Read-only policy evaluation; logs resolution decisions" }, "relations": [ { "source_id": "PolicyResolutionService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ConfigManager", "target_ref": "[ConfigManager]" }, { "source_id": "PolicyResolutionService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "RepositoryRelations", "target_ref": "[RepositoryRelations]" }, { "source_id": "PolicyResolutionService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "clean_release_exceptions", "target_ref": "[clean_release_exceptions]" } @@ -81416,71 +59461,17 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:PolicyResolutionService:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: clean-release, policy, registry, trusted-resolution, immutable-snapshots\n# @PURPOSE: Resolve trusted policy and registry snapshots from ConfigManager without runtime overrides.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[ConfigManager]\n# @RELATION: [DEPENDS_ON] ->[RepositoryRelations]\n# @RELATION: [DEPENDS_ON] ->[clean_release_exceptions]\n# @INVARIANT: Trusted snapshot resolution is based only on ConfigManager active identifiers.\n# @DATA_CONTRACT: PolicyRequest -> ResolutionResult\n# @PRE: PolicyRepository and Manifest are available\n# @POST: ResolutionResult with matched policies\n# @SIDE_EFFECT: Read-only policy evaluation; logs resolution decisions\n\nfrom __future__ import annotations\n\nfrom typing import Optional, Tuple\n\nfrom ...models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom .exceptions import PolicyResolutionError\nfrom .repository import CleanReleaseRepository\n\n\n# [DEF:resolve_trusted_policy_snapshots:Function]\n# @PURPOSE: Resolve immutable trusted policy and registry snapshots using active config IDs only.\n# @PRE: ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots.\n# @POST: Returns immutable policy and registry snapshots; runtime override attempts are rejected.\n# @SIDE_EFFECT: None.\ndef resolve_trusted_policy_snapshots(\n *,\n config_manager,\n repository: CleanReleaseRepository,\n policy_id_override: Optional[str] = None,\n registry_id_override: Optional[str] = None,\n) -> Tuple[CleanPolicySnapshot, SourceRegistrySnapshot]:\n if policy_id_override is not None or registry_id_override is not None:\n raise PolicyResolutionError(\n \"override attempt is forbidden for trusted policy resolution\"\n )\n\n config = config_manager.get_config()\n clean_release_settings = getattr(\n getattr(config, \"settings\", None), \"clean_release\", None\n )\n if clean_release_settings is None:\n raise PolicyResolutionError(\"clean_release settings are missing\")\n\n policy_id = getattr(clean_release_settings, \"active_policy_id\", None)\n registry_id = getattr(clean_release_settings, \"active_registry_id\", None)\n\n if not policy_id:\n raise PolicyResolutionError(\n \"missing trusted profile: active_policy_id is not configured\"\n )\n if not registry_id:\n raise PolicyResolutionError(\n \"missing trusted registry: active_registry_id is not configured\"\n )\n\n policy_snapshot = repository.get_policy(policy_id)\n if policy_snapshot is None:\n raise PolicyResolutionError(\n f\"trusted policy snapshot '{policy_id}' was not found\"\n )\n\n registry_snapshot = repository.get_registry(registry_id)\n if registry_snapshot is None:\n raise PolicyResolutionError(\n f\"trusted registry snapshot '{registry_id}' was not found\"\n )\n\n if not bool(getattr(policy_snapshot, \"immutable\", False)):\n raise PolicyResolutionError(\"policy snapshot must be immutable\")\n if not bool(getattr(registry_snapshot, \"immutable\", False)):\n raise PolicyResolutionError(\"registry snapshot must be immutable\")\n\n return policy_snapshot, registry_snapshot\n\n\n# [/DEF:resolve_trusted_policy_snapshots:Function]\n\n# [/DEF:PolicyResolutionService:Module]\n" + "anchor_syntax": "region", + "body": "# #region PolicyResolutionService [C:5] [TYPE Module] [SEMANTICS clean-release, policy, resolution, registry]\n# @BRIEF Resolve trusted policy and registry snapshots from ConfigManager without runtime overrides.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @RELATION DEPENDS_ON -> [clean_release_exceptions]\n# @INVARIANT: Trusted snapshot resolution is based only on ConfigManager active identifiers.\n# @DATA_CONTRACT: PolicyRequest -> ResolutionResult\n# @PRE: PolicyRepository and Manifest are available\n# @POST: ResolutionResult with matched policies\n# @SIDE_EFFECT: Read-only policy evaluation; logs resolution decisions\n\nfrom __future__ import annotations\n\nfrom ...models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom .exceptions import PolicyResolutionError\nfrom .repository import CleanReleaseRepository\n\n\n# #region resolve_trusted_policy_snapshots [TYPE Function]\n# @BRIEF Resolve immutable trusted policy and registry snapshots using active config IDs only.\n# @PRE: ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots.\n# @POST: Returns immutable policy and registry snapshots; runtime override attempts are rejected.\n# @SIDE_EFFECT: None.\ndef resolve_trusted_policy_snapshots(\n *,\n config_manager,\n repository: CleanReleaseRepository,\n policy_id_override: str | None = None,\n registry_id_override: str | None = None,\n) -> tuple[CleanPolicySnapshot, SourceRegistrySnapshot]:\n if policy_id_override is not None or registry_id_override is not None:\n raise PolicyResolutionError(\n \"override attempt is forbidden for trusted policy resolution\"\n )\n\n config = config_manager.get_config()\n clean_release_settings = getattr(\n getattr(config, \"settings\", None), \"clean_release\", None\n )\n if clean_release_settings is None:\n raise PolicyResolutionError(\"clean_release settings are missing\")\n\n policy_id = getattr(clean_release_settings, \"active_policy_id\", None)\n registry_id = getattr(clean_release_settings, \"active_registry_id\", None)\n\n if not policy_id:\n raise PolicyResolutionError(\n \"missing trusted profile: active_policy_id is not configured\"\n )\n if not registry_id:\n raise PolicyResolutionError(\n \"missing trusted registry: active_registry_id is not configured\"\n )\n\n policy_snapshot = repository.get_policy(policy_id)\n if policy_snapshot is None:\n raise PolicyResolutionError(\n f\"trusted policy snapshot '{policy_id}' was not found\"\n )\n\n registry_snapshot = repository.get_registry(registry_id)\n if registry_snapshot is None:\n raise PolicyResolutionError(\n f\"trusted registry snapshot '{registry_id}' was not found\"\n )\n\n if not bool(getattr(policy_snapshot, \"immutable\", False)):\n raise PolicyResolutionError(\"policy snapshot must be immutable\")\n if not bool(getattr(registry_snapshot, \"immutable\", False)):\n raise PolicyResolutionError(\"registry snapshot must be immutable\")\n\n return policy_snapshot, registry_snapshot\n\n\n# #endregion resolve_trusted_policy_snapshots\n\n# #endregion PolicyResolutionService\n" }, { "contract_id": "resolve_trusted_policy_snapshots", "contract_type": "Function", "file_path": "backend/src/services/clean_release/policy_resolution_service.py", - "start_line": 24, - "end_line": 80, + "start_line": 20, + "end_line": 76, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81528,45 +59519,39 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:resolve_trusted_policy_snapshots:Function]\n# @PURPOSE: Resolve immutable trusted policy and registry snapshots using active config IDs only.\n# @PRE: ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots.\n# @POST: Returns immutable policy and registry snapshots; runtime override attempts are rejected.\n# @SIDE_EFFECT: None.\ndef resolve_trusted_policy_snapshots(\n *,\n config_manager,\n repository: CleanReleaseRepository,\n policy_id_override: Optional[str] = None,\n registry_id_override: Optional[str] = None,\n) -> Tuple[CleanPolicySnapshot, SourceRegistrySnapshot]:\n if policy_id_override is not None or registry_id_override is not None:\n raise PolicyResolutionError(\n \"override attempt is forbidden for trusted policy resolution\"\n )\n\n config = config_manager.get_config()\n clean_release_settings = getattr(\n getattr(config, \"settings\", None), \"clean_release\", None\n )\n if clean_release_settings is None:\n raise PolicyResolutionError(\"clean_release settings are missing\")\n\n policy_id = getattr(clean_release_settings, \"active_policy_id\", None)\n registry_id = getattr(clean_release_settings, \"active_registry_id\", None)\n\n if not policy_id:\n raise PolicyResolutionError(\n \"missing trusted profile: active_policy_id is not configured\"\n )\n if not registry_id:\n raise PolicyResolutionError(\n \"missing trusted registry: active_registry_id is not configured\"\n )\n\n policy_snapshot = repository.get_policy(policy_id)\n if policy_snapshot is None:\n raise PolicyResolutionError(\n f\"trusted policy snapshot '{policy_id}' was not found\"\n )\n\n registry_snapshot = repository.get_registry(registry_id)\n if registry_snapshot is None:\n raise PolicyResolutionError(\n f\"trusted registry snapshot '{registry_id}' was not found\"\n )\n\n if not bool(getattr(policy_snapshot, \"immutable\", False)):\n raise PolicyResolutionError(\"policy snapshot must be immutable\")\n if not bool(getattr(registry_snapshot, \"immutable\", False)):\n raise PolicyResolutionError(\"registry snapshot must be immutable\")\n\n return policy_snapshot, registry_snapshot\n\n\n# [/DEF:resolve_trusted_policy_snapshots:Function]\n" + "anchor_syntax": "region", + "body": "# #region resolve_trusted_policy_snapshots [TYPE Function]\n# @BRIEF Resolve immutable trusted policy and registry snapshots using active config IDs only.\n# @PRE: ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots.\n# @POST: Returns immutable policy and registry snapshots; runtime override attempts are rejected.\n# @SIDE_EFFECT: None.\ndef resolve_trusted_policy_snapshots(\n *,\n config_manager,\n repository: CleanReleaseRepository,\n policy_id_override: str | None = None,\n registry_id_override: str | None = None,\n) -> tuple[CleanPolicySnapshot, SourceRegistrySnapshot]:\n if policy_id_override is not None or registry_id_override is not None:\n raise PolicyResolutionError(\n \"override attempt is forbidden for trusted policy resolution\"\n )\n\n config = config_manager.get_config()\n clean_release_settings = getattr(\n getattr(config, \"settings\", None), \"clean_release\", None\n )\n if clean_release_settings is None:\n raise PolicyResolutionError(\"clean_release settings are missing\")\n\n policy_id = getattr(clean_release_settings, \"active_policy_id\", None)\n registry_id = getattr(clean_release_settings, \"active_registry_id\", None)\n\n if not policy_id:\n raise PolicyResolutionError(\n \"missing trusted profile: active_policy_id is not configured\"\n )\n if not registry_id:\n raise PolicyResolutionError(\n \"missing trusted registry: active_registry_id is not configured\"\n )\n\n policy_snapshot = repository.get_policy(policy_id)\n if policy_snapshot is None:\n raise PolicyResolutionError(\n f\"trusted policy snapshot '{policy_id}' was not found\"\n )\n\n registry_snapshot = repository.get_registry(registry_id)\n if registry_snapshot is None:\n raise PolicyResolutionError(\n f\"trusted registry snapshot '{registry_id}' was not found\"\n )\n\n if not bool(getattr(policy_snapshot, \"immutable\", False)):\n raise PolicyResolutionError(\"policy snapshot must be immutable\")\n if not bool(getattr(registry_snapshot, \"immutable\", False)):\n raise PolicyResolutionError(\"registry snapshot must be immutable\")\n\n return policy_snapshot, registry_snapshot\n\n\n# #endregion resolve_trusted_policy_snapshots\n" }, { "contract_id": "PreparationService", "contract_type": "Module", "file_path": "backend/src/services/clean_release/preparation_service.py", "start_line": 1, - "end_line": 113, + "end_line": 111, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Candidate preparation always persists manifest and candidate status deterministically.", "LAYER": "Domain", - "PURPOSE": "Prepare release candidate by policy evaluation and deterministic manifest creation.", - "SEMANTICS": [ - "clean-release", - "preparation", - "manifest", - "policy-evaluation" - ] + "PURPOSE": "Prepare release candidate by policy evaluation and deterministic manifest creation." }, "relations": [ { "source_id": "PreparationService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "PolicyEngine", "target_ref": "[PolicyEngine]" }, { "source_id": "PreparationService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ManifestBuilder", "target_ref": "[ManifestBuilder]" }, { "source_id": "PreparationService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "RepositoryRelations", "target_ref": "[RepositoryRelations]" } @@ -81580,71 +59565,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:PreparationService:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, preparation, manifest, policy-evaluation\n# @PURPOSE: 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 datetime import datetime, timezone\nfrom typing import Dict, Iterable\n\nfrom .manifest_builder import build_distribution_manifest\nfrom .policy_engine import CleanPolicyEngine\nfrom .repository import CleanReleaseRepository\nfrom .enums import CandidateStatus\nfrom ...models.clean_release import ReleaseCandidateStatus\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(timezone.utc).isoformat(),\n }\n\n\n# [DEF:prepare_candidate_legacy:Function]\n# @PURPOSE: 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# [/DEF:prepare_candidate_legacy:Function]\n# [/DEF:PreparationService:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "prepare_candidate_legacy", "contract_type": "Function", "file_path": "backend/src/services/clean_release/preparation_service.py", - "start_line": 92, - "end_line": 112, + "start_line": 90, + "end_line": 110, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81682,15 +59613,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:prepare_candidate_legacy:Function]\n# @PURPOSE: 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# [/DEF:prepare_candidate_legacy:Function]\n" + "anchor_syntax": "region", + "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": "_get_or_init_publications_store", "contract_type": "Function", "file_path": "backend/src/services/clean_release/publication_service.py", - "start_line": 26, - "end_line": 40, + "start_line": 23, + "end_line": 37, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81728,15 +59659,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_get_or_init_publications_store:Function]\n# @PURPOSE: 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 setattr(repository, \"publication_records\", publications)\n return publications\n\n\n# [/DEF:_get_or_init_publications_store:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_latest_publication_for_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/publication_service.py", - "start_line": 43, - "end_line": 65, + "start_line": 40, + "end_line": 62, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81774,15 +59705,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_latest_publication_for_candidate:Function]\n# @PURPOSE: 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=timezone.utc),\n reverse=True,\n )[0]\n\n\n# [/DEF:_latest_publication_for_candidate:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "_latest_approval_for_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/publication_service.py", - "start_line": 68, - "end_line": 86, + "start_line": 65, + "end_line": 83, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81820,15 +59751,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_latest_approval_for_candidate:Function]\n# @PURPOSE: 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=timezone.utc),\n reverse=True,\n )[0]\n\n\n# [/DEF:_latest_approval_for_candidate:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "publish_candidate", "contract_type": "Function", "file_path": "backend/src/services/clean_release/publication_service.py", - "start_line": 89, - "end_line": 166, + "start_line": 86, + "end_line": 163, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81866,15 +59797,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:publish_candidate:Function]\n# @PURPOSE: 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\"[REASON] 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: # noqa: BLE001\n logger.explore(\n f\"[EXPLORE] 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(timezone.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\"[REFLECT] Publication persisted candidate_id={candidate_id} publication_id={record.id}\"\n )\n return record\n\n\n# [/DEF:publish_candidate:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "revoke_publication", "contract_type": "Function", "file_path": "backend/src/services/clean_release/publication_service.py", - "start_line": 169, - "end_line": 213, + "start_line": 166, + "end_line": 210, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -81912,32 +59843,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:revoke_publication:Function]\n# @PURPOSE: 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\"[REASON] 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\"[REFLECT] Publication revoked publication_id={publication_id}\")\n return record\n\n\n# [/DEF:revoke_publication:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ReportBuilder", "contract_type": "Module", "file_path": "backend/src/services/clean_release/report_builder.py", "start_line": 1, - "end_line": 79, + "end_line": 76, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[ComplianceRun, List[ComplianceViolation]] -> Output[ComplianceReport]", "INVARIANT": "blocking_violations_count never exceeds violations_count.", "LAYER": "Domain", "POST": "Returns immutable report payloads with consistent violation counters and operator summary content.", "PRE": "Compliance run is terminal and repository persistence is available for report storage.", "PURPOSE": "Build and persist compliance reports with consistent counter invariants.", - "SEMANTICS": [ - "clean-release", - "report", - "audit", - "counters", - "violations" - ], "SIDE_EFFECT": "Writes report artifacts to repository when persistence helpers are invoked.", "TEST_CONTRACT": "ComplianceCheckRun,List[ComplianceViolation] -> ComplianceReport", "TEST_EDGE": "missing_operator_summary -> non-terminal run prevents report creation and summary generation", @@ -81947,13 +59871,13 @@ "relations": [ { "source_id": "ReportBuilder", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" }, { "source_id": "ReportBuilder", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "RepositoryRelations", "target_ref": "[RepositoryRelations]" } @@ -82038,57 +59962,21 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReportBuilder:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: clean-release, report, audit, counters, violations\n# @PURPOSE: Build and persist compliance reports with consistent counter invariants.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[CleanReleaseModels]\n# @RELATION: [DEPENDS_ON] ->[RepositoryRelations]\n# @INVARIANT: blocking_violations_count never exceeds violations_count.\n# @TEST_CONTRACT: ComplianceCheckRun,List[ComplianceViolation] -> ComplianceReport\n# @TEST_FIXTURE: blocked_with_two_violations -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json\n# @TEST_EDGE: empty_violations_for_blocked -> BLOCKED run with zero blocking violations raises ValueError\n# @TEST_EDGE: counter_mismatch -> blocking counter cannot exceed total violations counter\n# @TEST_EDGE: missing_operator_summary -> non-terminal run prevents report creation and summary generation\n# @TEST_INVARIANT: blocking_count_le_total_count -> VERIFIED_BY: [counter_mismatch, empty_violations_for_blocked]\n# @DATA_CONTRACT: Input[ComplianceRun, List[ComplianceViolation]] -> Output[ComplianceReport]\n# @PRE: Compliance run is terminal and repository persistence is available for report storage.\n# @POST: Returns immutable report payloads with consistent violation counters and operator summary content.\n# @SIDE_EFFECT: Writes report artifacts to repository when persistence helpers are invoked.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\nfrom uuid import uuid4\nfrom typing import List\n\nfrom .enums import RunStatus, ComplianceDecision\nfrom ...models.clean_release import ComplianceRun, ComplianceReport, ComplianceViolation\nfrom .repository import CleanReleaseRepository\n\n\nclass ComplianceReportBuilder:\n def __init__(self, repository: CleanReleaseRepository):\n self.repository = repository\n\n def build_report_payload(\n self, check_run: ComplianceRun, violations: List[ComplianceViolation]\n ) -> ComplianceReport:\n if check_run.status == RunStatus.RUNNING:\n raise ValueError(\"Cannot build report for non-terminal run\")\n\n violations_count = len(violations)\n blocking_violations_count = sum(\n 1\n for v in violations\n if bool(getattr(v, \"blocked_release\", False))\n or bool(getattr(v, \"evidence_json\", {}).get(\"blocked_release\", False))\n )\n\n if (\n check_run.final_status == ComplianceDecision.BLOCKED\n and blocking_violations_count <= 0\n ):\n raise ValueError(\"Blocked run requires at least one blocking violation\")\n\n summary = (\n \"Compliance passed with no blocking violations\"\n if check_run.final_status == ComplianceDecision.PASSED\n else f\"Blocked with {blocking_violations_count} blocking violation(s)\"\n )\n\n return ComplianceReport(\n id=f\"CCR-{uuid4()}\",\n run_id=check_run.id,\n candidate_id=check_run.candidate_id,\n generated_at=datetime.now(timezone.utc),\n final_status=check_run.final_status,\n summary_json={\n \"operator_summary\": summary,\n \"violations_count\": violations_count,\n \"blocking_violations_count\": blocking_violations_count,\n },\n immutable=True,\n )\n\n def persist_report(self, report: ComplianceReport) -> ComplianceReport:\n return self.repository.save_report(report)\n\n\n# [/DEF:ReportBuilder:Module]\n" + "anchor_syntax": "region", + "body": "# #region ReportBuilder [C:5] [TYPE Module] [SEMANTICS report, clean-release, compliance, builder, render]\n# @BRIEF Build and persist compliance reports with consistent counter invariants.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [CleanReleaseModels]\n# @RELATION DEPENDS_ON -> [RepositoryRelations]\n# @INVARIANT: blocking_violations_count never exceeds violations_count.\n# @TEST_CONTRACT: ComplianceCheckRun,List[ComplianceViolation] -> ComplianceReport\n# @TEST_FIXTURE: blocked_with_two_violations -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json\n# @TEST_EDGE: empty_violations_for_blocked -> BLOCKED run with zero blocking violations raises ValueError\n# @TEST_EDGE: counter_mismatch -> blocking counter cannot exceed total violations counter\n# @TEST_EDGE: missing_operator_summary -> non-terminal run prevents report creation and summary generation\n# @TEST_INVARIANT: blocking_count_le_total_count -> VERIFIED_BY: [counter_mismatch, empty_violations_for_blocked]\n# @DATA_CONTRACT: Input[ComplianceRun, List[ComplianceViolation]] -> Output[ComplianceReport]\n# @PRE: Compliance run is terminal and repository persistence is available for report storage.\n# @POST: Returns immutable report payloads with consistent violation counters and operator summary content.\n# @SIDE_EFFECT: Writes report artifacts to repository when persistence helpers are invoked.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom uuid import uuid4\n\nfrom ...models.clean_release import ComplianceReport, ComplianceRun, ComplianceViolation\nfrom .enums import ComplianceDecision, RunStatus\nfrom .repository import CleanReleaseRepository\n\n\nclass ComplianceReportBuilder:\n def __init__(self, repository: CleanReleaseRepository):\n self.repository = repository\n\n def build_report_payload(\n self, check_run: ComplianceRun, violations: list[ComplianceViolation]\n ) -> ComplianceReport:\n if check_run.status == RunStatus.RUNNING:\n raise ValueError(\"Cannot build report for non-terminal run\")\n\n violations_count = len(violations)\n blocking_violations_count = sum(\n 1\n for v in violations\n if bool(getattr(v, \"blocked_release\", False))\n or bool(getattr(v, \"evidence_json\", {}).get(\"blocked_release\", False))\n )\n\n if (\n check_run.final_status == ComplianceDecision.BLOCKED\n and blocking_violations_count <= 0\n ):\n raise ValueError(\"Blocked run requires at least one blocking violation\")\n\n summary = (\n \"Compliance passed with no blocking violations\"\n if check_run.final_status == ComplianceDecision.PASSED\n else f\"Blocked with {blocking_violations_count} blocking violation(s)\"\n )\n\n return ComplianceReport(\n id=f\"CCR-{uuid4()}\",\n run_id=check_run.id,\n candidate_id=check_run.candidate_id,\n generated_at=datetime.now(UTC),\n final_status=check_run.final_status,\n summary_json={\n \"operator_summary\": summary,\n \"violations_count\": violations_count,\n \"blocking_violations_count\": blocking_violations_count,\n },\n immutable=True,\n )\n\n def persist_report(self, report: ComplianceReport) -> ComplianceReport:\n return self.repository.save_report(report)\n\n\n# #endregion ReportBuilder\n" }, { "contract_id": "clean_release_repositories", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/__init__.py", "start_line": 1, - "end_line": 29, + "end_line": 28, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Export all clean release repositories." }, "relations": [ @@ -82100,19 +59988,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:clean_release_repositories:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Export all clean release repositories.\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom .candidate_repository import CandidateRepository\nfrom .artifact_repository import ArtifactRepository\nfrom .manifest_repository import ManifestRepository\nfrom .policy_repository import PolicyRepository\nfrom .compliance_repository import ComplianceRepository\nfrom .report_repository import ReportRepository\nfrom .approval_repository import ApprovalRepository\nfrom .publication_repository import PublicationRepository\nfrom .audit_repository import AuditRepository, CleanReleaseAuditLog\n\n__all__ = [\n \"CandidateRepository\",\n \"ArtifactRepository\",\n \"ManifestRepository\",\n \"PolicyRepository\",\n \"ComplianceRepository\",\n \"ReportRepository\",\n \"ApprovalRepository\",\n \"PublicationRepository\",\n \"AuditRepository\",\n \"CleanReleaseAuditLog\"\n]\n\n# [/DEF:clean_release_repositories:Module]\n" + "anchor_syntax": "region", + "body": "# #region clean_release_repositories [C:3] [TYPE Module] [SEMANTICS clean-release, repository, package, export]\n# @BRIEF Export all clean release repositories.\n# @RELATION DEPENDS_ON -> sqlalchemy\n\nfrom .approval_repository import ApprovalRepository\nfrom .artifact_repository import ArtifactRepository\nfrom .audit_repository import AuditRepository, CleanReleaseAuditLog\nfrom .candidate_repository import CandidateRepository\nfrom .compliance_repository import ComplianceRepository\nfrom .manifest_repository import ManifestRepository\nfrom .policy_repository import PolicyRepository\nfrom .publication_repository import PublicationRepository\nfrom .report_repository import ReportRepository\n\n__all__ = [\n \"ApprovalRepository\",\n \"ArtifactRepository\",\n \"AuditRepository\",\n \"CandidateRepository\",\n \"CleanReleaseAuditLog\",\n \"ComplianceRepository\",\n \"ManifestRepository\",\n \"PolicyRepository\",\n \"PublicationRepository\",\n \"ReportRepository\"\n]\n\n# #endregion clean_release_repositories\n" }, { "contract_id": "approval_repository", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/approval_repository.py", "start_line": 1, - "end_line": 54, + "end_line": 55, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query approval decisions." }, @@ -82125,19 +60013,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:approval_repository:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query approval decisions.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom src.models.clean_release import ApprovalDecision\nfrom src.core.logger import belief_scope\n\nclass ApprovalRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for ApprovalDecision.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, decision: ApprovalDecision) -> ApprovalDecision:\n \"\"\"\n @PURPOSE: Persist an approval decision.\n @POST: Decision is committed and refreshed.\n \"\"\"\n with belief_scope(\"ApprovalRepository.save\"):\n self.db.add(decision)\n self.db.commit()\n self.db.refresh(decision)\n return decision\n\n def get_by_id(self, decision_id: str) -> Optional[ApprovalDecision]:\n \"\"\"\n @PURPOSE: Retrieve a decision by ID.\n \"\"\"\n with belief_scope(\"ApprovalRepository.get_by_id\"):\n return self.db.query(ApprovalDecision).filter(ApprovalDecision.id == decision_id).first()\n\n def get_latest_for_candidate(self, candidate_id: str) -> Optional[ApprovalDecision]:\n \"\"\"\n @PURPOSE: Retrieve the latest decision for a candidate.\n \"\"\"\n with belief_scope(\"ApprovalRepository.get_latest_for_candidate\"):\n return self.db.query(ApprovalDecision)\\\n .filter(ApprovalDecision.candidate_id == candidate_id)\\\n .order_by(ApprovalDecision.decided_at.desc())\\\n .first()\n\n def list_by_candidate(self, candidate_id: str) -> List[ApprovalDecision]:\n \"\"\"\n @PURPOSE: List all decisions for a specific candidate.\n \"\"\"\n with belief_scope(\"ApprovalRepository.list_by_candidate\"):\n return self.db.query(ApprovalDecision).filter(ApprovalDecision.candidate_id == candidate_id).all()\n\n# [/DEF:approval_repository:Module]\n" + "anchor_syntax": "region", + "body": "# #region approval_repository [C:3] [TYPE Module] [SEMANTICS clean-release, approval, repository, persistence, crud]\n# @BRIEF Persist and query approval decisions.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> sqlalchemy\n\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import ApprovalDecision\n\n\nclass ApprovalRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for ApprovalDecision.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, decision: ApprovalDecision) -> ApprovalDecision:\n \"\"\"\n @PURPOSE: Persist an approval decision.\n @POST: Decision is committed and refreshed.\n \"\"\"\n with belief_scope(\"ApprovalRepository.save\"):\n self.db.add(decision)\n self.db.commit()\n self.db.refresh(decision)\n return decision\n\n def get_by_id(self, decision_id: str) -> ApprovalDecision | None:\n \"\"\"\n @PURPOSE: Retrieve a decision by ID.\n \"\"\"\n with belief_scope(\"ApprovalRepository.get_by_id\"):\n return self.db.query(ApprovalDecision).filter(ApprovalDecision.id == decision_id).first()\n\n def get_latest_for_candidate(self, candidate_id: str) -> ApprovalDecision | None:\n \"\"\"\n @PURPOSE: Retrieve the latest decision for a candidate.\n \"\"\"\n with belief_scope(\"ApprovalRepository.get_latest_for_candidate\"):\n return self.db.query(ApprovalDecision)\\\n .filter(ApprovalDecision.candidate_id == candidate_id)\\\n .order_by(ApprovalDecision.decided_at.desc())\\\n .first()\n\n def list_by_candidate(self, candidate_id: str) -> list[ApprovalDecision]:\n \"\"\"\n @PURPOSE: List all decisions for a specific candidate.\n \"\"\"\n with belief_scope(\"ApprovalRepository.list_by_candidate\"):\n return self.db.query(ApprovalDecision).filter(ApprovalDecision.candidate_id == candidate_id).all()\n\n# #endregion approval_repository\n" }, { "contract_id": "artifact_repository", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/artifact_repository.py", "start_line": 1, - "end_line": 55, + "end_line": 56, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query candidate artifacts." }, @@ -82150,19 +60038,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:artifact_repository:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query candidate artifacts.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom src.models.clean_release import CandidateArtifact\nfrom src.core.logger import belief_scope\n\nclass ArtifactRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for CandidateArtifact.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, artifact: CandidateArtifact) -> CandidateArtifact:\n \"\"\"\n @PURPOSE: Persist an artifact.\n @POST: Artifact is committed and refreshed.\n \"\"\"\n with belief_scope(\"ArtifactRepository.save\"):\n self.db.add(artifact)\n self.db.commit()\n self.db.refresh(artifact)\n return artifact\n\n def save_all(self, artifacts: List[CandidateArtifact]) -> List[CandidateArtifact]:\n \"\"\"\n @PURPOSE: Persist multiple artifacts in a single transaction.\n \"\"\"\n with belief_scope(\"ArtifactRepository.save_all\"):\n self.db.add_all(artifacts)\n self.db.commit()\n for artifact in artifacts:\n self.db.refresh(artifact)\n return artifacts\n\n def get_by_id(self, artifact_id: str) -> Optional[CandidateArtifact]:\n \"\"\"\n @PURPOSE: Retrieve an artifact by ID.\n \"\"\"\n with belief_scope(\"ArtifactRepository.get_by_id\"):\n return self.db.query(CandidateArtifact).filter(CandidateArtifact.id == artifact_id).first()\n\n def list_by_candidate(self, candidate_id: str) -> List[CandidateArtifact]:\n \"\"\"\n @PURPOSE: List all artifacts for a specific candidate.\n \"\"\"\n with belief_scope(\"ArtifactRepository.list_by_candidate\"):\n return self.db.query(CandidateArtifact).filter(CandidateArtifact.candidate_id == candidate_id).all()\n\n# [/DEF:artifact_repository:Module]\n" + "anchor_syntax": "region", + "body": "# #region artifact_repository [C:3] [TYPE Module] [SEMANTICS clean-release, artifact, repository, persistence, crud]\n# @BRIEF Persist and query candidate artifacts.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> sqlalchemy\n\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import CandidateArtifact\n\n\nclass ArtifactRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for CandidateArtifact.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, artifact: CandidateArtifact) -> CandidateArtifact:\n \"\"\"\n @PURPOSE: Persist an artifact.\n @POST: Artifact is committed and refreshed.\n \"\"\"\n with belief_scope(\"ArtifactRepository.save\"):\n self.db.add(artifact)\n self.db.commit()\n self.db.refresh(artifact)\n return artifact\n\n def save_all(self, artifacts: list[CandidateArtifact]) -> list[CandidateArtifact]:\n \"\"\"\n @PURPOSE: Persist multiple artifacts in a single transaction.\n \"\"\"\n with belief_scope(\"ArtifactRepository.save_all\"):\n self.db.add_all(artifacts)\n self.db.commit()\n for artifact in artifacts:\n self.db.refresh(artifact)\n return artifacts\n\n def get_by_id(self, artifact_id: str) -> CandidateArtifact | None:\n \"\"\"\n @PURPOSE: Retrieve an artifact by ID.\n \"\"\"\n with belief_scope(\"ArtifactRepository.get_by_id\"):\n return self.db.query(CandidateArtifact).filter(CandidateArtifact.id == artifact_id).first()\n\n def list_by_candidate(self, candidate_id: str) -> list[CandidateArtifact]:\n \"\"\"\n @PURPOSE: List all artifacts for a specific candidate.\n \"\"\"\n with belief_scope(\"ArtifactRepository.list_by_candidate\"):\n return self.db.query(CandidateArtifact).filter(CandidateArtifact.candidate_id == candidate_id).all()\n\n# #endregion artifact_repository\n" }, { "contract_id": "audit_repository", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/audit_repository.py", "start_line": 1, - "end_line": 47, + "end_line": 43, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query audit logs for clean release operations." }, @@ -82175,19 +60063,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:audit_repository:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query audit logs for clean release operations.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import Column, String, DateTime, JSON\nfrom src.models.mapping import Base\nfrom src.core.logger import belief_scope\nfrom datetime import datetime\nimport uuid\n\nfrom src.models.clean_release import CleanReleaseAuditLog\n\nclass AuditRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for CleanReleaseAuditLog.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def log(self, action: str, actor: str, candidate_id: Optional[str] = None, details: Optional[dict] = None) -> CleanReleaseAuditLog:\n \"\"\"\n @PURPOSE: Create an audit log entry.\n \"\"\"\n with belief_scope(\"AuditRepository.log\"):\n entry = CleanReleaseAuditLog(\n action=action,\n actor=actor,\n candidate_id=candidate_id,\n details_json=details or {}\n )\n self.db.add(entry)\n self.db.commit()\n self.db.refresh(entry)\n return entry\n\n def list_by_candidate(self, candidate_id: str) -> List[CleanReleaseAuditLog]:\n \"\"\"\n @PURPOSE: List all audit entries for a specific candidate.\n \"\"\"\n with belief_scope(\"AuditRepository.list_by_candidate\"):\n return self.db.query(CleanReleaseAuditLog).filter(CleanReleaseAuditLog.candidate_id == candidate_id).all()\n\n# [/DEF:audit_repository:Module]\n" + "anchor_syntax": "region", + "body": "# #region audit_repository [C:3] [TYPE Module] [SEMANTICS clean-release, audit, repository, persistence, crud]\n# @BRIEF Persist and query audit logs for clean release operations.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> sqlalchemy\n\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import CleanReleaseAuditLog\n\n\nclass AuditRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for CleanReleaseAuditLog.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def log(self, action: str, actor: str, candidate_id: str | None = None, details: dict | None = None) -> CleanReleaseAuditLog:\n \"\"\"\n @PURPOSE: Create an audit log entry.\n \"\"\"\n with belief_scope(\"AuditRepository.log\"):\n entry = CleanReleaseAuditLog(\n action=action,\n actor=actor,\n candidate_id=candidate_id,\n details_json=details or {}\n )\n self.db.add(entry)\n self.db.commit()\n self.db.refresh(entry)\n return entry\n\n def list_by_candidate(self, candidate_id: str) -> list[CleanReleaseAuditLog]:\n \"\"\"\n @PURPOSE: List all audit entries for a specific candidate.\n \"\"\"\n with belief_scope(\"AuditRepository.list_by_candidate\"):\n return self.db.query(CleanReleaseAuditLog).filter(CleanReleaseAuditLog.candidate_id == candidate_id).all()\n\n# #endregion audit_repository\n" }, { "contract_id": "candidate_repository", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/candidate_repository.py", "start_line": 1, - "end_line": 48, + "end_line": 49, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query release candidates." }, @@ -82200,19 +60088,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:candidate_repository:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query release candidates.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom src.models.clean_release import ReleaseCandidate\nfrom src.core.logger import belief_scope\n\nclass CandidateRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for ReleaseCandidate.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, candidate: ReleaseCandidate) -> ReleaseCandidate:\n \"\"\"\n @PURPOSE: Persist a release candidate.\n @POST: Candidate is committed and refreshed.\n \"\"\"\n with belief_scope(\"CandidateRepository.save\"):\n # [REASON] Using merge to handle both create and update.\n # Note: In a real implementation, we might want to use a separate DB model \n # if the domain model differs significantly from the DB schema.\n # For now, we assume the domain model is compatible with SQLAlchemy Base if registered.\n self.db.add(candidate)\n self.db.commit()\n self.db.refresh(candidate)\n return candidate\n\n def get_by_id(self, candidate_id: str) -> Optional[ReleaseCandidate]:\n \"\"\"\n @PURPOSE: Retrieve a candidate by ID.\n \"\"\"\n with belief_scope(\"CandidateRepository.get_by_id\"):\n return self.db.query(ReleaseCandidate).filter(ReleaseCandidate.id == candidate_id).first()\n\n def list_all(self) -> List[ReleaseCandidate]:\n \"\"\"\n @PURPOSE: List all candidates.\n \"\"\"\n with belief_scope(\"CandidateRepository.list_all\"):\n return self.db.query(ReleaseCandidate).all()\n\n# [/DEF:candidate_repository:Module]\n" + "anchor_syntax": "region", + "body": "# #region candidate_repository [C:3] [TYPE Module] [SEMANTICS clean-release, candidate, repository, persistence, crud]\n# @BRIEF Persist and query release candidates.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> sqlalchemy\n\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import ReleaseCandidate\n\n\nclass CandidateRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for ReleaseCandidate.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, candidate: ReleaseCandidate) -> ReleaseCandidate:\n \"\"\"\n @PURPOSE: Persist a release candidate.\n @POST: Candidate is committed and refreshed.\n \"\"\"\n with belief_scope(\"CandidateRepository.save\"):\n # [REASON] Using merge to handle both create and update.\n # Note: In a real implementation, we might want to use a separate DB model\n # if the domain model differs significantly from the DB schema.\n # For now, we assume the domain model is compatible with SQLAlchemy Base if registered.\n self.db.add(candidate)\n self.db.commit()\n self.db.refresh(candidate)\n return candidate\n\n def get_by_id(self, candidate_id: str) -> ReleaseCandidate | None:\n \"\"\"\n @PURPOSE: Retrieve a candidate by ID.\n \"\"\"\n with belief_scope(\"CandidateRepository.get_by_id\"):\n return self.db.query(ReleaseCandidate).filter(ReleaseCandidate.id == candidate_id).first()\n\n def list_all(self) -> list[ReleaseCandidate]:\n \"\"\"\n @PURPOSE: List all candidates.\n \"\"\"\n with belief_scope(\"CandidateRepository.list_all\"):\n return self.db.query(ReleaseCandidate).all()\n\n# #endregion candidate_repository\n" }, { "contract_id": "compliance_repository", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/compliance_repository.py", "start_line": 1, - "end_line": 88, + "end_line": 89, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query compliance runs, stage runs, and violations." }, @@ -82225,19 +60113,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:compliance_repository:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query compliance runs, stage runs, and violations.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom src.models.clean_release import ComplianceRun, ComplianceStageRun, ComplianceViolation\nfrom src.core.logger import belief_scope\n\nclass ComplianceRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for Compliance execution records.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save_run(self, run: ComplianceRun) -> ComplianceRun:\n \"\"\"\n @PURPOSE: Persist a compliance run.\n \"\"\"\n with belief_scope(\"ComplianceRepository.save_run\"):\n self.db.add(run)\n self.db.commit()\n self.db.refresh(run)\n return run\n\n def get_run(self, run_id: str) -> Optional[ComplianceRun]:\n \"\"\"\n @PURPOSE: Retrieve a compliance run by ID.\n \"\"\"\n with belief_scope(\"ComplianceRepository.get_run\"):\n return self.db.query(ComplianceRun).filter(ComplianceRun.id == run_id).first()\n\n def list_runs_by_candidate(self, candidate_id: str) -> List[ComplianceRun]:\n \"\"\"\n @PURPOSE: List all runs for a specific candidate.\n \"\"\"\n with belief_scope(\"ComplianceRepository.list_runs_by_candidate\"):\n return self.db.query(ComplianceRun).filter(ComplianceRun.candidate_id == candidate_id).all()\n\n def save_stage_run(self, stage_run: ComplianceStageRun) -> ComplianceStageRun:\n \"\"\"\n @PURPOSE: Persist a stage execution record.\n \"\"\"\n with belief_scope(\"ComplianceRepository.save_stage_run\"):\n self.db.add(stage_run)\n self.db.commit()\n self.db.refresh(stage_run)\n return stage_run\n\n def list_stages_by_run(self, run_id: str) -> List[ComplianceStageRun]:\n \"\"\"\n @PURPOSE: List all stage runs for a specific compliance run.\n \"\"\"\n with belief_scope(\"ComplianceRepository.list_stages_by_run\"):\n return self.db.query(ComplianceStageRun).filter(ComplianceStageRun.run_id == run_id).all()\n\n def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:\n \"\"\"\n @PURPOSE: Persist a compliance violation.\n \"\"\"\n with belief_scope(\"ComplianceRepository.save_violation\"):\n self.db.add(violation)\n self.db.commit()\n self.db.refresh(violation)\n return violation\n\n def save_violations(self, violations: List[ComplianceViolation]) -> List[ComplianceViolation]:\n \"\"\"\n @PURPOSE: Persist multiple violations.\n \"\"\"\n with belief_scope(\"ComplianceRepository.save_violations\"):\n self.db.add_all(violations)\n self.db.commit()\n for v in violations:\n self.db.refresh(v)\n return violations\n\n def list_violations_by_run(self, run_id: str) -> List[ComplianceViolation]:\n \"\"\"\n @PURPOSE: List all violations for a specific compliance run.\n \"\"\"\n with belief_scope(\"ComplianceRepository.list_violations_by_run\"):\n return self.db.query(ComplianceViolation).filter(ComplianceViolation.run_id == run_id).all()\n\n# [/DEF:compliance_repository:Module]\n" + "anchor_syntax": "region", + "body": "# #region compliance_repository [C:3] [TYPE Module] [SEMANTICS clean-release, compliance, repository, persistence, crud]\n# @BRIEF Persist and query compliance runs, stage runs, and violations.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> sqlalchemy\n\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import ComplianceRun, ComplianceStageRun, ComplianceViolation\n\n\nclass ComplianceRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for Compliance execution records.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save_run(self, run: ComplianceRun) -> ComplianceRun:\n \"\"\"\n @PURPOSE: Persist a compliance run.\n \"\"\"\n with belief_scope(\"ComplianceRepository.save_run\"):\n self.db.add(run)\n self.db.commit()\n self.db.refresh(run)\n return run\n\n def get_run(self, run_id: str) -> ComplianceRun | None:\n \"\"\"\n @PURPOSE: Retrieve a compliance run by ID.\n \"\"\"\n with belief_scope(\"ComplianceRepository.get_run\"):\n return self.db.query(ComplianceRun).filter(ComplianceRun.id == run_id).first()\n\n def list_runs_by_candidate(self, candidate_id: str) -> list[ComplianceRun]:\n \"\"\"\n @PURPOSE: List all runs for a specific candidate.\n \"\"\"\n with belief_scope(\"ComplianceRepository.list_runs_by_candidate\"):\n return self.db.query(ComplianceRun).filter(ComplianceRun.candidate_id == candidate_id).all()\n\n def save_stage_run(self, stage_run: ComplianceStageRun) -> ComplianceStageRun:\n \"\"\"\n @PURPOSE: Persist a stage execution record.\n \"\"\"\n with belief_scope(\"ComplianceRepository.save_stage_run\"):\n self.db.add(stage_run)\n self.db.commit()\n self.db.refresh(stage_run)\n return stage_run\n\n def list_stages_by_run(self, run_id: str) -> list[ComplianceStageRun]:\n \"\"\"\n @PURPOSE: List all stage runs for a specific compliance run.\n \"\"\"\n with belief_scope(\"ComplianceRepository.list_stages_by_run\"):\n return self.db.query(ComplianceStageRun).filter(ComplianceStageRun.run_id == run_id).all()\n\n def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:\n \"\"\"\n @PURPOSE: Persist a compliance violation.\n \"\"\"\n with belief_scope(\"ComplianceRepository.save_violation\"):\n self.db.add(violation)\n self.db.commit()\n self.db.refresh(violation)\n return violation\n\n def save_violations(self, violations: list[ComplianceViolation]) -> list[ComplianceViolation]:\n \"\"\"\n @PURPOSE: Persist multiple violations.\n \"\"\"\n with belief_scope(\"ComplianceRepository.save_violations\"):\n self.db.add_all(violations)\n self.db.commit()\n for v in violations:\n self.db.refresh(v)\n return violations\n\n def list_violations_by_run(self, run_id: str) -> list[ComplianceViolation]:\n \"\"\"\n @PURPOSE: List all violations for a specific compliance run.\n \"\"\"\n with belief_scope(\"ComplianceRepository.list_violations_by_run\"):\n return self.db.query(ComplianceViolation).filter(ComplianceViolation.run_id == run_id).all()\n\n# #endregion compliance_repository\n" }, { "contract_id": "ManifestRepositoryModule", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/manifest_repository.py", "start_line": 1, - "end_line": 93, + "end_line": 87, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query distribution manifests." }, @@ -82262,19 +60150,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ManifestRepositoryModule:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query distribution manifests.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> DistributionManifest\n# @RELATION: DEPENDS_ON -> sqlalchemy\n# @RELATION: DEPENDS_ON -> belief_scope\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom src.models.clean_release import DistributionManifest\nfrom src.core.logger import belief_scope\n\n\n# [DEF:ManifestRepository:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Encapsulates database CRUD operations for DistributionManifest entities.\n# @RELATION: DEPENDS_ON -> DistributionManifest\n# @RELATION: DEPENDS_ON -> sqlalchemy.Session\nclass ManifestRepository:\n \"\"\"Repository for distribution manifest persistence.\"\"\"\n\n # [DEF:ManifestRepository.__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initialize repository with an active SQLAlchemy session.\n # @PRE: db is a valid SQLAlchemy Session instance.\n # @POST: Repository is ready for database operations.\n def __init__(self, db: Session):\n self.db = db\n # [/DEF:ManifestRepository.__init__:Function]\n\n # [DEF:ManifestRepository.save:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist a DistributionManifest to the database.\n # @PRE: manifest is a valid DistributionManifest instance with required fields populated.\n # @POST: Manifest is committed to database and refreshed with generated ID.\n # @SIDE_EFFECT: Database commit via session.commit().\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def save(self, manifest: DistributionManifest) -> DistributionManifest:\n with belief_scope(\"ManifestRepository.save\"):\n self.db.add(manifest)\n self.db.commit()\n self.db.refresh(manifest)\n return manifest\n # [/DEF:ManifestRepository.save:Function]\n\n # [DEF:ManifestRepository.get_by_id:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Retrieve a single DistributionManifest by its primary key.\n # @PRE: manifest_id is a valid string identifier.\n # @POST: Returns DistributionManifest if found, None otherwise.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_by_id(self, manifest_id: str) -> Optional[DistributionManifest]:\n with belief_scope(\"ManifestRepository.get_by_id\"):\n return self.db.query(DistributionManifest).filter(\n DistributionManifest.id == manifest_id\n ).first()\n # [/DEF:ManifestRepository.get_by_id:Function]\n\n # [DEF:ManifestRepository.get_latest_for_candidate:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Retrieve the most recent manifest version for a given candidate.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns the highest manifest_version manifest for the candidate, or None.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_latest_for_candidate(self, candidate_id: str) -> Optional[DistributionManifest]:\n with belief_scope(\"ManifestRepository.get_latest_for_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n # [/DEF:ManifestRepository.get_latest_for_candidate:Function]\n\n # [DEF:ManifestRepository.list_by_candidate:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: List all manifests for a specific candidate, ordered by version.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns a list of DistributionManifest instances (may be empty).\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def list_by_candidate(self, candidate_id: str) -> List[DistributionManifest]:\n with belief_scope(\"ManifestRepository.list_by_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .all()\n )\n # [/DEF:ManifestRepository.list_by_candidate:Function]\n\n# [/DEF:ManifestRepository:Class]\n\n# [/DEF:ManifestRepositoryModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region ManifestRepositoryModule [C:3] [TYPE Module] [SEMANTICS clean-release, manifest, repository, persistence, crud]\n# @BRIEF Persist and query distribution manifests.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> DistributionManifest\n# @RELATION DEPENDS_ON -> sqlalchemy\n# @RELATION DEPENDS_ON -> belief_scope\n\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import DistributionManifest\n\n\n# #region ManifestRepository [C:3] [TYPE Class]\n# @BRIEF Encapsulates database CRUD operations for DistributionManifest entities.\n# @RELATION DEPENDS_ON -> DistributionManifest\n# @RELATION DEPENDS_ON -> sqlalchemy.Session\nclass ManifestRepository:\n \"\"\"Repository for distribution manifest persistence.\"\"\"\n\n # region ManifestRepository.__init__ [TYPE Function]\n # @PURPOSE: Initialize repository with an active SQLAlchemy session.\n # @PRE: db is a valid SQLAlchemy Session instance.\n # @POST: Repository is ready for database operations.\n def __init__(self, db: Session):\n self.db = db\n # endregion ManifestRepository.__init__\n\n # region ManifestRepository.save [TYPE Function]\n # @PURPOSE: Persist a DistributionManifest to the database.\n # @PRE: manifest is a valid DistributionManifest instance with required fields populated.\n # @POST: Manifest is committed to database and refreshed with generated ID.\n # @SIDE_EFFECT: Database commit via session.commit().\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def save(self, manifest: DistributionManifest) -> DistributionManifest:\n with belief_scope(\"ManifestRepository.save\"):\n self.db.add(manifest)\n self.db.commit()\n self.db.refresh(manifest)\n return manifest\n # endregion ManifestRepository.save\n\n # region ManifestRepository.get_by_id [TYPE Function]\n # @PURPOSE: Retrieve a single DistributionManifest by its primary key.\n # @PRE: manifest_id is a valid string identifier.\n # @POST: Returns DistributionManifest if found, None otherwise.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_by_id(self, manifest_id: str) -> DistributionManifest | None:\n with belief_scope(\"ManifestRepository.get_by_id\"):\n return self.db.query(DistributionManifest).filter(\n DistributionManifest.id == manifest_id\n ).first()\n # endregion ManifestRepository.get_by_id\n\n # region ManifestRepository.get_latest_for_candidate [TYPE Function]\n # @PURPOSE: Retrieve the most recent manifest version for a given candidate.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns the highest manifest_version manifest for the candidate, or None.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_latest_for_candidate(self, candidate_id: str) -> DistributionManifest | None:\n with belief_scope(\"ManifestRepository.get_latest_for_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n # endregion ManifestRepository.get_latest_for_candidate\n\n # region ManifestRepository.list_by_candidate [TYPE Function]\n # @PURPOSE: List all manifests for a specific candidate, ordered by version.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns a list of DistributionManifest instances (may be empty).\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def list_by_candidate(self, candidate_id: str) -> list[DistributionManifest]:\n with belief_scope(\"ManifestRepository.list_by_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .all()\n )\n # endregion ManifestRepository.list_by_candidate\n\n# #endregion ManifestRepository\n\n# #endregion ManifestRepositoryModule\n" }, { "contract_id": "ManifestRepository", "contract_type": "Class", "file_path": "backend/src/services/clean_release/repositories/manifest_repository.py", "start_line": 15, - "end_line": 91, + "end_line": 85, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Encapsulates database CRUD operations for DistributionManifest entities." }, "relations": [ @@ -82292,263 +60180,8 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ManifestRepository:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Encapsulates database CRUD operations for DistributionManifest entities.\n# @RELATION: DEPENDS_ON -> DistributionManifest\n# @RELATION: DEPENDS_ON -> sqlalchemy.Session\nclass ManifestRepository:\n \"\"\"Repository for distribution manifest persistence.\"\"\"\n\n # [DEF:ManifestRepository.__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initialize repository with an active SQLAlchemy session.\n # @PRE: db is a valid SQLAlchemy Session instance.\n # @POST: Repository is ready for database operations.\n def __init__(self, db: Session):\n self.db = db\n # [/DEF:ManifestRepository.__init__:Function]\n\n # [DEF:ManifestRepository.save:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist a DistributionManifest to the database.\n # @PRE: manifest is a valid DistributionManifest instance with required fields populated.\n # @POST: Manifest is committed to database and refreshed with generated ID.\n # @SIDE_EFFECT: Database commit via session.commit().\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def save(self, manifest: DistributionManifest) -> DistributionManifest:\n with belief_scope(\"ManifestRepository.save\"):\n self.db.add(manifest)\n self.db.commit()\n self.db.refresh(manifest)\n return manifest\n # [/DEF:ManifestRepository.save:Function]\n\n # [DEF:ManifestRepository.get_by_id:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Retrieve a single DistributionManifest by its primary key.\n # @PRE: manifest_id is a valid string identifier.\n # @POST: Returns DistributionManifest if found, None otherwise.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_by_id(self, manifest_id: str) -> Optional[DistributionManifest]:\n with belief_scope(\"ManifestRepository.get_by_id\"):\n return self.db.query(DistributionManifest).filter(\n DistributionManifest.id == manifest_id\n ).first()\n # [/DEF:ManifestRepository.get_by_id:Function]\n\n # [DEF:ManifestRepository.get_latest_for_candidate:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Retrieve the most recent manifest version for a given candidate.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns the highest manifest_version manifest for the candidate, or None.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_latest_for_candidate(self, candidate_id: str) -> Optional[DistributionManifest]:\n with belief_scope(\"ManifestRepository.get_latest_for_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n # [/DEF:ManifestRepository.get_latest_for_candidate:Function]\n\n # [DEF:ManifestRepository.list_by_candidate:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: List all manifests for a specific candidate, ordered by version.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns a list of DistributionManifest instances (may be empty).\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def list_by_candidate(self, candidate_id: str) -> List[DistributionManifest]:\n with belief_scope(\"ManifestRepository.list_by_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .all()\n )\n # [/DEF:ManifestRepository.list_by_candidate:Function]\n\n# [/DEF:ManifestRepository:Class]\n" - }, - { - "contract_id": "ManifestRepository.__init__", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/repositories/manifest_repository.py", - "start_line": 23, - "end_line": 30, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Repository is ready for database operations.", - "PRE": "db is a valid SQLAlchemy Session instance.", - "PURPOSE": "Initialize repository with an active SQLAlchemy session." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ManifestRepository.__init__:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Initialize repository with an active SQLAlchemy session.\n # @PRE: db is a valid SQLAlchemy Session instance.\n # @POST: Repository is ready for database operations.\n def __init__(self, db: Session):\n self.db = db\n # [/DEF:ManifestRepository.__init__:Function]\n" - }, - { - "contract_id": "ManifestRepository.save", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/repositories/manifest_repository.py", - "start_line": 32, - "end_line": 45, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Manifest is committed to database and refreshed with generated ID.", - "PRE": "manifest is a valid DistributionManifest instance with required fields populated.", - "PURPOSE": "Persist a DistributionManifest to the database.", - "SIDE_EFFECT": "Database commit via session.commit()." - }, - "relations": [ - { - "source_id": "ManifestRepository.save", - "relation_type": "DEPENDS_ON", - "target_id": "DistributionManifest", - "target_ref": "DistributionManifest" - } - ], - "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" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ManifestRepository.save:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist a DistributionManifest to the database.\n # @PRE: manifest is a valid DistributionManifest instance with required fields populated.\n # @POST: Manifest is committed to database and refreshed with generated ID.\n # @SIDE_EFFECT: Database commit via session.commit().\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def save(self, manifest: DistributionManifest) -> DistributionManifest:\n with belief_scope(\"ManifestRepository.save\"):\n self.db.add(manifest)\n self.db.commit()\n self.db.refresh(manifest)\n return manifest\n # [/DEF:ManifestRepository.save:Function]\n" - }, - { - "contract_id": "ManifestRepository.get_by_id", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/repositories/manifest_repository.py", - "start_line": 47, - "end_line": 58, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "POST": "Returns DistributionManifest if found, None otherwise.", - "PRE": "manifest_id is a valid string identifier.", - "PURPOSE": "Retrieve a single DistributionManifest by its primary key." - }, - "relations": [ - { - "source_id": "ManifestRepository.get_by_id", - "relation_type": "DEPENDS_ON", - "target_id": "DistributionManifest", - "target_ref": "DistributionManifest" - } - ], - "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": "def", - "body": " # [DEF:ManifestRepository.get_by_id:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Retrieve a single DistributionManifest by its primary key.\n # @PRE: manifest_id is a valid string identifier.\n # @POST: Returns DistributionManifest if found, None otherwise.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_by_id(self, manifest_id: str) -> Optional[DistributionManifest]:\n with belief_scope(\"ManifestRepository.get_by_id\"):\n return self.db.query(DistributionManifest).filter(\n DistributionManifest.id == manifest_id\n ).first()\n # [/DEF:ManifestRepository.get_by_id:Function]\n" - }, - { - "contract_id": "ManifestRepository.get_latest_for_candidate", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/repositories/manifest_repository.py", - "start_line": 60, - "end_line": 74, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns the highest manifest_version manifest for the candidate, or None.", - "PRE": "candidate_id is a valid string identifier.", - "PURPOSE": "Retrieve the most recent manifest version for a given candidate." - }, - "relations": [ - { - "source_id": "ManifestRepository.get_latest_for_candidate", - "relation_type": "DEPENDS_ON", - "target_id": "DistributionManifest", - "target_ref": "DistributionManifest" - } - ], - "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": "def", - "body": " # [DEF:ManifestRepository.get_latest_for_candidate:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Retrieve the most recent manifest version for a given candidate.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns the highest manifest_version manifest for the candidate, or None.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_latest_for_candidate(self, candidate_id: str) -> Optional[DistributionManifest]:\n with belief_scope(\"ManifestRepository.get_latest_for_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n # [/DEF:ManifestRepository.get_latest_for_candidate:Function]\n" - }, - { - "contract_id": "ManifestRepository.list_by_candidate", - "contract_type": "Function", - "file_path": "backend/src/services/clean_release/repositories/manifest_repository.py", - "start_line": 76, - "end_line": 89, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "POST": "Returns a list of DistributionManifest instances (may be empty).", - "PRE": "candidate_id is a valid string identifier.", - "PURPOSE": "List all manifests for a specific candidate, ordered by version." - }, - "relations": [ - { - "source_id": "ManifestRepository.list_by_candidate", - "relation_type": "DEPENDS_ON", - "target_id": "DistributionManifest", - "target_ref": "DistributionManifest" - } - ], - "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": "def", - "body": " # [DEF:ManifestRepository.list_by_candidate:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: List all manifests for a specific candidate, ordered by version.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns a list of DistributionManifest instances (may be empty).\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def list_by_candidate(self, candidate_id: str) -> List[DistributionManifest]:\n with belief_scope(\"ManifestRepository.list_by_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .all()\n )\n # [/DEF:ManifestRepository.list_by_candidate:Function]\n" + "anchor_syntax": "region", + "body": "# #region ManifestRepository [C:3] [TYPE Class]\n# @BRIEF Encapsulates database CRUD operations for DistributionManifest entities.\n# @RELATION DEPENDS_ON -> DistributionManifest\n# @RELATION DEPENDS_ON -> sqlalchemy.Session\nclass ManifestRepository:\n \"\"\"Repository for distribution manifest persistence.\"\"\"\n\n # region ManifestRepository.__init__ [TYPE Function]\n # @PURPOSE: Initialize repository with an active SQLAlchemy session.\n # @PRE: db is a valid SQLAlchemy Session instance.\n # @POST: Repository is ready for database operations.\n def __init__(self, db: Session):\n self.db = db\n # endregion ManifestRepository.__init__\n\n # region ManifestRepository.save [TYPE Function]\n # @PURPOSE: Persist a DistributionManifest to the database.\n # @PRE: manifest is a valid DistributionManifest instance with required fields populated.\n # @POST: Manifest is committed to database and refreshed with generated ID.\n # @SIDE_EFFECT: Database commit via session.commit().\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def save(self, manifest: DistributionManifest) -> DistributionManifest:\n with belief_scope(\"ManifestRepository.save\"):\n self.db.add(manifest)\n self.db.commit()\n self.db.refresh(manifest)\n return manifest\n # endregion ManifestRepository.save\n\n # region ManifestRepository.get_by_id [TYPE Function]\n # @PURPOSE: Retrieve a single DistributionManifest by its primary key.\n # @PRE: manifest_id is a valid string identifier.\n # @POST: Returns DistributionManifest if found, None otherwise.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_by_id(self, manifest_id: str) -> DistributionManifest | None:\n with belief_scope(\"ManifestRepository.get_by_id\"):\n return self.db.query(DistributionManifest).filter(\n DistributionManifest.id == manifest_id\n ).first()\n # endregion ManifestRepository.get_by_id\n\n # region ManifestRepository.get_latest_for_candidate [TYPE Function]\n # @PURPOSE: Retrieve the most recent manifest version for a given candidate.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns the highest manifest_version manifest for the candidate, or None.\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def get_latest_for_candidate(self, candidate_id: str) -> DistributionManifest | None:\n with belief_scope(\"ManifestRepository.get_latest_for_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n # endregion ManifestRepository.get_latest_for_candidate\n\n # region ManifestRepository.list_by_candidate [TYPE Function]\n # @PURPOSE: List all manifests for a specific candidate, ordered by version.\n # @PRE: candidate_id is a valid string identifier.\n # @POST: Returns a list of DistributionManifest instances (may be empty).\n # @RELATION: DEPENDS_ON -> DistributionManifest\n def list_by_candidate(self, candidate_id: str) -> list[DistributionManifest]:\n with belief_scope(\"ManifestRepository.list_by_candidate\"):\n return (\n self.db.query(DistributionManifest)\n .filter(DistributionManifest.candidate_id == candidate_id)\n .all()\n )\n # endregion ManifestRepository.list_by_candidate\n\n# #endregion ManifestRepository\n" }, { "contract_id": "policy_repository", @@ -82559,7 +60192,7 @@ "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query policy and registry snapshots." }, @@ -82572,19 +60205,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:policy_repository:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query policy and registry snapshots.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom src.core.logger import belief_scope\n\nclass PolicyRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for Policy and Registry snapshots.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save_policy_snapshot(self, snapshot: CleanPolicySnapshot) -> CleanPolicySnapshot:\n \"\"\"\n @PURPOSE: Persist a policy snapshot.\n \"\"\"\n with belief_scope(\"PolicyRepository.save_policy_snapshot\"):\n self.db.add(snapshot)\n self.db.commit()\n self.db.refresh(snapshot)\n return snapshot\n\n def get_policy_snapshot(self, snapshot_id: str) -> Optional[CleanPolicySnapshot]:\n \"\"\"\n @PURPOSE: Retrieve a policy snapshot by ID.\n \"\"\"\n with belief_scope(\"PolicyRepository.get_policy_snapshot\"):\n return self.db.query(CleanPolicySnapshot).filter(CleanPolicySnapshot.id == snapshot_id).first()\n\n def save_registry_snapshot(self, snapshot: SourceRegistrySnapshot) -> SourceRegistrySnapshot:\n \"\"\"\n @PURPOSE: Persist a registry snapshot.\n \"\"\"\n with belief_scope(\"PolicyRepository.save_registry_snapshot\"):\n self.db.add(snapshot)\n self.db.commit()\n self.db.refresh(snapshot)\n return snapshot\n\n def get_registry_snapshot(self, snapshot_id: str) -> Optional[SourceRegistrySnapshot]:\n \"\"\"\n @PURPOSE: Retrieve a registry snapshot by ID.\n \"\"\"\n with belief_scope(\"PolicyRepository.get_registry_snapshot\"):\n return self.db.query(SourceRegistrySnapshot).filter(SourceRegistrySnapshot.id == snapshot_id).first()\n\n# [/DEF:policy_repository:Module]\n" + "anchor_syntax": "region", + "body": "# #region policy_repository [C:3] [TYPE Module] [SEMANTICS clean-release, policy, repository, persistence, crud]\n# @BRIEF Persist and query policy and registry snapshots.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> sqlalchemy\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\n\n\nclass PolicyRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for Policy and Registry snapshots.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save_policy_snapshot(self, snapshot: CleanPolicySnapshot) -> CleanPolicySnapshot:\n \"\"\"\n @PURPOSE: Persist a policy snapshot.\n \"\"\"\n with belief_scope(\"PolicyRepository.save_policy_snapshot\"):\n self.db.add(snapshot)\n self.db.commit()\n self.db.refresh(snapshot)\n return snapshot\n\n def get_policy_snapshot(self, snapshot_id: str) -> CleanPolicySnapshot | None:\n \"\"\"\n @PURPOSE: Retrieve a policy snapshot by ID.\n \"\"\"\n with belief_scope(\"PolicyRepository.get_policy_snapshot\"):\n return self.db.query(CleanPolicySnapshot).filter(CleanPolicySnapshot.id == snapshot_id).first()\n\n def save_registry_snapshot(self, snapshot: SourceRegistrySnapshot) -> SourceRegistrySnapshot:\n \"\"\"\n @PURPOSE: Persist a registry snapshot.\n \"\"\"\n with belief_scope(\"PolicyRepository.save_registry_snapshot\"):\n self.db.add(snapshot)\n self.db.commit()\n self.db.refresh(snapshot)\n return snapshot\n\n def get_registry_snapshot(self, snapshot_id: str) -> SourceRegistrySnapshot | None:\n \"\"\"\n @PURPOSE: Retrieve a registry snapshot by ID.\n \"\"\"\n with belief_scope(\"PolicyRepository.get_registry_snapshot\"):\n return self.db.query(SourceRegistrySnapshot).filter(SourceRegistrySnapshot.id == snapshot_id).first()\n\n# #endregion policy_repository\n" }, { "contract_id": "publication_repository", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/publication_repository.py", "start_line": 1, - "end_line": 54, + "end_line": 55, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query publication records." }, @@ -82597,19 +60230,19 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:publication_repository:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query publication records.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom src.models.clean_release import PublicationRecord\nfrom src.core.logger import belief_scope\n\nclass PublicationRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for PublicationRecord.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, record: PublicationRecord) -> PublicationRecord:\n \"\"\"\n @PURPOSE: Persist a publication record.\n @POST: Record is committed and refreshed.\n \"\"\"\n with belief_scope(\"PublicationRepository.save\"):\n self.db.add(record)\n self.db.commit()\n self.db.refresh(record)\n return record\n\n def get_by_id(self, record_id: str) -> Optional[PublicationRecord]:\n \"\"\"\n @PURPOSE: Retrieve a record by ID.\n \"\"\"\n with belief_scope(\"PublicationRepository.get_by_id\"):\n return self.db.query(PublicationRecord).filter(PublicationRecord.id == record_id).first()\n\n def get_latest_for_candidate(self, candidate_id: str) -> Optional[PublicationRecord]:\n \"\"\"\n @PURPOSE: Retrieve the latest record for a candidate.\n \"\"\"\n with belief_scope(\"PublicationRepository.get_latest_for_candidate\"):\n return self.db.query(PublicationRecord)\\\n .filter(PublicationRecord.candidate_id == candidate_id)\\\n .order_by(PublicationRecord.published_at.desc())\\\n .first()\n\n def list_by_candidate(self, candidate_id: str) -> List[PublicationRecord]:\n \"\"\"\n @PURPOSE: List all records for a specific candidate.\n \"\"\"\n with belief_scope(\"PublicationRepository.list_by_candidate\"):\n return self.db.query(PublicationRecord).filter(PublicationRecord.candidate_id == candidate_id).all()\n\n# [/DEF:publication_repository:Module]\n" + "anchor_syntax": "region", + "body": "# #region publication_repository [C:3] [TYPE Module] [SEMANTICS clean-release, publication, repository, persistence, crud]\n# @BRIEF Persist and query publication records.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> sqlalchemy\n\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import PublicationRecord\n\n\nclass PublicationRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for PublicationRecord.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, record: PublicationRecord) -> PublicationRecord:\n \"\"\"\n @PURPOSE: Persist a publication record.\n @POST: Record is committed and refreshed.\n \"\"\"\n with belief_scope(\"PublicationRepository.save\"):\n self.db.add(record)\n self.db.commit()\n self.db.refresh(record)\n return record\n\n def get_by_id(self, record_id: str) -> PublicationRecord | None:\n \"\"\"\n @PURPOSE: Retrieve a record by ID.\n \"\"\"\n with belief_scope(\"PublicationRepository.get_by_id\"):\n return self.db.query(PublicationRecord).filter(PublicationRecord.id == record_id).first()\n\n def get_latest_for_candidate(self, candidate_id: str) -> PublicationRecord | None:\n \"\"\"\n @PURPOSE: Retrieve the latest record for a candidate.\n \"\"\"\n with belief_scope(\"PublicationRepository.get_latest_for_candidate\"):\n return self.db.query(PublicationRecord)\\\n .filter(PublicationRecord.candidate_id == candidate_id)\\\n .order_by(PublicationRecord.published_at.desc())\\\n .first()\n\n def list_by_candidate(self, candidate_id: str) -> list[PublicationRecord]:\n \"\"\"\n @PURPOSE: List all records for a specific candidate.\n \"\"\"\n with belief_scope(\"PublicationRepository.list_by_candidate\"):\n return self.db.query(PublicationRecord).filter(PublicationRecord.candidate_id == candidate_id).all()\n\n# #endregion publication_repository\n" }, { "contract_id": "report_repository", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repositories/report_repository.py", "start_line": 1, - "end_line": 51, + "end_line": 52, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Persist and query compliance reports." }, @@ -82622,33 +60255,27 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:report_repository:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist and query compliance reports.\n# @LAYER: Infra\n# @RELATION: DEPENDS_ON -> sqlalchemy\n\nfrom typing import Optional, List\nfrom sqlalchemy.orm import Session\nfrom src.models.clean_release import ComplianceReport\nfrom src.core.logger import belief_scope\n\nclass ReportRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for ComplianceReport.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, report: ComplianceReport) -> ComplianceReport:\n \"\"\"\n @PURPOSE: Persist a compliance report.\n @POST: Report is committed and refreshed.\n \"\"\"\n with belief_scope(\"ReportRepository.save\"):\n self.db.add(report)\n self.db.commit()\n self.db.refresh(report)\n return report\n\n def get_by_id(self, report_id: str) -> Optional[ComplianceReport]:\n \"\"\"\n @PURPOSE: Retrieve a report by ID.\n \"\"\"\n with belief_scope(\"ReportRepository.get_by_id\"):\n return self.db.query(ComplianceReport).filter(ComplianceReport.id == report_id).first()\n\n def get_by_run(self, run_id: str) -> Optional[ComplianceReport]:\n \"\"\"\n @PURPOSE: Retrieve a report for a specific compliance run.\n \"\"\"\n with belief_scope(\"ReportRepository.get_by_run\"):\n return self.db.query(ComplianceReport).filter(ComplianceReport.run_id == run_id).first()\n\n def list_by_candidate(self, candidate_id: str) -> List[ComplianceReport]:\n \"\"\"\n @PURPOSE: List all reports for a specific candidate.\n \"\"\"\n with belief_scope(\"ReportRepository.list_by_candidate\"):\n return self.db.query(ComplianceReport).filter(ComplianceReport.candidate_id == candidate_id).all()\n\n# [/DEF:report_repository:Module]\n" + "anchor_syntax": "region", + "body": "# #region report_repository [C:3] [TYPE Module] [SEMANTICS clean-release, report, repository, persistence, crud]\n# @BRIEF Persist and query compliance reports.\n# @LAYER: Infra\n# @RELATION DEPENDS_ON -> sqlalchemy\n\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope\nfrom src.models.clean_release import ComplianceReport\n\n\nclass ReportRepository:\n \"\"\"\n @PURPOSE: Encapsulates database operations for ComplianceReport.\n \"\"\"\n def __init__(self, db: Session):\n self.db = db\n\n def save(self, report: ComplianceReport) -> ComplianceReport:\n \"\"\"\n @PURPOSE: Persist a compliance report.\n @POST: Report is committed and refreshed.\n \"\"\"\n with belief_scope(\"ReportRepository.save\"):\n self.db.add(report)\n self.db.commit()\n self.db.refresh(report)\n return report\n\n def get_by_id(self, report_id: str) -> ComplianceReport | None:\n \"\"\"\n @PURPOSE: Retrieve a report by ID.\n \"\"\"\n with belief_scope(\"ReportRepository.get_by_id\"):\n return self.db.query(ComplianceReport).filter(ComplianceReport.id == report_id).first()\n\n def get_by_run(self, run_id: str) -> ComplianceReport | None:\n \"\"\"\n @PURPOSE: Retrieve a report for a specific compliance run.\n \"\"\"\n with belief_scope(\"ReportRepository.get_by_run\"):\n return self.db.query(ComplianceReport).filter(ComplianceReport.run_id == run_id).first()\n\n def list_by_candidate(self, candidate_id: str) -> list[ComplianceReport]:\n \"\"\"\n @PURPOSE: List all reports for a specific candidate.\n \"\"\"\n with belief_scope(\"ReportRepository.list_by_candidate\"):\n return self.db.query(ComplianceReport).filter(ComplianceReport.candidate_id == candidate_id).all()\n\n# #endregion report_repository\n" }, { "contract_id": "RepositoryRelations", "contract_type": "Module", "file_path": "backend/src/services/clean_release/repository.py", "start_line": 1, - "end_line": 180, + "end_line": 178, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Repository operations are side-effect free outside explicit save/update calls.", "LAYER": "Infra", - "PURPOSE": "Provide repository adapter for clean release entities with deterministic access methods.", - "SEMANTICS": [ - "clean-release", - "repository", - "persistence", - "in-memory" - ] + "PURPOSE": "Provide repository adapter for clean release entities with deterministic access methods." }, "relations": [ { "source_id": "RepositoryRelations", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" } @@ -82662,35 +60289,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:RepositoryRelations:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, repository, persistence, in-memory\n# @PURPOSE: 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, Dict, List, Optional, cast\n\nfrom ...models.clean_release import (\n CleanPolicySnapshot,\n ComplianceRun,\n ComplianceReport,\n ComplianceStageRun,\n ComplianceViolation,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\n\n\n# [DEF:CleanReleaseRepository:Class]\n# @PURPOSE: Data access object for clean release lifecycle.\n@dataclass\nclass CleanReleaseRepository:\n candidates: Dict[str, ReleaseCandidate] = field(default_factory=dict)\n policies: Dict[str, CleanPolicySnapshot] = field(default_factory=dict)\n registries: Dict[str, SourceRegistrySnapshot] = field(default_factory=dict)\n artifacts: Dict[str, object] = field(default_factory=dict)\n manifests: Dict[str, DistributionManifest] = field(default_factory=dict)\n check_runs: Dict[str, ComplianceRun] = field(default_factory=dict)\n stage_runs: Dict[str, ComplianceStageRun] = field(default_factory=dict)\n reports: Dict[str, ComplianceReport] = field(default_factory=dict)\n violations: Dict[str, ComplianceViolation] = field(default_factory=dict)\n audit_events: List[Dict[str, Any]] = field(default_factory=list)\n\n def _entity_id(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"id\")))\n\n def _candidate_ref(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"candidate_id\")))\n\n def _run_ref(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"run_id\")))\n\n def save_candidate(self, candidate: ReleaseCandidate) -> ReleaseCandidate:\n cast(Dict[str, ReleaseCandidate], self.candidates)[\n self._entity_id(candidate)\n ] = candidate\n return candidate\n\n def get_candidate(self, candidate_id: str) -> Optional[ReleaseCandidate]:\n return self.candidates.get(candidate_id)\n\n def save_policy(self, policy: CleanPolicySnapshot) -> CleanPolicySnapshot:\n cast(Dict[str, CleanPolicySnapshot], self.policies)[self._entity_id(policy)] = (\n policy\n )\n return policy\n\n def get_policy(self, policy_id: str) -> Optional[CleanPolicySnapshot]:\n return self.policies.get(policy_id)\n\n def get_active_policy(self) -> Optional[CleanPolicySnapshot]:\n # In-memory repo doesn't track 'active' flag on snapshot,\n # this should be resolved by facade using ConfigManager.\n return next(iter(self.policies.values()), None)\n\n def save_registry(self, registry: SourceRegistrySnapshot) -> SourceRegistrySnapshot:\n cast(Dict[str, SourceRegistrySnapshot], self.registries)[\n self._entity_id(registry)\n ] = registry\n return registry\n\n def get_registry(self, registry_id: str) -> Optional[SourceRegistrySnapshot]:\n return self.registries.get(registry_id)\n\n def save_artifact(self, artifact) -> object:\n cast(Dict[str, object], self.artifacts)[self._entity_id(artifact)] = artifact\n return artifact\n\n def get_artifacts_by_candidate(self, candidate_id: str) -> List[object]:\n artifacts = cast(List[Any], list(self.artifacts.values()))\n return [\n artifact\n for artifact in artifacts\n if self._candidate_ref(artifact) == candidate_id\n ]\n\n def save_manifest(self, manifest: DistributionManifest) -> DistributionManifest:\n cast(Dict[str, DistributionManifest], self.manifests)[\n self._entity_id(manifest)\n ] = manifest\n return manifest\n\n def get_manifest(self, manifest_id: str) -> Optional[DistributionManifest]:\n return self.manifests.get(manifest_id)\n\n def save_distribution_manifest(\n self, manifest: DistributionManifest\n ) -> DistributionManifest:\n return self.save_manifest(manifest)\n\n def get_distribution_manifest(\n self, manifest_id: str\n ) -> Optional[DistributionManifest]:\n return self.get_manifest(manifest_id)\n\n def save_check_run(self, check_run: ComplianceRun) -> ComplianceRun:\n cast(Dict[str, ComplianceRun], self.check_runs)[self._entity_id(check_run)] = (\n check_run\n )\n return check_run\n\n def save_stage_run(self, stage_run: ComplianceStageRun) -> ComplianceStageRun:\n cast(Dict[str, ComplianceStageRun], self.stage_runs)[\n self._entity_id(stage_run)\n ] = stage_run\n return stage_run\n\n def get_check_run(self, check_run_id: str) -> Optional[ComplianceRun]:\n return self.check_runs.get(check_run_id)\n\n def save_compliance_run(self, run: ComplianceRun) -> ComplianceRun:\n return self.save_check_run(run)\n\n def get_compliance_run(self, run_id: str) -> Optional[ComplianceRun]:\n return self.get_check_run(run_id)\n\n def save_report(self, report: ComplianceReport) -> ComplianceReport:\n report_id = self._entity_id(report)\n store = cast(Dict[str, ComplianceReport], self.reports)\n existing = store.get(report_id)\n if existing is not None:\n raise ValueError(\n f\"immutable report snapshot already exists for id={report_id}\"\n )\n store[report_id] = report\n return report\n\n def get_report(self, report_id: str) -> Optional[ComplianceReport]:\n return self.reports.get(report_id)\n\n def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:\n cast(Dict[str, ComplianceViolation], self.violations)[\n self._entity_id(violation)\n ] = violation\n return violation\n\n def get_violations_by_run(self, run_id: str) -> List[ComplianceViolation]:\n violations = cast(List[ComplianceViolation], list(self.violations.values()))\n return [\n violation for violation in violations if self._run_ref(violation) == run_id\n ]\n\n def get_manifests_by_candidate(\n self, candidate_id: str\n ) -> List[DistributionManifest]:\n manifests = cast(List[DistributionManifest], list(self.manifests.values()))\n return [\n manifest\n for manifest in manifests\n if self._candidate_ref(manifest) == candidate_id\n ]\n\n def append_audit_event(self, payload: Dict[str, Any]) -> None:\n self.audit_events.append(payload)\n\n def clear_history(self) -> None:\n self.check_runs.clear()\n self.reports.clear()\n self.violations.clear()\n\n\n# [/DEF:CleanReleaseRepository:Class]\n# [/DEF:RepositoryRelations:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "CleanReleaseRepository", "contract_type": "Class", "file_path": "backend/src/services/clean_release/repository.py", - "start_line": 26, - "end_line": 179, + "start_line": 24, + "end_line": 177, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -82708,33 +60317,27 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseRepository:Class]\n# @PURPOSE: Data access object for clean release lifecycle.\n@dataclass\nclass CleanReleaseRepository:\n candidates: Dict[str, ReleaseCandidate] = field(default_factory=dict)\n policies: Dict[str, CleanPolicySnapshot] = field(default_factory=dict)\n registries: Dict[str, SourceRegistrySnapshot] = field(default_factory=dict)\n artifacts: Dict[str, object] = field(default_factory=dict)\n manifests: Dict[str, DistributionManifest] = field(default_factory=dict)\n check_runs: Dict[str, ComplianceRun] = field(default_factory=dict)\n stage_runs: Dict[str, ComplianceStageRun] = field(default_factory=dict)\n reports: Dict[str, ComplianceReport] = field(default_factory=dict)\n violations: Dict[str, ComplianceViolation] = field(default_factory=dict)\n audit_events: List[Dict[str, Any]] = field(default_factory=list)\n\n def _entity_id(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"id\")))\n\n def _candidate_ref(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"candidate_id\")))\n\n def _run_ref(self, entity: Any) -> str:\n return cast(str, str(getattr(cast(Any, entity), \"run_id\")))\n\n def save_candidate(self, candidate: ReleaseCandidate) -> ReleaseCandidate:\n cast(Dict[str, ReleaseCandidate], self.candidates)[\n self._entity_id(candidate)\n ] = candidate\n return candidate\n\n def get_candidate(self, candidate_id: str) -> Optional[ReleaseCandidate]:\n return self.candidates.get(candidate_id)\n\n def save_policy(self, policy: CleanPolicySnapshot) -> CleanPolicySnapshot:\n cast(Dict[str, CleanPolicySnapshot], self.policies)[self._entity_id(policy)] = (\n policy\n )\n return policy\n\n def get_policy(self, policy_id: str) -> Optional[CleanPolicySnapshot]:\n return self.policies.get(policy_id)\n\n def get_active_policy(self) -> Optional[CleanPolicySnapshot]:\n # In-memory repo doesn't track 'active' flag on snapshot,\n # this should be resolved by facade using ConfigManager.\n return next(iter(self.policies.values()), None)\n\n def save_registry(self, registry: SourceRegistrySnapshot) -> SourceRegistrySnapshot:\n cast(Dict[str, SourceRegistrySnapshot], self.registries)[\n self._entity_id(registry)\n ] = registry\n return registry\n\n def get_registry(self, registry_id: str) -> Optional[SourceRegistrySnapshot]:\n return self.registries.get(registry_id)\n\n def save_artifact(self, artifact) -> object:\n cast(Dict[str, object], self.artifacts)[self._entity_id(artifact)] = artifact\n return artifact\n\n def get_artifacts_by_candidate(self, candidate_id: str) -> List[object]:\n artifacts = cast(List[Any], list(self.artifacts.values()))\n return [\n artifact\n for artifact in artifacts\n if self._candidate_ref(artifact) == candidate_id\n ]\n\n def save_manifest(self, manifest: DistributionManifest) -> DistributionManifest:\n cast(Dict[str, DistributionManifest], self.manifests)[\n self._entity_id(manifest)\n ] = manifest\n return manifest\n\n def get_manifest(self, manifest_id: str) -> Optional[DistributionManifest]:\n return self.manifests.get(manifest_id)\n\n def save_distribution_manifest(\n self, manifest: DistributionManifest\n ) -> DistributionManifest:\n return self.save_manifest(manifest)\n\n def get_distribution_manifest(\n self, manifest_id: str\n ) -> Optional[DistributionManifest]:\n return self.get_manifest(manifest_id)\n\n def save_check_run(self, check_run: ComplianceRun) -> ComplianceRun:\n cast(Dict[str, ComplianceRun], self.check_runs)[self._entity_id(check_run)] = (\n check_run\n )\n return check_run\n\n def save_stage_run(self, stage_run: ComplianceStageRun) -> ComplianceStageRun:\n cast(Dict[str, ComplianceStageRun], self.stage_runs)[\n self._entity_id(stage_run)\n ] = stage_run\n return stage_run\n\n def get_check_run(self, check_run_id: str) -> Optional[ComplianceRun]:\n return self.check_runs.get(check_run_id)\n\n def save_compliance_run(self, run: ComplianceRun) -> ComplianceRun:\n return self.save_check_run(run)\n\n def get_compliance_run(self, run_id: str) -> Optional[ComplianceRun]:\n return self.get_check_run(run_id)\n\n def save_report(self, report: ComplianceReport) -> ComplianceReport:\n report_id = self._entity_id(report)\n store = cast(Dict[str, ComplianceReport], self.reports)\n existing = store.get(report_id)\n if existing is not None:\n raise ValueError(\n f\"immutable report snapshot already exists for id={report_id}\"\n )\n store[report_id] = report\n return report\n\n def get_report(self, report_id: str) -> Optional[ComplianceReport]:\n return self.reports.get(report_id)\n\n def save_violation(self, violation: ComplianceViolation) -> ComplianceViolation:\n cast(Dict[str, ComplianceViolation], self.violations)[\n self._entity_id(violation)\n ] = violation\n return violation\n\n def get_violations_by_run(self, run_id: str) -> List[ComplianceViolation]:\n violations = cast(List[ComplianceViolation], list(self.violations.values()))\n return [\n violation for violation in violations if self._run_ref(violation) == run_id\n ]\n\n def get_manifests_by_candidate(\n self, candidate_id: str\n ) -> List[DistributionManifest]:\n manifests = cast(List[DistributionManifest], list(self.manifests.values()))\n return [\n manifest\n for manifest in manifests\n if self._candidate_ref(manifest) == candidate_id\n ]\n\n def append_audit_event(self, payload: Dict[str, Any]) -> None:\n self.audit_events.append(payload)\n\n def clear_history(self) -> None:\n self.check_runs.clear()\n self.reports.clear()\n self.violations.clear()\n\n\n# [/DEF:CleanReleaseRepository:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "SourceIsolation", "contract_type": "Module", "file_path": "backend/src/services/clean_release/source_isolation.py", "start_line": 1, - "end_line": 39, + "end_line": 37, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Any endpoint outside enabled registry entries is treated as external-source violation.", "LAYER": "Domain", - "PURPOSE": "Validate that all resource endpoints belong to the approved internal source registry.", - "SEMANTICS": [ - "clean-release", - "source-isolation", - "internal-only", - "validation" - ] + "PURPOSE": "Validate that all resource endpoints belong to the approved internal source registry." }, "relations": [ { "source_id": "SourceIsolation", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" } @@ -82748,59 +60351,35 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:SourceIsolation:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, source-isolation, internal-only, validation\n# @PURPOSE: 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 typing import Dict, Iterable, List\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# [/DEF:SourceIsolation:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ComplianceStages", "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/__init__.py", "start_line": 1, - "end_line": 126, + "end_line": 125, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Stage order remains deterministic for all compliance runs.", "LAYER": "Domain", - "PURPOSE": "Define compliance stage order and helper functions for deterministic run-state evaluation.", - "SEMANTICS": [ - "clean-release", - "compliance", - "stages", - "state-machine" - ] + "PURPOSE": "Define compliance stage order and helper functions for deterministic run-state evaluation." }, "relations": [ { "source_id": "ComplianceStages", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" }, { "source_id": "ComplianceStages", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ComplianceStageBase", "target_ref": "[ComplianceStageBase]" } @@ -82814,53 +60393,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceStages:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, compliance, stages, state-machine\n# @PURPOSE: 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 typing import Dict, Iterable, List\n\nfrom ..enums import ComplianceDecision, ComplianceStageName\nfrom ....models.clean_release import (\n ComplianceStageRun,\n CheckFinalStatus,\n CheckStageResult,\n CheckStageStatus,\n)\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# [DEF:build_default_stages:Function]\n# @PURPOSE: 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# [/DEF:build_default_stages:Function]\n\n\n# [DEF:stage_result_map:Function]\n# @PURPOSE: 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# [/DEF:stage_result_map:Function]\n\n\n# [DEF:missing_mandatory_stages:Function]\n# @PURPOSE: 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# [/DEF:missing_mandatory_stages:Function]\n\n\n# [DEF:derive_final_status:Function]\n# @PURPOSE: 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# [/DEF:derive_final_status:Function]\n# [/DEF:ComplianceStages:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "build_default_stages", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/__init__.py", - "start_line": 35, - "end_line": 48, + "start_line": 34, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -82898,15 +60441,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_default_stages:Function]\n# @PURPOSE: 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# [/DEF:build_default_stages:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "stage_result_map", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/__init__.py", - "start_line": 51, - "end_line": 87, + "start_line": 50, + "end_line": 86, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -82944,15 +60487,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:stage_result_map:Function]\n# @PURPOSE: 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# [/DEF:stage_result_map:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "missing_mandatory_stages", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/__init__.py", - "start_line": 90, - "end_line": 100, + "start_line": 89, + "end_line": 99, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -82990,15 +60533,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:missing_mandatory_stages:Function]\n# @PURPOSE: 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# [/DEF:missing_mandatory_stages:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "derive_final_status", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/__init__.py", - "start_line": 103, - "end_line": 125, + "start_line": 102, + "end_line": 124, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83036,40 +60579,33 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:derive_final_status:Function]\n# @PURPOSE: 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# [/DEF:derive_final_status:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ComplianceStageBase", "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/base.py", "start_line": 1, - "end_line": 134, + "end_line": 132, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Stage execution is deterministic for equal input context.", "LAYER": "Domain", - "PURPOSE": "Define shared contracts and helpers for pluggable clean-release compliance stages.", - "SEMANTICS": [ - "clean-release", - "compliance", - "stages", - "contracts", - "base" - ] + "PURPOSE": "Define shared contracts and helpers for pluggable clean-release compliance stages." }, "relations": [ { "source_id": "ComplianceStageBase", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseModels", "target_ref": "[CleanReleaseModels]" }, { "source_id": "ComplianceStageBase", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LoggerModule", "target_ref": "[LoggerModule]" } @@ -83083,53 +60619,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceStageBase:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, compliance, stages, contracts, base\n# @PURPOSE: 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 datetime, timezone\nfrom typing import Any, Dict, List, 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# [DEF:ComplianceStageContext:Class]\n# @PURPOSE: Immutable input envelope passed to each compliance stage.\n@dataclass(frozen=True)\nclass ComplianceStageContext:\n run: ComplianceRun\n candidate: ReleaseCandidate\n manifest: DistributionManifest\n policy: CleanPolicySnapshot\n registry: SourceRegistrySnapshot\n\n\n# [/DEF:ComplianceStageContext:Class]\n\n\n# [DEF:StageExecutionResult:Class]\n# @PURPOSE: Structured stage output containing decision, details and violations.\n@dataclass\nclass StageExecutionResult:\n decision: ComplianceDecision\n details_json: Dict[str, Any] = field(default_factory=dict)\n violations: List[ComplianceViolation] = field(default_factory=list)\n\n\n# [/DEF:StageExecutionResult:Class]\n\n\n# [DEF:ComplianceStage:Class]\n# @PURPOSE: Protocol for pluggable stage implementations.\nclass ComplianceStage(Protocol):\n stage_name: ComplianceStageName\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult: ...\n\n\n# [/DEF:ComplianceStage:Class]\n\n\n# [DEF:build_stage_run_record:Function]\n# @PURPOSE: 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(timezone.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# [/DEF:build_stage_run_record:Function]\n\n\n# [DEF:build_violation:Function]\n# @PURPOSE: 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# [/DEF:build_violation:Function]\n\n# [/DEF:ComplianceStageBase:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ComplianceStageContext", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 31, - "end_line": 42, + "start_line": 29, + "end_line": 40, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83147,15 +60647,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceStageContext:Class]\n# @PURPOSE: Immutable input envelope passed to each compliance stage.\n@dataclass(frozen=True)\nclass ComplianceStageContext:\n run: ComplianceRun\n candidate: ReleaseCandidate\n manifest: DistributionManifest\n policy: CleanPolicySnapshot\n registry: SourceRegistrySnapshot\n\n\n# [/DEF:ComplianceStageContext:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "StageExecutionResult", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 45, - "end_line": 54, + "start_line": 43, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83173,15 +60673,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:StageExecutionResult:Class]\n# @PURPOSE: Structured stage output containing decision, details and violations.\n@dataclass\nclass StageExecutionResult:\n decision: ComplianceDecision\n details_json: Dict[str, Any] = field(default_factory=dict)\n violations: List[ComplianceViolation] = field(default_factory=list)\n\n\n# [/DEF:StageExecutionResult:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "ComplianceStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 57, - "end_line": 65, + "start_line": 55, + "end_line": 63, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83199,15 +60699,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ComplianceStage:Class]\n# @PURPOSE: Protocol for pluggable stage implementations.\nclass ComplianceStage(Protocol):\n stage_name: ComplianceStageName\n\n def execute(self, context: ComplianceStageContext) -> StageExecutionResult: ...\n\n\n# [/DEF:ComplianceStage:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "build_stage_run_record", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 68, - "end_line": 96, + "start_line": 66, + "end_line": 94, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83245,15 +60745,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_stage_run_record:Function]\n# @PURPOSE: 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(timezone.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# [/DEF:build_stage_run_record:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "build_violation", "contract_type": "Function", "file_path": "backend/src/services/clean_release/stages/base.py", - "start_line": 99, - "end_line": 132, + "start_line": 97, + "end_line": 130, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83291,38 +60791,33 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_violation:Function]\n# @PURPOSE: 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# [/DEF:build_violation:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "data_purity", "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/data_purity.py", "start_line": 1, - "end_line": 68, + "end_line": 66, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "prohibited_detected_count > 0 always yields BLOCKED stage decision.", "LAYER": "Domain", - "PURPOSE": "Evaluate manifest purity counters and emit blocking violations for prohibited artifacts.", - "SEMANTICS": [ - "clean-release", - "compliance-stage", - "data-purity" - ] + "PURPOSE": "Evaluate manifest purity counters and emit blocking violations for prohibited artifacts." }, "relations": [ { "source_id": "data_purity", - "relation_type": "[IMPLEMENTS]", + "relation_type": "IMPLEMENTS", "target_id": "ComplianceStage", "target_ref": "[ComplianceStage]" }, { "source_id": "data_purity", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ComplianceStageBase", "target_ref": "[ComplianceStageBase]" } @@ -83336,53 +60831,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [IMPLEMENTS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[IMPLEMENTS]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:data_purity:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, compliance-stage, data-purity\n# @PURPOSE: 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# [DEF:DataPurityStage:Class]\n# @PURPOSE: 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# [/DEF:DataPurityStage:Class]\n\n# [/DEF:data_purity:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "DataPurityStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/data_purity.py", - "start_line": 17, - "end_line": 66, + "start_line": 15, + "end_line": 64, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83420,39 +60879,33 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DataPurityStage:Class]\n# @PURPOSE: 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# [/DEF:DataPurityStage:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "internal_sources_only", "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/internal_sources_only.py", "start_line": 1, - "end_line": 87, + "end_line": 85, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Any source host outside allowed_hosts yields BLOCKED decision with at least one violation.", "LAYER": "Domain", - "PURPOSE": "Verify manifest-declared sources belong to trusted internal registry allowlist.", - "SEMANTICS": [ - "clean-release", - "compliance-stage", - "source-isolation", - "registry" - ] + "PURPOSE": "Verify manifest-declared sources belong to trusted internal registry allowlist." }, "relations": [ { "source_id": "internal_sources_only", - "relation_type": "[IMPLEMENTS]", + "relation_type": "IMPLEMENTS", "target_id": "ComplianceStage", "target_ref": "[ComplianceStage]" }, { "source_id": "internal_sources_only", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ComplianceStageBase", "target_ref": "[ComplianceStageBase]" } @@ -83466,53 +60919,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [IMPLEMENTS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[IMPLEMENTS]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:internal_sources_only:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, compliance-stage, source-isolation, registry\n# @PURPOSE: 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# [DEF:InternalSourcesOnlyStage:Class]\n# @PURPOSE: 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# [/DEF:InternalSourcesOnlyStage:Class]\n\n# [/DEF:internal_sources_only:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "InternalSourcesOnlyStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/internal_sources_only.py", - "start_line": 17, - "end_line": 85, + "start_line": 15, + "end_line": 83, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83550,40 +60967,33 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:InternalSourcesOnlyStage:Class]\n# @PURPOSE: 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# [/DEF:InternalSourcesOnlyStage:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "manifest_consistency", "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/manifest_consistency.py", "start_line": 1, - "end_line": 72, + "end_line": 70, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Digest mismatch between run and manifest yields ERROR with blocking violation evidence.", "LAYER": "Domain", - "PURPOSE": "Ensure run is bound to the exact manifest snapshot and digest used at run creation time.", - "SEMANTICS": [ - "clean-release", - "compliance-stage", - "manifest", - "consistency", - "digest" - ] + "PURPOSE": "Ensure run is bound to the exact manifest snapshot and digest used at run creation time." }, "relations": [ { "source_id": "manifest_consistency", - "relation_type": "[IMPLEMENTS]", + "relation_type": "IMPLEMENTS", "target_id": "ComplianceStage", "target_ref": "[ComplianceStage]" }, { "source_id": "manifest_consistency", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ComplianceStageBase", "target_ref": "[ComplianceStageBase]" } @@ -83597,53 +61007,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [IMPLEMENTS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[IMPLEMENTS]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:manifest_consistency:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, compliance-stage, manifest, consistency, digest\n# @PURPOSE: 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# [DEF:ManifestConsistencyStage:Class]\n# @PURPOSE: 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# [/DEF:ManifestConsistencyStage:Class]\n\n# [/DEF:manifest_consistency:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ManifestConsistencyStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/manifest_consistency.py", - "start_line": 17, - "end_line": 70, + "start_line": 15, + "end_line": 68, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83681,39 +61055,33 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ManifestConsistencyStage:Class]\n# @PURPOSE: 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# [/DEF:ManifestConsistencyStage:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "no_external_endpoints", "contract_type": "Module", "file_path": "backend/src/services/clean_release/stages/no_external_endpoints.py", "start_line": 1, - "end_line": 93, + "end_line": 91, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Endpoint outside allowed scheme/host always yields BLOCKED stage decision.", "LAYER": "Domain", - "PURPOSE": "Block manifest payloads that expose external endpoints outside trusted schemes and hosts.", - "SEMANTICS": [ - "clean-release", - "compliance-stage", - "endpoints", - "network" - ] + "PURPOSE": "Block manifest payloads that expose external endpoints outside trusted schemes and hosts." }, "relations": [ { "source_id": "no_external_endpoints", - "relation_type": "[IMPLEMENTS]", + "relation_type": "IMPLEMENTS", "target_id": "ComplianceStage", "target_ref": "[ComplianceStage]" }, { "source_id": "no_external_endpoints", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ComplianceStageBase", "target_ref": "[ComplianceStageBase]" } @@ -83727,53 +61095,17 @@ "actual_complexity": 3, "contract_type": "Module" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [IMPLEMENTS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[IMPLEMENTS]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:no_external_endpoints:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, compliance-stage, endpoints, network\n# @PURPOSE: 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# [DEF:NoExternalEndpointsStage:Class]\n# @PURPOSE: 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# [/DEF:NoExternalEndpointsStage:Class]\n\n# [/DEF:no_external_endpoints:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "NoExternalEndpointsStage", "contract_type": "Class", "file_path": "backend/src/services/clean_release/stages/no_external_endpoints.py", - "start_line": 19, - "end_line": 91, + "start_line": 17, + "end_line": 89, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -83811,25 +61143,20 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:NoExternalEndpointsStage:Class]\n# @PURPOSE: 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# [/DEF:NoExternalEndpointsStage:Class]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "dataset_review", "contract_type": "Module", "file_path": "backend/src/services/dataset_review/__init__.py", "start_line": 1, - "end_line": 8, + "end_line": 7, "tier": "TIER_1", "complexity": 1, "metadata": { "LAYER": "Services", - "PURPOSE": "Provides services for dataset-centered orchestration flow.", - "SEMANTICS": [ - "dataset", - "review", - "orchestration" - ] + "PURPOSE": "Provides services for dataset-centered orchestration flow." }, "relations": [ { @@ -83877,19 +61204,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:dataset_review:Module]\n#\n# @SEMANTICS: dataset, review, orchestration\n# @PURPOSE: Provides services for dataset-centered orchestration flow.\n# @RELATION: EXPORTS ->[DatasetReviewOrchestrator:Class]\n# @LAYER: Services\n#\n# [/DEF:dataset_review:Module]\n" + "anchor_syntax": "region", + "body": "# #region dataset_review [TYPE Module] [SEMANTICS dataset, review, orchestration, package]\n#\n# @BRIEF Provides services for dataset-centered orchestration flow.\n# @RELATION EXPORTS -> [DatasetReviewOrchestrator:Class]\n# @LAYER: Services\n#\n# #endregion dataset_review\n" }, { "contract_id": "ClarificationEngine", "contract_type": "Module", "file_path": "backend/src/services/dataset_review/clarification_engine.py", "start_line": 1, - "end_line": 306, + "end_line": 288, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "DATA_CONTRACT": "Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult]", "INVARIANT": "Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible.", "LAYER": "Domain", @@ -83898,14 +61225,6 @@ "PURPOSE": "Manage one-question-at-a-time clarification state, deterministic answer persistence, and readiness/finding updates.", "RATIONALE": "Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module.", "REJECTED": "Keeping all clarification logic in one file because it exceeded the fractal limit.", - "SEMANTICS": [ - "dataset_review", - "clarification", - "question_payload", - "answer_persistence", - "readiness", - "findings" - ], "SIDE_EFFECT": "Persists clarification answers, question/session states, and related readiness/finding changes." }, "relations": [ @@ -83984,218 +61303,70 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationEngine:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: dataset_review, clarification, question_payload, answer_persistence, readiness, findings\n# @PURPOSE: Manage one-question-at-a-time clarification state, deterministic answer persistence, and readiness/finding updates.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION: DEPENDS_ON -> [ClarificationSession]\n# @RELATION: DEPENDS_ON -> [ClarificationQuestion]\n# @RELATION: DEPENDS_ON -> [ClarificationAnswer]\n# @RELATION: DEPENDS_ON -> [ValidationFinding]\n# @RELATION: DISPATCHES -> [ClarificationHelpers:Module]\n# @PRE: Target session contains a persisted clarification aggregate in the current ownership scope.\n# @POST: Active clarification payload exposes one highest-priority unresolved question, and each recorded answer is persisted before pointer/readiness mutation.\n# @SIDE_EFFECT: Persists clarification answers, question/session states, and related readiness/finding changes.\n# @DATA_CONTRACT: Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult]\n# @INVARIANT: Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible.\n# @RATIONALE: Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module.\n# @REJECTED: Keeping all clarification logic in one file because it exceeded the fractal limit.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom typing import List, Optional\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationAnswer,\n ClarificationQuestion,\n ClarificationSession,\n ClarificationStatus,\n DatasetReviewSession,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SessionPhase,\n ValidationFinding,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\nfrom src.services.dataset_review.clarification_pkg._helpers import (\n select_next_open_question,\n count_resolved_questions,\n count_remaining_questions,\n normalize_answer_value,\n build_impact_summary,\n upsert_clarification_finding,\n derive_readiness_state,\n derive_recommended_action,\n)\n\nlog = MarkerLogger(\"ClarificationEngine\")\n\n\n# [DEF:ClarificationQuestionPayload:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed active-question payload returned to the API layer.\n@dataclass\nclass ClarificationQuestionPayload:\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: Optional[str]\n priority: int\n state: QuestionState\n options: list[dict[str, object]] = field(default_factory=list)\n\n\n# [/DEF:ClarificationQuestionPayload:Class]\n\n\n# [DEF:ClarificationStateResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Clarification state result carrying the current session, active payload, and changed findings.\n@dataclass\nclass ClarificationStateResult:\n clarification_session: ClarificationSession\n current_question: Optional[ClarificationQuestionPayload]\n session: DatasetReviewSession\n changed_findings: List[ValidationFinding] = field(default_factory=list)\n\n\n# [/DEF:ClarificationStateResult:Class]\n\n\n# [DEF:ClarificationAnswerCommand:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed answer command for clarification state mutation.\n@dataclass\nclass ClarificationAnswerCommand:\n session: DatasetReviewSession\n question_id: str\n answer_kind: AnswerKind\n answer_value: Optional[str]\n user: User\n\n\n# [/DEF:ClarificationAnswerCommand:Class]\n\n\n# [DEF:ClarificationEngine:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Provide deterministic one-question-at-a-time clarification selection and answer persistence.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION: CALLS -> [ClarificationHelpers:Module]\n# @PRE: Repository is bound to the current request transaction scope.\n# @POST: Returned clarification state is persistence-backed and aligned with session readiness/recommended action.\n# @SIDE_EFFECT: Mutates clarification answers, session flags, and related clarification findings.\nclass ClarificationEngine:\n # [DEF:ClarificationEngine_init:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind repository dependency for clarification persistence operations.\n def __init__(self, repository: DatasetReviewSessionRepository) -> None:\n self.repository = repository\n\n # [/DEF:ClarificationEngine_init:Function]\n\n # [DEF:build_question_payload:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Return the one active highest-priority clarification question payload.\n # @PRE: Session contains unresolved clarification state or a resumable clarification session.\n # @POST: Returns exactly one active/open question payload or None when no unresolved question remains.\n # @SIDE_EFFECT: Normalizes the active-question pointer and clarification status in persistence.\n def build_question_payload(\n self, session: DatasetReviewSession,\n ) -> Optional[ClarificationQuestionPayload]:\n with belief_scope(\"ClarificationEngine.build_question_payload\"):\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n log.reason(\"No clarification session found\", payload={\"session_id\": session.session_id})\n return None\n\n active_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n active_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n\n if not active_questions:\n clarification_session.current_question_id = None\n clarification_session.status = ClarificationStatus.COMPLETED\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n if session.current_phase == SessionPhase.CLARIFICATION:\n session.current_phase = SessionPhase.REVIEW\n self.repository.db.commit()\n log.reflect(\"No unresolved clarification question remains\", payload={\"session_id\": session.session_id})\n return None\n\n selected_question = active_questions[0]\n clarification_session.current_question_id = selected_question.question_id\n clarification_session.status = ClarificationStatus.ACTIVE\n session.readiness_state = ReadinessState.CLARIFICATION_ACTIVE\n session.recommended_action = RecommendedAction.ANSWER_NEXT_QUESTION\n session.current_phase = SessionPhase.CLARIFICATION\n\n log.reason(\"Selected active clarification question\", payload={\"session_id\": session.session_id, \"question_id\": selected_question.question_id, \"priority\": selected_question.priority})\n self.repository.db.commit()\n\n payload = ClarificationQuestionPayload(\n question_id=selected_question.question_id,\n clarification_session_id=selected_question.clarification_session_id,\n topic_ref=selected_question.topic_ref,\n question_text=selected_question.question_text,\n why_it_matters=selected_question.why_it_matters,\n current_guess=selected_question.current_guess,\n priority=selected_question.priority,\n state=selected_question.state,\n options=[\n {\"option_id\": o.option_id, \"question_id\": o.question_id, \"label\": o.label, \"value\": o.value, \"is_recommended\": o.is_recommended, \"display_order\": o.display_order}\n for o in sorted(selected_question.options, key=lambda item: (item.display_order, item.label, item.option_id))\n ],\n )\n log.reflect(\"Clarification payload built\", payload={\"session_id\": session.session_id, \"question_id\": payload.question_id, \"option_count\": len(payload.options)})\n return payload\n\n # [/DEF:build_question_payload:Function]\n\n # [DEF:record_answer:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Persist one clarification answer before any pointer/readiness mutation.\n # @PRE: Target question belongs to the session's active clarification session and is still open.\n # @POST: Answer row is persisted before current-question pointer advances.\n # @SIDE_EFFECT: Inserts answer row, mutates question/session states, updates clarification findings, and commits.\n def record_answer(self, command: ClarificationAnswerCommand) -> ClarificationStateResult:\n with belief_scope(\"ClarificationEngine.record_answer\"):\n session = command.session\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n log.explore(\"Cannot record clarification answer because no clarification session exists\", payload={\"session_id\": session.session_id}, error=\"No clarification session found\")\n raise ValueError(\"Clarification session not found\")\n\n question = self._find_question(clarification_session, command.question_id)\n if question is None:\n log.explore(\"Cannot record clarification answer for foreign or missing question\", payload={\"session_id\": session.session_id, \"question_id\": command.question_id}, error=\"Question not found in clarification session\")\n raise ValueError(\"Clarification question not found\")\n\n if question.answer is not None:\n log.explore(\"Rejected duplicate clarification answer submission\", payload={\"session_id\": session.session_id, \"question_id\": command.question_id}, error=\"Question already answered\")\n raise ValueError(\"Clarification question already answered\")\n\n if clarification_session.current_question_id and clarification_session.current_question_id != question.question_id:\n log.explore(\"Rejected answer for non-active clarification question\", payload={\"session_id\": session.session_id, \"question_id\": question.question_id, \"current_question_id\": clarification_session.current_question_id}, error=\"Only active question can be answered\")\n raise ValueError(\"Only the active clarification question can be answered\")\n\n normalized_answer_value = normalize_answer_value(command.answer_kind, command.answer_value, question)\n\n log.reason(\"Persisting clarification answer before state advancement\", payload={\"session_id\": session.session_id, \"question_id\": question.question_id, \"answer_kind\": command.answer_kind.value})\n persisted_answer = ClarificationAnswer(\n question_id=question.question_id,\n answer_kind=command.answer_kind,\n answer_value=normalized_answer_value,\n answered_by_user_id=command.user.id,\n impact_summary=build_impact_summary(question, command.answer_kind, normalized_answer_value),\n )\n self.repository.db.add(persisted_answer)\n self.repository.db.flush()\n\n changed_finding = upsert_clarification_finding(\n session=session, question=question, answer_kind=command.answer_kind,\n answer_value=normalized_answer_value, db_session=self.repository.db,\n )\n\n if command.answer_kind == AnswerKind.SELECTED:\n question.state = QuestionState.ANSWERED\n elif command.answer_kind == AnswerKind.CUSTOM:\n question.state = QuestionState.ANSWERED\n elif command.answer_kind == AnswerKind.SKIPPED:\n question.state = QuestionState.SKIPPED\n elif command.answer_kind == AnswerKind.EXPERT_REVIEW:\n question.state = QuestionState.EXPERT_REVIEW\n\n question.updated_at = datetime.utcnow()\n self.repository.db.flush()\n\n clarification_session.resolved_count = count_resolved_questions(clarification_session)\n clarification_session.remaining_count = count_remaining_questions(clarification_session)\n clarification_session.summary_delta = self.summarize_progress(clarification_session)\n clarification_session.updated_at = datetime.utcnow()\n\n next_question = select_next_open_question(clarification_session)\n clarification_session.current_question_id = next_question.question_id if next_question else None\n clarification_session.status = ClarificationStatus.ACTIVE if next_question else ClarificationStatus.COMPLETED\n if clarification_session.status == ClarificationStatus.COMPLETED:\n clarification_session.completed_at = datetime.utcnow()\n\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n session.current_phase = SessionPhase.CLARIFICATION if clarification_session.current_question_id else SessionPhase.REVIEW\n self.repository.bump_session_version(session)\n\n self.repository.db.commit()\n self.repository.db.refresh(session)\n\n log.reflect(\"Clarification answer recorded and session advanced\", payload={\"session_id\": session.session_id, \"question_id\": question.question_id, \"next_question_id\": clarification_session.current_question_id, \"readiness_state\": session.readiness_state.value, \"remaining_count\": clarification_session.remaining_count})\n\n return ClarificationStateResult(\n clarification_session=clarification_session,\n current_question=self.build_question_payload(session),\n session=session,\n changed_findings=[changed_finding] if changed_finding else [],\n )\n\n # [/DEF:record_answer:Function]\n\n # [DEF:summarize_progress:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Produce a compact progress summary for pause/resume and completion UX.\n def summarize_progress(self, clarification_session: ClarificationSession) -> str:\n resolved = count_resolved_questions(clarification_session)\n remaining = count_remaining_questions(clarification_session)\n return f\"{resolved} resolved, {remaining} unresolved\"\n\n # [/DEF:summarize_progress:Function]\n\n # [DEF:_get_latest_clarification_session:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Select the latest clarification session for the current dataset review aggregate.\n def _get_latest_clarification_session(self, session: DatasetReviewSession) -> Optional[ClarificationSession]:\n if not session.clarification_sessions:\n return None\n ordered = sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)\n return ordered[0]\n\n # [/DEF:_get_latest_clarification_session:Function]\n\n # [DEF:_find_question:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Resolve a clarification question from the active clarification aggregate.\n def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> Optional[ClarificationQuestion]:\n for q in clarification_session.questions:\n if q.question_id == question_id:\n return q\n return None\n\n # [/DEF:_find_question:Function]\n\n\n# [/DEF:ClarificationEngine:Class]\n\n# [/DEF:ClarificationEngine:Module]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationEngine [C:4] [TYPE Module] [SEMANTICS pydantic, dataset, clarification, finding, resolution]\n# @BRIEF Manage one-question-at-a-time clarification state, deterministic answer persistence, and readiness/finding updates.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\n# @RELATION DEPENDS_ON -> [ClarificationAnswer]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DISPATCHES -> [ClarificationHelpers:Module]\n# @PRE: Target session contains a persisted clarification aggregate in the current ownership scope.\n# @POST: Active clarification payload exposes one highest-priority unresolved question, and each recorded answer is persisted before pointer/readiness mutation.\n# @SIDE_EFFECT: Persists clarification answers, question/session states, and related readiness/finding changes.\n# @DATA_CONTRACT: Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult]\n# @INVARIANT: Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible.\n# @RATIONALE: Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module.\n# @REJECTED: Keeping all clarification logic in one file because it exceeded the fractal limit.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationAnswer,\n ClarificationQuestion,\n ClarificationSession,\n ClarificationStatus,\n DatasetReviewSession,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SessionPhase,\n ValidationFinding,\n)\nfrom src.services.dataset_review.clarification_pkg._helpers import (\n build_impact_summary,\n count_remaining_questions,\n count_resolved_questions,\n derive_readiness_state,\n derive_recommended_action,\n normalize_answer_value,\n select_next_open_question,\n upsert_clarification_finding,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\n\n\n# #region ClarificationQuestionPayload [C:2] [TYPE Class]\n# @BRIEF Typed active-question payload returned to the API layer.\n@dataclass\nclass ClarificationQuestionPayload:\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None\n priority: int\n state: QuestionState\n options: list[dict[str, object]] = field(default_factory=list)\n\n\n# #endregion ClarificationQuestionPayload\n\n\n# #region ClarificationStateResult [C:2] [TYPE Class]\n# @BRIEF Clarification state result carrying the current session, active payload, and changed findings.\n@dataclass\nclass ClarificationStateResult:\n clarification_session: ClarificationSession\n current_question: ClarificationQuestionPayload | None\n session: DatasetReviewSession\n changed_findings: list[ValidationFinding] = field(default_factory=list)\n\n\n# #endregion ClarificationStateResult\n\n\n# #region ClarificationAnswerCommand [C:2] [TYPE Class]\n# @BRIEF Typed answer command for clarification state mutation.\n@dataclass\nclass ClarificationAnswerCommand:\n session: DatasetReviewSession\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None\n user: User\n\n\n# #endregion ClarificationAnswerCommand\n\n\n# #region ClarificationEngine [C:4] [TYPE Class]\n# @BRIEF Provide deterministic one-question-at-a-time clarification selection and answer persistence.\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION CALLS -> [ClarificationHelpers:Module]\n# @PRE: Repository is bound to the current request transaction scope.\n# @POST: Returned clarification state is persistence-backed and aligned with session readiness/recommended action.\n# @SIDE_EFFECT: Mutates clarification answers, session flags, and related clarification findings.\nclass ClarificationEngine:\n # region ClarificationEngine_init [TYPE Function]\n # @PURPOSE: Bind repository dependency for clarification persistence operations.\n def __init__(self, repository: DatasetReviewSessionRepository) -> None:\n self.repository = repository\n\n # endregion ClarificationEngine_init\n\n # region build_question_payload [TYPE Function]\n # @PURPOSE: Return the one active highest-priority clarification question payload.\n # @PRE: Session contains unresolved clarification state or a resumable clarification session.\n # @POST: Returns exactly one active/open question payload or None when no unresolved question remains.\n # @SIDE_EFFECT: Normalizes the active-question pointer and clarification status in persistence.\n def build_question_payload(\n self, session: DatasetReviewSession,\n ) -> ClarificationQuestionPayload | None:\n with belief_scope(\"ClarificationEngine.build_question_payload\"):\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n logger.reason(\"No clarification session found\", extra={\"session_id\": session.session_id})\n return None\n\n active_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n active_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n\n if not active_questions:\n clarification_session.current_question_id = None\n clarification_session.status = ClarificationStatus.COMPLETED\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n if session.current_phase == SessionPhase.CLARIFICATION:\n session.current_phase = SessionPhase.REVIEW\n self.repository.db.commit()\n logger.reflect(\"No unresolved clarification question remains\", extra={\"session_id\": session.session_id})\n return None\n\n selected_question = active_questions[0]\n clarification_session.current_question_id = selected_question.question_id\n clarification_session.status = ClarificationStatus.ACTIVE\n session.readiness_state = ReadinessState.CLARIFICATION_ACTIVE\n session.recommended_action = RecommendedAction.ANSWER_NEXT_QUESTION\n session.current_phase = SessionPhase.CLARIFICATION\n\n logger.reason(\"Selected active clarification question\", extra={\"session_id\": session.session_id, \"question_id\": selected_question.question_id, \"priority\": selected_question.priority})\n self.repository.db.commit()\n\n payload = ClarificationQuestionPayload(\n question_id=selected_question.question_id,\n clarification_session_id=selected_question.clarification_session_id,\n topic_ref=selected_question.topic_ref,\n question_text=selected_question.question_text,\n why_it_matters=selected_question.why_it_matters,\n current_guess=selected_question.current_guess,\n priority=selected_question.priority,\n state=selected_question.state,\n options=[\n {\"option_id\": o.option_id, \"question_id\": o.question_id, \"label\": o.label, \"value\": o.value, \"is_recommended\": o.is_recommended, \"display_order\": o.display_order}\n for o in sorted(selected_question.options, key=lambda item: (item.display_order, item.label, item.option_id))\n ],\n )\n logger.reflect(\"Clarification payload built\", extra={\"session_id\": session.session_id, \"question_id\": payload.question_id, \"option_count\": len(payload.options)})\n return payload\n\n # endregion build_question_payload\n\n # region record_answer [TYPE Function]\n # @PURPOSE: Persist one clarification answer before any pointer/readiness mutation.\n # @PRE: Target question belongs to the session's active clarification session and is still open.\n # @POST: Answer row is persisted before current-question pointer advances.\n # @SIDE_EFFECT: Inserts answer row, mutates question/session states, updates clarification findings, and commits.\n def record_answer(self, command: ClarificationAnswerCommand) -> ClarificationStateResult:\n with belief_scope(\"ClarificationEngine.record_answer\"):\n session = command.session\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n logger.explore(\"Cannot record clarification answer because no clarification session exists\", extra={\"session_id\": session.session_id})\n raise ValueError(\"Clarification session not found\")\n\n question = self._find_question(clarification_session, command.question_id)\n if question is None:\n logger.explore(\"Cannot record clarification answer for foreign or missing question\", extra={\"session_id\": session.session_id, \"question_id\": command.question_id})\n raise ValueError(\"Clarification question not found\")\n\n if question.answer is not None:\n logger.explore(\"Rejected duplicate clarification answer submission\", extra={\"session_id\": session.session_id, \"question_id\": command.question_id})\n raise ValueError(\"Clarification question already answered\")\n\n if clarification_session.current_question_id and clarification_session.current_question_id != question.question_id:\n logger.explore(\"Rejected answer for non-active clarification question\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"current_question_id\": clarification_session.current_question_id})\n raise ValueError(\"Only the active clarification question can be answered\")\n\n normalized_answer_value = normalize_answer_value(command.answer_kind, command.answer_value, question)\n\n logger.reason(\"Persisting clarification answer before state advancement\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"answer_kind\": command.answer_kind.value})\n persisted_answer = ClarificationAnswer(\n question_id=question.question_id,\n answer_kind=command.answer_kind,\n answer_value=normalized_answer_value,\n answered_by_user_id=command.user.id,\n impact_summary=build_impact_summary(question, command.answer_kind, normalized_answer_value),\n )\n self.repository.db.add(persisted_answer)\n self.repository.db.flush()\n\n changed_finding = upsert_clarification_finding(\n session=session, question=question, answer_kind=command.answer_kind,\n answer_value=normalized_answer_value, db_session=self.repository.db,\n )\n\n if command.answer_kind == AnswerKind.SELECTED or command.answer_kind == AnswerKind.CUSTOM:\n question.state = QuestionState.ANSWERED\n elif command.answer_kind == AnswerKind.SKIPPED:\n question.state = QuestionState.SKIPPED\n elif command.answer_kind == AnswerKind.EXPERT_REVIEW:\n question.state = QuestionState.EXPERT_REVIEW\n\n question.updated_at = datetime.utcnow()\n self.repository.db.flush()\n\n clarification_session.resolved_count = count_resolved_questions(clarification_session)\n clarification_session.remaining_count = count_remaining_questions(clarification_session)\n clarification_session.summary_delta = self.summarize_progress(clarification_session)\n clarification_session.updated_at = datetime.utcnow()\n\n next_question = select_next_open_question(clarification_session)\n clarification_session.current_question_id = next_question.question_id if next_question else None\n clarification_session.status = ClarificationStatus.ACTIVE if next_question else ClarificationStatus.COMPLETED\n if clarification_session.status == ClarificationStatus.COMPLETED:\n clarification_session.completed_at = datetime.utcnow()\n\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n session.current_phase = SessionPhase.CLARIFICATION if clarification_session.current_question_id else SessionPhase.REVIEW\n self.repository.bump_session_version(session)\n\n self.repository.db.commit()\n self.repository.db.refresh(session)\n\n logger.reflect(\"Clarification answer recorded and session advanced\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"next_question_id\": clarification_session.current_question_id, \"readiness_state\": session.readiness_state.value, \"remaining_count\": clarification_session.remaining_count})\n\n return ClarificationStateResult(\n clarification_session=clarification_session,\n current_question=self.build_question_payload(session),\n session=session,\n changed_findings=[changed_finding] if changed_finding else [],\n )\n\n # endregion record_answer\n\n # region summarize_progress [TYPE Function]\n # @PURPOSE: Produce a compact progress summary for pause/resume and completion UX.\n def summarize_progress(self, clarification_session: ClarificationSession) -> str:\n resolved = count_resolved_questions(clarification_session)\n remaining = count_remaining_questions(clarification_session)\n return f\"{resolved} resolved, {remaining} unresolved\"\n\n # endregion summarize_progress\n\n # region _get_latest_clarification_session [TYPE Function]\n # @PURPOSE: Select the latest clarification session for the current dataset review aggregate.\n def _get_latest_clarification_session(self, session: DatasetReviewSession) -> ClarificationSession | None:\n if not session.clarification_sessions:\n return None\n ordered = sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)\n return ordered[0]\n\n # endregion _get_latest_clarification_session\n\n # region _find_question [TYPE Function]\n # @PURPOSE: Resolve a clarification question from the active clarification aggregate.\n def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> ClarificationQuestion | None:\n for q in clarification_session.questions:\n if q.question_id == question_id:\n return q\n return None\n\n # endregion _find_question\n\n\n# #endregion ClarificationEngine\n\n# #endregion ClarificationEngine\n" }, { "contract_id": "ClarificationQuestionPayload", "contract_type": "Class", "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 59, - "end_line": 75, + "start_line": 53, + "end_line": 68, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Typed active-question payload returned to the API layer." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationQuestionPayload:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed active-question payload returned to the API layer.\n@dataclass\nclass ClarificationQuestionPayload:\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: Optional[str]\n priority: int\n state: QuestionState\n options: list[dict[str, object]] = field(default_factory=list)\n\n\n# [/DEF:ClarificationQuestionPayload:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationQuestionPayload [C:2] [TYPE Class]\n# @BRIEF Typed active-question payload returned to the API layer.\n@dataclass\nclass ClarificationQuestionPayload:\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None\n priority: int\n state: QuestionState\n options: list[dict[str, object]] = field(default_factory=list)\n\n\n# #endregion ClarificationQuestionPayload\n" }, { "contract_id": "ClarificationStateResult", "contract_type": "Class", "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 78, - "end_line": 89, + "start_line": 71, + "end_line": 81, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Clarification state result carrying the current session, active payload, and changed findings." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationStateResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Clarification state result carrying the current session, active payload, and changed findings.\n@dataclass\nclass ClarificationStateResult:\n clarification_session: ClarificationSession\n current_question: Optional[ClarificationQuestionPayload]\n session: DatasetReviewSession\n changed_findings: List[ValidationFinding] = field(default_factory=list)\n\n\n# [/DEF:ClarificationStateResult:Class]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationStateResult [C:2] [TYPE Class]\n# @BRIEF Clarification state result carrying the current session, active payload, and changed findings.\n@dataclass\nclass ClarificationStateResult:\n clarification_session: ClarificationSession\n current_question: ClarificationQuestionPayload | None\n session: DatasetReviewSession\n changed_findings: list[ValidationFinding] = field(default_factory=list)\n\n\n# #endregion ClarificationStateResult\n" }, { "contract_id": "ClarificationAnswerCommand", "contract_type": "Class", "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 92, - "end_line": 104, + "start_line": 84, + "end_line": 95, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Typed answer command for clarification state mutation." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationAnswerCommand:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed answer command for clarification state mutation.\n@dataclass\nclass ClarificationAnswerCommand:\n session: DatasetReviewSession\n question_id: str\n answer_kind: AnswerKind\n answer_value: Optional[str]\n user: User\n\n\n# [/DEF:ClarificationAnswerCommand:Class]\n" - }, - { - "contract_id": "ClarificationEngine_init", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 116, - "end_line": 122, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Bind repository dependency for clarification persistence operations." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:ClarificationEngine_init:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind repository dependency for clarification persistence operations.\n def __init__(self, repository: DatasetReviewSessionRepository) -> None:\n self.repository = repository\n\n # [/DEF:ClarificationEngine_init:Function]\n" - }, - { - "contract_id": "build_question_payload", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 124, - "end_line": 182, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "POST": "Returns exactly one active/open question payload or None when no unresolved question remains.", - "PRE": "Session contains unresolved clarification state or a resumable clarification session.", - "PURPOSE": "Return the one active highest-priority clarification question payload.", - "SIDE_EFFECT": "Normalizes the active-question pointer and clarification status in persistence." - }, - "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": "def", - "body": " # [DEF:build_question_payload:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Return the one active highest-priority clarification question payload.\n # @PRE: Session contains unresolved clarification state or a resumable clarification session.\n # @POST: Returns exactly one active/open question payload or None when no unresolved question remains.\n # @SIDE_EFFECT: Normalizes the active-question pointer and clarification status in persistence.\n def build_question_payload(\n self, session: DatasetReviewSession,\n ) -> Optional[ClarificationQuestionPayload]:\n with belief_scope(\"ClarificationEngine.build_question_payload\"):\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n log.reason(\"No clarification session found\", payload={\"session_id\": session.session_id})\n return None\n\n active_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n active_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n\n if not active_questions:\n clarification_session.current_question_id = None\n clarification_session.status = ClarificationStatus.COMPLETED\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n if session.current_phase == SessionPhase.CLARIFICATION:\n session.current_phase = SessionPhase.REVIEW\n self.repository.db.commit()\n log.reflect(\"No unresolved clarification question remains\", payload={\"session_id\": session.session_id})\n return None\n\n selected_question = active_questions[0]\n clarification_session.current_question_id = selected_question.question_id\n clarification_session.status = ClarificationStatus.ACTIVE\n session.readiness_state = ReadinessState.CLARIFICATION_ACTIVE\n session.recommended_action = RecommendedAction.ANSWER_NEXT_QUESTION\n session.current_phase = SessionPhase.CLARIFICATION\n\n log.reason(\"Selected active clarification question\", payload={\"session_id\": session.session_id, \"question_id\": selected_question.question_id, \"priority\": selected_question.priority})\n self.repository.db.commit()\n\n payload = ClarificationQuestionPayload(\n question_id=selected_question.question_id,\n clarification_session_id=selected_question.clarification_session_id,\n topic_ref=selected_question.topic_ref,\n question_text=selected_question.question_text,\n why_it_matters=selected_question.why_it_matters,\n current_guess=selected_question.current_guess,\n priority=selected_question.priority,\n state=selected_question.state,\n options=[\n {\"option_id\": o.option_id, \"question_id\": o.question_id, \"label\": o.label, \"value\": o.value, \"is_recommended\": o.is_recommended, \"display_order\": o.display_order}\n for o in sorted(selected_question.options, key=lambda item: (item.display_order, item.label, item.option_id))\n ],\n )\n log.reflect(\"Clarification payload built\", payload={\"session_id\": session.session_id, \"question_id\": payload.question_id, \"option_count\": len(payload.options)})\n return payload\n\n # [/DEF:build_question_payload:Function]\n" - }, - { - "contract_id": "record_answer", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 184, - "end_line": 269, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "POST": "Answer row is persisted before current-question pointer advances.", - "PRE": "Target question belongs to the session's active clarification session and is still open.", - "PURPOSE": "Persist one clarification answer before any pointer/readiness mutation.", - "SIDE_EFFECT": "Inserts answer row, mutates question/session states, updates clarification findings, and commits." - }, - "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": "def", - "body": " # [DEF:record_answer:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Persist one clarification answer before any pointer/readiness mutation.\n # @PRE: Target question belongs to the session's active clarification session and is still open.\n # @POST: Answer row is persisted before current-question pointer advances.\n # @SIDE_EFFECT: Inserts answer row, mutates question/session states, updates clarification findings, and commits.\n def record_answer(self, command: ClarificationAnswerCommand) -> ClarificationStateResult:\n with belief_scope(\"ClarificationEngine.record_answer\"):\n session = command.session\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n log.explore(\"Cannot record clarification answer because no clarification session exists\", payload={\"session_id\": session.session_id}, error=\"No clarification session found\")\n raise ValueError(\"Clarification session not found\")\n\n question = self._find_question(clarification_session, command.question_id)\n if question is None:\n log.explore(\"Cannot record clarification answer for foreign or missing question\", payload={\"session_id\": session.session_id, \"question_id\": command.question_id}, error=\"Question not found in clarification session\")\n raise ValueError(\"Clarification question not found\")\n\n if question.answer is not None:\n log.explore(\"Rejected duplicate clarification answer submission\", payload={\"session_id\": session.session_id, \"question_id\": command.question_id}, error=\"Question already answered\")\n raise ValueError(\"Clarification question already answered\")\n\n if clarification_session.current_question_id and clarification_session.current_question_id != question.question_id:\n log.explore(\"Rejected answer for non-active clarification question\", payload={\"session_id\": session.session_id, \"question_id\": question.question_id, \"current_question_id\": clarification_session.current_question_id}, error=\"Only active question can be answered\")\n raise ValueError(\"Only the active clarification question can be answered\")\n\n normalized_answer_value = normalize_answer_value(command.answer_kind, command.answer_value, question)\n\n log.reason(\"Persisting clarification answer before state advancement\", payload={\"session_id\": session.session_id, \"question_id\": question.question_id, \"answer_kind\": command.answer_kind.value})\n persisted_answer = ClarificationAnswer(\n question_id=question.question_id,\n answer_kind=command.answer_kind,\n answer_value=normalized_answer_value,\n answered_by_user_id=command.user.id,\n impact_summary=build_impact_summary(question, command.answer_kind, normalized_answer_value),\n )\n self.repository.db.add(persisted_answer)\n self.repository.db.flush()\n\n changed_finding = upsert_clarification_finding(\n session=session, question=question, answer_kind=command.answer_kind,\n answer_value=normalized_answer_value, db_session=self.repository.db,\n )\n\n if command.answer_kind == AnswerKind.SELECTED:\n question.state = QuestionState.ANSWERED\n elif command.answer_kind == AnswerKind.CUSTOM:\n question.state = QuestionState.ANSWERED\n elif command.answer_kind == AnswerKind.SKIPPED:\n question.state = QuestionState.SKIPPED\n elif command.answer_kind == AnswerKind.EXPERT_REVIEW:\n question.state = QuestionState.EXPERT_REVIEW\n\n question.updated_at = datetime.utcnow()\n self.repository.db.flush()\n\n clarification_session.resolved_count = count_resolved_questions(clarification_session)\n clarification_session.remaining_count = count_remaining_questions(clarification_session)\n clarification_session.summary_delta = self.summarize_progress(clarification_session)\n clarification_session.updated_at = datetime.utcnow()\n\n next_question = select_next_open_question(clarification_session)\n clarification_session.current_question_id = next_question.question_id if next_question else None\n clarification_session.status = ClarificationStatus.ACTIVE if next_question else ClarificationStatus.COMPLETED\n if clarification_session.status == ClarificationStatus.COMPLETED:\n clarification_session.completed_at = datetime.utcnow()\n\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n session.current_phase = SessionPhase.CLARIFICATION if clarification_session.current_question_id else SessionPhase.REVIEW\n self.repository.bump_session_version(session)\n\n self.repository.db.commit()\n self.repository.db.refresh(session)\n\n log.reflect(\"Clarification answer recorded and session advanced\", payload={\"session_id\": session.session_id, \"question_id\": question.question_id, \"next_question_id\": clarification_session.current_question_id, \"readiness_state\": session.readiness_state.value, \"remaining_count\": clarification_session.remaining_count})\n\n return ClarificationStateResult(\n clarification_session=clarification_session,\n current_question=self.build_question_payload(session),\n session=session,\n changed_findings=[changed_finding] if changed_finding else [],\n )\n\n # [/DEF:record_answer:Function]\n" - }, - { - "contract_id": "summarize_progress", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 271, - "end_line": 279, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Produce a compact progress summary for pause/resume and completion UX." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:summarize_progress:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Produce a compact progress summary for pause/resume and completion UX.\n def summarize_progress(self, clarification_session: ClarificationSession) -> str:\n resolved = count_resolved_questions(clarification_session)\n remaining = count_remaining_questions(clarification_session)\n return f\"{resolved} resolved, {remaining} unresolved\"\n\n # [/DEF:summarize_progress:Function]\n" - }, - { - "contract_id": "_get_latest_clarification_session", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 281, - "end_line": 290, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Select the latest clarification session for the current dataset review aggregate." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:_get_latest_clarification_session:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Select the latest clarification session for the current dataset review aggregate.\n def _get_latest_clarification_session(self, session: DatasetReviewSession) -> Optional[ClarificationSession]:\n if not session.clarification_sessions:\n return None\n ordered = sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)\n return ordered[0]\n\n # [/DEF:_get_latest_clarification_session:Function]\n" - }, - { - "contract_id": "_find_question", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/clarification_engine.py", - "start_line": 292, - "end_line": 301, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Resolve a clarification question from the active clarification aggregate." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_find_question:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Resolve a clarification question from the active clarification aggregate.\n def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> Optional[ClarificationQuestion]:\n for q in clarification_session.questions:\n if q.question_id == question_id:\n return q\n return None\n\n # [/DEF:_find_question:Function]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationAnswerCommand [C:2] [TYPE Class]\n# @BRIEF Typed answer command for clarification state mutation.\n@dataclass\nclass ClarificationAnswerCommand:\n session: DatasetReviewSession\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None\n user: User\n\n\n# #endregion ClarificationAnswerCommand\n" }, { "contract_id": "ClarificationHelpers", "contract_type": "Module", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", "start_line": 1, - "end_line": 220, + "end_line": 209, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", "PURPOSE": "Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation." }, @@ -84208,36 +61379,36 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ClarificationHelpers:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewModels]\n\nfrom __future__ import annotations\n\nimport uuid\nfrom datetime import datetime\nfrom typing import List, Optional\n\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationAnswer,\n ClarificationQuestion,\n ClarificationSession,\n DatasetReviewSession,\n FindingArea,\n FindingSeverity,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n ValidationFinding,\n)\n\n\n# [DEF:select_next_open_question:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Select the next unresolved question in deterministic priority order.\ndef select_next_open_question(\n clarification_session: ClarificationSession,\n) -> Optional[ClarificationQuestion]:\n open_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n if not open_questions:\n return None\n open_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n return open_questions[0]\n\n\n# [/DEF:select_next_open_question:Function]\n\n\n# [DEF:count_resolved_questions:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Count questions whose answers fully resolved the ambiguity.\ndef count_resolved_questions(clarification_session: ClarificationSession) -> int:\n return sum(1 for q in clarification_session.questions if q.state == QuestionState.ANSWERED)\n\n\n# [/DEF:count_resolved_questions:Function]\n\n\n# [DEF:count_remaining_questions:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Count questions still unresolved or deferred after clarification interaction.\ndef count_remaining_questions(clarification_session: ClarificationSession) -> int:\n return sum(\n 1\n for q in clarification_session.questions\n if q.state in {QuestionState.OPEN, QuestionState.SKIPPED, QuestionState.EXPERT_REVIEW}\n )\n\n\n# [/DEF:count_remaining_questions:Function]\n\n\n# [DEF:normalize_answer_value:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Validate and normalize answer payload based on answer kind and active question options.\ndef normalize_answer_value(\n answer_kind: AnswerKind,\n answer_value: Optional[str],\n question: ClarificationQuestion,\n) -> Optional[str]:\n normalized = str(answer_value).strip() if answer_value is not None else None\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM} and not normalized:\n raise ValueError(\"answer_value is required for selected or custom clarification answers\")\n if answer_kind == AnswerKind.SELECTED:\n allowed_values = {option.value for option in question.options}\n if normalized not in allowed_values:\n raise ValueError(\"answer_value must match one of the current clarification options\")\n if answer_kind == AnswerKind.SKIPPED:\n return normalized or \"skipped\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return normalized or \"expert_review\"\n return normalized\n\n\n# [/DEF:normalize_answer_value:Function]\n\n\n# [DEF:build_impact_summary:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build a compact audit note describing how the clarification answer affects session state.\ndef build_impact_summary(\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: Optional[str],\n) -> str:\n if answer_kind == AnswerKind.SKIPPED:\n return f\"Clarification for {question.topic_ref} was skipped and remains unresolved.\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return f\"Clarification for {question.topic_ref} was deferred for expert review.\"\n return f\"Clarification for {question.topic_ref} recorded as '{answer_value}'.\"\n\n\n# [/DEF:build_impact_summary:Function]\n\n\n# [DEF:upsert_clarification_finding:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules.\n# @RELATION: DEPENDS_ON -> [ValidationFinding]\ndef upsert_clarification_finding(\n session: DatasetReviewSession,\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: Optional[str],\n db_session,\n) -> Optional[ValidationFinding]:\n caused_by_ref = f\"clarification:{question.question_id}\"\n existing = next(\n (f for f in session.findings if f.area == FindingArea.CLARIFICATION and f.caused_by_ref == caused_by_ref),\n None,\n )\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n resolution_state = ResolutionState.RESOLVED\n resolved_at = datetime.utcnow()\n message = f\"Clarified '{question.topic_ref}' with answer '{answer_value}'.\"\n elif answer_kind == AnswerKind.SKIPPED:\n resolution_state = ResolutionState.SKIPPED\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' was skipped and still needs review.\"\n else:\n resolution_state = ResolutionState.EXPERT_REVIEW\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' requires expert review.\"\n\n if existing is None:\n existing = ValidationFinding(\n finding_id=str(uuid.uuid4()),\n session_id=session.session_id,\n area=FindingArea.CLARIFICATION,\n severity=FindingSeverity.WARNING,\n code=\"CLARIFICATION_PENDING\",\n title=\"Clarification pending\",\n message=message,\n resolution_state=resolution_state,\n resolution_note=None,\n caused_by_ref=caused_by_ref,\n created_at=datetime.utcnow(),\n resolved_at=resolved_at,\n )\n db_session.add(existing)\n session.findings.append(existing)\n else:\n existing.message = message\n existing.resolution_state = resolution_state\n existing.resolved_at = resolved_at\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n existing.code = \"CLARIFICATION_RESOLVED\"\n existing.title = \"Clarification resolved\"\n elif answer_kind == AnswerKind.SKIPPED:\n existing.code = \"CLARIFICATION_SKIPPED\"\n existing.title = \"Clarification skipped\"\n else:\n existing.code = \"CLARIFICATION_EXPERT_REVIEW\"\n existing.title = \"Clarification requires expert review\"\n\n return existing\n\n\n# [/DEF:upsert_clarification_finding:Function]\n\n\n# [DEF:derive_readiness_state:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Recompute readiness after clarification mutation while preserving unresolved visibility semantics.\ndef derive_readiness_state(\n session: DatasetReviewSession,\n clarification_session: Optional[ClarificationSession],\n) -> ReadinessState:\n if clarification_session is None:\n return session.readiness_state\n if clarification_session.current_question_id:\n return ReadinessState.CLARIFICATION_ACTIVE\n if clarification_session.remaining_count > 0:\n return ReadinessState.CLARIFICATION_NEEDED\n return ReadinessState.REVIEW_READY\n\n\n# [/DEF:derive_readiness_state:Function]\n\n\n# [DEF:derive_recommended_action:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Recompute next-action guidance after clarification mutations.\ndef derive_recommended_action(\n session: DatasetReviewSession,\n clarification_session: Optional[ClarificationSession],\n) -> RecommendedAction:\n if clarification_session is None:\n return session.recommended_action\n if clarification_session.current_question_id:\n return RecommendedAction.ANSWER_NEXT_QUESTION\n if clarification_session.remaining_count > 0:\n return RecommendedAction.START_CLARIFICATION\n return RecommendedAction.REVIEW_DOCUMENTATION\n\n\n# [/DEF:derive_recommended_action:Function]\n\n\n# [/DEF:ClarificationHelpers:Module]\n" + "anchor_syntax": "region", + "body": "# #region ClarificationHelpers [C:3] [TYPE Module] [SEMANTICS dataset, clarification, question, selection, normalization]\n# @BRIEF Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n\nfrom __future__ import annotations\n\nimport uuid\nfrom datetime import datetime\n\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationQuestion,\n ClarificationSession,\n DatasetReviewSession,\n FindingArea,\n FindingSeverity,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n ValidationFinding,\n)\n\n\n# #region select_next_open_question [C:2] [TYPE Function]\n# @BRIEF Select the next unresolved question in deterministic priority order.\ndef select_next_open_question(\n clarification_session: ClarificationSession,\n) -> ClarificationQuestion | None:\n open_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n if not open_questions:\n return None\n open_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n return open_questions[0]\n\n\n# #endregion select_next_open_question\n\n\n# #region count_resolved_questions [C:1] [TYPE Function]\n# @BRIEF Count questions whose answers fully resolved the ambiguity.\ndef count_resolved_questions(clarification_session: ClarificationSession) -> int:\n return sum(1 for q in clarification_session.questions if q.state == QuestionState.ANSWERED)\n\n\n# #endregion count_resolved_questions\n\n\n# #region count_remaining_questions [C:1] [TYPE Function]\n# @BRIEF Count questions still unresolved or deferred after clarification interaction.\ndef count_remaining_questions(clarification_session: ClarificationSession) -> int:\n return sum(\n 1\n for q in clarification_session.questions\n if q.state in {QuestionState.OPEN, QuestionState.SKIPPED, QuestionState.EXPERT_REVIEW}\n )\n\n\n# #endregion count_remaining_questions\n\n\n# #region normalize_answer_value [C:2] [TYPE Function]\n# @BRIEF Validate and normalize answer payload based on answer kind and active question options.\ndef normalize_answer_value(\n answer_kind: AnswerKind,\n answer_value: str | None,\n question: ClarificationQuestion,\n) -> str | None:\n normalized = str(answer_value).strip() if answer_value is not None else None\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM} and not normalized:\n raise ValueError(\"answer_value is required for selected or custom clarification answers\")\n if answer_kind == AnswerKind.SELECTED:\n allowed_values = {option.value for option in question.options}\n if normalized not in allowed_values:\n raise ValueError(\"answer_value must match one of the current clarification options\")\n if answer_kind == AnswerKind.SKIPPED:\n return normalized or \"skipped\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return normalized or \"expert_review\"\n return normalized\n\n\n# #endregion normalize_answer_value\n\n\n# #region build_impact_summary [C:1] [TYPE Function]\n# @BRIEF Build a compact audit note describing how the clarification answer affects session state.\ndef build_impact_summary(\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: str | None,\n) -> str:\n if answer_kind == AnswerKind.SKIPPED:\n return f\"Clarification for {question.topic_ref} was skipped and remains unresolved.\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return f\"Clarification for {question.topic_ref} was deferred for expert review.\"\n return f\"Clarification for {question.topic_ref} recorded as '{answer_value}'.\"\n\n\n# #endregion build_impact_summary\n\n\n# #region upsert_clarification_finding [C:3] [TYPE Function]\n# @BRIEF Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules.\n# @RELATION DEPENDS_ON -> [ValidationFinding]\ndef upsert_clarification_finding(\n session: DatasetReviewSession,\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: str | None,\n db_session,\n) -> ValidationFinding | None:\n caused_by_ref = f\"clarification:{question.question_id}\"\n existing = next(\n (f for f in session.findings if f.area == FindingArea.CLARIFICATION and f.caused_by_ref == caused_by_ref),\n None,\n )\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n resolution_state = ResolutionState.RESOLVED\n resolved_at = datetime.utcnow()\n message = f\"Clarified '{question.topic_ref}' with answer '{answer_value}'.\"\n elif answer_kind == AnswerKind.SKIPPED:\n resolution_state = ResolutionState.SKIPPED\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' was skipped and still needs review.\"\n else:\n resolution_state = ResolutionState.EXPERT_REVIEW\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' requires expert review.\"\n\n if existing is None:\n existing = ValidationFinding(\n finding_id=str(uuid.uuid4()),\n session_id=session.session_id,\n area=FindingArea.CLARIFICATION,\n severity=FindingSeverity.WARNING,\n code=\"CLARIFICATION_PENDING\",\n title=\"Clarification pending\",\n message=message,\n resolution_state=resolution_state,\n resolution_note=None,\n caused_by_ref=caused_by_ref,\n created_at=datetime.utcnow(),\n resolved_at=resolved_at,\n )\n db_session.add(existing)\n session.findings.append(existing)\n else:\n existing.message = message\n existing.resolution_state = resolution_state\n existing.resolved_at = resolved_at\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n existing.code = \"CLARIFICATION_RESOLVED\"\n existing.title = \"Clarification resolved\"\n elif answer_kind == AnswerKind.SKIPPED:\n existing.code = \"CLARIFICATION_SKIPPED\"\n existing.title = \"Clarification skipped\"\n else:\n existing.code = \"CLARIFICATION_EXPERT_REVIEW\"\n existing.title = \"Clarification requires expert review\"\n\n return existing\n\n\n# #endregion upsert_clarification_finding\n\n\n# #region derive_readiness_state [C:2] [TYPE Function]\n# @BRIEF Recompute readiness after clarification mutation while preserving unresolved visibility semantics.\ndef derive_readiness_state(\n session: DatasetReviewSession,\n clarification_session: ClarificationSession | None,\n) -> ReadinessState:\n if clarification_session is None:\n return session.readiness_state\n if clarification_session.current_question_id:\n return ReadinessState.CLARIFICATION_ACTIVE\n if clarification_session.remaining_count > 0:\n return ReadinessState.CLARIFICATION_NEEDED\n return ReadinessState.REVIEW_READY\n\n\n# #endregion derive_readiness_state\n\n\n# #region derive_recommended_action [C:2] [TYPE Function]\n# @BRIEF Recompute next-action guidance after clarification mutations.\ndef derive_recommended_action(\n session: DatasetReviewSession,\n clarification_session: ClarificationSession | None,\n) -> RecommendedAction:\n if clarification_session is None:\n return session.recommended_action\n if clarification_session.current_question_id:\n return RecommendedAction.ANSWER_NEXT_QUESTION\n if clarification_session.remaining_count > 0:\n return RecommendedAction.START_CLARIFICATION\n return RecommendedAction.REVIEW_DOCUMENTATION\n\n\n# #endregion derive_recommended_action\n\n\n# #endregion ClarificationHelpers\n" }, { "contract_id": "select_next_open_question", "contract_type": "Function", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", - "start_line": 29, - "end_line": 44, + "start_line": 26, + "end_line": 40, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Select the next unresolved question in deterministic priority order." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:select_next_open_question:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Select the next unresolved question in deterministic priority order.\ndef select_next_open_question(\n clarification_session: ClarificationSession,\n) -> Optional[ClarificationQuestion]:\n open_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n if not open_questions:\n return None\n open_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n return open_questions[0]\n\n\n# [/DEF:select_next_open_question:Function]\n" + "anchor_syntax": "region", + "body": "# #region select_next_open_question [C:2] [TYPE Function]\n# @BRIEF Select the next unresolved question in deterministic priority order.\ndef select_next_open_question(\n clarification_session: ClarificationSession,\n) -> ClarificationQuestion | None:\n open_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n if not open_questions:\n return None\n open_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n return open_questions[0]\n\n\n# #endregion select_next_open_question\n" }, { "contract_id": "count_resolved_questions", "contract_type": "Function", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", - "start_line": 47, - "end_line": 54, + "start_line": 43, + "end_line": 49, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Count questions whose answers fully resolved the ambiguity." }, "relations": [], @@ -84252,19 +61423,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:count_resolved_questions:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Count questions whose answers fully resolved the ambiguity.\ndef count_resolved_questions(clarification_session: ClarificationSession) -> int:\n return sum(1 for q in clarification_session.questions if q.state == QuestionState.ANSWERED)\n\n\n# [/DEF:count_resolved_questions:Function]\n" + "anchor_syntax": "region", + "body": "# #region count_resolved_questions [C:1] [TYPE Function]\n# @BRIEF Count questions whose answers fully resolved the ambiguity.\ndef count_resolved_questions(clarification_session: ClarificationSession) -> int:\n return sum(1 for q in clarification_session.questions if q.state == QuestionState.ANSWERED)\n\n\n# #endregion count_resolved_questions\n" }, { "contract_id": "count_remaining_questions", "contract_type": "Function", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", - "start_line": 57, - "end_line": 68, + "start_line": 52, + "end_line": 62, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Count questions still unresolved or deferred after clarification interaction." }, "relations": [], @@ -84279,36 +61450,36 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:count_remaining_questions:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Count questions still unresolved or deferred after clarification interaction.\ndef count_remaining_questions(clarification_session: ClarificationSession) -> int:\n return sum(\n 1\n for q in clarification_session.questions\n if q.state in {QuestionState.OPEN, QuestionState.SKIPPED, QuestionState.EXPERT_REVIEW}\n )\n\n\n# [/DEF:count_remaining_questions:Function]\n" + "anchor_syntax": "region", + "body": "# #region count_remaining_questions [C:1] [TYPE Function]\n# @BRIEF Count questions still unresolved or deferred after clarification interaction.\ndef count_remaining_questions(clarification_session: ClarificationSession) -> int:\n return sum(\n 1\n for q in clarification_session.questions\n if q.state in {QuestionState.OPEN, QuestionState.SKIPPED, QuestionState.EXPERT_REVIEW}\n )\n\n\n# #endregion count_remaining_questions\n" }, { "contract_id": "normalize_answer_value", "contract_type": "Function", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", - "start_line": 71, - "end_line": 93, + "start_line": 65, + "end_line": 86, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Validate and normalize answer payload based on answer kind and active question options." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:normalize_answer_value:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Validate and normalize answer payload based on answer kind and active question options.\ndef normalize_answer_value(\n answer_kind: AnswerKind,\n answer_value: Optional[str],\n question: ClarificationQuestion,\n) -> Optional[str]:\n normalized = str(answer_value).strip() if answer_value is not None else None\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM} and not normalized:\n raise ValueError(\"answer_value is required for selected or custom clarification answers\")\n if answer_kind == AnswerKind.SELECTED:\n allowed_values = {option.value for option in question.options}\n if normalized not in allowed_values:\n raise ValueError(\"answer_value must match one of the current clarification options\")\n if answer_kind == AnswerKind.SKIPPED:\n return normalized or \"skipped\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return normalized or \"expert_review\"\n return normalized\n\n\n# [/DEF:normalize_answer_value:Function]\n" + "anchor_syntax": "region", + "body": "# #region normalize_answer_value [C:2] [TYPE Function]\n# @BRIEF Validate and normalize answer payload based on answer kind and active question options.\ndef normalize_answer_value(\n answer_kind: AnswerKind,\n answer_value: str | None,\n question: ClarificationQuestion,\n) -> str | None:\n normalized = str(answer_value).strip() if answer_value is not None else None\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM} and not normalized:\n raise ValueError(\"answer_value is required for selected or custom clarification answers\")\n if answer_kind == AnswerKind.SELECTED:\n allowed_values = {option.value for option in question.options}\n if normalized not in allowed_values:\n raise ValueError(\"answer_value must match one of the current clarification options\")\n if answer_kind == AnswerKind.SKIPPED:\n return normalized or \"skipped\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return normalized or \"expert_review\"\n return normalized\n\n\n# #endregion normalize_answer_value\n" }, { "contract_id": "build_impact_summary", "contract_type": "Function", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", - "start_line": 96, - "end_line": 111, + "start_line": 89, + "end_line": 103, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 1, "PURPOSE": "Build a compact audit note describing how the clarification answer affects session state." }, "relations": [], @@ -84323,19 +61494,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:build_impact_summary:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Build a compact audit note describing how the clarification answer affects session state.\ndef build_impact_summary(\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: Optional[str],\n) -> str:\n if answer_kind == AnswerKind.SKIPPED:\n return f\"Clarification for {question.topic_ref} was skipped and remains unresolved.\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return f\"Clarification for {question.topic_ref} was deferred for expert review.\"\n return f\"Clarification for {question.topic_ref} recorded as '{answer_value}'.\"\n\n\n# [/DEF:build_impact_summary:Function]\n" + "anchor_syntax": "region", + "body": "# #region build_impact_summary [C:1] [TYPE Function]\n# @BRIEF Build a compact audit note describing how the clarification answer affects session state.\ndef build_impact_summary(\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: str | None,\n) -> str:\n if answer_kind == AnswerKind.SKIPPED:\n return f\"Clarification for {question.topic_ref} was skipped and remains unresolved.\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return f\"Clarification for {question.topic_ref} was deferred for expert review.\"\n return f\"Clarification for {question.topic_ref} recorded as '{answer_value}'.\"\n\n\n# #endregion build_impact_summary\n" }, { "contract_id": "upsert_clarification_finding", "contract_type": "Function", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", - "start_line": 114, - "end_line": 179, + "start_line": 106, + "end_line": 170, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules." }, "relations": [ @@ -84347,42 +61518,42 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:upsert_clarification_finding:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules.\n# @RELATION: DEPENDS_ON -> [ValidationFinding]\ndef upsert_clarification_finding(\n session: DatasetReviewSession,\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: Optional[str],\n db_session,\n) -> Optional[ValidationFinding]:\n caused_by_ref = f\"clarification:{question.question_id}\"\n existing = next(\n (f for f in session.findings if f.area == FindingArea.CLARIFICATION and f.caused_by_ref == caused_by_ref),\n None,\n )\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n resolution_state = ResolutionState.RESOLVED\n resolved_at = datetime.utcnow()\n message = f\"Clarified '{question.topic_ref}' with answer '{answer_value}'.\"\n elif answer_kind == AnswerKind.SKIPPED:\n resolution_state = ResolutionState.SKIPPED\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' was skipped and still needs review.\"\n else:\n resolution_state = ResolutionState.EXPERT_REVIEW\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' requires expert review.\"\n\n if existing is None:\n existing = ValidationFinding(\n finding_id=str(uuid.uuid4()),\n session_id=session.session_id,\n area=FindingArea.CLARIFICATION,\n severity=FindingSeverity.WARNING,\n code=\"CLARIFICATION_PENDING\",\n title=\"Clarification pending\",\n message=message,\n resolution_state=resolution_state,\n resolution_note=None,\n caused_by_ref=caused_by_ref,\n created_at=datetime.utcnow(),\n resolved_at=resolved_at,\n )\n db_session.add(existing)\n session.findings.append(existing)\n else:\n existing.message = message\n existing.resolution_state = resolution_state\n existing.resolved_at = resolved_at\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n existing.code = \"CLARIFICATION_RESOLVED\"\n existing.title = \"Clarification resolved\"\n elif answer_kind == AnswerKind.SKIPPED:\n existing.code = \"CLARIFICATION_SKIPPED\"\n existing.title = \"Clarification skipped\"\n else:\n existing.code = \"CLARIFICATION_EXPERT_REVIEW\"\n existing.title = \"Clarification requires expert review\"\n\n return existing\n\n\n# [/DEF:upsert_clarification_finding:Function]\n" + "anchor_syntax": "region", + "body": "# #region upsert_clarification_finding [C:3] [TYPE Function]\n# @BRIEF Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules.\n# @RELATION DEPENDS_ON -> [ValidationFinding]\ndef upsert_clarification_finding(\n session: DatasetReviewSession,\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: str | None,\n db_session,\n) -> ValidationFinding | None:\n caused_by_ref = f\"clarification:{question.question_id}\"\n existing = next(\n (f for f in session.findings if f.area == FindingArea.CLARIFICATION and f.caused_by_ref == caused_by_ref),\n None,\n )\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n resolution_state = ResolutionState.RESOLVED\n resolved_at = datetime.utcnow()\n message = f\"Clarified '{question.topic_ref}' with answer '{answer_value}'.\"\n elif answer_kind == AnswerKind.SKIPPED:\n resolution_state = ResolutionState.SKIPPED\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' was skipped and still needs review.\"\n else:\n resolution_state = ResolutionState.EXPERT_REVIEW\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' requires expert review.\"\n\n if existing is None:\n existing = ValidationFinding(\n finding_id=str(uuid.uuid4()),\n session_id=session.session_id,\n area=FindingArea.CLARIFICATION,\n severity=FindingSeverity.WARNING,\n code=\"CLARIFICATION_PENDING\",\n title=\"Clarification pending\",\n message=message,\n resolution_state=resolution_state,\n resolution_note=None,\n caused_by_ref=caused_by_ref,\n created_at=datetime.utcnow(),\n resolved_at=resolved_at,\n )\n db_session.add(existing)\n session.findings.append(existing)\n else:\n existing.message = message\n existing.resolution_state = resolution_state\n existing.resolved_at = resolved_at\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n existing.code = \"CLARIFICATION_RESOLVED\"\n existing.title = \"Clarification resolved\"\n elif answer_kind == AnswerKind.SKIPPED:\n existing.code = \"CLARIFICATION_SKIPPED\"\n existing.title = \"Clarification skipped\"\n else:\n existing.code = \"CLARIFICATION_EXPERT_REVIEW\"\n existing.title = \"Clarification requires expert review\"\n\n return existing\n\n\n# #endregion upsert_clarification_finding\n" }, { "contract_id": "derive_readiness_state", "contract_type": "Function", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", - "start_line": 182, - "end_line": 198, + "start_line": 173, + "end_line": 188, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Recompute readiness after clarification mutation while preserving unresolved visibility semantics." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:derive_readiness_state:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Recompute readiness after clarification mutation while preserving unresolved visibility semantics.\ndef derive_readiness_state(\n session: DatasetReviewSession,\n clarification_session: Optional[ClarificationSession],\n) -> ReadinessState:\n if clarification_session is None:\n return session.readiness_state\n if clarification_session.current_question_id:\n return ReadinessState.CLARIFICATION_ACTIVE\n if clarification_session.remaining_count > 0:\n return ReadinessState.CLARIFICATION_NEEDED\n return ReadinessState.REVIEW_READY\n\n\n# [/DEF:derive_readiness_state:Function]\n" + "anchor_syntax": "region", + "body": "# #region derive_readiness_state [C:2] [TYPE Function]\n# @BRIEF Recompute readiness after clarification mutation while preserving unresolved visibility semantics.\ndef derive_readiness_state(\n session: DatasetReviewSession,\n clarification_session: ClarificationSession | None,\n) -> ReadinessState:\n if clarification_session is None:\n return session.readiness_state\n if clarification_session.current_question_id:\n return ReadinessState.CLARIFICATION_ACTIVE\n if clarification_session.remaining_count > 0:\n return ReadinessState.CLARIFICATION_NEEDED\n return ReadinessState.REVIEW_READY\n\n\n# #endregion derive_readiness_state\n" }, { "contract_id": "derive_recommended_action", "contract_type": "Function", "file_path": "backend/src/services/dataset_review/clarification_pkg/_helpers.py", - "start_line": 201, - "end_line": 217, + "start_line": 191, + "end_line": 206, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Recompute next-action guidance after clarification mutations." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:derive_recommended_action:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Recompute next-action guidance after clarification mutations.\ndef derive_recommended_action(\n session: DatasetReviewSession,\n clarification_session: Optional[ClarificationSession],\n) -> RecommendedAction:\n if clarification_session is None:\n return session.recommended_action\n if clarification_session.current_question_id:\n return RecommendedAction.ANSWER_NEXT_QUESTION\n if clarification_session.remaining_count > 0:\n return RecommendedAction.START_CLARIFICATION\n return RecommendedAction.REVIEW_DOCUMENTATION\n\n\n# [/DEF:derive_recommended_action:Function]\n" + "anchor_syntax": "region", + "body": "# #region derive_recommended_action [C:2] [TYPE Function]\n# @BRIEF Recompute next-action guidance after clarification mutations.\ndef derive_recommended_action(\n session: DatasetReviewSession,\n clarification_session: ClarificationSession | None,\n) -> RecommendedAction:\n if clarification_session is None:\n return session.recommended_action\n if clarification_session.current_question_id:\n return RecommendedAction.ANSWER_NEXT_QUESTION\n if clarification_session.remaining_count > 0:\n return RecommendedAction.START_CLARIFICATION\n return RecommendedAction.REVIEW_DOCUMENTATION\n\n\n# #endregion derive_recommended_action\n" }, { "contract_id": "SessionEventLoggerModule", @@ -84393,31 +61564,24 @@ "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "DATA_CONTRACT": "Input[SessionEventPayload] -> Output[SessionEvent]", "LAYER": "Domain", "POST": "Every logged event is committed as an explicit, queryable audit record with deterministic event metadata.", "PRE": "Caller provides an owned session scope and an authenticated actor identifier for each persisted mutation event.", "PURPOSE": "Persist explicit session mutation events for dataset-review audit trails without weakening ownership or approval invariants.", - "SEMANTICS": [ - "dataset_review", - "audit", - "session_events", - "persistence", - "observability" - ], "SIDE_EFFECT": "Inserts persisted session event rows and emits runtime belief-state logs for audit-sensitive mutations." }, "relations": [ { "source_id": "SessionEventLoggerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SessionEvent", "target_ref": "[SessionEvent]" }, { "source_id": "SessionEventLoggerModule", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "DatasetReviewSession", "target_ref": "[DatasetReviewSession]" } @@ -84431,88 +61595,52 @@ "actual_complexity": 4, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionEventLoggerModule:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: dataset_review, audit, session_events, persistence, observability\n# @PURPOSE: Persist explicit session mutation events for dataset-review audit trails without weakening ownership or approval invariants.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[SessionEvent]\n# @RELATION: [DEPENDS_ON] ->[DatasetReviewSession]\n# @PRE: Caller provides an owned session scope and an authenticated actor identifier for each persisted mutation event.\n# @POST: Every logged event is committed as an explicit, queryable audit record with deterministic event metadata.\n# @SIDE_EFFECT: Inserts persisted session event rows and emits runtime belief-state logs for audit-sensitive mutations.\n# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\n\nfrom __future__ import annotations\n\n# [DEF:SessionEventLoggerImports:Block]\nfrom dataclasses import dataclass, field\nfrom typing import Any, Dict, Optional\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.dataset_review import DatasetReviewSession, SessionEvent\n# [/DEF:SessionEventLoggerImports:Block]\n\nlog = MarkerLogger(\"SessionEventLogger\")\n\n\n# [DEF:SessionEventPayload:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed input contract for one persisted dataset-review session audit event.\n@dataclass(frozen=True)\nclass SessionEventPayload:\n session_id: str\n actor_user_id: str\n event_type: str\n event_summary: str\n current_phase: Optional[str] = None\n readiness_state: Optional[str] = None\n event_details: Dict[str, Any] = field(default_factory=dict)\n# [/DEF:SessionEventPayload:Class]\n\n\n# [DEF:SessionEventLogger:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Persist explicit dataset-review session audit events with meaningful runtime reasoning logs.\n# @RELATION: [DEPENDS_ON] ->[SessionEvent]\n# @RELATION: [DEPENDS_ON] ->[SessionEventPayload]\n# @PRE: The database session is live and payload identifiers are non-empty.\n# @POST: Returns the committed session event row with a stable identifier and stored detail payload.\n# @SIDE_EFFECT: Writes one audit row to persistence and emits logger.reason/logger.reflect traces.\n# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\nclass SessionEventLogger:\n # [DEF:SessionEventLogger_init:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.\n def __init__(self, db: Session) -> None:\n self.db = db\n # [/DEF:SessionEventLogger_init:Function]\n\n # [DEF:log_event:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.\n # @RELATION: [DEPENDS_ON] ->[SessionEvent]\n # @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.\n # @POST: Returns the committed SessionEvent record with normalized detail payload.\n # @SIDE_EFFECT: Inserts and commits one session_events row.\n # @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\n def log_event(self, payload: SessionEventPayload) -> SessionEvent:\n with belief_scope(\"SessionEventLogger.log_event\"):\n session_id = str(payload.session_id or \"\").strip()\n actor_user_id = str(payload.actor_user_id or \"\").strip()\n event_type = str(payload.event_type or \"\").strip()\n event_summary = str(payload.event_summary or \"\").strip()\n\n if not session_id:\n log.explore(\"Session event logging rejected because session_id is empty\", error=\"session_id is empty\")\n raise ValueError(\"session_id must be non-empty\")\n if not actor_user_id:\n log.explore(\"Session event logging rejected because actor_user_id is empty\", error=\"actor_user_id is empty\", payload={\"session_id\": session_id})\n raise ValueError(\"actor_user_id must be non-empty\")\n if not event_type:\n log.explore(\"Session event logging rejected because event_type is empty\", error=\"event_type is empty\", payload={\"session_id\": session_id, \"actor_user_id\": actor_user_id})\n raise ValueError(\"event_type must be non-empty\")\n if not event_summary:\n log.explore(\"Session event logging rejected because event_summary is empty\", error=\"event_summary is empty\", payload={\"session_id\": session_id, \"event_type\": event_type})\n raise ValueError(\"event_summary must be non-empty\")\n\n normalized_details = dict(payload.event_details or {})\n log.reason(\n \"Persisting explicit dataset-review session audit event\",\n payload={\n \"session_id\": session_id,\n \"actor_user_id\": actor_user_id,\n \"event_type\": event_type,\n \"current_phase\": payload.current_phase,\n \"readiness_state\": payload.readiness_state,\n },\n )\n\n event = SessionEvent(\n session_id=session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=payload.current_phase,\n readiness_state=payload.readiness_state,\n event_details=normalized_details,\n )\n self.db.add(event)\n self.db.commit()\n self.db.refresh(event)\n\n log.reflect(\n \"Dataset-review session audit event persisted\",\n payload={\n \"session_id\": session_id,\n \"session_event_id\": event.session_event_id,\n \"event_type\": event.event_type,\n },\n )\n return event\n # [/DEF:log_event:Function]\n\n # [DEF:log_for_session:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.\n # @RELATION: [CALLS] ->[SessionEventLogger.log_event]\n def log_for_session(\n self,\n session: DatasetReviewSession,\n *,\n actor_user_id: str,\n event_type: str,\n event_summary: str,\n event_details: Optional[Dict[str, Any]] = None,\n ) -> SessionEvent:\n return self.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=session.current_phase.value if session.current_phase else None,\n readiness_state=session.readiness_state.value if session.readiness_state else None,\n event_details=dict(event_details or {}),\n )\n )\n # [/DEF:log_for_session:Function]\n# [/DEF:SessionEventLogger:Class]\n\n# [/DEF:SessionEventLoggerModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region SessionEventLoggerModule [C:4] [TYPE Module] [SEMANTICS sqlalchemy, dataset, event, logging, session]\n# @BRIEF Persist explicit session mutation events for dataset-review audit trails without weakening ownership or approval invariants.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @PRE: Caller provides an owned session scope and an authenticated actor identifier for each persisted mutation event.\n# @POST: Every logged event is committed as an explicit, queryable audit record with deterministic event metadata.\n# @SIDE_EFFECT: Inserts persisted session event rows and emits runtime belief-state logs for audit-sensitive mutations.\n# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\n\nfrom __future__ import annotations\n\n# #region SessionEventLoggerImports [TYPE Block]\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import DatasetReviewSession, SessionEvent\n\n# #endregion SessionEventLoggerImports\n\n\n# #region SessionEventPayload [C:2] [TYPE Class]\n# @BRIEF Typed input contract for one persisted dataset-review session audit event.\n@dataclass(frozen=True)\nclass SessionEventPayload:\n session_id: str\n actor_user_id: str\n event_type: str\n event_summary: str\n current_phase: str | None = None\n readiness_state: str | None = None\n event_details: dict[str, Any] = field(default_factory=dict)\n# #endregion SessionEventPayload\n\n\n# #region SessionEventLogger [C:4] [TYPE Class]\n# @BRIEF Persist explicit dataset-review session audit events with meaningful runtime reasoning logs.\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @RELATION DEPENDS_ON -> [SessionEventPayload]\n# @PRE: The database session is live and payload identifiers are non-empty.\n# @POST: Returns the committed session event row with a stable identifier and stored detail payload.\n# @SIDE_EFFECT: Writes one audit row to persistence and emits logger.reason/logger.reflect traces.\n# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\nclass SessionEventLogger:\n # region SessionEventLogger_init [TYPE Function]\n # @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.\n def __init__(self, db: Session) -> None:\n self.db = db\n # endregion SessionEventLogger_init\n\n # region log_event [TYPE Function]\n # @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.\n # @RELATION: [DEPENDS_ON] ->[SessionEvent]\n # @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.\n # @POST: Returns the committed SessionEvent record with normalized detail payload.\n # @SIDE_EFFECT: Inserts and commits one session_events row.\n # @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\n def log_event(self, payload: SessionEventPayload) -> SessionEvent:\n with belief_scope(\"SessionEventLogger.log_event\"):\n session_id = str(payload.session_id or \"\").strip()\n actor_user_id = str(payload.actor_user_id or \"\").strip()\n event_type = str(payload.event_type or \"\").strip()\n event_summary = str(payload.event_summary or \"\").strip()\n\n if not session_id:\n logger.explore(\"Session event logging rejected because session_id is empty\")\n raise ValueError(\"session_id must be non-empty\")\n if not actor_user_id:\n logger.explore(\n \"Session event logging rejected because actor_user_id is empty\",\n extra={\"session_id\": session_id},\n )\n raise ValueError(\"actor_user_id must be non-empty\")\n if not event_type:\n logger.explore(\n \"Session event logging rejected because event_type is empty\",\n extra={\"session_id\": session_id, \"actor_user_id\": actor_user_id},\n )\n raise ValueError(\"event_type must be non-empty\")\n if not event_summary:\n logger.explore(\n \"Session event logging rejected because event_summary is empty\",\n extra={\"session_id\": session_id, \"event_type\": event_type},\n )\n raise ValueError(\"event_summary must be non-empty\")\n\n normalized_details = dict(payload.event_details or {})\n logger.reason(\n \"Persisting explicit dataset-review session audit event\",\n extra={\n \"session_id\": session_id,\n \"actor_user_id\": actor_user_id,\n \"event_type\": event_type,\n \"current_phase\": payload.current_phase,\n \"readiness_state\": payload.readiness_state,\n },\n )\n\n event = SessionEvent(\n session_id=session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=payload.current_phase,\n readiness_state=payload.readiness_state,\n event_details=normalized_details,\n )\n self.db.add(event)\n self.db.commit()\n self.db.refresh(event)\n\n logger.reflect(\n \"Dataset-review session audit event persisted\",\n extra={\n \"session_id\": session_id,\n \"session_event_id\": event.session_event_id,\n \"event_type\": event.event_type,\n },\n )\n return event\n # endregion log_event\n\n # region log_for_session [TYPE Function]\n # @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.\n # @RELATION: [CALLS] ->[SessionEventLogger.log_event]\n def log_for_session(\n self,\n session: DatasetReviewSession,\n *,\n actor_user_id: str,\n event_type: str,\n event_summary: str,\n event_details: dict[str, Any] | None = None,\n ) -> SessionEvent:\n return self.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=session.current_phase.value if session.current_phase else None,\n readiness_state=session.readiness_state.value if session.readiness_state else None,\n event_details=dict(event_details or {}),\n )\n )\n # endregion log_for_session\n# #endregion SessionEventLogger\n\n# #endregion SessionEventLoggerModule\n" }, { "contract_id": "SessionEventLoggerImports", "contract_type": "Block", "file_path": "backend/src/services/dataset_review/event_logger.py", - "start_line": 15, - "end_line": 24, + "start_line": 13, + "end_line": 22, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SessionEventLoggerImports:Block]\nfrom dataclasses import dataclass, field\nfrom typing import Any, Dict, Optional\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.dataset_review import DatasetReviewSession, SessionEvent\n# [/DEF:SessionEventLoggerImports:Block]\n" + "anchor_syntax": "region", + "body": "# #region SessionEventLoggerImports [TYPE Block]\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import DatasetReviewSession, SessionEvent\n\n# #endregion SessionEventLoggerImports\n" }, { "contract_id": "SessionEventPayload", "contract_type": "Class", "file_path": "backend/src/services/dataset_review/event_logger.py", - "start_line": 29, - "end_line": 41, + "start_line": 25, + "end_line": 36, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Typed input contract for one persisted dataset-review session audit event." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:SessionEventPayload:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed input contract for one persisted dataset-review session audit event.\n@dataclass(frozen=True)\nclass SessionEventPayload:\n session_id: str\n actor_user_id: str\n event_type: str\n event_summary: str\n current_phase: Optional[str] = None\n readiness_state: Optional[str] = None\n event_details: Dict[str, Any] = field(default_factory=dict)\n# [/DEF:SessionEventPayload:Class]\n" + "anchor_syntax": "region", + "body": "# #region SessionEventPayload [C:2] [TYPE Class]\n# @BRIEF Typed input contract for one persisted dataset-review session audit event.\n@dataclass(frozen=True)\nclass SessionEventPayload:\n session_id: str\n actor_user_id: str\n event_type: str\n event_summary: str\n current_phase: str | None = None\n readiness_state: str | None = None\n event_details: dict[str, Any] = field(default_factory=dict)\n# #endregion SessionEventPayload\n" }, { "contract_id": "SessionEventLogger", "contract_type": "Class", "file_path": "backend/src/services/dataset_review/event_logger.py", - "start_line": 44, + "start_line": 39, "end_line": 150, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "DATA_CONTRACT": "Input[SessionEventPayload] -> Output[SessionEvent]", "POST": "Returns the committed session event row with a stable identifier and stored detail payload.", "PRE": "The database session is live and payload identifiers are non-empty.", @@ -84522,13 +61650,13 @@ "relations": [ { "source_id": "SessionEventLogger", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SessionEvent", "target_ref": "[SessionEvent]" }, { "source_id": "SessionEventLogger", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SessionEventPayload", "target_ref": "[SessionEventPayload]" } @@ -84542,182 +61670,21 @@ "actual_complexity": 4, "contract_type": "Class" } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:SessionEventLogger:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Persist explicit dataset-review session audit events with meaningful runtime reasoning logs.\n# @RELATION: [DEPENDS_ON] ->[SessionEvent]\n# @RELATION: [DEPENDS_ON] ->[SessionEventPayload]\n# @PRE: The database session is live and payload identifiers are non-empty.\n# @POST: Returns the committed session event row with a stable identifier and stored detail payload.\n# @SIDE_EFFECT: Writes one audit row to persistence and emits logger.reason/logger.reflect traces.\n# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\nclass SessionEventLogger:\n # [DEF:SessionEventLogger_init:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.\n def __init__(self, db: Session) -> None:\n self.db = db\n # [/DEF:SessionEventLogger_init:Function]\n\n # [DEF:log_event:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.\n # @RELATION: [DEPENDS_ON] ->[SessionEvent]\n # @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.\n # @POST: Returns the committed SessionEvent record with normalized detail payload.\n # @SIDE_EFFECT: Inserts and commits one session_events row.\n # @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\n def log_event(self, payload: SessionEventPayload) -> SessionEvent:\n with belief_scope(\"SessionEventLogger.log_event\"):\n session_id = str(payload.session_id or \"\").strip()\n actor_user_id = str(payload.actor_user_id or \"\").strip()\n event_type = str(payload.event_type or \"\").strip()\n event_summary = str(payload.event_summary or \"\").strip()\n\n if not session_id:\n log.explore(\"Session event logging rejected because session_id is empty\", error=\"session_id is empty\")\n raise ValueError(\"session_id must be non-empty\")\n if not actor_user_id:\n log.explore(\"Session event logging rejected because actor_user_id is empty\", error=\"actor_user_id is empty\", payload={\"session_id\": session_id})\n raise ValueError(\"actor_user_id must be non-empty\")\n if not event_type:\n log.explore(\"Session event logging rejected because event_type is empty\", error=\"event_type is empty\", payload={\"session_id\": session_id, \"actor_user_id\": actor_user_id})\n raise ValueError(\"event_type must be non-empty\")\n if not event_summary:\n log.explore(\"Session event logging rejected because event_summary is empty\", error=\"event_summary is empty\", payload={\"session_id\": session_id, \"event_type\": event_type})\n raise ValueError(\"event_summary must be non-empty\")\n\n normalized_details = dict(payload.event_details or {})\n log.reason(\n \"Persisting explicit dataset-review session audit event\",\n payload={\n \"session_id\": session_id,\n \"actor_user_id\": actor_user_id,\n \"event_type\": event_type,\n \"current_phase\": payload.current_phase,\n \"readiness_state\": payload.readiness_state,\n },\n )\n\n event = SessionEvent(\n session_id=session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=payload.current_phase,\n readiness_state=payload.readiness_state,\n event_details=normalized_details,\n )\n self.db.add(event)\n self.db.commit()\n self.db.refresh(event)\n\n log.reflect(\n \"Dataset-review session audit event persisted\",\n payload={\n \"session_id\": session_id,\n \"session_event_id\": event.session_event_id,\n \"event_type\": event.event_type,\n },\n )\n return event\n # [/DEF:log_event:Function]\n\n # [DEF:log_for_session:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.\n # @RELATION: [CALLS] ->[SessionEventLogger.log_event]\n def log_for_session(\n self,\n session: DatasetReviewSession,\n *,\n actor_user_id: str,\n event_type: str,\n event_summary: str,\n event_details: Optional[Dict[str, Any]] = None,\n ) -> SessionEvent:\n return self.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=session.current_phase.value if session.current_phase else None,\n readiness_state=session.readiness_state.value if session.readiness_state else None,\n event_details=dict(event_details or {}),\n )\n )\n # [/DEF:log_for_session:Function]\n# [/DEF:SessionEventLogger:Class]\n" - }, - { - "contract_id": "SessionEventLogger_init", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/event_logger.py", - "start_line": 54, - "end_line": 59, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Bind a live SQLAlchemy session to the session-event logger." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:SessionEventLogger_init:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.\n def __init__(self, db: Session) -> None:\n self.db = db\n # [/DEF:SessionEventLogger_init:Function]\n" - }, - { - "contract_id": "log_event", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/event_logger.py", - "start_line": 61, - "end_line": 123, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[SessionEventPayload] -> Output[SessionEvent]", - "POST": "Returns the committed SessionEvent record with normalized detail payload.", - "PRE": "session_id, actor_user_id, event_type, and event_summary are non-empty.", - "PURPOSE": "Persist one explicit session event row for an owned dataset-review mutation.", - "SIDE_EFFECT": "Inserts and commits one session_events row." - }, - "relations": [ - { - "source_id": "log_event", - "relation_type": "[DEPENDS_ON]", - "target_id": "SessionEvent", - "target_ref": "[SessionEvent]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:log_event:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.\n # @RELATION: [DEPENDS_ON] ->[SessionEvent]\n # @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.\n # @POST: Returns the committed SessionEvent record with normalized detail payload.\n # @SIDE_EFFECT: Inserts and commits one session_events row.\n # @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\n def log_event(self, payload: SessionEventPayload) -> SessionEvent:\n with belief_scope(\"SessionEventLogger.log_event\"):\n session_id = str(payload.session_id or \"\").strip()\n actor_user_id = str(payload.actor_user_id or \"\").strip()\n event_type = str(payload.event_type or \"\").strip()\n event_summary = str(payload.event_summary or \"\").strip()\n\n if not session_id:\n log.explore(\"Session event logging rejected because session_id is empty\", error=\"session_id is empty\")\n raise ValueError(\"session_id must be non-empty\")\n if not actor_user_id:\n log.explore(\"Session event logging rejected because actor_user_id is empty\", error=\"actor_user_id is empty\", payload={\"session_id\": session_id})\n raise ValueError(\"actor_user_id must be non-empty\")\n if not event_type:\n log.explore(\"Session event logging rejected because event_type is empty\", error=\"event_type is empty\", payload={\"session_id\": session_id, \"actor_user_id\": actor_user_id})\n raise ValueError(\"event_type must be non-empty\")\n if not event_summary:\n log.explore(\"Session event logging rejected because event_summary is empty\", error=\"event_summary is empty\", payload={\"session_id\": session_id, \"event_type\": event_type})\n raise ValueError(\"event_summary must be non-empty\")\n\n normalized_details = dict(payload.event_details or {})\n log.reason(\n \"Persisting explicit dataset-review session audit event\",\n payload={\n \"session_id\": session_id,\n \"actor_user_id\": actor_user_id,\n \"event_type\": event_type,\n \"current_phase\": payload.current_phase,\n \"readiness_state\": payload.readiness_state,\n },\n )\n\n event = SessionEvent(\n session_id=session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=payload.current_phase,\n readiness_state=payload.readiness_state,\n event_details=normalized_details,\n )\n self.db.add(event)\n self.db.commit()\n self.db.refresh(event)\n\n log.reflect(\n \"Dataset-review session audit event persisted\",\n payload={\n \"session_id\": session_id,\n \"session_event_id\": event.session_event_id,\n \"event_type\": event.event_type,\n },\n )\n return event\n # [/DEF:log_event:Function]\n" - }, - { - "contract_id": "log_for_session", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/event_logger.py", - "start_line": 125, - "end_line": 149, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Convenience wrapper for logging an event directly from a session aggregate root." - }, - "relations": [ - { - "source_id": "log_for_session", - "relation_type": "[CALLS]", - "target_id": "SessionEventLogger.log_event", - "target_ref": "[SessionEventLogger.log_event]" - } - ], - "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" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:log_for_session:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.\n # @RELATION: [CALLS] ->[SessionEventLogger.log_event]\n def log_for_session(\n self,\n session: DatasetReviewSession,\n *,\n actor_user_id: str,\n event_type: str,\n event_summary: str,\n event_details: Optional[Dict[str, Any]] = None,\n ) -> SessionEvent:\n return self.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=session.current_phase.value if session.current_phase else None,\n readiness_state=session.readiness_state.value if session.readiness_state else None,\n event_details=dict(event_details or {}),\n )\n )\n # [/DEF:log_for_session:Function]\n" + "anchor_syntax": "region", + "body": "# #region SessionEventLogger [C:4] [TYPE Class]\n# @BRIEF Persist explicit dataset-review session audit events with meaningful runtime reasoning logs.\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @RELATION DEPENDS_ON -> [SessionEventPayload]\n# @PRE: The database session is live and payload identifiers are non-empty.\n# @POST: Returns the committed session event row with a stable identifier and stored detail payload.\n# @SIDE_EFFECT: Writes one audit row to persistence and emits logger.reason/logger.reflect traces.\n# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\nclass SessionEventLogger:\n # region SessionEventLogger_init [TYPE Function]\n # @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.\n def __init__(self, db: Session) -> None:\n self.db = db\n # endregion SessionEventLogger_init\n\n # region log_event [TYPE Function]\n # @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.\n # @RELATION: [DEPENDS_ON] ->[SessionEvent]\n # @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.\n # @POST: Returns the committed SessionEvent record with normalized detail payload.\n # @SIDE_EFFECT: Inserts and commits one session_events row.\n # @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]\n def log_event(self, payload: SessionEventPayload) -> SessionEvent:\n with belief_scope(\"SessionEventLogger.log_event\"):\n session_id = str(payload.session_id or \"\").strip()\n actor_user_id = str(payload.actor_user_id or \"\").strip()\n event_type = str(payload.event_type or \"\").strip()\n event_summary = str(payload.event_summary or \"\").strip()\n\n if not session_id:\n logger.explore(\"Session event logging rejected because session_id is empty\")\n raise ValueError(\"session_id must be non-empty\")\n if not actor_user_id:\n logger.explore(\n \"Session event logging rejected because actor_user_id is empty\",\n extra={\"session_id\": session_id},\n )\n raise ValueError(\"actor_user_id must be non-empty\")\n if not event_type:\n logger.explore(\n \"Session event logging rejected because event_type is empty\",\n extra={\"session_id\": session_id, \"actor_user_id\": actor_user_id},\n )\n raise ValueError(\"event_type must be non-empty\")\n if not event_summary:\n logger.explore(\n \"Session event logging rejected because event_summary is empty\",\n extra={\"session_id\": session_id, \"event_type\": event_type},\n )\n raise ValueError(\"event_summary must be non-empty\")\n\n normalized_details = dict(payload.event_details or {})\n logger.reason(\n \"Persisting explicit dataset-review session audit event\",\n extra={\n \"session_id\": session_id,\n \"actor_user_id\": actor_user_id,\n \"event_type\": event_type,\n \"current_phase\": payload.current_phase,\n \"readiness_state\": payload.readiness_state,\n },\n )\n\n event = SessionEvent(\n session_id=session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=payload.current_phase,\n readiness_state=payload.readiness_state,\n event_details=normalized_details,\n )\n self.db.add(event)\n self.db.commit()\n self.db.refresh(event)\n\n logger.reflect(\n \"Dataset-review session audit event persisted\",\n extra={\n \"session_id\": session_id,\n \"session_event_id\": event.session_event_id,\n \"event_type\": event.event_type,\n },\n )\n return event\n # endregion log_event\n\n # region log_for_session [TYPE Function]\n # @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.\n # @RELATION: [CALLS] ->[SessionEventLogger.log_event]\n def log_for_session(\n self,\n session: DatasetReviewSession,\n *,\n actor_user_id: str,\n event_type: str,\n event_summary: str,\n event_details: dict[str, Any] | None = None,\n ) -> SessionEvent:\n return self.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=actor_user_id,\n event_type=event_type,\n event_summary=event_summary,\n current_phase=session.current_phase.value if session.current_phase else None,\n readiness_state=session.readiness_state.value if session.readiness_state else None,\n event_details=dict(event_details or {}),\n )\n )\n # endregion log_for_session\n# #endregion SessionEventLogger\n" }, { "contract_id": "DatasetReviewOrchestrator", "contract_type": "Module", "file_path": "backend/src/services/dataset_review/orchestrator.py", "start_line": 1, - "end_line": 613, + "end_line": 595, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext]", "INVARIANT": "Launch is blocked unless a current session has no open blocking findings, all launch-sensitive mappings are approved, and a non-stale Superset-generated compiled preview matches the current input fingerprint.", "LAYER": "Domain", @@ -84726,13 +61693,6 @@ "PURPOSE": "Coordinate dataset review session startup and lifecycle-safe intake recovery for one authenticated user.", "RATIONALE": "Original 1198-line monolith violated INV_7 (400-line module limit). Decomposed into commands and helpers sub-modules while preserving the orchestrator class as the single entry point.", "REJECTED": "Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x.", - "SEMANTICS": [ - "dataset_review", - "orchestration", - "session_lifecycle", - "intake", - "recovery" - ], "SIDE_EFFECT": "creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts." }, "relations": [ @@ -84780,22 +61740,242 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewOrchestrator:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: dataset_review, orchestration, session_lifecycle, intake, recovery\n# @PURPOSE: Coordinate dataset review session startup and lifecycle-safe intake recovery for one authenticated user.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION: DEPENDS_ON -> [SemanticSourceResolver]\n# @RELATION: DEPENDS_ON -> [SupersetContextExtractor]\n# @RELATION: DEPENDS_ON -> [SupersetCompilationAdapter]\n# @RELATION: DEPENDS_ON -> [TaskManager]\n# @RELATION: DISPATCHES -> [OrchestratorHelpers:Module]\n# @RELATION: DISPATCHES -> [OrchestratorCommands:Module]\n# @PRE: session mutations must execute inside a persisted session boundary scoped to one authenticated user.\n# @POST: state transitions are persisted atomically and emit observable progress for long-running steps.\n# @SIDE_EFFECT: creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts.\n# @DATA_CONTRACT: Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext]\n# @INVARIANT: Launch is blocked unless a current session has no open blocking findings, all launch-sensitive mappings are approved, and a non-stale Superset-generated compiled preview matches the current input fingerprint.\n# @RATIONALE: Original 1198-line monolith violated INV_7 (400-line module limit). Decomposed into commands and helpers sub-modules while preserving the orchestrator class as the single entry point.\n# @REJECTED: Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, cast\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.core.task_manager import TaskManager\nfrom src.core.utils.superset_compilation_adapter import (\n PreviewCompilationPayload,\n SqlLabLaunchPayload,\n SupersetCompilationAdapter,\n)\nfrom src.core.utils.superset_context_extractor import (\n SupersetContextExtractor,\n SupersetParsedContext,\n)\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n ApprovalState,\n BusinessSummarySource,\n CompiledPreview,\n ConfidenceState,\n DatasetProfile,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n ImportedFilter,\n LaunchStatus,\n MappingMethod,\n MappingStatus,\n PreviewStatus,\n RecommendedAction,\n ReadinessState,\n ResolutionState,\n SessionPhase,\n SessionStatus,\n TemplateVariable,\n ValidationFinding,\n VariableKind,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\nfrom src.services.dataset_review.semantic_resolver import SemanticSourceResolver\nfrom src.services.dataset_review.event_logger import SessionEventPayload\nfrom src.services.dataset_review.orchestrator_pkg._commands import (\n StartSessionCommand,\n StartSessionResult,\n PreparePreviewCommand,\n PreparePreviewResult,\n LaunchDatasetCommand,\n LaunchDatasetResult,\n)\nfrom src.services.dataset_review.orchestrator_pkg._helpers import (\n parse_dataset_selection,\n build_initial_profile,\n build_partial_recovery_findings,\n build_execution_snapshot,\n build_launch_blockers,\n get_latest_preview,\n compute_preview_fingerprint,\n extract_effective_filter_value,\n)\n\nlog = MarkerLogger(\"Orchestrator\")\n\n\n# [DEF:DatasetReviewOrchestrator:Class]\n# @COMPLEXITY: 5\n# @PURPOSE: Coordinate safe session startup while preserving cross-user isolation and explicit partial recovery.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION: DEPENDS_ON -> [SupersetContextExtractor]\n# @RELATION: DEPENDS_ON -> [TaskManager]\n# @RELATION: DEPENDS_ON -> [ConfigManager]\n# @RELATION: DEPENDS_ON -> [SemanticSourceResolver]\n# @RELATION: CALLS -> [OrchestratorHelpers:Module]\n# @PRE: constructor dependencies are valid and tied to the current request/task scope.\n# @POST: orchestrator instance can execute session-scoped mutations for one authenticated user.\n# @SIDE_EFFECT: downstream operations may persist session/profile/finding state and enqueue background tasks.\n# @DATA_CONTRACT: Input[StartSessionCommand] -> Output[StartSessionResult]\n# @INVARIANT: session ownership is preserved on every mutation and recovery remains explicit when partial.\nclass DatasetReviewOrchestrator:\n # [DEF:DatasetReviewOrchestrator_init:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.\n # @PRE: repository/config_manager are valid collaborators for the current request scope.\n # @POST: Instance holds collaborator references used by start/preview/launch orchestration methods.\n def __init__(\n self,\n repository: DatasetReviewSessionRepository,\n config_manager: ConfigManager,\n task_manager: Optional[TaskManager] = None,\n semantic_resolver: Optional[SemanticSourceResolver] = None,\n ) -> None:\n self.repository = repository\n self.config_manager = config_manager\n self.task_manager = task_manager\n self.semantic_resolver = semantic_resolver or SemanticSourceResolver()\n\n # [/DEF:DatasetReviewOrchestrator_init:Function]\n\n # [DEF:start_session:Function]\n # @COMPLEXITY: 5\n # @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery.\n # @RELATION: CALLS -> [SupersetContextExtractor.parse_superset_link]\n # @RELATION: CALLS -> [TaskManager.create_task]\n # @PRE: source input is non-empty and environment is accessible.\n # @POST: session exists in persisted storage with intake/recovery state and task linkage when async work is required.\n # @SIDE_EFFECT: persists session and may enqueue recovery task.\n # @DATA_CONTRACT: Input[StartSessionCommand] -> Output[StartSessionResult]\n # @INVARIANT: no cross-user session leakage occurs; session and follow-up task remain owned by the authenticated user.\n def start_session(self, command: StartSessionCommand) -> StartSessionResult:\n with belief_scope(\"DatasetReviewOrchestrator.start_session\"):\n normalized_source_kind = str(command.source_kind or \"\").strip()\n normalized_source_input = str(command.source_input or \"\").strip()\n normalized_environment_id = str(command.environment_id or \"\").strip()\n\n if not normalized_source_input:\n log.explore(\"Blocked dataset review session start due to empty source input\", error=\"source_input was empty\")\n raise ValueError(\"source_input must be non-empty\")\n\n if normalized_source_kind not in {\"superset_link\", \"dataset_selection\"}:\n log.explore(\"Blocked dataset review session start due to unsupported source kind\", payload={\"source_kind\": normalized_source_kind}, error=\"Unsupported source_kind\")\n raise ValueError(\"source_kind must be 'superset_link' or 'dataset_selection'\")\n\n environment = self.config_manager.get_environment(normalized_environment_id)\n if environment is None:\n log.explore(\"Blocked dataset review session start because environment was not found\", payload={\"environment_id\": normalized_environment_id}, error=\"Environment not found in config\")\n raise ValueError(\"Environment not found\")\n\n log.reason(\"Starting dataset review session\", payload={\"user_id\": command.user.id, \"environment_id\": normalized_environment_id, \"source_kind\": normalized_source_kind})\n\n parsed_context: Optional[SupersetParsedContext] = None\n findings: List[ValidationFinding] = []\n dataset_ref = normalized_source_input\n dataset_id: Optional[int] = None\n dashboard_id: Optional[int] = None\n readiness_state = ReadinessState.IMPORTING\n recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n current_phase = SessionPhase.RECOVERY\n\n if normalized_source_kind == \"superset_link\":\n extractor = SupersetContextExtractor(environment)\n parsed_context = extractor.parse_superset_link(normalized_source_input)\n dataset_ref = parsed_context.dataset_ref\n dataset_id = parsed_context.dataset_id\n dashboard_id = parsed_context.dashboard_id\n\n if parsed_context.partial_recovery:\n readiness_state = ReadinessState.RECOVERY_REQUIRED\n recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n findings.extend(build_partial_recovery_findings(parsed_context))\n else:\n readiness_state = ReadinessState.REVIEW_READY\n else:\n dataset_ref, dataset_id = parse_dataset_selection(normalized_source_input)\n readiness_state = ReadinessState.REVIEW_READY\n current_phase = SessionPhase.REVIEW\n\n session = DatasetReviewSession(\n user_id=command.user.id,\n environment_id=normalized_environment_id,\n source_kind=normalized_source_kind,\n source_input=normalized_source_input,\n dataset_ref=dataset_ref,\n dataset_id=dataset_id,\n dashboard_id=dashboard_id,\n readiness_state=readiness_state,\n recommended_action=recommended_action,\n status=SessionStatus.ACTIVE,\n current_phase=current_phase,\n )\n persisted_session = cast(Any, self.repository.create_session(session))\n\n recovered_filters: List[ImportedFilter] = []\n template_variables: List[TemplateVariable] = []\n execution_mappings: List[ExecutionMapping] = []\n if normalized_source_kind == \"superset_link\" and parsed_context is not None:\n recovered_filters, template_variables, execution_mappings, findings = (\n self._build_recovery_bootstrap(\n environment=environment,\n session=persisted_session,\n parsed_context=parsed_context,\n findings=findings,\n )\n )\n\n profile = build_initial_profile(\n session_id=persisted_session.session_id,\n parsed_context=parsed_context,\n dataset_ref=dataset_ref,\n )\n self.repository.event_log.log_event(\n SessionEventPayload(\n session_id=persisted_session.session_id,\n actor_user_id=command.user.id,\n event_type=\"session_started\",\n event_summary=\"Dataset review session shell created\",\n current_phase=persisted_session.current_phase.value,\n readiness_state=persisted_session.readiness_state.value,\n event_details={\n \"source_kind\": persisted_session.source_kind,\n \"dataset_ref\": persisted_session.dataset_ref,\n \"dataset_id\": persisted_session.dataset_id,\n \"dashboard_id\": persisted_session.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n },\n )\n )\n persisted_session = self.repository.save_profile_and_findings(\n persisted_session.session_id,\n command.user.id,\n profile,\n findings,\n )\n if recovered_filters or template_variables or execution_mappings:\n persisted_session = self.repository.save_recovery_state(\n persisted_session.session_id,\n command.user.id,\n recovered_filters,\n template_variables,\n execution_mappings,\n )\n\n active_task_id = self._enqueue_recovery_task(\n command=command,\n session=persisted_session,\n parsed_context=parsed_context,\n )\n if active_task_id:\n persisted_session.active_task_id = active_task_id\n self.repository.bump_session_version(persisted_session)\n self.repository.db.commit()\n self.repository.db.refresh(persisted_session)\n self.repository.event_log.log_event(\n SessionEventPayload(\n session_id=persisted_session.session_id,\n actor_user_id=command.user.id,\n event_type=\"recovery_task_linked\",\n event_summary=\"Recovery task linked to dataset review session\",\n current_phase=persisted_session.current_phase.value,\n readiness_state=persisted_session.readiness_state.value,\n event_details={\"task_id\": active_task_id},\n )\n )\n log.reason(\"Linked recovery task to started dataset review session\", payload={\"session_id\": persisted_session.session_id, \"task_id\": active_task_id})\n\n log.reflect(\"Dataset review session start completed\", payload={\"session_id\": persisted_session.session_id, \"dataset_ref\": persisted_session.dataset_ref, \"readiness_state\": persisted_session.readiness_state.value, \"active_task_id\": persisted_session.active_task_id, \"finding_count\": len(findings)})\n return StartSessionResult(\n session=persisted_session,\n parsed_context=parsed_context,\n findings=findings,\n )\n\n # [/DEF:start_session:Function]\n\n # [DEF:prepare_launch_preview:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.\n # @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview]\n # @PRE: all required variables have candidate values or explicitly accepted defaults.\n # @POST: returns preview artifact in pending, ready, failed, or stale state.\n # @SIDE_EFFECT: persists preview attempt and upstream compilation diagnostics.\n # @DATA_CONTRACT: Input[PreparePreviewCommand] -> Output[PreparePreviewResult]\n def prepare_launch_preview(self, command: PreparePreviewCommand) -> PreparePreviewResult:\n with belief_scope(\"DatasetReviewOrchestrator.prepare_launch_preview\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n log.explore(\"Preview preparation rejected because owned session was not found\", payload={\"session_id\": command.session_id, \"user_id\": command.user.id}, error=\"Session not found or access denied\")\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Preview requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n preview_blockers = execution_snapshot[\"preview_blockers\"]\n if preview_blockers:\n log.explore(\"Preview preparation blocked by incomplete execution context\", payload={\"session_id\": session.session_id, \"blocked_reasons\": preview_blockers}, error=\"Preview blockers present\")\n raise ValueError(\"Preview blocked: \" + \"; \".join(preview_blockers))\n\n adapter = SupersetCompilationAdapter(environment)\n preview = adapter.compile_preview(\n PreviewCompilationPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_fingerprint=execution_snapshot[\"preview_fingerprint\"],\n template_params=execution_snapshot[\"template_params\"],\n effective_filters=execution_snapshot[\"effective_filters\"],\n )\n )\n persisted_preview = self.repository.save_preview(\n session.session_id,\n command.user.id,\n preview,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.PREVIEW\n session.last_activity_at = datetime.utcnow()\n if persisted_preview.preview_status == PreviewStatus.READY:\n launch_blockers = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=persisted_preview)\n if launch_blockers:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.APPROVE_MAPPING\n else:\n session.readiness_state = ReadinessState.RUN_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.PARTIALLY_READY\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_log.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"preview_generated\",\n event_summary=\"Superset preview generation persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value, \"preview_fingerprint\": persisted_preview.preview_fingerprint},\n )\n )\n\n log.reflect(\"Superset preview preparation completed\", payload={\"session_id\": session.session_id, \"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value})\n return PreparePreviewResult(session=session, preview=persisted_preview, blocked_reasons=[])\n\n # [/DEF:prepare_launch_preview:Function]\n\n # [DEF:launch_dataset:Function]\n # @COMPLEXITY: 5\n # @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay.\n # @RELATION: CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]\n # @PRE: session is run-ready and compiled preview is current.\n # @POST: returns persisted run context with SQL Lab session reference and launch outcome.\n # @SIDE_EFFECT: creates SQL Lab execution session and audit snapshot.\n # @DATA_CONTRACT: Input[LaunchDatasetCommand] -> Output[LaunchDatasetResult]\n # @INVARIANT: launch remains blocked unless blocking findings are closed, approvals are satisfied, and the latest preview fingerprint matches current execution inputs.\n def launch_dataset(self, command: LaunchDatasetCommand) -> LaunchDatasetResult:\n with belief_scope(\"DatasetReviewOrchestrator.launch_dataset\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n log.explore(\"Launch rejected because owned session was not found\", payload={\"session_id\": command.session_id, \"user_id\": command.user.id}, error=\"Session not found or access denied\")\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Launch requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n current_preview = get_latest_preview(session)\n launch_blockers_list = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=current_preview)\n if launch_blockers_list:\n log.explore(\"Launch gate blocked dataset execution\", payload={\"session_id\": session.session_id, \"blocked_reasons\": launch_blockers_list}, error=\"Launch blockers present\")\n raise ValueError(\"Launch blocked: \" + \"; \".join(launch_blockers_list))\n\n adapter = SupersetCompilationAdapter(environment)\n try:\n sql_lab_session_ref = adapter.create_sql_lab_session(\n SqlLabLaunchPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_id=current_preview.preview_id,\n compiled_sql=str(current_preview.compiled_sql or \"\"),\n template_params=execution_snapshot[\"template_params\"],\n )\n )\n launch_status = LaunchStatus.STARTED\n launch_error = None\n except Exception as exc:\n log.explore(\"SQL Lab launch failed after passing gates\", payload={\"session_id\": session.session_id}, error=str(exc))\n sql_lab_session_ref = \"unavailable\"\n launch_status = LaunchStatus.FAILED\n launch_error = str(exc)\n\n run_context = DatasetRunContext(\n session_id=session.session_id,\n dataset_ref=session.dataset_ref,\n environment_id=session.environment_id,\n preview_id=current_preview.preview_id,\n sql_lab_session_ref=sql_lab_session_ref,\n effective_filters=execution_snapshot[\"effective_filters\"],\n template_params=execution_snapshot[\"template_params\"],\n approved_mapping_ids=execution_snapshot[\"approved_mapping_ids\"],\n semantic_decision_refs=execution_snapshot[\"semantic_decision_refs\"],\n open_warning_refs=execution_snapshot[\"open_warning_refs\"],\n launch_status=launch_status,\n launch_error=launch_error,\n )\n persisted_run_context = self.repository.save_run_context(\n session.session_id,\n command.user.id,\n run_context,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.LAUNCH\n session.last_activity_at = datetime.utcnow()\n if launch_status == LaunchStatus.FAILED:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.RUN_IN_PROGRESS\n session.recommended_action = RecommendedAction.EXPORT_OUTPUTS\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_log.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"dataset_launch_requested\",\n event_summary=\"Dataset launch handoff persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"run_context_id\": persisted_run_context.run_context_id, \"launch_status\": persisted_run_context.launch_status.value, \"preview_id\": persisted_run_context.preview_id, \"sql_lab_session_ref\": persisted_run_context.sql_lab_session_ref},\n )\n )\n\n log.reflect(\"Dataset launch orchestration completed with audited run context\", payload={\"session_id\": session.session_id, \"run_context_id\": persisted_run_context.run_context_id, \"launch_status\": persisted_run_context.launch_status.value})\n return LaunchDatasetResult(session=session, run_context=persisted_run_context, blocked_reasons=[])\n\n # [/DEF:launch_dataset:Function]\n\n # [DEF:_build_recovery_bootstrap:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.\n # @PRE: session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.\n # @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.\n # @SIDE_EFFECT: Performs Superset reads through the extractor and may append warning findings for incomplete recovery.\n def _build_recovery_bootstrap(\n self,\n environment,\n session: DatasetReviewSession,\n parsed_context: SupersetParsedContext,\n findings: List[ValidationFinding],\n ) -> tuple[List[ImportedFilter], List[TemplateVariable], List[ExecutionMapping], List[ValidationFinding]]:\n session_record = cast(Any, session)\n extractor = SupersetContextExtractor(environment)\n imported_filters_payload = extractor.recover_imported_filters(parsed_context)\n if imported_filters_payload is None:\n imported_filters_payload = []\n imported_filters = [\n ImportedFilter(\n session_id=session_record.session_id,\n filter_name=str(item.get(\"filter_name\") or f\"imported_filter_{index}\"),\n display_name=item.get(\"display_name\"),\n raw_value=item.get(\"raw_value\"),\n raw_value_masked=bool(item.get(\"raw_value_masked\", False)),\n normalized_value=item.get(\"normalized_value\"),\n source=FilterSource(str(item.get(\"source\") or FilterSource.SUPERSET_URL.value)),\n confidence_state=FilterConfidenceState(str(item.get(\"confidence_state\") or FilterConfidenceState.UNRESOLVED.value)),\n requires_confirmation=bool(item.get(\"requires_confirmation\", False)),\n recovery_status=FilterRecoveryStatus(str(item.get(\"recovery_status\") or FilterRecoveryStatus.PARTIAL.value)),\n notes=item.get(\"notes\"),\n )\n for index, item in enumerate(imported_filters_payload)\n ]\n\n template_variables: List[TemplateVariable] = []\n execution_mappings: List[ExecutionMapping] = []\n\n if session.dataset_id is not None:\n try:\n dataset_payload = parsed_context.dataset_payload\n if not isinstance(dataset_payload, dict):\n dataset_payload = extractor.client.get_dataset_detail(session_record.dataset_id)\n discovered_variables = extractor.discover_template_variables(dataset_payload)\n template_variables = [\n TemplateVariable(\n session_id=session_record.session_id,\n variable_name=str(item.get(\"variable_name\") or f\"variable_{index}\"),\n expression_source=str(item.get(\"expression_source\") or \"\"),\n variable_kind=VariableKind(str(item.get(\"variable_kind\") or VariableKind.UNKNOWN.value)),\n is_required=bool(item.get(\"is_required\", True)),\n default_value=item.get(\"default_value\"),\n mapping_status=MappingStatus(str(item.get(\"mapping_status\") or MappingStatus.UNMAPPED.value)),\n )\n for index, item in enumerate(discovered_variables)\n ]\n except Exception as exc:\n if \"dataset_template_variable_discovery_failed\" not in parsed_context.unresolved_references:\n parsed_context.unresolved_references.append(\"dataset_template_variable_discovery_failed\")\n if not any(f.caused_by_ref == \"dataset_template_variable_discovery_failed\" for f in findings):\n findings.append(\n ValidationFinding(\n area=FindingArea.TEMPLATE_MAPPING,\n severity=FindingSeverity.WARNING,\n code=\"TEMPLATE_VARIABLE_DISCOVERY_FAILED\",\n title=\"Template variables could not be discovered\",\n message=\"Session remains usable, but dataset template variables still need review.\",\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=\"dataset_template_variable_discovery_failed\",\n )\n )\n log.explore(\"Template variable discovery failed during session bootstrap\", payload={\"session_id\": session_record.session_id, \"dataset_id\": session_record.dataset_id}, error=str(exc))\n\n filter_lookup = {str(f.filter_name or \"\").strip().lower(): f for f in imported_filters if str(f.filter_name or \"\").strip()}\n for tv in template_variables:\n matched_filter = filter_lookup.get(str(tv.variable_name or \"\").strip().lower())\n if matched_filter is None:\n continue\n requires_explicit_approval = bool(matched_filter.requires_confirmation or matched_filter.recovery_status != FilterRecoveryStatus.RECOVERED)\n execution_mappings.append(\n ExecutionMapping(\n session_id=session_record.session_id,\n filter_id=matched_filter.filter_id,\n variable_id=tv.variable_id,\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=matched_filter.raw_value,\n effective_value=matched_filter.normalized_value if matched_filter.normalized_value is not None else matched_filter.raw_value,\n transformation_note=\"Bootstrapped from Superset recovery context\",\n warning_level=None,\n requires_explicit_approval=requires_explicit_approval,\n approval_state=ApprovalState.PENDING if requires_explicit_approval else ApprovalState.NOT_REQUIRED,\n approved_by_user_id=None,\n approved_at=None,\n )\n )\n\n return imported_filters, template_variables, execution_mappings, findings\n\n # [/DEF:_build_recovery_bootstrap:Function]\n\n # [DEF:_enqueue_recovery_task:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Link session start to observable async recovery when task infrastructure is available.\n # @PRE: session is already persisted.\n # @POST: returns task identifier when a task could be enqueued, otherwise None.\n # @SIDE_EFFECT: may create one background task for progressive recovery.\n def _enqueue_recovery_task(\n self,\n command: StartSessionCommand,\n session: DatasetReviewSession,\n parsed_context: Optional[SupersetParsedContext],\n ) -> Optional[str]:\n session_record = cast(Any, session)\n if self.task_manager is None:\n log.reason(\"Dataset review session started without task manager; continuing synchronously\", payload={\"session_id\": session_record.session_id})\n return None\n\n task_params: Dict[str, Any] = {\n \"session_id\": session_record.session_id,\n \"user_id\": command.user.id,\n \"environment_id\": session_record.environment_id,\n \"source_kind\": session_record.source_kind,\n \"source_input\": session_record.source_input,\n \"dataset_ref\": session_record.dataset_ref,\n \"dataset_id\": session_record.dataset_id,\n \"dashboard_id\": session_record.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n }\n\n create_task = getattr(self.task_manager, \"create_task\", None)\n if create_task is None:\n log.explore(\"Task manager has no create_task method; skipping recovery enqueue\", error=\"create_task method not found\")\n return None\n\n try:\n task_object = create_task(plugin_id=\"dataset-review-recovery\", params=task_params)\n except TypeError:\n log.explore(\"Recovery task enqueue skipped because task manager create_task contract is incompatible\", payload={\"session_id\": session_record.session_id}, error=\"TypeError from create_task\")\n return None\n\n task_id = getattr(task_object, \"id\", None)\n return str(task_id) if task_id else None\n\n # [/DEF:_enqueue_recovery_task:Function]\n\n\n# [/DEF:DatasetReviewOrchestrator:Class]\n\n# [/DEF:DatasetReviewOrchestrator:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewOrchestrator [C:5] [TYPE Module] [SEMANTICS pydantic, dataset, review, orchestration, session]\n# @BRIEF Coordinate dataset review session startup and lifecycle-safe intake recovery for one authenticated user.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [SemanticSourceResolver]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractor]\n# @RELATION DEPENDS_ON -> [SupersetCompilationAdapter]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DISPATCHES -> [OrchestratorHelpers:Module]\n# @RELATION DISPATCHES -> [OrchestratorCommands:Module]\n# @PRE: session mutations must execute inside a persisted session boundary scoped to one authenticated user.\n# @POST: state transitions are persisted atomically and emit observable progress for long-running steps.\n# @SIDE_EFFECT: creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts.\n# @DATA_CONTRACT: Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext]\n# @INVARIANT: Launch is blocked unless a current session has no open blocking findings, all launch-sensitive mappings are approved, and a non-stale Superset-generated compiled preview matches the current input fingerprint.\n# @RATIONALE: Original 1198-line monolith violated INV_7 (400-line module limit). Decomposed into commands and helpers sub-modules while preserving the orchestrator class as the single entry point.\n# @REJECTED: Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any, cast\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.core.utils.superset_compilation_adapter import (\n PreviewCompilationPayload,\n SqlLabLaunchPayload,\n SupersetCompilationAdapter,\n)\nfrom src.core.utils.superset_context_extractor import (\n SupersetContextExtractor,\n SupersetParsedContext,\n)\nfrom src.models.dataset_review import (\n ApprovalState,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n ImportedFilter,\n LaunchStatus,\n MappingMethod,\n MappingStatus,\n PreviewStatus,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n SessionPhase,\n SessionStatus,\n TemplateVariable,\n ValidationFinding,\n VariableKind,\n)\nfrom src.services.dataset_review.event_logger import SessionEventPayload\nfrom src.services.dataset_review.orchestrator_pkg._commands import (\n LaunchDatasetCommand,\n LaunchDatasetResult,\n PreparePreviewCommand,\n PreparePreviewResult,\n StartSessionCommand,\n StartSessionResult,\n)\nfrom src.services.dataset_review.orchestrator_pkg._helpers import (\n build_execution_snapshot,\n build_initial_profile,\n build_launch_blockers,\n build_partial_recovery_findings,\n get_latest_preview,\n parse_dataset_selection,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\nfrom src.services.dataset_review.semantic_resolver import SemanticSourceResolver\n\nlogger = cast(Any, logger)\n\n\n# #region DatasetReviewOrchestrator [C:5] [TYPE Class]\n# @BRIEF Coordinate safe session startup while preserving cross-user isolation and explicit partial recovery.\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractor]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SemanticSourceResolver]\n# @RELATION CALLS -> [OrchestratorHelpers:Module]\n# @PRE: constructor dependencies are valid and tied to the current request/task scope.\n# @POST: orchestrator instance can execute session-scoped mutations for one authenticated user.\n# @SIDE_EFFECT: downstream operations may persist session/profile/finding state and enqueue background tasks.\n# @DATA_CONTRACT: Input[StartSessionCommand] -> Output[StartSessionResult]\n# @INVARIANT: session ownership is preserved on every mutation and recovery remains explicit when partial.\nclass DatasetReviewOrchestrator:\n # region DatasetReviewOrchestrator_init [TYPE Function]\n # @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.\n # @PRE: repository/config_manager are valid collaborators for the current request scope.\n # @POST: Instance holds collaborator references used by start/preview/launch orchestration methods.\n def __init__(\n self,\n repository: DatasetReviewSessionRepository,\n config_manager: ConfigManager,\n task_manager: TaskManager | None = None,\n semantic_resolver: SemanticSourceResolver | None = None,\n ) -> None:\n self.repository = repository\n self.config_manager = config_manager\n self.task_manager = task_manager\n self.semantic_resolver = semantic_resolver or SemanticSourceResolver()\n\n # endregion DatasetReviewOrchestrator_init\n\n # region start_session [TYPE Function]\n # @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery.\n # @RELATION: CALLS -> [SupersetContextExtractor.parse_superset_link]\n # @RELATION: CALLS -> [TaskManager.create_task]\n # @PRE: source input is non-empty and environment is accessible.\n # @POST: session exists in persisted storage with intake/recovery state and task linkage when async work is required.\n # @SIDE_EFFECT: persists session and may enqueue recovery task.\n # @DATA_CONTRACT: Input[StartSessionCommand] -> Output[StartSessionResult]\n # @INVARIANT: no cross-user session leakage occurs; session and follow-up task remain owned by the authenticated user.\n def start_session(self, command: StartSessionCommand) -> StartSessionResult:\n with belief_scope(\"DatasetReviewOrchestrator.start_session\"):\n normalized_source_kind = str(command.source_kind or \"\").strip()\n normalized_source_input = str(command.source_input or \"\").strip()\n normalized_environment_id = str(command.environment_id or \"\").strip()\n\n if not normalized_source_input:\n logger.explore(\"Blocked dataset review session start due to empty source input\")\n raise ValueError(\"source_input must be non-empty\")\n\n if normalized_source_kind not in {\"superset_link\", \"dataset_selection\"}:\n logger.explore(\"Blocked dataset review session start due to unsupported source kind\", extra={\"source_kind\": normalized_source_kind})\n raise ValueError(\"source_kind must be 'superset_link' or 'dataset_selection'\")\n\n environment = self.config_manager.get_environment(normalized_environment_id)\n if environment is None:\n logger.explore(\"Blocked dataset review session start because environment was not found\", extra={\"environment_id\": normalized_environment_id})\n raise ValueError(\"Environment not found\")\n\n logger.reason(\"Starting dataset review session\", extra={\"user_id\": command.user.id, \"environment_id\": normalized_environment_id, \"source_kind\": normalized_source_kind})\n\n parsed_context: SupersetParsedContext | None = None\n findings: list[ValidationFinding] = []\n dataset_ref = normalized_source_input\n dataset_id: int | None = None\n dashboard_id: int | None = None\n readiness_state = ReadinessState.IMPORTING\n recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n current_phase = SessionPhase.RECOVERY\n\n if normalized_source_kind == \"superset_link\":\n extractor = SupersetContextExtractor(environment)\n parsed_context = extractor.parse_superset_link(normalized_source_input)\n dataset_ref = parsed_context.dataset_ref\n dataset_id = parsed_context.dataset_id\n dashboard_id = parsed_context.dashboard_id\n\n if parsed_context.partial_recovery:\n readiness_state = ReadinessState.RECOVERY_REQUIRED\n recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n findings.extend(build_partial_recovery_findings(parsed_context))\n else:\n readiness_state = ReadinessState.REVIEW_READY\n else:\n dataset_ref, dataset_id = parse_dataset_selection(normalized_source_input)\n readiness_state = ReadinessState.REVIEW_READY\n current_phase = SessionPhase.REVIEW\n\n session = DatasetReviewSession(\n user_id=command.user.id,\n environment_id=normalized_environment_id,\n source_kind=normalized_source_kind,\n source_input=normalized_source_input,\n dataset_ref=dataset_ref,\n dataset_id=dataset_id,\n dashboard_id=dashboard_id,\n readiness_state=readiness_state,\n recommended_action=recommended_action,\n status=SessionStatus.ACTIVE,\n current_phase=current_phase,\n )\n persisted_session = cast(Any, self.repository.create_session(session))\n\n recovered_filters: list[ImportedFilter] = []\n template_variables: list[TemplateVariable] = []\n execution_mappings: list[ExecutionMapping] = []\n if normalized_source_kind == \"superset_link\" and parsed_context is not None:\n recovered_filters, template_variables, execution_mappings, findings = (\n self._build_recovery_bootstrap(\n environment=environment,\n session=persisted_session,\n parsed_context=parsed_context,\n findings=findings,\n )\n )\n\n profile = build_initial_profile(\n session_id=persisted_session.session_id,\n parsed_context=parsed_context,\n dataset_ref=dataset_ref,\n )\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=persisted_session.session_id,\n actor_user_id=command.user.id,\n event_type=\"session_started\",\n event_summary=\"Dataset review session shell created\",\n current_phase=persisted_session.current_phase.value,\n readiness_state=persisted_session.readiness_state.value,\n event_details={\n \"source_kind\": persisted_session.source_kind,\n \"dataset_ref\": persisted_session.dataset_ref,\n \"dataset_id\": persisted_session.dataset_id,\n \"dashboard_id\": persisted_session.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n },\n )\n )\n persisted_session = self.repository.save_profile_and_findings(\n persisted_session.session_id,\n command.user.id,\n profile,\n findings,\n )\n if recovered_filters or template_variables or execution_mappings:\n persisted_session = self.repository.save_recovery_state(\n persisted_session.session_id,\n command.user.id,\n recovered_filters,\n template_variables,\n execution_mappings,\n )\n\n active_task_id = self._enqueue_recovery_task(\n command=command,\n session=persisted_session,\n parsed_context=parsed_context,\n )\n if active_task_id:\n persisted_session.active_task_id = active_task_id\n self.repository.bump_session_version(persisted_session)\n self.repository.db.commit()\n self.repository.db.refresh(persisted_session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=persisted_session.session_id,\n actor_user_id=command.user.id,\n event_type=\"recovery_task_linked\",\n event_summary=\"Recovery task linked to dataset review session\",\n current_phase=persisted_session.current_phase.value,\n readiness_state=persisted_session.readiness_state.value,\n event_details={\"task_id\": active_task_id},\n )\n )\n logger.reason(\"Linked recovery task to started dataset review session\", extra={\"session_id\": persisted_session.session_id, \"task_id\": active_task_id})\n\n logger.reflect(\"Dataset review session start completed\", extra={\"session_id\": persisted_session.session_id, \"dataset_ref\": persisted_session.dataset_ref, \"readiness_state\": persisted_session.readiness_state.value, \"active_task_id\": persisted_session.active_task_id, \"finding_count\": len(findings)})\n return StartSessionResult(\n session=persisted_session,\n parsed_context=parsed_context,\n findings=findings,\n )\n\n # endregion start_session\n\n # region prepare_launch_preview [TYPE Function]\n # @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.\n # @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview]\n # @PRE: all required variables have candidate values or explicitly accepted defaults.\n # @POST: returns preview artifact in pending, ready, failed, or stale state.\n # @SIDE_EFFECT: persists preview attempt and upstream compilation diagnostics.\n # @DATA_CONTRACT: Input[PreparePreviewCommand] -> Output[PreparePreviewResult]\n def prepare_launch_preview(self, command: PreparePreviewCommand) -> PreparePreviewResult:\n with belief_scope(\"DatasetReviewOrchestrator.prepare_launch_preview\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n logger.explore(\"Preview preparation rejected because owned session was not found\", extra={\"session_id\": command.session_id, \"user_id\": command.user.id})\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Preview requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n preview_blockers = execution_snapshot[\"preview_blockers\"]\n if preview_blockers:\n logger.explore(\"Preview preparation blocked by incomplete execution context\", extra={\"session_id\": session.session_id, \"blocked_reasons\": preview_blockers})\n raise ValueError(\"Preview blocked: \" + \"; \".join(preview_blockers))\n\n adapter = SupersetCompilationAdapter(environment)\n preview = adapter.compile_preview(\n PreviewCompilationPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_fingerprint=execution_snapshot[\"preview_fingerprint\"],\n template_params=execution_snapshot[\"template_params\"],\n effective_filters=execution_snapshot[\"effective_filters\"],\n )\n )\n persisted_preview = self.repository.save_preview(\n session.session_id,\n command.user.id,\n preview,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.PREVIEW\n session.last_activity_at = datetime.utcnow()\n if persisted_preview.preview_status == PreviewStatus.READY:\n launch_blockers = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=persisted_preview)\n if launch_blockers:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.APPROVE_MAPPING\n else:\n session.readiness_state = ReadinessState.RUN_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.PARTIALLY_READY\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"preview_generated\",\n event_summary=\"Superset preview generation persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value, \"preview_fingerprint\": persisted_preview.preview_fingerprint},\n )\n )\n\n logger.reflect(\"Superset preview preparation completed\", extra={\"session_id\": session.session_id, \"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value})\n return PreparePreviewResult(session=session, preview=persisted_preview, blocked_reasons=[])\n\n # endregion prepare_launch_preview\n\n # region launch_dataset [TYPE Function]\n # @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay.\n # @RELATION: CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]\n # @PRE: session is run-ready and compiled preview is current.\n # @POST: returns persisted run context with SQL Lab session reference and launch outcome.\n # @SIDE_EFFECT: creates SQL Lab execution session and audit snapshot.\n # @DATA_CONTRACT: Input[LaunchDatasetCommand] -> Output[LaunchDatasetResult]\n # @INVARIANT: launch remains blocked unless blocking findings are closed, approvals are satisfied, and the latest preview fingerprint matches current execution inputs.\n def launch_dataset(self, command: LaunchDatasetCommand) -> LaunchDatasetResult:\n with belief_scope(\"DatasetReviewOrchestrator.launch_dataset\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n logger.explore(\"Launch rejected because owned session was not found\", extra={\"session_id\": command.session_id, \"user_id\": command.user.id})\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Launch requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n current_preview = get_latest_preview(session)\n launch_blockers_list = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=current_preview)\n if launch_blockers_list:\n logger.explore(\"Launch gate blocked dataset execution\", extra={\"session_id\": session.session_id, \"blocked_reasons\": launch_blockers_list})\n raise ValueError(\"Launch blocked: \" + \"; \".join(launch_blockers_list))\n\n adapter = SupersetCompilationAdapter(environment)\n try:\n sql_lab_session_ref = adapter.create_sql_lab_session(\n SqlLabLaunchPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_id=current_preview.preview_id,\n compiled_sql=str(current_preview.compiled_sql or \"\"),\n template_params=execution_snapshot[\"template_params\"],\n )\n )\n launch_status = LaunchStatus.STARTED\n launch_error = None\n except Exception as exc:\n logger.explore(\"SQL Lab launch failed after passing gates\", extra={\"session_id\": session.session_id, \"error\": str(exc)})\n sql_lab_session_ref = \"unavailable\"\n launch_status = LaunchStatus.FAILED\n launch_error = str(exc)\n\n run_context = DatasetRunContext(\n session_id=session.session_id,\n dataset_ref=session.dataset_ref,\n environment_id=session.environment_id,\n preview_id=current_preview.preview_id,\n sql_lab_session_ref=sql_lab_session_ref,\n effective_filters=execution_snapshot[\"effective_filters\"],\n template_params=execution_snapshot[\"template_params\"],\n approved_mapping_ids=execution_snapshot[\"approved_mapping_ids\"],\n semantic_decision_refs=execution_snapshot[\"semantic_decision_refs\"],\n open_warning_refs=execution_snapshot[\"open_warning_refs\"],\n launch_status=launch_status,\n launch_error=launch_error,\n )\n persisted_run_context = self.repository.save_run_context(\n session.session_id,\n command.user.id,\n run_context,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.LAUNCH\n session.last_activity_at = datetime.utcnow()\n if launch_status == LaunchStatus.FAILED:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.RUN_IN_PROGRESS\n session.recommended_action = RecommendedAction.EXPORT_OUTPUTS\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"dataset_launch_requested\",\n event_summary=\"Dataset launch handoff persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"run_context_id\": persisted_run_context.run_context_id, \"launch_status\": persisted_run_context.launch_status.value, \"preview_id\": persisted_run_context.preview_id, \"sql_lab_session_ref\": persisted_run_context.sql_lab_session_ref},\n )\n )\n\n logger.reflect(\"Dataset launch orchestration completed with audited run context\", extra={\"session_id\": session.session_id, \"run_context_id\": persisted_run_context.run_context_id, \"launch_status\": persisted_run_context.launch_status.value})\n return LaunchDatasetResult(session=session, run_context=persisted_run_context, blocked_reasons=[])\n\n # endregion launch_dataset\n\n # region _build_recovery_bootstrap [TYPE Function]\n # @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.\n # @PRE: session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.\n # @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.\n # @SIDE_EFFECT: Performs Superset reads through the extractor and may append warning findings for incomplete recovery.\n def _build_recovery_bootstrap(\n self,\n environment,\n session: DatasetReviewSession,\n parsed_context: SupersetParsedContext,\n findings: list[ValidationFinding],\n ) -> tuple[list[ImportedFilter], list[TemplateVariable], list[ExecutionMapping], list[ValidationFinding]]:\n session_record = cast(Any, session)\n extractor = SupersetContextExtractor(environment)\n imported_filters_payload = extractor.recover_imported_filters(parsed_context)\n if imported_filters_payload is None:\n imported_filters_payload = []\n imported_filters = [\n ImportedFilter(\n session_id=session_record.session_id,\n filter_name=str(item.get(\"filter_name\") or f\"imported_filter_{index}\"),\n display_name=item.get(\"display_name\"),\n raw_value=item.get(\"raw_value\"),\n raw_value_masked=bool(item.get(\"raw_value_masked\", False)),\n normalized_value=item.get(\"normalized_value\"),\n source=FilterSource(str(item.get(\"source\") or FilterSource.SUPERSET_URL.value)),\n confidence_state=FilterConfidenceState(str(item.get(\"confidence_state\") or FilterConfidenceState.UNRESOLVED.value)),\n requires_confirmation=bool(item.get(\"requires_confirmation\", False)),\n recovery_status=FilterRecoveryStatus(str(item.get(\"recovery_status\") or FilterRecoveryStatus.PARTIAL.value)),\n notes=item.get(\"notes\"),\n )\n for index, item in enumerate(imported_filters_payload)\n ]\n\n template_variables: list[TemplateVariable] = []\n execution_mappings: list[ExecutionMapping] = []\n\n if session.dataset_id is not None:\n try:\n dataset_payload = parsed_context.dataset_payload\n if not isinstance(dataset_payload, dict):\n dataset_payload = extractor.client.get_dataset_detail(session_record.dataset_id)\n discovered_variables = extractor.discover_template_variables(dataset_payload)\n template_variables = [\n TemplateVariable(\n session_id=session_record.session_id,\n variable_name=str(item.get(\"variable_name\") or f\"variable_{index}\"),\n expression_source=str(item.get(\"expression_source\") or \"\"),\n variable_kind=VariableKind(str(item.get(\"variable_kind\") or VariableKind.UNKNOWN.value)),\n is_required=bool(item.get(\"is_required\", True)),\n default_value=item.get(\"default_value\"),\n mapping_status=MappingStatus(str(item.get(\"mapping_status\") or MappingStatus.UNMAPPED.value)),\n )\n for index, item in enumerate(discovered_variables)\n ]\n except Exception as exc:\n if \"dataset_template_variable_discovery_failed\" not in parsed_context.unresolved_references:\n parsed_context.unresolved_references.append(\"dataset_template_variable_discovery_failed\")\n if not any(f.caused_by_ref == \"dataset_template_variable_discovery_failed\" for f in findings):\n findings.append(\n ValidationFinding(\n area=FindingArea.TEMPLATE_MAPPING,\n severity=FindingSeverity.WARNING,\n code=\"TEMPLATE_VARIABLE_DISCOVERY_FAILED\",\n title=\"Template variables could not be discovered\",\n message=\"Session remains usable, but dataset template variables still need review.\",\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=\"dataset_template_variable_discovery_failed\",\n )\n )\n logger.explore(\"Template variable discovery failed during session bootstrap\", extra={\"session_id\": session_record.session_id, \"dataset_id\": session_record.dataset_id, \"error\": str(exc)})\n\n filter_lookup = {str(f.filter_name or \"\").strip().lower(): f for f in imported_filters if str(f.filter_name or \"\").strip()}\n for tv in template_variables:\n matched_filter = filter_lookup.get(str(tv.variable_name or \"\").strip().lower())\n if matched_filter is None:\n continue\n requires_explicit_approval = bool(matched_filter.requires_confirmation or matched_filter.recovery_status != FilterRecoveryStatus.RECOVERED)\n execution_mappings.append(\n ExecutionMapping(\n session_id=session_record.session_id,\n filter_id=matched_filter.filter_id,\n variable_id=tv.variable_id,\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=matched_filter.raw_value,\n effective_value=matched_filter.normalized_value if matched_filter.normalized_value is not None else matched_filter.raw_value,\n transformation_note=\"Bootstrapped from Superset recovery context\",\n warning_level=None,\n requires_explicit_approval=requires_explicit_approval,\n approval_state=ApprovalState.PENDING if requires_explicit_approval else ApprovalState.NOT_REQUIRED,\n approved_by_user_id=None,\n approved_at=None,\n )\n )\n\n return imported_filters, template_variables, execution_mappings, findings\n\n # endregion _build_recovery_bootstrap\n\n # region _enqueue_recovery_task [TYPE Function]\n # @PURPOSE: Link session start to observable async recovery when task infrastructure is available.\n # @PRE: session is already persisted.\n # @POST: returns task identifier when a task could be enqueued, otherwise None.\n # @SIDE_EFFECT: may create one background task for progressive recovery.\n def _enqueue_recovery_task(\n self,\n command: StartSessionCommand,\n session: DatasetReviewSession,\n parsed_context: SupersetParsedContext | None,\n ) -> str | None:\n session_record = cast(Any, session)\n if self.task_manager is None:\n logger.reason(\"Dataset review session started without task manager; continuing synchronously\", extra={\"session_id\": session_record.session_id})\n return None\n\n task_params: dict[str, Any] = {\n \"session_id\": session_record.session_id,\n \"user_id\": command.user.id,\n \"environment_id\": session_record.environment_id,\n \"source_kind\": session_record.source_kind,\n \"source_input\": session_record.source_input,\n \"dataset_ref\": session_record.dataset_ref,\n \"dataset_id\": session_record.dataset_id,\n \"dashboard_id\": session_record.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n }\n\n create_task = getattr(self.task_manager, \"create_task\", None)\n if create_task is None:\n logger.explore(\"Task manager has no create_task method; skipping recovery enqueue\")\n return None\n\n try:\n task_object = create_task(plugin_id=\"dataset-review-recovery\", params=task_params)\n except TypeError:\n logger.explore(\"Recovery task enqueue skipped because task manager create_task contract is incompatible\", extra={\"session_id\": session_record.session_id})\n return None\n\n task_id = getattr(task_object, \"id\", None)\n return str(task_id) if task_id else None\n\n # endregion _enqueue_recovery_task\n\n\n# #endregion DatasetReviewOrchestrator\n\n# #endregion DatasetReviewOrchestrator\n" }, { - "contract_id": "DatasetReviewOrchestrator_init", + "contract_id": "OrchestratorCommands", + "contract_type": "Module", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", + "start_line": 1, + "end_line": 94, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "LAYER": "Domain", + "PURPOSE": "Typed command and result dataclasses for dataset review orchestration boundary." + }, + "relations": [ + { + "source_id": "OrchestratorCommands", + "relation_type": "DEPENDS_ON", + "target_id": "DatasetReviewModels", + "target_ref": "[DatasetReviewModels]" + }, + { + "source_id": "OrchestratorCommands", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetContextExtractor", + "target_ref": "[SupersetContextExtractor]" + } + ], + "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", + "body": "# #region OrchestratorCommands [C:2] [TYPE Module] [SEMANTICS dataset, review, command, dataclass, boundary]\n# @BRIEF Typed command and result dataclasses for dataset review orchestration boundary.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractor]\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\n\nfrom src.core.utils.superset_context_extractor import SupersetParsedContext\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n CompiledPreview,\n DatasetReviewSession,\n DatasetRunContext,\n ValidationFinding,\n)\n\n\n# #region StartSessionCommand [C:2] [TYPE Class]\n# @BRIEF Typed input contract for starting a dataset review session.\n@dataclass\nclass StartSessionCommand:\n user: User\n environment_id: str\n source_kind: str\n source_input: str\n\n\n# #endregion StartSessionCommand\n\n\n# #region StartSessionResult [C:2] [TYPE Class]\n# @BRIEF Session-start result carrying the persisted session and intake recovery metadata.\n@dataclass\nclass StartSessionResult:\n session: DatasetReviewSession\n parsed_context: SupersetParsedContext | None = None\n findings: list[ValidationFinding] = field(default_factory=list)\n\n\n# #endregion StartSessionResult\n\n\n# #region PreparePreviewCommand [C:2] [TYPE Class]\n# @BRIEF Typed input contract for compiling one Superset-backed session preview.\n@dataclass\nclass PreparePreviewCommand:\n user: User\n session_id: str\n expected_version: int | None = None\n\n\n# #endregion PreparePreviewCommand\n\n\n# #region PreparePreviewResult [C:2] [TYPE Class]\n# @BRIEF Result contract for one persisted compiled preview attempt.\n@dataclass\nclass PreparePreviewResult:\n session: DatasetReviewSession\n preview: CompiledPreview\n blocked_reasons: list[str] = field(default_factory=list)\n\n\n# #endregion PreparePreviewResult\n\n\n# #region LaunchDatasetCommand [C:2] [TYPE Class]\n# @BRIEF Typed input contract for launching one dataset-review session into SQL Lab.\n@dataclass\nclass LaunchDatasetCommand:\n user: User\n session_id: str\n expected_version: int | None = None\n\n\n# #endregion LaunchDatasetCommand\n\n\n# #region LaunchDatasetResult [C:2] [TYPE Class]\n# @BRIEF Launch result carrying immutable run context and any gate blockers.\n@dataclass\nclass LaunchDatasetResult:\n session: DatasetReviewSession\n run_context: DatasetRunContext\n blocked_reasons: list[str] = field(default_factory=list)\n\n\n# #endregion LaunchDatasetResult\n\n\n# #endregion OrchestratorCommands\n" + }, + { + "contract_id": "StartSessionCommand", + "contract_type": "Class", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", + "start_line": 21, + "end_line": 31, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Typed input contract for starting a dataset review session." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region StartSessionCommand [C:2] [TYPE Class]\n# @BRIEF Typed input contract for starting a dataset review session.\n@dataclass\nclass StartSessionCommand:\n user: User\n environment_id: str\n source_kind: str\n source_input: str\n\n\n# #endregion StartSessionCommand\n" + }, + { + "contract_id": "StartSessionResult", + "contract_type": "Class", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", + "start_line": 34, + "end_line": 43, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Session-start result carrying the persisted session and intake recovery metadata." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region StartSessionResult [C:2] [TYPE Class]\n# @BRIEF Session-start result carrying the persisted session and intake recovery metadata.\n@dataclass\nclass StartSessionResult:\n session: DatasetReviewSession\n parsed_context: SupersetParsedContext | None = None\n findings: list[ValidationFinding] = field(default_factory=list)\n\n\n# #endregion StartSessionResult\n" + }, + { + "contract_id": "PreparePreviewCommand", + "contract_type": "Class", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", + "start_line": 46, + "end_line": 55, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Typed input contract for compiling one Superset-backed session preview." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region PreparePreviewCommand [C:2] [TYPE Class]\n# @BRIEF Typed input contract for compiling one Superset-backed session preview.\n@dataclass\nclass PreparePreviewCommand:\n user: User\n session_id: str\n expected_version: int | None = None\n\n\n# #endregion PreparePreviewCommand\n" + }, + { + "contract_id": "PreparePreviewResult", + "contract_type": "Class", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", + "start_line": 58, + "end_line": 67, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Result contract for one persisted compiled preview attempt." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region PreparePreviewResult [C:2] [TYPE Class]\n# @BRIEF Result contract for one persisted compiled preview attempt.\n@dataclass\nclass PreparePreviewResult:\n session: DatasetReviewSession\n preview: CompiledPreview\n blocked_reasons: list[str] = field(default_factory=list)\n\n\n# #endregion PreparePreviewResult\n" + }, + { + "contract_id": "LaunchDatasetCommand", + "contract_type": "Class", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", + "start_line": 70, + "end_line": 79, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Typed input contract for launching one dataset-review session into SQL Lab." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region LaunchDatasetCommand [C:2] [TYPE Class]\n# @BRIEF Typed input contract for launching one dataset-review session into SQL Lab.\n@dataclass\nclass LaunchDatasetCommand:\n user: User\n session_id: str\n expected_version: int | None = None\n\n\n# #endregion LaunchDatasetCommand\n" + }, + { + "contract_id": "LaunchDatasetResult", + "contract_type": "Class", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", + "start_line": 82, + "end_line": 91, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Launch result carrying immutable run context and any gate blockers." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region LaunchDatasetResult [C:2] [TYPE Class]\n# @BRIEF Launch result carrying immutable run context and any gate blockers.\n@dataclass\nclass LaunchDatasetResult:\n session: DatasetReviewSession\n run_context: DatasetRunContext\n blocked_reasons: list[str] = field(default_factory=list)\n\n\n# #endregion LaunchDatasetResult\n" + }, + { + "contract_id": "OrchestratorHelpers", + "contract_type": "Module", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 1, + "end_line": 338, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "LAYER": "Domain", + "POST": "Helper results are deterministic and do not mutate persistence directly.", + "PRE": "Caller provides a loaded session aggregate with hydrated child collections.", + "PURPOSE": "Pure helper methods extracted from DatasetReviewOrchestrator for INV_7 compliance — snapshot, blockers, fingerprint, recovery bootstrap." + }, + "relations": [ + { + "source_id": "OrchestratorHelpers", + "relation_type": "DEPENDS_ON", + "target_id": "DatasetReviewModels", + "target_ref": "[DatasetReviewModels]" + }, + { + "source_id": "OrchestratorHelpers", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetContextExtractor", + "target_ref": "[SupersetContextExtractor]" + } + ], + "schema_warnings": [ + { + "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", + "body": "# #region OrchestratorHelpers [C:4] [TYPE Module] [SEMANTICS dataset, review, snapshot, fingerprint, recovery]\n# @BRIEF Pure helper methods extracted from DatasetReviewOrchestrator for INV_7 compliance — snapshot, blockers, fingerprint, recovery bootstrap.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractor]\n# @PRE: Caller provides a loaded session aggregate with hydrated child collections.\n# @POST: Helper results are deterministic and do not mutate persistence directly.\n\nfrom __future__ import annotations\n\nimport hashlib\nimport json\nfrom datetime import datetime\nfrom typing import Any, cast\n\nfrom src.core.logger import logger\nfrom src.models.dataset_review import (\n ApprovalState,\n BusinessSummarySource,\n CompiledPreview,\n ConfidenceState,\n DatasetProfile,\n DatasetReviewSession,\n FindingArea,\n FindingSeverity,\n PreviewStatus,\n ResolutionState,\n ValidationFinding,\n)\n\nlogger = cast(Any, logger)\n\n\n# #region parse_dataset_selection [C:2] [TYPE Function]\n# @BRIEF Normalize dataset-selection payload into canonical session references.\ndef parse_dataset_selection(source_input: str) -> tuple[str, int | None]:\n normalized = str(source_input or \"\").strip()\n if not normalized:\n raise ValueError(\"dataset selection input must be non-empty\")\n if normalized.isdigit():\n dataset_id = int(normalized)\n return f\"dataset:{dataset_id}\", dataset_id\n if normalized.startswith(\"dataset:\"):\n suffix = normalized.split(\":\", 1)[1].strip()\n if suffix.isdigit():\n return normalized, int(suffix)\n return normalized, None\n return normalized, None\n\n\n# #endregion parse_dataset_selection\n\n\n# #region build_initial_profile [C:2] [TYPE Function]\n# @BRIEF Create the first profile snapshot so exports and detail views remain usable immediately after intake.\ndef build_initial_profile(\n session_id: str,\n parsed_context: Any | None,\n dataset_ref: str,\n) -> DatasetProfile:\n dataset_name = (\n dataset_ref.split(\".\")[-1] if dataset_ref else \"Unresolved dataset\"\n )\n business_summary = (\n f\"Review session initialized for {dataset_ref}.\"\n if dataset_ref\n else \"Review session initialized with unresolved dataset context.\"\n )\n confidence_state = (\n ConfidenceState.MIXED\n if parsed_context and getattr(parsed_context, \"partial_recovery\", False)\n else ConfidenceState.MOSTLY_CONFIRMED\n )\n return DatasetProfile(\n session_id=session_id,\n dataset_name=dataset_name or \"Unresolved dataset\",\n schema_name=dataset_ref.split(\".\")[0] if \".\" in dataset_ref else None,\n business_summary=business_summary,\n business_summary_source=BusinessSummarySource.IMPORTED,\n description=\"Initial review profile created from source intake.\",\n dataset_type=\"unknown\",\n is_sqllab_view=False,\n completeness_score=0.25,\n confidence_state=confidence_state,\n has_blocking_findings=False,\n has_warning_findings=bool(\n parsed_context and getattr(parsed_context, \"partial_recovery\", False)\n ),\n manual_summary_locked=False,\n )\n\n\n# #endregion build_initial_profile\n\n\n# #region build_partial_recovery_findings [C:3] [TYPE Function]\n# @BRIEF Project partial Superset intake recovery into explicit findings without blocking session usability.\n# @PRE: parsed_context.partial_recovery is true.\n# @POST: Returns warning-level findings that preserve usable but incomplete state.\ndef build_partial_recovery_findings(parsed_context: Any) -> list[ValidationFinding]:\n findings: list[ValidationFinding] = []\n for unresolved_ref in getattr(parsed_context, \"unresolved_references\", []):\n findings.append(\n ValidationFinding(\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"PARTIAL_SUPERSET_RECOVERY\",\n title=\"Superset context recovered partially\",\n message=(\n \"Session remains usable, but some Superset context requires review: \"\n f\"{unresolved_ref.replace('_', ' ')}.\"\n ),\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=unresolved_ref,\n )\n )\n return findings\n\n\n# #endregion build_partial_recovery_findings\n\n\n# #region extract_effective_filter_value [C:2] [TYPE Function]\n# @BRIEF Separate normalized filter payload metadata from the user-facing effective filter value.\ndef extract_effective_filter_value(\n normalized_value: Any, raw_value: Any\n) -> Any:\n if isinstance(normalized_value, dict) and (\n \"filter_clauses\" in normalized_value\n or \"extra_form_data\" in normalized_value\n ):\n return raw_value\n return normalized_value if normalized_value is not None else raw_value\n\n\n# #endregion extract_effective_filter_value\n\n\n# #region build_execution_snapshot [C:4] [TYPE Function]\n# @BRIEF Build effective filters, template params, approvals, and fingerprint for preview and launch gating.\n# @PRE: Session aggregate includes imported filters, template variables, and current execution mappings.\n# @POST: Returns deterministic execution snapshot for current session state without mutating persistence.\ndef build_execution_snapshot(session: DatasetReviewSession) -> dict[str, Any]:\n session_record = cast(Any, session)\n filter_lookup = {\n item.filter_id: item for item in session_record.imported_filters\n }\n variable_lookup = {\n item.variable_id: item for item in session_record.template_variables\n }\n\n effective_filters: list[dict[str, Any]] = []\n template_params: dict[str, Any] = {}\n approved_mapping_ids: list[str] = []\n open_warning_refs: list[str] = []\n preview_blockers: list[str] = []\n mapped_filter_ids: set[str] = set()\n\n for mapping in session_record.execution_mappings:\n imported_filter = filter_lookup.get(mapping.filter_id)\n template_variable = variable_lookup.get(mapping.variable_id)\n if imported_filter is None:\n preview_blockers.append(f\"mapping:{mapping.mapping_id}:missing_filter\")\n continue\n if template_variable is None:\n preview_blockers.append(f\"mapping:{mapping.mapping_id}:missing_variable\")\n continue\n\n effective_value = mapping.effective_value\n if effective_value is None:\n effective_value = extract_effective_filter_value(\n imported_filter.normalized_value, imported_filter.raw_value,\n )\n if effective_value is None:\n effective_value = template_variable.default_value\n\n if effective_value is None and template_variable.is_required:\n preview_blockers.append(\n f\"variable:{template_variable.variable_name}:missing_required_value\"\n )\n continue\n\n mapped_filter_ids.add(imported_filter.filter_id)\n if effective_value is not None:\n mapped_filter_payload = {\n \"mapping_id\": mapping.mapping_id,\n \"filter_id\": imported_filter.filter_id,\n \"filter_name\": imported_filter.filter_name,\n \"variable_id\": template_variable.variable_id,\n \"variable_name\": template_variable.variable_name,\n \"effective_value\": effective_value,\n \"raw_input_value\": mapping.raw_input_value,\n }\n if isinstance(imported_filter.normalized_value, dict):\n mapped_filter_payload[\"display_name\"] = imported_filter.display_name\n mapped_filter_payload[\"normalized_filter_payload\"] = (\n imported_filter.normalized_value\n )\n effective_filters.append(mapped_filter_payload)\n template_params[template_variable.variable_name] = effective_value\n if mapping.approval_state == ApprovalState.APPROVED:\n approved_mapping_ids.append(mapping.mapping_id)\n if (\n mapping.requires_explicit_approval\n and mapping.approval_state != ApprovalState.APPROVED\n ):\n open_warning_refs.append(mapping.mapping_id)\n\n for imported_filter in session_record.imported_filters:\n if imported_filter.filter_id in mapped_filter_ids:\n continue\n effective_value = extract_effective_filter_value(\n imported_filter.normalized_value, imported_filter.raw_value,\n )\n if effective_value is None:\n continue\n effective_filters.append(\n {\n \"filter_id\": imported_filter.filter_id,\n \"filter_name\": imported_filter.filter_name,\n \"display_name\": imported_filter.display_name,\n \"effective_value\": effective_value,\n \"raw_input_value\": imported_filter.raw_value,\n \"normalized_filter_payload\": imported_filter.normalized_value,\n }\n )\n\n mapped_variable_ids = {\n mapping.variable_id for mapping in session_record.execution_mappings\n }\n for variable in session_record.template_variables:\n if variable.variable_id in mapped_variable_ids:\n continue\n if variable.default_value is not None:\n template_params[variable.variable_name] = variable.default_value\n continue\n if variable.is_required:\n preview_blockers.append(f\"variable:{variable.variable_name}:unmapped\")\n\n semantic_decision_refs = [\n field.field_id\n for field in session.semantic_fields\n if field.is_locked\n or not field.needs_review\n or field.provenance.value != \"unresolved\"\n ]\n preview_fingerprint = compute_preview_fingerprint(\n {\n \"dataset_id\": session_record.dataset_id,\n \"template_params\": template_params,\n \"effective_filters\": effective_filters,\n }\n )\n return {\n \"effective_filters\": effective_filters,\n \"template_params\": template_params,\n \"approved_mapping_ids\": sorted(approved_mapping_ids),\n \"semantic_decision_refs\": sorted(semantic_decision_refs),\n \"open_warning_refs\": sorted(open_warning_refs),\n \"preview_blockers\": sorted(set(preview_blockers)),\n \"preview_fingerprint\": preview_fingerprint,\n }\n\n\n# #endregion build_execution_snapshot\n\n\n# #region build_launch_blockers [C:3] [TYPE Function]\n# @BRIEF Enforce launch gates from findings, approvals, and current preview truth.\n# @PRE: execution_snapshot was computed from current session state.\n# @POST: Returns explicit blocker codes for every unmet launch invariant.\ndef build_launch_blockers(\n session: DatasetReviewSession,\n execution_snapshot: dict[str, Any],\n preview: CompiledPreview | None,\n) -> list[str]:\n session_record = cast(Any, session)\n blockers = list(execution_snapshot[\"preview_blockers\"])\n\n for finding in session_record.findings:\n if (\n finding.severity == FindingSeverity.BLOCKING\n and finding.resolution_state\n not in {ResolutionState.RESOLVED, ResolutionState.APPROVED}\n ):\n blockers.append(f\"finding:{finding.code}:blocking\")\n for mapping in session_record.execution_mappings:\n if (\n mapping.requires_explicit_approval\n and mapping.approval_state != ApprovalState.APPROVED\n ):\n blockers.append(f\"mapping:{mapping.mapping_id}:approval_required\")\n\n if preview is None:\n blockers.append(\"preview:missing\")\n else:\n if preview.preview_status != PreviewStatus.READY:\n blockers.append(f\"preview:{preview.preview_status.value}\")\n if preview.preview_fingerprint != execution_snapshot[\"preview_fingerprint\"]:\n blockers.append(\"preview:fingerprint_mismatch\")\n\n return sorted(set(blockers))\n\n\n# #endregion build_launch_blockers\n\n\n# #region get_latest_preview [C:2] [TYPE Function]\n# @BRIEF Resolve the current latest preview snapshot for one session aggregate.\ndef get_latest_preview(session: DatasetReviewSession) -> CompiledPreview | None:\n session_record = cast(Any, session)\n if not session_record.previews:\n return None\n if session_record.last_preview_id:\n for preview in session_record.previews:\n if preview.preview_id == session_record.last_preview_id:\n return preview\n return sorted(\n session_record.previews,\n key=lambda item: (item.created_at or datetime.min, item.preview_id),\n reverse=True,\n )[0]\n\n\n# #endregion get_latest_preview\n\n\n# #region compute_preview_fingerprint [C:1] [TYPE Function]\n# @BRIEF Produce deterministic execution fingerprint for preview truth and staleness checks.\ndef compute_preview_fingerprint(payload: dict[str, Any]) -> str:\n serialized = json.dumps(payload, sort_keys=True, default=str)\n return hashlib.sha256(serialized.encode(\"utf-8\")).hexdigest()\n\n\n# #endregion compute_preview_fingerprint\n\n\n# #endregion OrchestratorHelpers\n" + }, + { + "contract_id": "parse_dataset_selection", "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator.py", - "start_line": 111, - "end_line": 128, + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 34, + "end_line": 51, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Normalize dataset-selection payload into canonical session references." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region parse_dataset_selection [C:2] [TYPE Function]\n# @BRIEF Normalize dataset-selection payload into canonical session references.\ndef parse_dataset_selection(source_input: str) -> tuple[str, int | None]:\n normalized = str(source_input or \"\").strip()\n if not normalized:\n raise ValueError(\"dataset selection input must be non-empty\")\n if normalized.isdigit():\n dataset_id = int(normalized)\n return f\"dataset:{dataset_id}\", dataset_id\n if normalized.startswith(\"dataset:\"):\n suffix = normalized.split(\":\", 1)[1].strip()\n if suffix.isdigit():\n return normalized, int(suffix)\n return normalized, None\n return normalized, None\n\n\n# #endregion parse_dataset_selection\n" + }, + { + "contract_id": "build_initial_profile", + "contract_type": "Function", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 54, + "end_line": 93, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Create the first profile snapshot so exports and detail views remain usable immediately after intake." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region build_initial_profile [C:2] [TYPE Function]\n# @BRIEF Create the first profile snapshot so exports and detail views remain usable immediately after intake.\ndef build_initial_profile(\n session_id: str,\n parsed_context: Any | None,\n dataset_ref: str,\n) -> DatasetProfile:\n dataset_name = (\n dataset_ref.split(\".\")[-1] if dataset_ref else \"Unresolved dataset\"\n )\n business_summary = (\n f\"Review session initialized for {dataset_ref}.\"\n if dataset_ref\n else \"Review session initialized with unresolved dataset context.\"\n )\n confidence_state = (\n ConfidenceState.MIXED\n if parsed_context and getattr(parsed_context, \"partial_recovery\", False)\n else ConfidenceState.MOSTLY_CONFIRMED\n )\n return DatasetProfile(\n session_id=session_id,\n dataset_name=dataset_name or \"Unresolved dataset\",\n schema_name=dataset_ref.split(\".\")[0] if \".\" in dataset_ref else None,\n business_summary=business_summary,\n business_summary_source=BusinessSummarySource.IMPORTED,\n description=\"Initial review profile created from source intake.\",\n dataset_type=\"unknown\",\n is_sqllab_view=False,\n completeness_score=0.25,\n confidence_state=confidence_state,\n has_blocking_findings=False,\n has_warning_findings=bool(\n parsed_context and getattr(parsed_context, \"partial_recovery\", False)\n ),\n manual_summary_locked=False,\n )\n\n\n# #endregion build_initial_profile\n" + }, + { + "contract_id": "build_partial_recovery_findings", + "contract_type": "Function", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 96, + "end_line": 120, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "POST": "Instance holds collaborator references used by start/preview/launch orchestration methods.", - "PRE": "repository/config_manager are valid collaborators for the current request scope.", - "PURPOSE": "Bind repository, config, and task dependencies required by the orchestration boundary." + "COMPLEXITY": 3, + "POST": "Returns warning-level findings that preserve usable but incomplete state.", + "PRE": "parsed_context.partial_recovery is true.", + "PURPOSE": "Project partial Superset intake recovery into explicit findings without blocking session usability." }, "relations": [], "schema_warnings": [ @@ -84827,61 +62007,212 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:DatasetReviewOrchestrator_init:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.\n # @PRE: repository/config_manager are valid collaborators for the current request scope.\n # @POST: Instance holds collaborator references used by start/preview/launch orchestration methods.\n def __init__(\n self,\n repository: DatasetReviewSessionRepository,\n config_manager: ConfigManager,\n task_manager: Optional[TaskManager] = None,\n semantic_resolver: Optional[SemanticSourceResolver] = None,\n ) -> None:\n self.repository = repository\n self.config_manager = config_manager\n self.task_manager = task_manager\n self.semantic_resolver = semantic_resolver or SemanticSourceResolver()\n\n # [/DEF:DatasetReviewOrchestrator_init:Function]\n" + "anchor_syntax": "region", + "body": "# #region build_partial_recovery_findings [C:3] [TYPE Function]\n# @BRIEF Project partial Superset intake recovery into explicit findings without blocking session usability.\n# @PRE: parsed_context.partial_recovery is true.\n# @POST: Returns warning-level findings that preserve usable but incomplete state.\ndef build_partial_recovery_findings(parsed_context: Any) -> list[ValidationFinding]:\n findings: list[ValidationFinding] = []\n for unresolved_ref in getattr(parsed_context, \"unresolved_references\", []):\n findings.append(\n ValidationFinding(\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"PARTIAL_SUPERSET_RECOVERY\",\n title=\"Superset context recovered partially\",\n message=(\n \"Session remains usable, but some Superset context requires review: \"\n f\"{unresolved_ref.replace('_', ' ')}.\"\n ),\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=unresolved_ref,\n )\n )\n return findings\n\n\n# #endregion build_partial_recovery_findings\n" }, { - "contract_id": "prepare_launch_preview", + "contract_id": "extract_effective_filter_value", "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator.py", - "start_line": 285, - "end_line": 363, + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 123, + "end_line": 136, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Separate normalized filter payload metadata from the user-facing effective filter value." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region extract_effective_filter_value [C:2] [TYPE Function]\n# @BRIEF Separate normalized filter payload metadata from the user-facing effective filter value.\ndef extract_effective_filter_value(\n normalized_value: Any, raw_value: Any\n) -> Any:\n if isinstance(normalized_value, dict) and (\n \"filter_clauses\" in normalized_value\n or \"extra_form_data\" in normalized_value\n ):\n return raw_value\n return normalized_value if normalized_value is not None else raw_value\n\n\n# #endregion extract_effective_filter_value\n" + }, + { + "contract_id": "build_execution_snapshot", + "contract_type": "Function", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 139, + "end_line": 265, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[PreparePreviewCommand] -> Output[PreparePreviewResult]", - "POST": "returns preview artifact in pending, ready, failed, or stale state.", - "PRE": "all required variables have candidate values or explicitly accepted defaults.", - "PURPOSE": "Assemble effective execution inputs and trigger Superset-side preview compilation.", - "SIDE_EFFECT": "persists preview attempt and upstream compilation diagnostics." + "COMPLEXITY": 4, + "POST": "Returns deterministic execution snapshot for current session state without mutating persistence.", + "PRE": "Session aggregate includes imported filters, template variables, and current execution mappings.", + "PURPOSE": "Build effective filters, template params, approvals, and fingerprint for preview and launch gating." }, - "relations": [ - { - "source_id": "prepare_launch_preview", - "relation_type": "CALLS", - "target_id": "SupersetCompilationAdapter.compile_preview", - "target_ref": "[SupersetCompilationAdapter.compile_preview]" - } - ], + "relations": [], "schema_warnings": [ { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", + "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": "def", - "body": " # [DEF:prepare_launch_preview:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.\n # @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview]\n # @PRE: all required variables have candidate values or explicitly accepted defaults.\n # @POST: returns preview artifact in pending, ready, failed, or stale state.\n # @SIDE_EFFECT: persists preview attempt and upstream compilation diagnostics.\n # @DATA_CONTRACT: Input[PreparePreviewCommand] -> Output[PreparePreviewResult]\n def prepare_launch_preview(self, command: PreparePreviewCommand) -> PreparePreviewResult:\n with belief_scope(\"DatasetReviewOrchestrator.prepare_launch_preview\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n log.explore(\"Preview preparation rejected because owned session was not found\", payload={\"session_id\": command.session_id, \"user_id\": command.user.id}, error=\"Session not found or access denied\")\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Preview requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n preview_blockers = execution_snapshot[\"preview_blockers\"]\n if preview_blockers:\n log.explore(\"Preview preparation blocked by incomplete execution context\", payload={\"session_id\": session.session_id, \"blocked_reasons\": preview_blockers}, error=\"Preview blockers present\")\n raise ValueError(\"Preview blocked: \" + \"; \".join(preview_blockers))\n\n adapter = SupersetCompilationAdapter(environment)\n preview = adapter.compile_preview(\n PreviewCompilationPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_fingerprint=execution_snapshot[\"preview_fingerprint\"],\n template_params=execution_snapshot[\"template_params\"],\n effective_filters=execution_snapshot[\"effective_filters\"],\n )\n )\n persisted_preview = self.repository.save_preview(\n session.session_id,\n command.user.id,\n preview,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.PREVIEW\n session.last_activity_at = datetime.utcnow()\n if persisted_preview.preview_status == PreviewStatus.READY:\n launch_blockers = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=persisted_preview)\n if launch_blockers:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.APPROVE_MAPPING\n else:\n session.readiness_state = ReadinessState.RUN_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.PARTIALLY_READY\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_log.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"preview_generated\",\n event_summary=\"Superset preview generation persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value, \"preview_fingerprint\": persisted_preview.preview_fingerprint},\n )\n )\n\n log.reflect(\"Superset preview preparation completed\", payload={\"session_id\": session.session_id, \"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value})\n return PreparePreviewResult(session=session, preview=persisted_preview, blocked_reasons=[])\n\n # [/DEF:prepare_launch_preview:Function]\n" + "anchor_syntax": "region", + "body": "# #region build_execution_snapshot [C:4] [TYPE Function]\n# @BRIEF Build effective filters, template params, approvals, and fingerprint for preview and launch gating.\n# @PRE: Session aggregate includes imported filters, template variables, and current execution mappings.\n# @POST: Returns deterministic execution snapshot for current session state without mutating persistence.\ndef build_execution_snapshot(session: DatasetReviewSession) -> dict[str, Any]:\n session_record = cast(Any, session)\n filter_lookup = {\n item.filter_id: item for item in session_record.imported_filters\n }\n variable_lookup = {\n item.variable_id: item for item in session_record.template_variables\n }\n\n effective_filters: list[dict[str, Any]] = []\n template_params: dict[str, Any] = {}\n approved_mapping_ids: list[str] = []\n open_warning_refs: list[str] = []\n preview_blockers: list[str] = []\n mapped_filter_ids: set[str] = set()\n\n for mapping in session_record.execution_mappings:\n imported_filter = filter_lookup.get(mapping.filter_id)\n template_variable = variable_lookup.get(mapping.variable_id)\n if imported_filter is None:\n preview_blockers.append(f\"mapping:{mapping.mapping_id}:missing_filter\")\n continue\n if template_variable is None:\n preview_blockers.append(f\"mapping:{mapping.mapping_id}:missing_variable\")\n continue\n\n effective_value = mapping.effective_value\n if effective_value is None:\n effective_value = extract_effective_filter_value(\n imported_filter.normalized_value, imported_filter.raw_value,\n )\n if effective_value is None:\n effective_value = template_variable.default_value\n\n if effective_value is None and template_variable.is_required:\n preview_blockers.append(\n f\"variable:{template_variable.variable_name}:missing_required_value\"\n )\n continue\n\n mapped_filter_ids.add(imported_filter.filter_id)\n if effective_value is not None:\n mapped_filter_payload = {\n \"mapping_id\": mapping.mapping_id,\n \"filter_id\": imported_filter.filter_id,\n \"filter_name\": imported_filter.filter_name,\n \"variable_id\": template_variable.variable_id,\n \"variable_name\": template_variable.variable_name,\n \"effective_value\": effective_value,\n \"raw_input_value\": mapping.raw_input_value,\n }\n if isinstance(imported_filter.normalized_value, dict):\n mapped_filter_payload[\"display_name\"] = imported_filter.display_name\n mapped_filter_payload[\"normalized_filter_payload\"] = (\n imported_filter.normalized_value\n )\n effective_filters.append(mapped_filter_payload)\n template_params[template_variable.variable_name] = effective_value\n if mapping.approval_state == ApprovalState.APPROVED:\n approved_mapping_ids.append(mapping.mapping_id)\n if (\n mapping.requires_explicit_approval\n and mapping.approval_state != ApprovalState.APPROVED\n ):\n open_warning_refs.append(mapping.mapping_id)\n\n for imported_filter in session_record.imported_filters:\n if imported_filter.filter_id in mapped_filter_ids:\n continue\n effective_value = extract_effective_filter_value(\n imported_filter.normalized_value, imported_filter.raw_value,\n )\n if effective_value is None:\n continue\n effective_filters.append(\n {\n \"filter_id\": imported_filter.filter_id,\n \"filter_name\": imported_filter.filter_name,\n \"display_name\": imported_filter.display_name,\n \"effective_value\": effective_value,\n \"raw_input_value\": imported_filter.raw_value,\n \"normalized_filter_payload\": imported_filter.normalized_value,\n }\n )\n\n mapped_variable_ids = {\n mapping.variable_id for mapping in session_record.execution_mappings\n }\n for variable in session_record.template_variables:\n if variable.variable_id in mapped_variable_ids:\n continue\n if variable.default_value is not None:\n template_params[variable.variable_name] = variable.default_value\n continue\n if variable.is_required:\n preview_blockers.append(f\"variable:{variable.variable_name}:unmapped\")\n\n semantic_decision_refs = [\n field.field_id\n for field in session.semantic_fields\n if field.is_locked\n or not field.needs_review\n or field.provenance.value != \"unresolved\"\n ]\n preview_fingerprint = compute_preview_fingerprint(\n {\n \"dataset_id\": session_record.dataset_id,\n \"template_params\": template_params,\n \"effective_filters\": effective_filters,\n }\n )\n return {\n \"effective_filters\": effective_filters,\n \"template_params\": template_params,\n \"approved_mapping_ids\": sorted(approved_mapping_ids),\n \"semantic_decision_refs\": sorted(semantic_decision_refs),\n \"open_warning_refs\": sorted(open_warning_refs),\n \"preview_blockers\": sorted(set(preview_blockers)),\n \"preview_fingerprint\": preview_fingerprint,\n }\n\n\n# #endregion build_execution_snapshot\n" }, { - "contract_id": "_build_recovery_bootstrap", + "contract_id": "build_launch_blockers", "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator.py", - "start_line": 465, - "end_line": 563, + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 268, + "end_line": 305, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "POST": "Returns explicit blocker codes for every unmet launch invariant.", + "PRE": "execution_snapshot was computed from current session state.", + "PURPOSE": "Enforce launch gates from findings, approvals, and current preview truth." + }, + "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", + "body": "# #region build_launch_blockers [C:3] [TYPE Function]\n# @BRIEF Enforce launch gates from findings, approvals, and current preview truth.\n# @PRE: execution_snapshot was computed from current session state.\n# @POST: Returns explicit blocker codes for every unmet launch invariant.\ndef build_launch_blockers(\n session: DatasetReviewSession,\n execution_snapshot: dict[str, Any],\n preview: CompiledPreview | None,\n) -> list[str]:\n session_record = cast(Any, session)\n blockers = list(execution_snapshot[\"preview_blockers\"])\n\n for finding in session_record.findings:\n if (\n finding.severity == FindingSeverity.BLOCKING\n and finding.resolution_state\n not in {ResolutionState.RESOLVED, ResolutionState.APPROVED}\n ):\n blockers.append(f\"finding:{finding.code}:blocking\")\n for mapping in session_record.execution_mappings:\n if (\n mapping.requires_explicit_approval\n and mapping.approval_state != ApprovalState.APPROVED\n ):\n blockers.append(f\"mapping:{mapping.mapping_id}:approval_required\")\n\n if preview is None:\n blockers.append(\"preview:missing\")\n else:\n if preview.preview_status != PreviewStatus.READY:\n blockers.append(f\"preview:{preview.preview_status.value}\")\n if preview.preview_fingerprint != execution_snapshot[\"preview_fingerprint\"]:\n blockers.append(\"preview:fingerprint_mismatch\")\n\n return sorted(set(blockers))\n\n\n# #endregion build_launch_blockers\n" + }, + { + "contract_id": "get_latest_preview", + "contract_type": "Function", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 308, + "end_line": 325, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2, + "PURPOSE": "Resolve the current latest preview snapshot for one session aggregate." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region get_latest_preview [C:2] [TYPE Function]\n# @BRIEF Resolve the current latest preview snapshot for one session aggregate.\ndef get_latest_preview(session: DatasetReviewSession) -> CompiledPreview | None:\n session_record = cast(Any, session)\n if not session_record.previews:\n return None\n if session_record.last_preview_id:\n for preview in session_record.previews:\n if preview.preview_id == session_record.last_preview_id:\n return preview\n return sorted(\n session_record.previews,\n key=lambda item: (item.created_at or datetime.min, item.preview_id),\n reverse=True,\n )[0]\n\n\n# #endregion get_latest_preview\n" + }, + { + "contract_id": "compute_preview_fingerprint", + "contract_type": "Function", + "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", + "start_line": 328, + "end_line": 335, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1, + "PURPOSE": "Produce deterministic execution fingerprint for preview truth and staleness checks." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "body": "# #region compute_preview_fingerprint [C:1] [TYPE Function]\n# @BRIEF Produce deterministic execution fingerprint for preview truth and staleness checks.\ndef compute_preview_fingerprint(payload: dict[str, Any]) -> str:\n serialized = json.dumps(payload, sort_keys=True, default=str)\n return hashlib.sha256(serialized.encode(\"utf-8\")).hexdigest()\n\n\n# #endregion compute_preview_fingerprint\n" + }, + { + "contract_id": "SessionRepositoryMutations", + "contract_type": "Module", + "file_path": "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py", + "start_line": 1, + "end_line": 190, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", - "POST": "Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.", - "PRE": "session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.", - "PURPOSE": "Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.", - "SIDE_EFFECT": "Performs Superset reads through the extractor and may append warning findings for incomplete recovery." + "COMPLEXITY": 4, + "LAYER": "Domain", + "POST": "Session aggregate writes preserve ownership and version semantics.", + "PRE": "All mutations execute within authenticated request or task scope.", + "PURPOSE": "Persistence mutation operations for dataset review session aggregates — profile/findings, recovery state, preview, run context." + }, + "relations": [ + { + "source_id": "SessionRepositoryMutations", + "relation_type": "DEPENDS_ON", + "target_id": "DatasetReviewModels", + "target_ref": "[DatasetReviewModels]" + }, + { + "source_id": "SessionRepositoryMutations", + "relation_type": "DEPENDS_ON", + "target_id": "SessionEventLogger", + "target_ref": "[SessionEventLogger]" + } + ], + "schema_warnings": [ + { + "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", + "body": "# #region SessionRepositoryMutations [C:4] [TYPE Module] [SEMANTICS dataset, repository, mutation, session, persistence]\n# @BRIEF Persistence mutation operations for dataset review session aggregates — profile/findings, recovery state, preview, run context.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n# @RELATION DEPENDS_ON -> [SessionEventLogger]\n# @PRE: All mutations execute within authenticated request or task scope.\n# @POST: Session aggregate writes preserve ownership and version semantics.\n\nfrom __future__ import annotations\n\nfrom typing import Any, cast\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n CompiledPreview,\n DatasetProfile,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n ImportedFilter,\n TemplateVariable,\n ValidationFinding,\n)\nfrom src.services.dataset_review.event_logger import SessionEventLogger\n\nlogger = cast(Any, logger)\n\n\n# #region save_profile_and_findings [C:4] [TYPE Function]\n# @BRIEF Persist profile state and replace validation findings for an owned session in one transaction.\n# @PRE: session_id belongs to user_id and the supplied profile/findings belong to the same aggregate scope.\n# @POST: stored profile matches the current session and findings are replaced by the supplied collection.\n# @SIDE_EFFECT: updates profile rows, deletes stale findings, inserts current findings, and commits the transaction.\ndef save_profile_and_findings(\n db: Session,\n event_logger: SessionEventLogger,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n profile: DatasetProfile,\n findings: list[ValidationFinding],\n expected_version: int | None = None,\n) -> DatasetReviewSession:\n with belief_scope(\"save_profile_and_findings\"):\n session = get_owned_session(session_id, user_id)\n if expected_version is not None:\n require_session_version(session, expected_version)\n logger.reason(\"Persisting dataset profile and replacing validation findings\", extra={\"session_id\": session_id, \"user_id\": user_id, \"has_profile\": bool(profile), \"findings_count\": len(findings)})\n\n if profile:\n existing_profile = db.query(DatasetProfile).filter_by(session_id=session_id).first()\n if existing_profile:\n profile.profile_id = existing_profile.profile_id\n db.merge(profile)\n\n db.query(ValidationFinding).filter(ValidationFinding.session_id == session_id).delete()\n for finding in findings:\n cast(Any, finding).session_id = session_id\n db.add(finding)\n\n commit_session_mutation(session, expected_version=expected_version)\n logger.reflect(\"Dataset profile and validation findings committed\", extra={\"session_id\": session.session_id, \"user_id\": user_id, \"findings_count\": len(findings)})\n\n return session\n\n\n# #endregion save_profile_and_findings\n\n\n# #region save_recovery_state [C:4] [TYPE Function]\n# @BRIEF Persist imported filters, template variables, and initial execution mappings for one owned session.\n# @PRE: session_id belongs to user_id.\n# @POST: Recovery state persisted to database.\n# @SIDE_EFFECT: Writes to database.\ndef save_recovery_state(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n load_session_detail_fn,\n session_id: str,\n user_id: str,\n imported_filters: list[ImportedFilter],\n template_variables: list[TemplateVariable],\n execution_mappings: list[ExecutionMapping],\n expected_version: int | None = None,\n) -> DatasetReviewSession:\n with belief_scope(\"save_recovery_state\"):\n session = get_owned_session(session_id, user_id)\n if expected_version is not None:\n require_session_version(session, expected_version)\n logger.reason(\"Persisting dataset review recovery bootstrap state\", extra={\"session_id\": session_id, \"user_id\": user_id, \"imported_filters_count\": len(imported_filters), \"template_variables_count\": len(template_variables), \"execution_mappings_count\": len(execution_mappings)})\n\n db.query(ExecutionMapping).filter(ExecutionMapping.session_id == session_id).delete()\n db.query(TemplateVariable).filter(TemplateVariable.session_id == session_id).delete()\n db.query(ImportedFilter).filter(ImportedFilter.session_id == session_id).delete()\n\n for f in imported_filters:\n cast(Any, f).session_id = session_id\n db.add(f)\n for tv in template_variables:\n cast(Any, tv).session_id = session_id\n db.add(tv)\n db.flush()\n for em in execution_mappings:\n cast(Any, em).session_id = session_id\n db.add(em)\n\n commit_session_mutation(session, expected_version=expected_version)\n logger.reflect(\"Dataset review recovery bootstrap state committed\", extra={\"session_id\": session.session_id, \"user_id\": user_id})\n return load_session_detail_fn(session_id, user_id)\n\n\n# #endregion save_recovery_state\n\n\n# #region save_preview [C:3] [TYPE Function]\n# @BRIEF Persist a preview snapshot and mark prior session previews stale.\n# @PRE: session_id belongs to user_id and preview is prepared for the same session aggregate.\n# @POST: preview is persisted and the session points to the latest preview identifier.\n# @SIDE_EFFECT: updates prior preview statuses, inserts a preview row, mutates the parent session, and commits.\ndef save_preview(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n preview: CompiledPreview,\n expected_version: int | None = None,\n) -> CompiledPreview:\n with belief_scope(\"save_preview\"):\n session = get_owned_session(session_id, user_id)\n session_record = cast(Any, session)\n if expected_version is not None:\n require_session_version(session, expected_version)\n logger.reason(\"Persisting compiled preview and staling previous preview snapshots\", extra={\"session_id\": session_id, \"user_id\": user_id})\n\n db.query(CompiledPreview).filter(CompiledPreview.session_id == session_id).update({\"preview_status\": \"stale\"})\n db.add(preview)\n db.flush()\n session_record.last_preview_id = preview.preview_id\n\n commit_session_mutation(session, refresh_targets=[preview], expected_version=expected_version)\n logger.reflect(\"Compiled preview committed as latest session preview\", extra={\"session_id\": session.session_id, \"preview_id\": preview.preview_id})\n return preview\n\n\n# #endregion save_preview\n\n\n# #region save_run_context [C:3] [TYPE Function]\n# @BRIEF Persist an immutable launch audit snapshot for an owned session.\n# @PRE: session_id belongs to user_id and run_context targets the same aggregate.\n# @POST: run context is persisted and linked as the latest launch snapshot for the session.\n# @SIDE_EFFECT: inserts a run-context row, mutates the parent session pointer, and commits.\ndef save_run_context(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n run_context: DatasetRunContext,\n expected_version: int | None = None,\n) -> DatasetRunContext:\n with belief_scope(\"save_run_context\"):\n session = get_owned_session(session_id, user_id)\n session_record = cast(Any, session)\n if expected_version is not None:\n require_session_version(session, expected_version)\n logger.reason(\"Persisting dataset run context audit snapshot\", extra={\"session_id\": session_id, \"user_id\": user_id})\n\n db.add(run_context)\n db.flush()\n session_record.last_run_context_id = run_context.run_context_id\n\n commit_session_mutation(session, refresh_targets=[run_context], expected_version=expected_version)\n logger.reflect(\"Dataset run context committed as latest launch snapshot\", extra={\"session_id\": session.session_id, \"run_context_id\": run_context.run_context_id})\n return run_context\n\n\n# #endregion save_run_context\n\n\n# #endregion SessionRepositoryMutations\n" + }, + { + "contract_id": "save_profile_and_findings", + "contract_type": "Function", + "file_path": "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py", + "start_line": 31, + "end_line": 71, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "stored profile matches the current session and findings are replaced by the supplied collection.", + "PRE": "session_id belongs to user_id and the supplied profile/findings belong to the same aggregate scope.", + "PURPOSE": "Persist profile state and replace validation findings for an owned session in one transaction.", + "SIDE_EFFECT": "updates profile rows, deletes stale findings, inserts current findings, and commits the transaction." }, "relations": [], "schema_warnings": [ @@ -84895,23 +62226,53 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_build_recovery_bootstrap:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.\n # @PRE: session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.\n # @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.\n # @SIDE_EFFECT: Performs Superset reads through the extractor and may append warning findings for incomplete recovery.\n def _build_recovery_bootstrap(\n self,\n environment,\n session: DatasetReviewSession,\n parsed_context: SupersetParsedContext,\n findings: List[ValidationFinding],\n ) -> tuple[List[ImportedFilter], List[TemplateVariable], List[ExecutionMapping], List[ValidationFinding]]:\n session_record = cast(Any, session)\n extractor = SupersetContextExtractor(environment)\n imported_filters_payload = extractor.recover_imported_filters(parsed_context)\n if imported_filters_payload is None:\n imported_filters_payload = []\n imported_filters = [\n ImportedFilter(\n session_id=session_record.session_id,\n filter_name=str(item.get(\"filter_name\") or f\"imported_filter_{index}\"),\n display_name=item.get(\"display_name\"),\n raw_value=item.get(\"raw_value\"),\n raw_value_masked=bool(item.get(\"raw_value_masked\", False)),\n normalized_value=item.get(\"normalized_value\"),\n source=FilterSource(str(item.get(\"source\") or FilterSource.SUPERSET_URL.value)),\n confidence_state=FilterConfidenceState(str(item.get(\"confidence_state\") or FilterConfidenceState.UNRESOLVED.value)),\n requires_confirmation=bool(item.get(\"requires_confirmation\", False)),\n recovery_status=FilterRecoveryStatus(str(item.get(\"recovery_status\") or FilterRecoveryStatus.PARTIAL.value)),\n notes=item.get(\"notes\"),\n )\n for index, item in enumerate(imported_filters_payload)\n ]\n\n template_variables: List[TemplateVariable] = []\n execution_mappings: List[ExecutionMapping] = []\n\n if session.dataset_id is not None:\n try:\n dataset_payload = parsed_context.dataset_payload\n if not isinstance(dataset_payload, dict):\n dataset_payload = extractor.client.get_dataset_detail(session_record.dataset_id)\n discovered_variables = extractor.discover_template_variables(dataset_payload)\n template_variables = [\n TemplateVariable(\n session_id=session_record.session_id,\n variable_name=str(item.get(\"variable_name\") or f\"variable_{index}\"),\n expression_source=str(item.get(\"expression_source\") or \"\"),\n variable_kind=VariableKind(str(item.get(\"variable_kind\") or VariableKind.UNKNOWN.value)),\n is_required=bool(item.get(\"is_required\", True)),\n default_value=item.get(\"default_value\"),\n mapping_status=MappingStatus(str(item.get(\"mapping_status\") or MappingStatus.UNMAPPED.value)),\n )\n for index, item in enumerate(discovered_variables)\n ]\n except Exception as exc:\n if \"dataset_template_variable_discovery_failed\" not in parsed_context.unresolved_references:\n parsed_context.unresolved_references.append(\"dataset_template_variable_discovery_failed\")\n if not any(f.caused_by_ref == \"dataset_template_variable_discovery_failed\" for f in findings):\n findings.append(\n ValidationFinding(\n area=FindingArea.TEMPLATE_MAPPING,\n severity=FindingSeverity.WARNING,\n code=\"TEMPLATE_VARIABLE_DISCOVERY_FAILED\",\n title=\"Template variables could not be discovered\",\n message=\"Session remains usable, but dataset template variables still need review.\",\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=\"dataset_template_variable_discovery_failed\",\n )\n )\n log.explore(\"Template variable discovery failed during session bootstrap\", payload={\"session_id\": session_record.session_id, \"dataset_id\": session_record.dataset_id}, error=str(exc))\n\n filter_lookup = {str(f.filter_name or \"\").strip().lower(): f for f in imported_filters if str(f.filter_name or \"\").strip()}\n for tv in template_variables:\n matched_filter = filter_lookup.get(str(tv.variable_name or \"\").strip().lower())\n if matched_filter is None:\n continue\n requires_explicit_approval = bool(matched_filter.requires_confirmation or matched_filter.recovery_status != FilterRecoveryStatus.RECOVERED)\n execution_mappings.append(\n ExecutionMapping(\n session_id=session_record.session_id,\n filter_id=matched_filter.filter_id,\n variable_id=tv.variable_id,\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=matched_filter.raw_value,\n effective_value=matched_filter.normalized_value if matched_filter.normalized_value is not None else matched_filter.raw_value,\n transformation_note=\"Bootstrapped from Superset recovery context\",\n warning_level=None,\n requires_explicit_approval=requires_explicit_approval,\n approval_state=ApprovalState.PENDING if requires_explicit_approval else ApprovalState.NOT_REQUIRED,\n approved_by_user_id=None,\n approved_at=None,\n )\n )\n\n return imported_filters, template_variables, execution_mappings, findings\n\n # [/DEF:_build_recovery_bootstrap:Function]\n" + "anchor_syntax": "region", + "body": "# #region save_profile_and_findings [C:4] [TYPE Function]\n# @BRIEF Persist profile state and replace validation findings for an owned session in one transaction.\n# @PRE: session_id belongs to user_id and the supplied profile/findings belong to the same aggregate scope.\n# @POST: stored profile matches the current session and findings are replaced by the supplied collection.\n# @SIDE_EFFECT: updates profile rows, deletes stale findings, inserts current findings, and commits the transaction.\ndef save_profile_and_findings(\n db: Session,\n event_logger: SessionEventLogger,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n profile: DatasetProfile,\n findings: list[ValidationFinding],\n expected_version: int | None = None,\n) -> DatasetReviewSession:\n with belief_scope(\"save_profile_and_findings\"):\n session = get_owned_session(session_id, user_id)\n if expected_version is not None:\n require_session_version(session, expected_version)\n logger.reason(\"Persisting dataset profile and replacing validation findings\", extra={\"session_id\": session_id, \"user_id\": user_id, \"has_profile\": bool(profile), \"findings_count\": len(findings)})\n\n if profile:\n existing_profile = db.query(DatasetProfile).filter_by(session_id=session_id).first()\n if existing_profile:\n profile.profile_id = existing_profile.profile_id\n db.merge(profile)\n\n db.query(ValidationFinding).filter(ValidationFinding.session_id == session_id).delete()\n for finding in findings:\n cast(Any, finding).session_id = session_id\n db.add(finding)\n\n commit_session_mutation(session, expected_version=expected_version)\n logger.reflect(\"Dataset profile and validation findings committed\", extra={\"session_id\": session.session_id, \"user_id\": user_id, \"findings_count\": len(findings)})\n\n return session\n\n\n# #endregion save_profile_and_findings\n" }, { - "contract_id": "_enqueue_recovery_task", + "contract_id": "save_recovery_state", "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator.py", - "start_line": 565, - "end_line": 608, + "file_path": "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py", + "start_line": 74, + "end_line": 118, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4, + "POST": "Recovery state persisted to database.", + "PRE": "session_id belongs to user_id.", + "PURPOSE": "Persist imported filters, template variables, and initial execution mappings for one owned session.", + "SIDE_EFFECT": "Writes to database." + }, + "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", + "body": "# #region save_recovery_state [C:4] [TYPE Function]\n# @BRIEF Persist imported filters, template variables, and initial execution mappings for one owned session.\n# @PRE: session_id belongs to user_id.\n# @POST: Recovery state persisted to database.\n# @SIDE_EFFECT: Writes to database.\ndef save_recovery_state(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n load_session_detail_fn,\n session_id: str,\n user_id: str,\n imported_filters: list[ImportedFilter],\n template_variables: list[TemplateVariable],\n execution_mappings: list[ExecutionMapping],\n expected_version: int | None = None,\n) -> DatasetReviewSession:\n with belief_scope(\"save_recovery_state\"):\n session = get_owned_session(session_id, user_id)\n if expected_version is not None:\n require_session_version(session, expected_version)\n logger.reason(\"Persisting dataset review recovery bootstrap state\", extra={\"session_id\": session_id, \"user_id\": user_id, \"imported_filters_count\": len(imported_filters), \"template_variables_count\": len(template_variables), \"execution_mappings_count\": len(execution_mappings)})\n\n db.query(ExecutionMapping).filter(ExecutionMapping.session_id == session_id).delete()\n db.query(TemplateVariable).filter(TemplateVariable.session_id == session_id).delete()\n db.query(ImportedFilter).filter(ImportedFilter.session_id == session_id).delete()\n\n for f in imported_filters:\n cast(Any, f).session_id = session_id\n db.add(f)\n for tv in template_variables:\n cast(Any, tv).session_id = session_id\n db.add(tv)\n db.flush()\n for em in execution_mappings:\n cast(Any, em).session_id = session_id\n db.add(em)\n\n commit_session_mutation(session, expected_version=expected_version)\n logger.reflect(\"Dataset review recovery bootstrap state committed\", extra={\"session_id\": session.session_id, \"user_id\": user_id})\n return load_session_detail_fn(session_id, user_id)\n\n\n# #endregion save_recovery_state\n" + }, + { + "contract_id": "save_preview", + "contract_type": "Function", + "file_path": "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py", + "start_line": 121, + "end_line": 153, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "POST": "returns task identifier when a task could be enqueued, otherwise None.", - "PRE": "session is already persisted.", - "PURPOSE": "Link session start to observable async recovery when task infrastructure is available.", - "SIDE_EFFECT": "may create one background task for progressive recovery." + "COMPLEXITY": 3, + "POST": "preview is persisted and the session points to the latest preview identifier.", + "PRE": "session_id belongs to user_id and preview is prepared for the same session aggregate.", + "PURPOSE": "Persist a preview snapshot and mark prior session previews stale.", + "SIDE_EFFECT": "updates prior preview statuses, inserts a preview row, mutates the parent session, and commits." }, "relations": [], "schema_warnings": [ @@ -84952,957 +62313,76 @@ } } ], - "anchor_syntax": "def", - "body": " # [DEF:_enqueue_recovery_task:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Link session start to observable async recovery when task infrastructure is available.\n # @PRE: session is already persisted.\n # @POST: returns task identifier when a task could be enqueued, otherwise None.\n # @SIDE_EFFECT: may create one background task for progressive recovery.\n def _enqueue_recovery_task(\n self,\n command: StartSessionCommand,\n session: DatasetReviewSession,\n parsed_context: Optional[SupersetParsedContext],\n ) -> Optional[str]:\n session_record = cast(Any, session)\n if self.task_manager is None:\n log.reason(\"Dataset review session started without task manager; continuing synchronously\", payload={\"session_id\": session_record.session_id})\n return None\n\n task_params: Dict[str, Any] = {\n \"session_id\": session_record.session_id,\n \"user_id\": command.user.id,\n \"environment_id\": session_record.environment_id,\n \"source_kind\": session_record.source_kind,\n \"source_input\": session_record.source_input,\n \"dataset_ref\": session_record.dataset_ref,\n \"dataset_id\": session_record.dataset_id,\n \"dashboard_id\": session_record.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n }\n\n create_task = getattr(self.task_manager, \"create_task\", None)\n if create_task is None:\n log.explore(\"Task manager has no create_task method; skipping recovery enqueue\", error=\"create_task method not found\")\n return None\n\n try:\n task_object = create_task(plugin_id=\"dataset-review-recovery\", params=task_params)\n except TypeError:\n log.explore(\"Recovery task enqueue skipped because task manager create_task contract is incompatible\", payload={\"session_id\": session_record.session_id}, error=\"TypeError from create_task\")\n return None\n\n task_id = getattr(task_object, \"id\", None)\n return str(task_id) if task_id else None\n\n # [/DEF:_enqueue_recovery_task:Function]\n" + "anchor_syntax": "region", + "body": "# #region save_preview [C:3] [TYPE Function]\n# @BRIEF Persist a preview snapshot and mark prior session previews stale.\n# @PRE: session_id belongs to user_id and preview is prepared for the same session aggregate.\n# @POST: preview is persisted and the session points to the latest preview identifier.\n# @SIDE_EFFECT: updates prior preview statuses, inserts a preview row, mutates the parent session, and commits.\ndef save_preview(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n preview: CompiledPreview,\n expected_version: int | None = None,\n) -> CompiledPreview:\n with belief_scope(\"save_preview\"):\n session = get_owned_session(session_id, user_id)\n session_record = cast(Any, session)\n if expected_version is not None:\n require_session_version(session, expected_version)\n logger.reason(\"Persisting compiled preview and staling previous preview snapshots\", extra={\"session_id\": session_id, \"user_id\": user_id})\n\n db.query(CompiledPreview).filter(CompiledPreview.session_id == session_id).update({\"preview_status\": \"stale\"})\n db.add(preview)\n db.flush()\n session_record.last_preview_id = preview.preview_id\n\n commit_session_mutation(session, refresh_targets=[preview], expected_version=expected_version)\n logger.reflect(\"Compiled preview committed as latest session preview\", extra={\"session_id\": session.session_id, \"preview_id\": preview.preview_id})\n return preview\n\n\n# #endregion save_preview\n" }, { - "contract_id": "OrchestratorCommands", - "contract_type": "Module", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", - "start_line": 1, - "end_line": 102, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "LAYER": "Domain", - "PURPOSE": "Typed command and result dataclasses for dataset review orchestration boundary." - }, - "relations": [ - { - "source_id": "OrchestratorCommands", - "relation_type": "DEPENDS_ON", - "target_id": "DatasetReviewModels", - "target_ref": "[DatasetReviewModels]" - }, - { - "source_id": "OrchestratorCommands", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetContextExtractor", - "target_ref": "[SupersetContextExtractor]" - } - ], - "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": "def", - "body": "# [DEF:OrchestratorCommands:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed command and result dataclasses for dataset review orchestration boundary.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewModels]\n# @RELATION: DEPENDS_ON -> [SupersetContextExtractor]\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Any, Dict, List, Optional\n\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n CompiledPreview,\n DatasetReviewSession,\n DatasetRunContext,\n ValidationFinding,\n)\nfrom src.core.utils.superset_context_extractor import SupersetParsedContext\n\n\n# [DEF:StartSessionCommand:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed input contract for starting a dataset review session.\n@dataclass\nclass StartSessionCommand:\n user: User\n environment_id: str\n source_kind: str\n source_input: str\n\n\n# [/DEF:StartSessionCommand:Class]\n\n\n# [DEF:StartSessionResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Session-start result carrying the persisted session and intake recovery metadata.\n@dataclass\nclass StartSessionResult:\n session: DatasetReviewSession\n parsed_context: Optional[SupersetParsedContext] = None\n findings: List[ValidationFinding] = field(default_factory=list)\n\n\n# [/DEF:StartSessionResult:Class]\n\n\n# [DEF:PreparePreviewCommand:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed input contract for compiling one Superset-backed session preview.\n@dataclass\nclass PreparePreviewCommand:\n user: User\n session_id: str\n expected_version: Optional[int] = None\n\n\n# [/DEF:PreparePreviewCommand:Class]\n\n\n# [DEF:PreparePreviewResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Result contract for one persisted compiled preview attempt.\n@dataclass\nclass PreparePreviewResult:\n session: DatasetReviewSession\n preview: CompiledPreview\n blocked_reasons: List[str] = field(default_factory=list)\n\n\n# [/DEF:PreparePreviewResult:Class]\n\n\n# [DEF:LaunchDatasetCommand:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed input contract for launching one dataset-review session into SQL Lab.\n@dataclass\nclass LaunchDatasetCommand:\n user: User\n session_id: str\n expected_version: Optional[int] = None\n\n\n# [/DEF:LaunchDatasetCommand:Class]\n\n\n# [DEF:LaunchDatasetResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Launch result carrying immutable run context and any gate blockers.\n@dataclass\nclass LaunchDatasetResult:\n session: DatasetReviewSession\n run_context: DatasetRunContext\n blocked_reasons: List[str] = field(default_factory=list)\n\n\n# [/DEF:LaunchDatasetResult:Class]\n\n\n# [/DEF:OrchestratorCommands:Module]\n" - }, - { - "contract_id": "StartSessionCommand", - "contract_type": "Class", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", - "start_line": 23, - "end_line": 34, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Typed input contract for starting a dataset review session." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:StartSessionCommand:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed input contract for starting a dataset review session.\n@dataclass\nclass StartSessionCommand:\n user: User\n environment_id: str\n source_kind: str\n source_input: str\n\n\n# [/DEF:StartSessionCommand:Class]\n" - }, - { - "contract_id": "StartSessionResult", - "contract_type": "Class", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", - "start_line": 37, - "end_line": 47, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Session-start result carrying the persisted session and intake recovery metadata." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:StartSessionResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Session-start result carrying the persisted session and intake recovery metadata.\n@dataclass\nclass StartSessionResult:\n session: DatasetReviewSession\n parsed_context: Optional[SupersetParsedContext] = None\n findings: List[ValidationFinding] = field(default_factory=list)\n\n\n# [/DEF:StartSessionResult:Class]\n" - }, - { - "contract_id": "PreparePreviewCommand", - "contract_type": "Class", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", - "start_line": 50, - "end_line": 60, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Typed input contract for compiling one Superset-backed session preview." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:PreparePreviewCommand:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed input contract for compiling one Superset-backed session preview.\n@dataclass\nclass PreparePreviewCommand:\n user: User\n session_id: str\n expected_version: Optional[int] = None\n\n\n# [/DEF:PreparePreviewCommand:Class]\n" - }, - { - "contract_id": "PreparePreviewResult", - "contract_type": "Class", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", - "start_line": 63, - "end_line": 73, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Result contract for one persisted compiled preview attempt." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:PreparePreviewResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Result contract for one persisted compiled preview attempt.\n@dataclass\nclass PreparePreviewResult:\n session: DatasetReviewSession\n preview: CompiledPreview\n blocked_reasons: List[str] = field(default_factory=list)\n\n\n# [/DEF:PreparePreviewResult:Class]\n" - }, - { - "contract_id": "LaunchDatasetCommand", - "contract_type": "Class", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", - "start_line": 76, - "end_line": 86, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Typed input contract for launching one dataset-review session into SQL Lab." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:LaunchDatasetCommand:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Typed input contract for launching one dataset-review session into SQL Lab.\n@dataclass\nclass LaunchDatasetCommand:\n user: User\n session_id: str\n expected_version: Optional[int] = None\n\n\n# [/DEF:LaunchDatasetCommand:Class]\n" - }, - { - "contract_id": "LaunchDatasetResult", - "contract_type": "Class", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_commands.py", - "start_line": 89, - "end_line": 99, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Launch result carrying immutable run context and any gate blockers." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:LaunchDatasetResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Launch result carrying immutable run context and any gate blockers.\n@dataclass\nclass LaunchDatasetResult:\n session: DatasetReviewSession\n run_context: DatasetRunContext\n blocked_reasons: List[str] = field(default_factory=list)\n\n\n# [/DEF:LaunchDatasetResult:Class]\n" - }, - { - "contract_id": "OrchestratorHelpers", - "contract_type": "Module", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 1, - "end_line": 355, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "LAYER": "Domain", - "POST": "Helper results are deterministic and do not mutate persistence directly.", - "PRE": "Caller provides a loaded session aggregate with hydrated child collections.", - "PURPOSE": "Pure helper methods extracted from DatasetReviewOrchestrator for INV_7 compliance — snapshot, blockers, fingerprint, recovery bootstrap." - }, - "relations": [ - { - "source_id": "OrchestratorHelpers", - "relation_type": "DEPENDS_ON", - "target_id": "DatasetReviewModels", - "target_ref": "[DatasetReviewModels]" - }, - { - "source_id": "OrchestratorHelpers", - "relation_type": "DEPENDS_ON", - "target_id": "SupersetContextExtractor", - "target_ref": "[SupersetContextExtractor]" - } - ], - "schema_warnings": [ - { - "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": "def", - "body": "# [DEF:OrchestratorHelpers:Module]\n# @COMPLEXITY: 4\n# @PURPOSE: Pure helper methods extracted from DatasetReviewOrchestrator for INV_7 compliance — snapshot, blockers, fingerprint, recovery bootstrap.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewModels]\n# @RELATION: DEPENDS_ON -> [SupersetContextExtractor]\n# @PRE: Caller provides a loaded session aggregate with hydrated child collections.\n# @POST: Helper results are deterministic and do not mutate persistence directly.\n\nfrom __future__ import annotations\n\nimport hashlib\nimport json\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, cast\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.models.dataset_review import (\n ApprovalState,\n CompiledPreview,\n ConfidenceState,\n DatasetProfile,\n DatasetReviewSession,\n ExecutionMapping,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n ImportedFilter,\n MappingMethod,\n MappingStatus,\n PreviewStatus,\n ResolutionState,\n TemplateVariable,\n ValidationFinding,\n VariableKind,\n BusinessSummarySource,\n)\n\nlog = MarkerLogger(\"OrchestratorHelpers\")\n\n# [DEF:parse_dataset_selection:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize dataset-selection payload into canonical session references.\ndef parse_dataset_selection(source_input: str) -> tuple[str, Optional[int]]:\n normalized = str(source_input or \"\").strip()\n if not normalized:\n raise ValueError(\"dataset selection input must be non-empty\")\n if normalized.isdigit():\n dataset_id = int(normalized)\n return f\"dataset:{dataset_id}\", dataset_id\n if normalized.startswith(\"dataset:\"):\n suffix = normalized.split(\":\", 1)[1].strip()\n if suffix.isdigit():\n return normalized, int(suffix)\n return normalized, None\n return normalized, None\n\n\n# [/DEF:parse_dataset_selection:Function]\n\n\n# [DEF:build_initial_profile:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create the first profile snapshot so exports and detail views remain usable immediately after intake.\ndef build_initial_profile(\n session_id: str,\n parsed_context: Optional[Any],\n dataset_ref: str,\n) -> DatasetProfile:\n dataset_name = (\n dataset_ref.split(\".\")[-1] if dataset_ref else \"Unresolved dataset\"\n )\n business_summary = (\n f\"Review session initialized for {dataset_ref}.\"\n if dataset_ref\n else \"Review session initialized with unresolved dataset context.\"\n )\n confidence_state = (\n ConfidenceState.MIXED\n if parsed_context and getattr(parsed_context, \"partial_recovery\", False)\n else ConfidenceState.MOSTLY_CONFIRMED\n )\n return DatasetProfile(\n session_id=session_id,\n dataset_name=dataset_name or \"Unresolved dataset\",\n schema_name=dataset_ref.split(\".\")[0] if \".\" in dataset_ref else None,\n business_summary=business_summary,\n business_summary_source=BusinessSummarySource.IMPORTED,\n description=\"Initial review profile created from source intake.\",\n dataset_type=\"unknown\",\n is_sqllab_view=False,\n completeness_score=0.25,\n confidence_state=confidence_state,\n has_blocking_findings=False,\n has_warning_findings=bool(\n parsed_context and getattr(parsed_context, \"partial_recovery\", False)\n ),\n manual_summary_locked=False,\n )\n\n\n# [/DEF:build_initial_profile:Function]\n\n\n# [DEF:build_partial_recovery_findings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Project partial Superset intake recovery into explicit findings without blocking session usability.\n# @PRE: parsed_context.partial_recovery is true.\n# @POST: Returns warning-level findings that preserve usable but incomplete state.\ndef build_partial_recovery_findings(parsed_context: Any) -> List[ValidationFinding]:\n findings: List[ValidationFinding] = []\n for unresolved_ref in getattr(parsed_context, \"unresolved_references\", []):\n findings.append(\n ValidationFinding(\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"PARTIAL_SUPERSET_RECOVERY\",\n title=\"Superset context recovered partially\",\n message=(\n \"Session remains usable, but some Superset context requires review: \"\n f\"{unresolved_ref.replace('_', ' ')}.\"\n ),\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=unresolved_ref,\n )\n )\n return findings\n\n\n# [/DEF:build_partial_recovery_findings:Function]\n\n\n# [DEF:extract_effective_filter_value:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Separate normalized filter payload metadata from the user-facing effective filter value.\ndef extract_effective_filter_value(\n normalized_value: Any, raw_value: Any\n) -> Any:\n if isinstance(normalized_value, dict) and (\n \"filter_clauses\" in normalized_value\n or \"extra_form_data\" in normalized_value\n ):\n return raw_value\n return normalized_value if normalized_value is not None else raw_value\n\n\n# [/DEF:extract_effective_filter_value:Function]\n\n\n# [DEF:build_execution_snapshot:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Build effective filters, template params, approvals, and fingerprint for preview and launch gating.\n# @PRE: Session aggregate includes imported filters, template variables, and current execution mappings.\n# @POST: Returns deterministic execution snapshot for current session state without mutating persistence.\ndef build_execution_snapshot(session: DatasetReviewSession) -> Dict[str, Any]:\n session_record = cast(Any, session)\n filter_lookup = {\n item.filter_id: item for item in session_record.imported_filters\n }\n variable_lookup = {\n item.variable_id: item for item in session_record.template_variables\n }\n\n effective_filters: List[Dict[str, Any]] = []\n template_params: Dict[str, Any] = {}\n approved_mapping_ids: List[str] = []\n open_warning_refs: List[str] = []\n preview_blockers: List[str] = []\n mapped_filter_ids: set[str] = set()\n\n for mapping in session_record.execution_mappings:\n imported_filter = filter_lookup.get(mapping.filter_id)\n template_variable = variable_lookup.get(mapping.variable_id)\n if imported_filter is None:\n preview_blockers.append(f\"mapping:{mapping.mapping_id}:missing_filter\")\n continue\n if template_variable is None:\n preview_blockers.append(f\"mapping:{mapping.mapping_id}:missing_variable\")\n continue\n\n effective_value = mapping.effective_value\n if effective_value is None:\n effective_value = extract_effective_filter_value(\n imported_filter.normalized_value, imported_filter.raw_value,\n )\n if effective_value is None:\n effective_value = template_variable.default_value\n\n if effective_value is None and template_variable.is_required:\n preview_blockers.append(\n f\"variable:{template_variable.variable_name}:missing_required_value\"\n )\n continue\n\n mapped_filter_ids.add(imported_filter.filter_id)\n if effective_value is not None:\n mapped_filter_payload = {\n \"mapping_id\": mapping.mapping_id,\n \"filter_id\": imported_filter.filter_id,\n \"filter_name\": imported_filter.filter_name,\n \"variable_id\": template_variable.variable_id,\n \"variable_name\": template_variable.variable_name,\n \"effective_value\": effective_value,\n \"raw_input_value\": mapping.raw_input_value,\n }\n if isinstance(imported_filter.normalized_value, dict):\n mapped_filter_payload[\"display_name\"] = imported_filter.display_name\n mapped_filter_payload[\"normalized_filter_payload\"] = (\n imported_filter.normalized_value\n )\n effective_filters.append(mapped_filter_payload)\n template_params[template_variable.variable_name] = effective_value\n if mapping.approval_state == ApprovalState.APPROVED:\n approved_mapping_ids.append(mapping.mapping_id)\n if (\n mapping.requires_explicit_approval\n and mapping.approval_state != ApprovalState.APPROVED\n ):\n open_warning_refs.append(mapping.mapping_id)\n\n for imported_filter in session_record.imported_filters:\n if imported_filter.filter_id in mapped_filter_ids:\n continue\n effective_value = extract_effective_filter_value(\n imported_filter.normalized_value, imported_filter.raw_value,\n )\n if effective_value is None:\n continue\n effective_filters.append(\n {\n \"filter_id\": imported_filter.filter_id,\n \"filter_name\": imported_filter.filter_name,\n \"display_name\": imported_filter.display_name,\n \"effective_value\": effective_value,\n \"raw_input_value\": imported_filter.raw_value,\n \"normalized_filter_payload\": imported_filter.normalized_value,\n }\n )\n\n mapped_variable_ids = {\n mapping.variable_id for mapping in session_record.execution_mappings\n }\n for variable in session_record.template_variables:\n if variable.variable_id in mapped_variable_ids:\n continue\n if variable.default_value is not None:\n template_params[variable.variable_name] = variable.default_value\n continue\n if variable.is_required:\n preview_blockers.append(f\"variable:{variable.variable_name}:unmapped\")\n\n semantic_decision_refs = [\n field.field_id\n for field in session.semantic_fields\n if field.is_locked\n or not field.needs_review\n or field.provenance.value != \"unresolved\"\n ]\n preview_fingerprint = compute_preview_fingerprint(\n {\n \"dataset_id\": session_record.dataset_id,\n \"template_params\": template_params,\n \"effective_filters\": effective_filters,\n }\n )\n return {\n \"effective_filters\": effective_filters,\n \"template_params\": template_params,\n \"approved_mapping_ids\": sorted(approved_mapping_ids),\n \"semantic_decision_refs\": sorted(semantic_decision_refs),\n \"open_warning_refs\": sorted(open_warning_refs),\n \"preview_blockers\": sorted(set(preview_blockers)),\n \"preview_fingerprint\": preview_fingerprint,\n }\n\n\n# [/DEF:build_execution_snapshot:Function]\n\n\n# [DEF:build_launch_blockers:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Enforce launch gates from findings, approvals, and current preview truth.\n# @PRE: execution_snapshot was computed from current session state.\n# @POST: Returns explicit blocker codes for every unmet launch invariant.\ndef build_launch_blockers(\n session: DatasetReviewSession,\n execution_snapshot: Dict[str, Any],\n preview: Optional[CompiledPreview],\n) -> List[str]:\n session_record = cast(Any, session)\n blockers = list(execution_snapshot[\"preview_blockers\"])\n\n for finding in session_record.findings:\n if (\n finding.severity == FindingSeverity.BLOCKING\n and finding.resolution_state\n not in {ResolutionState.RESOLVED, ResolutionState.APPROVED}\n ):\n blockers.append(f\"finding:{finding.code}:blocking\")\n for mapping in session_record.execution_mappings:\n if (\n mapping.requires_explicit_approval\n and mapping.approval_state != ApprovalState.APPROVED\n ):\n blockers.append(f\"mapping:{mapping.mapping_id}:approval_required\")\n\n if preview is None:\n blockers.append(\"preview:missing\")\n else:\n if preview.preview_status != PreviewStatus.READY:\n blockers.append(f\"preview:{preview.preview_status.value}\")\n if preview.preview_fingerprint != execution_snapshot[\"preview_fingerprint\"]:\n blockers.append(\"preview:fingerprint_mismatch\")\n\n return sorted(set(blockers))\n\n\n# [/DEF:build_launch_blockers:Function]\n\n\n# [DEF:get_latest_preview:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve the current latest preview snapshot for one session aggregate.\ndef get_latest_preview(session: DatasetReviewSession) -> Optional[CompiledPreview]:\n session_record = cast(Any, session)\n if not session_record.previews:\n return None\n if session_record.last_preview_id:\n for preview in session_record.previews:\n if preview.preview_id == session_record.last_preview_id:\n return preview\n return sorted(\n session_record.previews,\n key=lambda item: (item.created_at or datetime.min, item.preview_id),\n reverse=True,\n )[0]\n\n\n# [/DEF:get_latest_preview:Function]\n\n\n# [DEF:compute_preview_fingerprint:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Produce deterministic execution fingerprint for preview truth and staleness checks.\ndef compute_preview_fingerprint(payload: Dict[str, Any]) -> str:\n serialized = json.dumps(payload, sort_keys=True, default=str)\n return hashlib.sha256(serialized.encode(\"utf-8\")).hexdigest()\n\n\n# [/DEF:compute_preview_fingerprint:Function]\n\n\n# [/DEF:OrchestratorHelpers:Module]\n" - }, - { - "contract_id": "parse_dataset_selection", + "contract_id": "save_run_context", "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 43, - "end_line": 61, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Normalize dataset-selection payload into canonical session references." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:parse_dataset_selection:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Normalize dataset-selection payload into canonical session references.\ndef parse_dataset_selection(source_input: str) -> tuple[str, Optional[int]]:\n normalized = str(source_input or \"\").strip()\n if not normalized:\n raise ValueError(\"dataset selection input must be non-empty\")\n if normalized.isdigit():\n dataset_id = int(normalized)\n return f\"dataset:{dataset_id}\", dataset_id\n if normalized.startswith(\"dataset:\"):\n suffix = normalized.split(\":\", 1)[1].strip()\n if suffix.isdigit():\n return normalized, int(suffix)\n return normalized, None\n return normalized, None\n\n\n# [/DEF:parse_dataset_selection:Function]\n" - }, - { - "contract_id": "build_initial_profile", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 64, - "end_line": 104, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Create the first profile snapshot so exports and detail views remain usable immediately after intake." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:build_initial_profile:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Create the first profile snapshot so exports and detail views remain usable immediately after intake.\ndef build_initial_profile(\n session_id: str,\n parsed_context: Optional[Any],\n dataset_ref: str,\n) -> DatasetProfile:\n dataset_name = (\n dataset_ref.split(\".\")[-1] if dataset_ref else \"Unresolved dataset\"\n )\n business_summary = (\n f\"Review session initialized for {dataset_ref}.\"\n if dataset_ref\n else \"Review session initialized with unresolved dataset context.\"\n )\n confidence_state = (\n ConfidenceState.MIXED\n if parsed_context and getattr(parsed_context, \"partial_recovery\", False)\n else ConfidenceState.MOSTLY_CONFIRMED\n )\n return DatasetProfile(\n session_id=session_id,\n dataset_name=dataset_name or \"Unresolved dataset\",\n schema_name=dataset_ref.split(\".\")[0] if \".\" in dataset_ref else None,\n business_summary=business_summary,\n business_summary_source=BusinessSummarySource.IMPORTED,\n description=\"Initial review profile created from source intake.\",\n dataset_type=\"unknown\",\n is_sqllab_view=False,\n completeness_score=0.25,\n confidence_state=confidence_state,\n has_blocking_findings=False,\n has_warning_findings=bool(\n parsed_context and getattr(parsed_context, \"partial_recovery\", False)\n ),\n manual_summary_locked=False,\n )\n\n\n# [/DEF:build_initial_profile:Function]\n" - }, - { - "contract_id": "build_partial_recovery_findings", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 107, - "end_line": 132, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns warning-level findings that preserve usable but incomplete state.", - "PRE": "parsed_context.partial_recovery is true.", - "PURPOSE": "Project partial Superset intake recovery into explicit findings without blocking session usability." - }, - "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": "def", - "body": "# [DEF:build_partial_recovery_findings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Project partial Superset intake recovery into explicit findings without blocking session usability.\n# @PRE: parsed_context.partial_recovery is true.\n# @POST: Returns warning-level findings that preserve usable but incomplete state.\ndef build_partial_recovery_findings(parsed_context: Any) -> List[ValidationFinding]:\n findings: List[ValidationFinding] = []\n for unresolved_ref in getattr(parsed_context, \"unresolved_references\", []):\n findings.append(\n ValidationFinding(\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"PARTIAL_SUPERSET_RECOVERY\",\n title=\"Superset context recovered partially\",\n message=(\n \"Session remains usable, but some Superset context requires review: \"\n f\"{unresolved_ref.replace('_', ' ')}.\"\n ),\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=unresolved_ref,\n )\n )\n return findings\n\n\n# [/DEF:build_partial_recovery_findings:Function]\n" - }, - { - "contract_id": "extract_effective_filter_value", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 135, - "end_line": 149, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Separate normalized filter payload metadata from the user-facing effective filter value." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:extract_effective_filter_value:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Separate normalized filter payload metadata from the user-facing effective filter value.\ndef extract_effective_filter_value(\n normalized_value: Any, raw_value: Any\n) -> Any:\n if isinstance(normalized_value, dict) and (\n \"filter_clauses\" in normalized_value\n or \"extra_form_data\" in normalized_value\n ):\n return raw_value\n return normalized_value if normalized_value is not None else raw_value\n\n\n# [/DEF:extract_effective_filter_value:Function]\n" - }, - { - "contract_id": "build_execution_snapshot", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 152, - "end_line": 279, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "POST": "Returns deterministic execution snapshot for current session state without mutating persistence.", - "PRE": "Session aggregate includes imported filters, template variables, and current execution mappings.", - "PURPOSE": "Build effective filters, template params, approvals, and fingerprint for preview and launch gating." - }, - "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": "def", - "body": "# [DEF:build_execution_snapshot:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Build effective filters, template params, approvals, and fingerprint for preview and launch gating.\n# @PRE: Session aggregate includes imported filters, template variables, and current execution mappings.\n# @POST: Returns deterministic execution snapshot for current session state without mutating persistence.\ndef build_execution_snapshot(session: DatasetReviewSession) -> Dict[str, Any]:\n session_record = cast(Any, session)\n filter_lookup = {\n item.filter_id: item for item in session_record.imported_filters\n }\n variable_lookup = {\n item.variable_id: item for item in session_record.template_variables\n }\n\n effective_filters: List[Dict[str, Any]] = []\n template_params: Dict[str, Any] = {}\n approved_mapping_ids: List[str] = []\n open_warning_refs: List[str] = []\n preview_blockers: List[str] = []\n mapped_filter_ids: set[str] = set()\n\n for mapping in session_record.execution_mappings:\n imported_filter = filter_lookup.get(mapping.filter_id)\n template_variable = variable_lookup.get(mapping.variable_id)\n if imported_filter is None:\n preview_blockers.append(f\"mapping:{mapping.mapping_id}:missing_filter\")\n continue\n if template_variable is None:\n preview_blockers.append(f\"mapping:{mapping.mapping_id}:missing_variable\")\n continue\n\n effective_value = mapping.effective_value\n if effective_value is None:\n effective_value = extract_effective_filter_value(\n imported_filter.normalized_value, imported_filter.raw_value,\n )\n if effective_value is None:\n effective_value = template_variable.default_value\n\n if effective_value is None and template_variable.is_required:\n preview_blockers.append(\n f\"variable:{template_variable.variable_name}:missing_required_value\"\n )\n continue\n\n mapped_filter_ids.add(imported_filter.filter_id)\n if effective_value is not None:\n mapped_filter_payload = {\n \"mapping_id\": mapping.mapping_id,\n \"filter_id\": imported_filter.filter_id,\n \"filter_name\": imported_filter.filter_name,\n \"variable_id\": template_variable.variable_id,\n \"variable_name\": template_variable.variable_name,\n \"effective_value\": effective_value,\n \"raw_input_value\": mapping.raw_input_value,\n }\n if isinstance(imported_filter.normalized_value, dict):\n mapped_filter_payload[\"display_name\"] = imported_filter.display_name\n mapped_filter_payload[\"normalized_filter_payload\"] = (\n imported_filter.normalized_value\n )\n effective_filters.append(mapped_filter_payload)\n template_params[template_variable.variable_name] = effective_value\n if mapping.approval_state == ApprovalState.APPROVED:\n approved_mapping_ids.append(mapping.mapping_id)\n if (\n mapping.requires_explicit_approval\n and mapping.approval_state != ApprovalState.APPROVED\n ):\n open_warning_refs.append(mapping.mapping_id)\n\n for imported_filter in session_record.imported_filters:\n if imported_filter.filter_id in mapped_filter_ids:\n continue\n effective_value = extract_effective_filter_value(\n imported_filter.normalized_value, imported_filter.raw_value,\n )\n if effective_value is None:\n continue\n effective_filters.append(\n {\n \"filter_id\": imported_filter.filter_id,\n \"filter_name\": imported_filter.filter_name,\n \"display_name\": imported_filter.display_name,\n \"effective_value\": effective_value,\n \"raw_input_value\": imported_filter.raw_value,\n \"normalized_filter_payload\": imported_filter.normalized_value,\n }\n )\n\n mapped_variable_ids = {\n mapping.variable_id for mapping in session_record.execution_mappings\n }\n for variable in session_record.template_variables:\n if variable.variable_id in mapped_variable_ids:\n continue\n if variable.default_value is not None:\n template_params[variable.variable_name] = variable.default_value\n continue\n if variable.is_required:\n preview_blockers.append(f\"variable:{variable.variable_name}:unmapped\")\n\n semantic_decision_refs = [\n field.field_id\n for field in session.semantic_fields\n if field.is_locked\n or not field.needs_review\n or field.provenance.value != \"unresolved\"\n ]\n preview_fingerprint = compute_preview_fingerprint(\n {\n \"dataset_id\": session_record.dataset_id,\n \"template_params\": template_params,\n \"effective_filters\": effective_filters,\n }\n )\n return {\n \"effective_filters\": effective_filters,\n \"template_params\": template_params,\n \"approved_mapping_ids\": sorted(approved_mapping_ids),\n \"semantic_decision_refs\": sorted(semantic_decision_refs),\n \"open_warning_refs\": sorted(open_warning_refs),\n \"preview_blockers\": sorted(set(preview_blockers)),\n \"preview_fingerprint\": preview_fingerprint,\n }\n\n\n# [/DEF:build_execution_snapshot:Function]\n" - }, - { - "contract_id": "build_launch_blockers", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 282, - "end_line": 320, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns explicit blocker codes for every unmet launch invariant.", - "PRE": "execution_snapshot was computed from current session state.", - "PURPOSE": "Enforce launch gates from findings, approvals, and current preview truth." - }, - "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": "def", - "body": "# [DEF:build_launch_blockers:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Enforce launch gates from findings, approvals, and current preview truth.\n# @PRE: execution_snapshot was computed from current session state.\n# @POST: Returns explicit blocker codes for every unmet launch invariant.\ndef build_launch_blockers(\n session: DatasetReviewSession,\n execution_snapshot: Dict[str, Any],\n preview: Optional[CompiledPreview],\n) -> List[str]:\n session_record = cast(Any, session)\n blockers = list(execution_snapshot[\"preview_blockers\"])\n\n for finding in session_record.findings:\n if (\n finding.severity == FindingSeverity.BLOCKING\n and finding.resolution_state\n not in {ResolutionState.RESOLVED, ResolutionState.APPROVED}\n ):\n blockers.append(f\"finding:{finding.code}:blocking\")\n for mapping in session_record.execution_mappings:\n if (\n mapping.requires_explicit_approval\n and mapping.approval_state != ApprovalState.APPROVED\n ):\n blockers.append(f\"mapping:{mapping.mapping_id}:approval_required\")\n\n if preview is None:\n blockers.append(\"preview:missing\")\n else:\n if preview.preview_status != PreviewStatus.READY:\n blockers.append(f\"preview:{preview.preview_status.value}\")\n if preview.preview_fingerprint != execution_snapshot[\"preview_fingerprint\"]:\n blockers.append(\"preview:fingerprint_mismatch\")\n\n return sorted(set(blockers))\n\n\n# [/DEF:build_launch_blockers:Function]\n" - }, - { - "contract_id": "get_latest_preview", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 323, - "end_line": 341, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Resolve the current latest preview snapshot for one session aggregate." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:get_latest_preview:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Resolve the current latest preview snapshot for one session aggregate.\ndef get_latest_preview(session: DatasetReviewSession) -> Optional[CompiledPreview]:\n session_record = cast(Any, session)\n if not session_record.previews:\n return None\n if session_record.last_preview_id:\n for preview in session_record.previews:\n if preview.preview_id == session_record.last_preview_id:\n return preview\n return sorted(\n session_record.previews,\n key=lambda item: (item.created_at or datetime.min, item.preview_id),\n reverse=True,\n )[0]\n\n\n# [/DEF:get_latest_preview:Function]\n" - }, - { - "contract_id": "compute_preview_fingerprint", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py", - "start_line": 344, - "end_line": 352, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Produce deterministic execution fingerprint for preview truth and staleness checks." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:compute_preview_fingerprint:Function]\n# @COMPLEXITY: 1\n# @PURPOSE: Produce deterministic execution fingerprint for preview truth and staleness checks.\ndef compute_preview_fingerprint(payload: Dict[str, Any]) -> str:\n serialized = json.dumps(payload, sort_keys=True, default=str)\n return hashlib.sha256(serialized.encode(\"utf-8\")).hexdigest()\n\n\n# [/DEF:compute_preview_fingerprint:Function]\n" - }, - { - "contract_id": "SessionRepositoryTests", - "contract_type": "Module", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 29, - "end_line": 718, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Unit tests for DatasetReviewSessionRepository." - }, - "relations": [ - { - "source_id": "SessionRepositoryTests", - "relation_type": "BELONGS_TO", - "target_id": "SrcRoot", - "target_ref": "SrcRoot" - } - ], - "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" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate BELONGS_TO is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "BELONGS_TO" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:SessionRepositoryTests:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 2\n# @PURPOSE: Unit tests for DatasetReviewSessionRepository.\n\n\n@pytest.fixture\ndef db_session():\n # [DEF:db_session:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows.\n # @RELATION: BINDS_TO -> [SessionRepositoryTests]\n engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n # Create test data\n user = User(\n id=\"user1\", username=\"testuser\", email=\"test@example.com\", password_hash=\"pw\"\n )\n env = Environment(\n id=\"env1\", name=\"Prod\", url=\"http://superset\", credentials_id=\"cred1\"\n )\n session.add_all([user, env])\n session.commit()\n\n yield session\n session.close()\n\n\n# [/DEF:db_session:Function]\n\n\n# [DEF:test_create_session:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_create_session(db_session):\n # @PURPOSE: Verify session creation and persistence.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n assert session.session_id is not None\n loaded = (\n db_session.query(DatasetReviewSession)\n .filter_by(session_id=session.session_id)\n .first()\n )\n assert loaded.user_id == \"user1\"\n assert loaded.version == 0\n\n\n# [/DEF:test_create_session:Function]\n\n\n# [DEF:test_require_session_version_conflict:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify optimistic-lock conflict is raised when caller version is stale.\ndef test_require_session_version_conflict(db_session):\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n version=2,\n )\n repo.create_session(session)\n\n with pytest.raises(DatasetReviewSessionVersionConflictError) as exc_info:\n repo.require_session_version(session, 1)\n\n assert exc_info.value.expected_version == 1\n assert exc_info.value.actual_version == 2\n\n\n# [/DEF:test_require_session_version_conflict:Function]\n\n\n# [DEF:test_bump_session_version_updates_last_activity:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify repository version bump increments monotonically and refreshes last activity.\ndef test_bump_session_version_updates_last_activity(db_session):\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n before_activity = session.last_activity_at\n next_version = repo.bump_session_version(session)\n\n assert next_version == 1\n assert session.version == 1\n assert session.last_activity_at >= before_activity\n\n\n# [/DEF:test_bump_session_version_updates_last_activity:Function]\n\n\n# [DEF:test_save_recovery_state_preserves_raw_value_masked_flag:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify imported-filter masking metadata persists with recovery bootstrap state.\ndef test_save_recovery_state_preserves_raw_value_masked_flag(db_session):\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n imported_filter = ImportedFilter(\n session_id=session.session_id,\n filter_name=\"email\",\n raw_value=\"***@example.com\",\n raw_value_masked=True,\n normalized_value=None,\n source=\"manual\",\n confidence_state=\"confirmed\",\n requires_confirmation=False,\n recovery_status=\"recovered\",\n )\n\n updated = repo.save_recovery_state(\n session.session_id,\n \"user1\",\n [imported_filter],\n [],\n [],\n )\n\n assert updated.imported_filters[0].raw_value_masked is True\n assert updated.version == 1\n\n\n# [/DEF:test_save_recovery_state_preserves_raw_value_masked_flag:Function]\n\n\n# [DEF:test_load_session_detail_ownership:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_load_session_detail_ownership(db_session):\n # @PURPOSE: Verify ownership enforcement in detail loading.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n # Correct user\n loaded = repo.load_session_detail(session.session_id, \"user1\")\n assert loaded is not None\n\n # Wrong user\n loaded_wrong = repo.load_session_detail(session.session_id, \"wrong_user\")\n assert loaded_wrong is None\n\n\n# [/DEF:test_load_session_detail_ownership:Function]\n\n\n# [DEF:test_load_session_detail_collaborator:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_load_session_detail_collaborator(db_session):\n # @PURPOSE: Verify collaborator access in detail loading.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n # Add collaborator\n collab_user = User(\n id=\"collab1\", username=\"collab\", email=\"c@e.com\", password_hash=\"p\"\n )\n db_session.add(collab_user)\n\n collaborator = SessionCollaborator(\n session_id=session.session_id,\n user_id=\"collab1\",\n role=SessionCollaboratorRole.REVIEWER,\n )\n db_session.add(collaborator)\n db_session.commit()\n\n # Collaborator access\n loaded = repo.load_session_detail(session.session_id, \"collab1\")\n assert loaded is not None\n assert loaded.session_id == session.session_id\n\n\n# [/DEF:test_load_session_detail_collaborator:Function]\n\n\n# [DEF:test_save_preview_marks_stale:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_save_preview_marks_stale(db_session):\n # @PURPOSE: Verify that saving a new preview marks old ones as stale.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n p1 = CompiledPreview(\n session_id=session.session_id, preview_status=\"ready\", preview_fingerprint=\"f1\"\n )\n repo.save_preview(session.session_id, \"user1\", p1)\n\n p2 = CompiledPreview(\n session_id=session.session_id, preview_status=\"ready\", preview_fingerprint=\"f2\"\n )\n repo.save_preview(session.session_id, \"user1\", p2)\n\n db_session.refresh(p1)\n assert p1.preview_status == \"stale\"\n assert p2.preview_status == \"ready\"\n assert session.last_preview_id == p2.preview_id\n\n\n# [/DEF:test_save_preview_marks_stale:Function]\n\n\n# [DEF:test_save_preview_increments_session_version_once_per_call:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify preview persistence itself contributes exactly one optimistic-lock version increment so higher orchestration layers do not need to bump again for the same preview mutation.\ndef test_save_preview_increments_session_version_once_per_call(db_session):\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n repo.save_preview(\n session.session_id,\n \"user1\",\n CompiledPreview(\n session_id=session.session_id,\n preview_status=\"ready\",\n preview_fingerprint=\"fp-ready\",\n ),\n )\n db_session.refresh(session)\n assert session.version == 1\n\n repo.save_preview(\n session.session_id,\n \"user1\",\n CompiledPreview(\n session_id=session.session_id,\n preview_status=\"pending\",\n preview_fingerprint=\"fp-pending\",\n ),\n )\n db_session.refresh(session)\n assert session.version == 2\n\n\n# [/DEF:test_save_preview_increments_session_version_once_per_call:Function]\n\n\n# [DEF:test_save_profile_and_findings:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_save_profile_and_findings(db_session):\n # @PURPOSE: Verify persistence of profile and findings.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n profile = DatasetProfile(\n session_id=session.session_id,\n dataset_name=\"Test DS\",\n business_summary=\"Summary\",\n business_summary_source=BusinessSummarySource.INFERRED,\n confidence_state=ConfidenceState.UNRESOLVED,\n )\n\n finding = ValidationFinding(\n session_id=session.session_id,\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.BLOCKING,\n code=\"ERR1\",\n title=\"Error\",\n message=\"Failure\",\n )\n\n repo.save_profile_and_findings(session.session_id, \"user1\", profile, [finding])\n\n updated_session = repo.load_session_detail(session.session_id, \"user1\")\n assert updated_session.profile.dataset_name == \"Test DS\"\n assert len(updated_session.findings) == 1\n assert updated_session.findings[0].code == \"ERR1\"\n assert updated_session.version == 1\n\n # Verify removal of old findings\n new_finding = ValidationFinding(\n session_id=session.session_id,\n area=FindingArea.DATASET_PROFILE,\n severity=FindingSeverity.WARNING,\n code=\"WARN1\",\n title=\"Warning\",\n message=\"Something\",\n )\n\n repo.save_profile_and_findings(session.session_id, \"user1\", profile, [new_finding])\n\n db_session.expire_all()\n final_session = repo.load_session_detail(session.session_id, \"user1\")\n assert len(final_session.findings) == 1\n assert final_session.findings[0].code == \"WARN1\"\n assert final_session.version == 2\n\n\n# [/DEF:test_save_profile_and_findings:Function]\n\n\n# [DEF:test_save_profile_and_findings_rejects_stale_concurrent_write:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify repository save path translates concurrent stale session writes into deterministic optimistic-lock conflicts.\ndef test_save_profile_and_findings_rejects_stale_concurrent_write(tmp_path: Path):\n db_path = tmp_path / \"dataset_review_session_repository.sqlite\"\n engine = create_engine(f\"sqlite:///{db_path}\")\n Base.metadata.create_all(engine)\n SessionFactory = sessionmaker(bind=engine)\n\n seed_session = SessionFactory()\n seed_session.add_all(\n [\n User(\n id=\"user1\",\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=\"pw\",\n ),\n Environment(\n id=\"env1\",\n name=\"Prod\",\n url=\"http://superset\",\n credentials_id=\"cred1\",\n ),\n ]\n )\n seed_session.commit()\n created_session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n seed_session.add(created_session)\n seed_session.commit()\n session_id = created_session.session_id\n seed_session.close()\n\n writer_a = SessionFactory()\n writer_b = SessionFactory()\n repo_a = DatasetReviewSessionRepository(writer_a)\n repo_b = DatasetReviewSessionRepository(writer_b)\n\n repo_a.save_profile_and_findings(\n session_id,\n \"user1\",\n DatasetProfile(\n session_id=session_id,\n dataset_name=\"Writer A\",\n business_summary=\"Summary A\",\n business_summary_source=BusinessSummarySource.INFERRED,\n confidence_state=ConfidenceState.UNRESOLVED,\n ),\n [\n ValidationFinding(\n session_id=session_id,\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"WARN_A\",\n title=\"Warning A\",\n message=\"Writer A saved first\",\n )\n ],\n )\n\n with pytest.raises(DatasetReviewSessionVersionConflictError) as exc_info:\n repo_b.save_profile_and_findings(\n session_id,\n \"user1\",\n DatasetProfile(\n session_id=session_id,\n dataset_name=\"Writer B\",\n business_summary=\"Summary B\",\n business_summary_source=BusinessSummarySource.INFERRED,\n confidence_state=ConfidenceState.UNRESOLVED,\n ),\n [\n ValidationFinding(\n session_id=session_id,\n area=FindingArea.DATASET_PROFILE,\n severity=FindingSeverity.BLOCKING,\n code=\"ERR_B\",\n title=\"Error B\",\n message=\"Writer B lost optimistic lock\",\n )\n ],\n expected_version=0,\n )\n\n assert exc_info.value.expected_version == 0\n assert exc_info.value.actual_version == 1\n\n writer_a.close()\n writer_b.close()\n\n\n# [/DEF:test_save_profile_and_findings_rejects_stale_concurrent_write:Function]\n\n\n# [DEF:test_save_run_context:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_save_run_context(db_session):\n # @PURPOSE: Verify saving of run context.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n rc = DatasetRunContext(\n session_id=session.session_id,\n dataset_ref=\"ds1\",\n environment_id=\"env1\",\n preview_id=\"p1\",\n sql_lab_session_ref=\"s1\",\n effective_filters={},\n template_params={},\n approved_mapping_ids=[],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=\"success\",\n )\n repo.save_run_context(session.session_id, \"user1\", rc)\n\n assert session.last_run_context_id == rc.run_context_id\n\n\n# [/DEF:test_save_run_context:Function]\n\n\n# [DEF:test_ensure_dataset_review_session_columns_adds_missing_legacy_columns:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify additive dataset review migration creates missing legacy columns for session and imported-filter tables without dropping rows.\ndef test_ensure_dataset_review_session_columns_adds_missing_legacy_columns():\n engine = create_engine(\"sqlite:///:memory:\")\n with engine.begin() as connection:\n connection.execute(\n text(\n \"\"\"\n CREATE TABLE dataset_review_sessions (\n session_id VARCHAR PRIMARY KEY,\n user_id VARCHAR NOT NULL,\n environment_id VARCHAR NOT NULL,\n source_kind VARCHAR NOT NULL,\n source_input VARCHAR NOT NULL,\n dataset_ref VARCHAR NOT NULL,\n dataset_id INTEGER,\n dashboard_id INTEGER,\n readiness_state VARCHAR NOT NULL,\n recommended_action VARCHAR NOT NULL,\n status VARCHAR NOT NULL,\n current_phase VARCHAR NOT NULL,\n active_task_id VARCHAR,\n last_preview_id VARCHAR,\n last_run_context_id VARCHAR,\n created_at DATETIME NOT NULL,\n updated_at DATETIME NOT NULL,\n last_activity_at DATETIME NOT NULL,\n closed_at DATETIME\n )\n \"\"\"\n )\n )\n connection.execute(\n text(\n \"\"\"\n CREATE TABLE imported_filters (\n filter_id VARCHAR PRIMARY KEY,\n session_id VARCHAR NOT NULL,\n filter_name VARCHAR NOT NULL,\n display_name VARCHAR,\n raw_value JSON NOT NULL,\n normalized_value JSON,\n source VARCHAR NOT NULL,\n confidence_state VARCHAR NOT NULL,\n requires_confirmation BOOLEAN NOT NULL DEFAULT FALSE,\n recovery_status VARCHAR NOT NULL,\n notes TEXT,\n created_at DATETIME NOT NULL,\n updated_at DATETIME NOT NULL\n )\n \"\"\"\n )\n )\n connection.execute(\n text(\n \"\"\"\n INSERT INTO dataset_review_sessions (\n session_id,\n user_id,\n environment_id,\n source_kind,\n source_input,\n dataset_ref,\n dataset_id,\n dashboard_id,\n readiness_state,\n recommended_action,\n status,\n current_phase,\n active_task_id,\n last_preview_id,\n last_run_context_id,\n created_at,\n updated_at,\n last_activity_at,\n closed_at\n ) VALUES (\n 'sess-legacy',\n 'user1',\n 'env1',\n 'superset_link',\n 'http://link',\n 'dataset1',\n NULL,\n NULL,\n 'EMPTY',\n 'IMPORT_FROM_SUPERSET',\n 'ACTIVE',\n 'INTAKE',\n NULL,\n NULL,\n NULL,\n '2026-03-21 00:00:00',\n '2026-03-21 00:00:00',\n '2026-03-21 00:00:00',\n NULL\n )\n \"\"\"\n )\n )\n connection.execute(\n text(\n \"\"\"\n INSERT INTO imported_filters (\n filter_id,\n session_id,\n filter_name,\n display_name,\n raw_value,\n normalized_value,\n source,\n confidence_state,\n requires_confirmation,\n recovery_status,\n notes,\n created_at,\n updated_at\n ) VALUES (\n 'filter-legacy',\n 'sess-legacy',\n 'country',\n 'Country',\n '\"DE\"',\n NULL,\n 'MANUAL',\n 'CONFIRMED',\n FALSE,\n 'RECOVERED',\n NULL,\n '2026-03-21 00:00:00',\n '2026-03-21 00:00:00'\n )\n \"\"\"\n )\n )\n\n _ensure_dataset_review_session_columns(engine)\n\n inspector = inspect(engine)\n session_column_names = {\n column[\"name\"] for column in inspector.get_columns(\"dataset_review_sessions\")\n }\n imported_filter_column_names = {\n column[\"name\"] for column in inspector.get_columns(\"imported_filters\")\n }\n assert \"version\" in session_column_names\n assert \"raw_value_masked\" in imported_filter_column_names\n\n with engine.connect() as connection:\n version_value = connection.execute(\n text(\n \"SELECT version FROM dataset_review_sessions WHERE session_id = 'sess-legacy'\"\n )\n ).scalar_one()\n raw_value_masked = connection.execute(\n text(\n \"SELECT raw_value_masked FROM imported_filters WHERE filter_id = 'filter-legacy'\"\n )\n ).scalar_one()\n assert version_value == 0\n assert raw_value_masked in (False, 0)\n\n\n# [/DEF:test_ensure_dataset_review_session_columns_adds_missing_legacy_columns:Function]\n\n\n# [DEF:test_list_sessions_for_user:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_list_sessions_for_user(db_session):\n # @PURPOSE: Verify listing of sessions by user.\n repo = DatasetReviewSessionRepository(db_session)\n s1 = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"k\",\n source_input=\"i\",\n dataset_ref=\"r1\",\n )\n s2 = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"k\",\n source_input=\"i\",\n dataset_ref=\"r2\",\n )\n s3 = DatasetReviewSession(\n user_id=\"other\",\n environment_id=\"env1\",\n source_kind=\"k\",\n source_input=\"i\",\n dataset_ref=\"r3\",\n )\n\n db_session.add_all([s1, s2, s3])\n db_session.commit()\n\n sessions = repo.list_sessions_for_user(\"user1\")\n assert len(sessions) == 2\n assert all(s.user_id == \"user1\" for s in sessions)\n\n\n# [/DEF:test_list_sessions_for_user:Function]\n# [/DEF:SessionRepositoryTests:Module]\n" - }, - { - "contract_id": "db_session", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 37, - "end_line": 60, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows." - }, - "relations": [ - { - "source_id": "db_session", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "[SessionRepositoryTests]" - } - ], - "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": "def", - "body": " # [DEF:db_session:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows.\n # @RELATION: BINDS_TO -> [SessionRepositoryTests]\n engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n # Create test data\n user = User(\n id=\"user1\", username=\"testuser\", email=\"test@example.com\", password_hash=\"pw\"\n )\n env = Environment(\n id=\"env1\", name=\"Prod\", url=\"http://superset\", credentials_id=\"cred1\"\n )\n session.add_all([user, env])\n session.commit()\n\n yield session\n session.close()\n\n\n# [/DEF:db_session:Function]\n" - }, - { - "contract_id": "test_require_session_version_conflict", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 90, - "end_line": 112, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify optimistic-lock conflict is raised when caller version is stale." - }, - "relations": [ - { - "source_id": "test_require_session_version_conflict", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_require_session_version_conflict:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify optimistic-lock conflict is raised when caller version is stale.\ndef test_require_session_version_conflict(db_session):\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n version=2,\n )\n repo.create_session(session)\n\n with pytest.raises(DatasetReviewSessionVersionConflictError) as exc_info:\n repo.require_session_version(session, 1)\n\n assert exc_info.value.expected_version == 1\n assert exc_info.value.actual_version == 2\n\n\n# [/DEF:test_require_session_version_conflict:Function]\n" - }, - { - "contract_id": "test_bump_session_version_updates_last_activity", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 115, - "end_line": 137, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify repository version bump increments monotonically and refreshes last activity." - }, - "relations": [ - { - "source_id": "test_bump_session_version_updates_last_activity", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_bump_session_version_updates_last_activity:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify repository version bump increments monotonically and refreshes last activity.\ndef test_bump_session_version_updates_last_activity(db_session):\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n before_activity = session.last_activity_at\n next_version = repo.bump_session_version(session)\n\n assert next_version == 1\n assert session.version == 1\n assert session.last_activity_at >= before_activity\n\n\n# [/DEF:test_bump_session_version_updates_last_activity:Function]\n" - }, - { - "contract_id": "test_save_recovery_state_preserves_raw_value_masked_flag", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 140, - "end_line": 178, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify imported-filter masking metadata persists with recovery bootstrap state." - }, - "relations": [ - { - "source_id": "test_save_recovery_state_preserves_raw_value_masked_flag", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_save_recovery_state_preserves_raw_value_masked_flag:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify imported-filter masking metadata persists with recovery bootstrap state.\ndef test_save_recovery_state_preserves_raw_value_masked_flag(db_session):\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n imported_filter = ImportedFilter(\n session_id=session.session_id,\n filter_name=\"email\",\n raw_value=\"***@example.com\",\n raw_value_masked=True,\n normalized_value=None,\n source=\"manual\",\n confidence_state=\"confirmed\",\n requires_confirmation=False,\n recovery_status=\"recovered\",\n )\n\n updated = repo.save_recovery_state(\n session.session_id,\n \"user1\",\n [imported_filter],\n [],\n [],\n )\n\n assert updated.imported_filters[0].raw_value_masked is True\n assert updated.version == 1\n\n\n# [/DEF:test_save_recovery_state_preserves_raw_value_masked_flag:Function]\n" - }, - { - "contract_id": "test_load_session_detail_ownership", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 181, - "end_line": 204, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify ownership enforcement in detail loading." - }, - "relations": [ - { - "source_id": "test_load_session_detail_ownership", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_load_session_detail_ownership:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_load_session_detail_ownership(db_session):\n # @PURPOSE: Verify ownership enforcement in detail loading.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n # Correct user\n loaded = repo.load_session_detail(session.session_id, \"user1\")\n assert loaded is not None\n\n # Wrong user\n loaded_wrong = repo.load_session_detail(session.session_id, \"wrong_user\")\n assert loaded_wrong is None\n\n\n# [/DEF:test_load_session_detail_ownership:Function]\n" - }, - { - "contract_id": "test_load_session_detail_collaborator", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 207, - "end_line": 241, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify collaborator access in detail loading." - }, - "relations": [ - { - "source_id": "test_load_session_detail_collaborator", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_load_session_detail_collaborator:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_load_session_detail_collaborator(db_session):\n # @PURPOSE: Verify collaborator access in detail loading.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n # Add collaborator\n collab_user = User(\n id=\"collab1\", username=\"collab\", email=\"c@e.com\", password_hash=\"p\"\n )\n db_session.add(collab_user)\n\n collaborator = SessionCollaborator(\n session_id=session.session_id,\n user_id=\"collab1\",\n role=SessionCollaboratorRole.REVIEWER,\n )\n db_session.add(collaborator)\n db_session.commit()\n\n # Collaborator access\n loaded = repo.load_session_detail(session.session_id, \"collab1\")\n assert loaded is not None\n assert loaded.session_id == session.session_id\n\n\n# [/DEF:test_load_session_detail_collaborator:Function]\n" - }, - { - "contract_id": "test_save_preview_marks_stale", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 244, - "end_line": 274, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify that saving a new preview marks old ones as stale." - }, - "relations": [ - { - "source_id": "test_save_preview_marks_stale", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_save_preview_marks_stale:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_save_preview_marks_stale(db_session):\n # @PURPOSE: Verify that saving a new preview marks old ones as stale.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n p1 = CompiledPreview(\n session_id=session.session_id, preview_status=\"ready\", preview_fingerprint=\"f1\"\n )\n repo.save_preview(session.session_id, \"user1\", p1)\n\n p2 = CompiledPreview(\n session_id=session.session_id, preview_status=\"ready\", preview_fingerprint=\"f2\"\n )\n repo.save_preview(session.session_id, \"user1\", p2)\n\n db_session.refresh(p1)\n assert p1.preview_status == \"stale\"\n assert p2.preview_status == \"ready\"\n assert session.last_preview_id == p2.preview_id\n\n\n# [/DEF:test_save_preview_marks_stale:Function]\n" - }, - { - "contract_id": "test_save_preview_increments_session_version_once_per_call", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 277, - "end_line": 316, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify preview persistence itself contributes exactly one optimistic-lock version increment so higher orchestration layers do not need to bump again for the same preview mutation." - }, - "relations": [ - { - "source_id": "test_save_preview_increments_session_version_once_per_call", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_save_preview_increments_session_version_once_per_call:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify preview persistence itself contributes exactly one optimistic-lock version increment so higher orchestration layers do not need to bump again for the same preview mutation.\ndef test_save_preview_increments_session_version_once_per_call(db_session):\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n repo.save_preview(\n session.session_id,\n \"user1\",\n CompiledPreview(\n session_id=session.session_id,\n preview_status=\"ready\",\n preview_fingerprint=\"fp-ready\",\n ),\n )\n db_session.refresh(session)\n assert session.version == 1\n\n repo.save_preview(\n session.session_id,\n \"user1\",\n CompiledPreview(\n session_id=session.session_id,\n preview_status=\"pending\",\n preview_fingerprint=\"fp-pending\",\n ),\n )\n db_session.refresh(session)\n assert session.version == 2\n\n\n# [/DEF:test_save_preview_increments_session_version_once_per_call:Function]\n" - }, - { - "contract_id": "test_save_profile_and_findings", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 319, - "end_line": 377, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify persistence of profile and findings." - }, - "relations": [ - { - "source_id": "test_save_profile_and_findings", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_save_profile_and_findings:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_save_profile_and_findings(db_session):\n # @PURPOSE: Verify persistence of profile and findings.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n profile = DatasetProfile(\n session_id=session.session_id,\n dataset_name=\"Test DS\",\n business_summary=\"Summary\",\n business_summary_source=BusinessSummarySource.INFERRED,\n confidence_state=ConfidenceState.UNRESOLVED,\n )\n\n finding = ValidationFinding(\n session_id=session.session_id,\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.BLOCKING,\n code=\"ERR1\",\n title=\"Error\",\n message=\"Failure\",\n )\n\n repo.save_profile_and_findings(session.session_id, \"user1\", profile, [finding])\n\n updated_session = repo.load_session_detail(session.session_id, \"user1\")\n assert updated_session.profile.dataset_name == \"Test DS\"\n assert len(updated_session.findings) == 1\n assert updated_session.findings[0].code == \"ERR1\"\n assert updated_session.version == 1\n\n # Verify removal of old findings\n new_finding = ValidationFinding(\n session_id=session.session_id,\n area=FindingArea.DATASET_PROFILE,\n severity=FindingSeverity.WARNING,\n code=\"WARN1\",\n title=\"Warning\",\n message=\"Something\",\n )\n\n repo.save_profile_and_findings(session.session_id, \"user1\", profile, [new_finding])\n\n db_session.expire_all()\n final_session = repo.load_session_detail(session.session_id, \"user1\")\n assert len(final_session.findings) == 1\n assert final_session.findings[0].code == \"WARN1\"\n assert final_session.version == 2\n\n\n# [/DEF:test_save_profile_and_findings:Function]\n" - }, - { - "contract_id": "test_save_profile_and_findings_rejects_stale_concurrent_write", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 380, - "end_line": 477, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify repository save path translates concurrent stale session writes into deterministic optimistic-lock conflicts." - }, - "relations": [ - { - "source_id": "test_save_profile_and_findings_rejects_stale_concurrent_write", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_save_profile_and_findings_rejects_stale_concurrent_write:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify repository save path translates concurrent stale session writes into deterministic optimistic-lock conflicts.\ndef test_save_profile_and_findings_rejects_stale_concurrent_write(tmp_path: Path):\n db_path = tmp_path / \"dataset_review_session_repository.sqlite\"\n engine = create_engine(f\"sqlite:///{db_path}\")\n Base.metadata.create_all(engine)\n SessionFactory = sessionmaker(bind=engine)\n\n seed_session = SessionFactory()\n seed_session.add_all(\n [\n User(\n id=\"user1\",\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=\"pw\",\n ),\n Environment(\n id=\"env1\",\n name=\"Prod\",\n url=\"http://superset\",\n credentials_id=\"cred1\",\n ),\n ]\n )\n seed_session.commit()\n created_session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n seed_session.add(created_session)\n seed_session.commit()\n session_id = created_session.session_id\n seed_session.close()\n\n writer_a = SessionFactory()\n writer_b = SessionFactory()\n repo_a = DatasetReviewSessionRepository(writer_a)\n repo_b = DatasetReviewSessionRepository(writer_b)\n\n repo_a.save_profile_and_findings(\n session_id,\n \"user1\",\n DatasetProfile(\n session_id=session_id,\n dataset_name=\"Writer A\",\n business_summary=\"Summary A\",\n business_summary_source=BusinessSummarySource.INFERRED,\n confidence_state=ConfidenceState.UNRESOLVED,\n ),\n [\n ValidationFinding(\n session_id=session_id,\n area=FindingArea.SOURCE_INTAKE,\n severity=FindingSeverity.WARNING,\n code=\"WARN_A\",\n title=\"Warning A\",\n message=\"Writer A saved first\",\n )\n ],\n )\n\n with pytest.raises(DatasetReviewSessionVersionConflictError) as exc_info:\n repo_b.save_profile_and_findings(\n session_id,\n \"user1\",\n DatasetProfile(\n session_id=session_id,\n dataset_name=\"Writer B\",\n business_summary=\"Summary B\",\n business_summary_source=BusinessSummarySource.INFERRED,\n confidence_state=ConfidenceState.UNRESOLVED,\n ),\n [\n ValidationFinding(\n session_id=session_id,\n area=FindingArea.DATASET_PROFILE,\n severity=FindingSeverity.BLOCKING,\n code=\"ERR_B\",\n title=\"Error B\",\n message=\"Writer B lost optimistic lock\",\n )\n ],\n expected_version=0,\n )\n\n assert exc_info.value.expected_version == 0\n assert exc_info.value.actual_version == 1\n\n writer_a.close()\n writer_b.close()\n\n\n# [/DEF:test_save_profile_and_findings_rejects_stale_concurrent_write:Function]\n" - }, - { - "contract_id": "test_save_run_context", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 480, - "end_line": 512, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify saving of run context." - }, - "relations": [ - { - "source_id": "test_save_run_context", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_save_run_context:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_save_run_context(db_session):\n # @PURPOSE: Verify saving of run context.\n repo = DatasetReviewSessionRepository(db_session)\n session = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"superset_link\",\n source_input=\"http://link\",\n dataset_ref=\"dataset1\",\n )\n repo.create_session(session)\n\n rc = DatasetRunContext(\n session_id=session.session_id,\n dataset_ref=\"ds1\",\n environment_id=\"env1\",\n preview_id=\"p1\",\n sql_lab_session_ref=\"s1\",\n effective_filters={},\n template_params={},\n approved_mapping_ids=[],\n semantic_decision_refs=[],\n open_warning_refs=[],\n launch_status=\"success\",\n )\n repo.save_run_context(session.session_id, \"user1\", rc)\n\n assert session.last_run_context_id == rc.run_context_id\n\n\n# [/DEF:test_save_run_context:Function]\n" - }, - { - "contract_id": "test_ensure_dataset_review_session_columns_adds_missing_legacy_columns", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 515, - "end_line": 679, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify additive dataset review migration creates missing legacy columns for session and imported-filter tables without dropping rows." - }, - "relations": [ - { - "source_id": "test_ensure_dataset_review_session_columns_adds_missing_legacy_columns", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_ensure_dataset_review_session_columns_adds_missing_legacy_columns:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\n# @PURPOSE: Verify additive dataset review migration creates missing legacy columns for session and imported-filter tables without dropping rows.\ndef test_ensure_dataset_review_session_columns_adds_missing_legacy_columns():\n engine = create_engine(\"sqlite:///:memory:\")\n with engine.begin() as connection:\n connection.execute(\n text(\n \"\"\"\n CREATE TABLE dataset_review_sessions (\n session_id VARCHAR PRIMARY KEY,\n user_id VARCHAR NOT NULL,\n environment_id VARCHAR NOT NULL,\n source_kind VARCHAR NOT NULL,\n source_input VARCHAR NOT NULL,\n dataset_ref VARCHAR NOT NULL,\n dataset_id INTEGER,\n dashboard_id INTEGER,\n readiness_state VARCHAR NOT NULL,\n recommended_action VARCHAR NOT NULL,\n status VARCHAR NOT NULL,\n current_phase VARCHAR NOT NULL,\n active_task_id VARCHAR,\n last_preview_id VARCHAR,\n last_run_context_id VARCHAR,\n created_at DATETIME NOT NULL,\n updated_at DATETIME NOT NULL,\n last_activity_at DATETIME NOT NULL,\n closed_at DATETIME\n )\n \"\"\"\n )\n )\n connection.execute(\n text(\n \"\"\"\n CREATE TABLE imported_filters (\n filter_id VARCHAR PRIMARY KEY,\n session_id VARCHAR NOT NULL,\n filter_name VARCHAR NOT NULL,\n display_name VARCHAR,\n raw_value JSON NOT NULL,\n normalized_value JSON,\n source VARCHAR NOT NULL,\n confidence_state VARCHAR NOT NULL,\n requires_confirmation BOOLEAN NOT NULL DEFAULT FALSE,\n recovery_status VARCHAR NOT NULL,\n notes TEXT,\n created_at DATETIME NOT NULL,\n updated_at DATETIME NOT NULL\n )\n \"\"\"\n )\n )\n connection.execute(\n text(\n \"\"\"\n INSERT INTO dataset_review_sessions (\n session_id,\n user_id,\n environment_id,\n source_kind,\n source_input,\n dataset_ref,\n dataset_id,\n dashboard_id,\n readiness_state,\n recommended_action,\n status,\n current_phase,\n active_task_id,\n last_preview_id,\n last_run_context_id,\n created_at,\n updated_at,\n last_activity_at,\n closed_at\n ) VALUES (\n 'sess-legacy',\n 'user1',\n 'env1',\n 'superset_link',\n 'http://link',\n 'dataset1',\n NULL,\n NULL,\n 'EMPTY',\n 'IMPORT_FROM_SUPERSET',\n 'ACTIVE',\n 'INTAKE',\n NULL,\n NULL,\n NULL,\n '2026-03-21 00:00:00',\n '2026-03-21 00:00:00',\n '2026-03-21 00:00:00',\n NULL\n )\n \"\"\"\n )\n )\n connection.execute(\n text(\n \"\"\"\n INSERT INTO imported_filters (\n filter_id,\n session_id,\n filter_name,\n display_name,\n raw_value,\n normalized_value,\n source,\n confidence_state,\n requires_confirmation,\n recovery_status,\n notes,\n created_at,\n updated_at\n ) VALUES (\n 'filter-legacy',\n 'sess-legacy',\n 'country',\n 'Country',\n '\"DE\"',\n NULL,\n 'MANUAL',\n 'CONFIRMED',\n FALSE,\n 'RECOVERED',\n NULL,\n '2026-03-21 00:00:00',\n '2026-03-21 00:00:00'\n )\n \"\"\"\n )\n )\n\n _ensure_dataset_review_session_columns(engine)\n\n inspector = inspect(engine)\n session_column_names = {\n column[\"name\"] for column in inspector.get_columns(\"dataset_review_sessions\")\n }\n imported_filter_column_names = {\n column[\"name\"] for column in inspector.get_columns(\"imported_filters\")\n }\n assert \"version\" in session_column_names\n assert \"raw_value_masked\" in imported_filter_column_names\n\n with engine.connect() as connection:\n version_value = connection.execute(\n text(\n \"SELECT version FROM dataset_review_sessions WHERE session_id = 'sess-legacy'\"\n )\n ).scalar_one()\n raw_value_masked = connection.execute(\n text(\n \"SELECT raw_value_masked FROM imported_filters WHERE filter_id = 'filter-legacy'\"\n )\n ).scalar_one()\n assert version_value == 0\n assert raw_value_masked in (False, 0)\n\n\n# [/DEF:test_ensure_dataset_review_session_columns_adds_missing_legacy_columns:Function]\n" - }, - { - "contract_id": "test_list_sessions_for_user", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py", - "start_line": 682, - "end_line": 717, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify listing of sessions by user." - }, - "relations": [ - { - "source_id": "test_list_sessions_for_user", - "relation_type": "BINDS_TO", - "target_id": "SessionRepositoryTests", - "target_ref": "SessionRepositoryTests" - } - ], - "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": "def", - "body": "# [DEF:test_list_sessions_for_user:Function]\n# @RELATION: BINDS_TO -> SessionRepositoryTests\ndef test_list_sessions_for_user(db_session):\n # @PURPOSE: Verify listing of sessions by user.\n repo = DatasetReviewSessionRepository(db_session)\n s1 = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"k\",\n source_input=\"i\",\n dataset_ref=\"r1\",\n )\n s2 = DatasetReviewSession(\n user_id=\"user1\",\n environment_id=\"env1\",\n source_kind=\"k\",\n source_input=\"i\",\n dataset_ref=\"r2\",\n )\n s3 = DatasetReviewSession(\n user_id=\"other\",\n environment_id=\"env1\",\n source_kind=\"k\",\n source_input=\"i\",\n dataset_ref=\"r3\",\n )\n\n db_session.add_all([s1, s2, s3])\n db_session.commit()\n\n sessions = repo.list_sessions_for_user(\"user1\")\n assert len(sessions) == 2\n assert all(s.user_id == \"user1\" for s in sessions)\n\n\n# [/DEF:test_list_sessions_for_user:Function]\n" - }, - { - "contract_id": "SessionRepositoryMutations", - "contract_type": "Module", "file_path": "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py", - "start_line": 1, - "end_line": 203, + "start_line": 156, + "end_line": 187, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": "4", - "LAYER": "Domain", - "POST": "Session aggregate writes preserve ownership and version semantics.", - "PRE": "All mutations execute within authenticated request or task scope.", - "PURPOSE": "Persistence mutation operations for dataset review session aggregates — profile/findings, recovery state, preview, run context." + "COMPLEXITY": 3, + "POST": "run context is persisted and linked as the latest launch snapshot for the session.", + "PRE": "session_id belongs to user_id and run_context targets the same aggregate.", + "PURPOSE": "Persist an immutable launch audit snapshot for an owned session.", + "SIDE_EFFECT": "inserts a run-context row, mutates the parent session pointer, and commits." }, - "relations": [ - { - "source_id": "SessionRepositoryMutations", - "relation_type": "DEPENDS_ON", - "target_id": "DatasetReviewModels", - "target_ref": "[DatasetReviewModels]" - }, - { - "source_id": "SessionRepositoryMutations", - "relation_type": "DEPENDS_ON", - "target_id": "SessionEventLogger", - "target_ref": "[SessionEventLogger]" - } - ], + "relations": [], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "POST", + "message": "@POST is forbidden for contract type 'Function' at C3", "detail": { - "actual_complexity": 4, - "contract_type": "Module" + "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": "def", - "body": "# [DEF:SessionRepositoryMutations:Module]\n# @COMPLEXITY: 4\n# @PURPOSE: Persistence mutation operations for dataset review session aggregates — profile/findings, recovery state, preview, run context.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewModels]\n# @RELATION: DEPENDS_ON -> [SessionEventLogger]\n# @PRE: All mutations execute within authenticated request or task scope.\n# @POST: Session aggregate writes preserve ownership and version semantics.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any, List, Optional, cast\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.dataset_review import (\n ClarificationQuestion,\n ClarificationSession,\n CompiledPreview,\n DatasetProfile,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n ImportedFilter,\n SemanticFieldEntry,\n SessionCollaborator,\n SessionEvent,\n TemplateVariable,\n ValidationFinding,\n)\nfrom src.services.dataset_review.event_logger import SessionEventLogger\n\nlog = MarkerLogger(\"SessionMutations\")\n\n\n# [DEF:save_profile_and_findings:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Persist profile state and replace validation findings for an owned session in one transaction.\n# @PRE: session_id belongs to user_id and the supplied profile/findings belong to the same aggregate scope.\n# @POST: stored profile matches the current session and findings are replaced by the supplied collection.\n# @SIDE_EFFECT: updates profile rows, deletes stale findings, inserts current findings, and commits the transaction.\ndef save_profile_and_findings(\n db: Session,\n event_logger: SessionEventLogger,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n profile: DatasetProfile,\n findings: List[ValidationFinding],\n expected_version: Optional[int] = None,\n) -> DatasetReviewSession:\n with belief_scope(\"save_profile_and_findings\"):\n session = get_owned_session(session_id, user_id)\n if expected_version is not None:\n require_session_version(session, expected_version)\n log.reason(\"Persisting dataset profile and replacing validation findings\", payload={\"session_id\": session_id, \"user_id\": user_id, \"has_profile\": bool(profile), \"findings_count\": len(findings)})\n\n if profile:\n existing_profile = db.query(DatasetProfile).filter_by(session_id=session_id).first()\n if existing_profile:\n profile.profile_id = existing_profile.profile_id\n db.merge(profile)\n\n db.query(ValidationFinding).filter(ValidationFinding.session_id == session_id).delete()\n for finding in findings:\n cast(Any, finding).session_id = session_id\n db.add(finding)\n\n commit_session_mutation(session, expected_version=expected_version)\n log.reflect(\"Dataset profile and validation findings committed\", payload={\"session_id\": session.session_id, \"user_id\": user_id, \"findings_count\": len(findings)})\n\n from src.services.dataset_review.repositories.session_repository import DatasetReviewSessionRepository\n return session\n\n\n# [/DEF:save_profile_and_findings:Function]\n\n\n# [DEF:save_recovery_state:Function]\n# @COMPLEXITY: 4\n# @PURPOSE: Persist imported filters, template variables, and initial execution mappings for one owned session.\n# @PRE: session_id belongs to user_id.\n# @POST: Recovery state persisted to database.\n# @SIDE_EFFECT: Writes to database.\ndef save_recovery_state(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n load_session_detail_fn,\n session_id: str,\n user_id: str,\n imported_filters: List[ImportedFilter],\n template_variables: List[TemplateVariable],\n execution_mappings: List[ExecutionMapping],\n expected_version: Optional[int] = None,\n) -> DatasetReviewSession:\n with belief_scope(\"save_recovery_state\"):\n session = get_owned_session(session_id, user_id)\n if expected_version is not None:\n require_session_version(session, expected_version)\n log.reason(\"Persisting dataset review recovery bootstrap state\", payload={\"session_id\": session_id, \"user_id\": user_id, \"imported_filters_count\": len(imported_filters), \"template_variables_count\": len(template_variables), \"execution_mappings_count\": len(execution_mappings)})\n\n db.query(ExecutionMapping).filter(ExecutionMapping.session_id == session_id).delete()\n db.query(TemplateVariable).filter(TemplateVariable.session_id == session_id).delete()\n db.query(ImportedFilter).filter(ImportedFilter.session_id == session_id).delete()\n\n for f in imported_filters:\n cast(Any, f).session_id = session_id\n db.add(f)\n for tv in template_variables:\n cast(Any, tv).session_id = session_id\n db.add(tv)\n db.flush()\n for em in execution_mappings:\n cast(Any, em).session_id = session_id\n db.add(em)\n\n commit_session_mutation(session, expected_version=expected_version)\n log.reflect(\"Dataset review recovery bootstrap state committed\", payload={\"session_id\": session.session_id, \"user_id\": user_id})\n return load_session_detail_fn(session_id, user_id)\n\n\n# [/DEF:save_recovery_state:Function]\n\n\n# [DEF:save_preview:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist a preview snapshot and mark prior session previews stale.\n# @PRE: session_id belongs to user_id and preview is prepared for the same session aggregate.\n# @POST: preview is persisted and the session points to the latest preview identifier.\n# @SIDE_EFFECT: updates prior preview statuses, inserts a preview row, mutates the parent session, and commits.\ndef save_preview(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n preview: CompiledPreview,\n expected_version: Optional[int] = None,\n) -> CompiledPreview:\n with belief_scope(\"save_preview\"):\n session = get_owned_session(session_id, user_id)\n session_record = cast(Any, session)\n if expected_version is not None:\n require_session_version(session, expected_version)\n log.reason(\"Persisting compiled preview and staling previous preview snapshots\", payload={\"session_id\": session_id, \"user_id\": user_id})\n\n db.query(CompiledPreview).filter(CompiledPreview.session_id == session_id).update({\"preview_status\": \"stale\"})\n db.add(preview)\n db.flush()\n session_record.last_preview_id = preview.preview_id\n\n commit_session_mutation(session, refresh_targets=[preview], expected_version=expected_version)\n log.reflect(\"Compiled preview committed as latest session preview\", payload={\"session_id\": session.session_id, \"preview_id\": preview.preview_id})\n return preview\n\n\n# [/DEF:save_preview:Function]\n\n\n# [DEF:save_run_context:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Persist an immutable launch audit snapshot for an owned session.\n# @PRE: session_id belongs to user_id and run_context targets the same aggregate.\n# @POST: run context is persisted and linked as the latest launch snapshot for the session.\n# @SIDE_EFFECT: inserts a run-context row, mutates the parent session pointer, and commits.\ndef save_run_context(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n run_context: DatasetRunContext,\n expected_version: Optional[int] = None,\n) -> DatasetRunContext:\n with belief_scope(\"save_run_context\"):\n session = get_owned_session(session_id, user_id)\n session_record = cast(Any, session)\n if expected_version is not None:\n require_session_version(session, expected_version)\n log.reason(\"Persisting dataset run context audit snapshot\", payload={\"session_id\": session_id, \"user_id\": user_id})\n\n db.add(run_context)\n db.flush()\n session_record.last_run_context_id = run_context.run_context_id\n\n commit_session_mutation(session, refresh_targets=[run_context], expected_version=expected_version)\n log.reflect(\"Dataset run context committed as latest launch snapshot\", payload={\"session_id\": session.session_id, \"run_context_id\": run_context.run_context_id})\n return run_context\n\n\n# [/DEF:save_run_context:Function]\n\n\n# [/DEF:SessionRepositoryMutations:Module]\n" + "anchor_syntax": "region", + "body": "# #region save_run_context [C:3] [TYPE Function]\n# @BRIEF Persist an immutable launch audit snapshot for an owned session.\n# @PRE: session_id belongs to user_id and run_context targets the same aggregate.\n# @POST: run context is persisted and linked as the latest launch snapshot for the session.\n# @SIDE_EFFECT: inserts a run-context row, mutates the parent session pointer, and commits.\ndef save_run_context(\n db: Session,\n get_owned_session,\n require_session_version,\n commit_session_mutation,\n session_id: str,\n user_id: str,\n run_context: DatasetRunContext,\n expected_version: int | None = None,\n) -> DatasetRunContext:\n with belief_scope(\"save_run_context\"):\n session = get_owned_session(session_id, user_id)\n session_record = cast(Any, session)\n if expected_version is not None:\n require_session_version(session, expected_version)\n logger.reason(\"Persisting dataset run context audit snapshot\", extra={\"session_id\": session_id, \"user_id\": user_id})\n\n db.add(run_context)\n db.flush()\n session_record.last_run_context_id = run_context.run_context_id\n\n commit_session_mutation(session, refresh_targets=[run_context], expected_version=expected_version)\n logger.reflect(\"Dataset run context committed as latest launch snapshot\", extra={\"session_id\": session.session_id, \"run_context_id\": run_context.run_context_id})\n return run_context\n\n\n# #endregion save_run_context\n" }, { "contract_id": "DatasetReviewSessionRepository", "contract_type": "Module", "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", "start_line": 1, - "end_line": 288, + "end_line": 273, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[SessionMutation] -> Output[PersistedSessionAggregate]", "INVARIANT": "answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.", "LAYER": "Domain", @@ -85946,486 +62426,65 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewSessionRepository:Module]\n# @COMPLEXITY: 5\n# @PURPOSE: Persist and retrieve dataset review session aggregates, including readiness, findings, semantic decisions, clarification state, previews, and run contexts.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION: DEPENDS_ON -> [DatasetProfile]\n# @RELATION: DEPENDS_ON -> [ValidationFinding]\n# @RELATION: DEPENDS_ON -> [CompiledPreview]\n# @RELATION: DISPATCHES -> [SessionRepositoryMutations:Module]\n# @PRE: repository operations execute within authenticated request or task scope.\n# @POST: session aggregate reads are structurally consistent and writes preserve ownership and version semantics.\n# @SIDE_EFFECT: reads and writes SQLAlchemy-backed session aggregates.\n# @DATA_CONTRACT: Input[SessionMutation] -> Output[PersistedSessionAggregate]\n# @INVARIANT: answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.\n# @RATIONALE: Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module.\n# @REJECTED: Keeping all repository operations in one file because it exceeded the fractal limit.\n\nfrom datetime import datetime\nfrom typing import Any, Optional, List\nfrom sqlalchemy import or_\nfrom sqlalchemy.orm import Session, joinedload\nfrom sqlalchemy.orm.exc import StaleDataError\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.dataset_review import (\n ClarificationQuestion,\n ClarificationSession,\n DatasetReviewSession,\n DatasetProfile,\n ValidationFinding,\n CompiledPreview,\n DatasetRunContext,\n ExecutionMapping,\n ImportedFilter,\n SemanticFieldEntry,\n SessionCollaborator,\n SessionEvent,\n TemplateVariable,\n)\nfrom src.services.dataset_review.event_logger import SessionEventLogger\n\nlog = MarkerLogger(\"SessionRepository\")\n\n\n# [DEF:DatasetReviewSessionVersionConflictError:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Signal optimistic-lock conflicts for dataset review session mutations.\nclass DatasetReviewSessionVersionConflictError(ValueError):\n def __init__(self, session_id: str, expected_version: int, actual_version: int):\n self.session_id = session_id\n self.expected_version = expected_version\n self.actual_version = actual_version\n super().__init__(\n f\"Session version conflict: expected {expected_version}, actual {actual_version}\"\n )\n\n\n# [/DEF:DatasetReviewSessionVersionConflictError:Class]\n\n\n# [DEF:DatasetReviewSessionRepository:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Enforce ownership-scoped persistence and retrieval for dataset review session aggregates.\n# @RELATION: DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION: DEPENDS_ON -> [SessionEventLogger]\n# @PRE: constructor receives a live SQLAlchemy session and callers provide authenticated user scope.\n# @POST: repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning.\n# @SIDE_EFFECT: mutates and queries the persistence layer through the injected database session.\nclass DatasetReviewSessionRepository:\n # [DEF:init_repo:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind one live SQLAlchemy session to the repository instance.\n # @PRE: db_session is not None\n # @POST: Repository instance initialized with valid session\n def __init__(self, db: Session):\n self.db = db\n self.event_logger = SessionEventLogger(db)\n\n # [/DEF:init_repo:Function]\n\n # [DEF:get_owned_session:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.\n # @PRE: session_id and user_id are non-empty identifiers from the authenticated ownership scope.\n # @POST: returns the owned session or raises a deterministic access error.\n def _get_owned_session(self, session_id: str, user_id: str) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.get_owned_session\"):\n log.reason(\"Resolving owner-scoped dataset review session\", payload={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.session_id == session_id, DatasetReviewSession.user_id == user_id)\n .first()\n )\n if not session:\n log.explore(\"Owner-scoped dataset review session lookup failed\", payload={\"session_id\": session_id, \"user_id\": user_id}, error=\"Session not found or access denied\")\n raise ValueError(\"Session not found or access denied\")\n log.reflect(\"Owner-scoped dataset review session resolved\", payload={\"session_id\": session.session_id})\n return session\n\n # [/DEF:get_owned_session:Function]\n\n # [DEF:create_sess:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist an initial dataset review session shell.\n # @POST: session is committed, refreshed, and returned with persisted identifiers.\n def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.create_session\"):\n log.reason(\"Persisting dataset review session shell\", payload={\"user_id\": session.user_id, \"environment_id\": session.environment_id})\n self.db.add(session)\n self.db.commit()\n self.db.refresh(session)\n log.reflect(\"Dataset review session shell persisted\", payload={\"session_id\": session.session_id})\n return session\n\n # [/DEF:create_sess:Function]\n\n # [DEF:require_session_version:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.\n # @POST: returns the same session when versions match; otherwise raises deterministic conflict error.\n def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.require_session_version\"):\n actual_version = int(getattr(session, \"version\", 0) or 0)\n log.reason(\"Checking optimistic-lock version\", payload={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version})\n if actual_version != expected_version:\n log.explore(\"Rejected mutation due to stale session version\", payload={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version}, error=\"Optimistic lock version mismatch\")\n raise DatasetReviewSessionVersionConflictError(str(session.session_id), expected_version, actual_version)\n log.reflect(\"Optimistic-lock version accepted\", payload={\"session_id\": session.session_id, \"version\": actual_version})\n return session\n\n # [/DEF:require_session_version:Function]\n\n # [DEF:bump_session_version:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.\n # @POST: session version increments monotonically.\n def bump_session_version(self, session: DatasetReviewSession) -> int:\n with belief_scope(\"DatasetReviewSessionRepository.bump_session_version\"):\n next_version = int(getattr(session, \"version\", 0) or 0) + 1\n setattr(session, \"version\", next_version)\n session.last_activity_at = datetime.utcnow()\n log.reflect(\"Prepared incremented session version\", payload={\"session_id\": session.session_id, \"version\": next_version})\n return next_version\n\n # [/DEF:bump_session_version:Function]\n\n # [DEF:commit_session_mutation:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.\n # @POST: session mutation is committed with one version increment or a deterministic conflict error is raised.\n def commit_session_mutation(\n self, session: DatasetReviewSession, *, refresh_targets: Optional[List[Any]] = None, expected_version: Optional[int] = None,\n ) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.commit_session_mutation\"):\n observed_version = int(expected_version if expected_version is not None else getattr(session, \"version\", 0) or 0)\n log.reason(\"Committing session mutation with optimistic lock\", payload={\"session_id\": session.session_id, \"observed_version\": observed_version})\n self.bump_session_version(session)\n try:\n self.db.commit()\n except StaleDataError as exc:\n self.db.rollback()\n actual_version_row = self.db.query(DatasetReviewSession.version).filter(DatasetReviewSession.session_id == session.session_id).first()\n actual_version = int(actual_version_row[0] or 0) if actual_version_row else 0\n log.explore(\"Session commit rejected by optimistic lock\", payload={\"session_id\": session.session_id, \"expected_version\": observed_version, \"actual_version\": actual_version}, error=\"StaleDataError on session commit\")\n raise DatasetReviewSessionVersionConflictError(session.session_id, observed_version, actual_version) from exc\n self.db.refresh(session)\n for target in refresh_targets or []:\n self.db.refresh(target)\n log.reflect(\"Session mutation committed\", payload={\"session_id\": session.session_id, \"version\": getattr(session, \"version\", None)})\n return session\n\n # [/DEF:commit_session_mutation:Function]\n\n # [DEF:load_detail:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Return the full session aggregate for API and frontend resume flows.\n # @POST: Returns SessionDetail with all fields populated or None.\n def load_session_detail(self, session_id: str, user_id: str) -> Optional[DatasetReviewSession]:\n with belief_scope(\"DatasetReviewSessionRepository.load_session_detail\"):\n log.reason(\"Loading dataset review session detail\", payload={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .outerjoin(SessionCollaborator, DatasetReviewSession.session_id == SessionCollaborator.session_id)\n .options(\n joinedload(DatasetReviewSession.profile),\n joinedload(DatasetReviewSession.findings),\n joinedload(DatasetReviewSession.collaborators),\n joinedload(DatasetReviewSession.semantic_sources),\n joinedload(DatasetReviewSession.semantic_fields).joinedload(SemanticFieldEntry.candidates),\n joinedload(DatasetReviewSession.imported_filters),\n joinedload(DatasetReviewSession.template_variables),\n joinedload(DatasetReviewSession.execution_mappings),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.options),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.answer),\n joinedload(DatasetReviewSession.previews),\n joinedload(DatasetReviewSession.run_contexts),\n joinedload(DatasetReviewSession.events),\n )\n .filter(DatasetReviewSession.session_id == session_id)\n .filter(or_(DatasetReviewSession.user_id == user_id, SessionCollaborator.user_id == user_id))\n .first()\n )\n log.reflect(\"Session detail lookup completed\", payload={\"session_id\": session_id, \"found\": bool(session)})\n return session\n\n # [/DEF:load_detail:Function]\n\n # [DEF:save_profile_and_findings:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Persist profile state and replace validation findings for an owned session.\n # @POST: stored profile matches the current session and findings are replaced.\n def save_profile_and_findings(\n self, session_id: str, user_id: str, profile: DatasetProfile, findings: List[ValidationFinding], expected_version: Optional[int] = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_profile_and_findings as _save\n return _save(\n self.db, self.event_logger, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, profile, findings, expected_version,\n )\n\n # [/DEF:save_profile_and_findings:Function]\n\n # [DEF:save_recovery_state:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist imported filters, template variables, and initial execution mappings.\n def save_recovery_state(\n self, session_id: str, user_id: str, imported_filters: List[ImportedFilter],\n template_variables: List[TemplateVariable], execution_mappings: List[ExecutionMapping],\n expected_version: Optional[int] = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_recovery_state as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, self.load_session_detail,\n session_id, user_id, imported_filters, template_variables, execution_mappings, expected_version,\n )\n\n # [/DEF:save_recovery_state:Function]\n\n # [DEF:save_preview:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist a preview snapshot and mark prior session previews stale.\n def save_preview(\n self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: Optional[int] = None,\n ) -> CompiledPreview:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_preview as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, preview, expected_version,\n )\n\n # [/DEF:save_preview:Function]\n\n # [DEF:save_run_context:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist an immutable launch audit snapshot for an owned session.\n def save_run_context(\n self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: Optional[int] = None,\n ) -> DatasetRunContext:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_run_context as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, run_context, expected_version,\n )\n\n # [/DEF:save_run_context:Function]\n\n # [DEF:list_user_sess:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: List review sessions owned by a specific user ordered by most recent update.\n def list_sessions_for_user(self, user_id: str) -> List[DatasetReviewSession]:\n with belief_scope(\"DatasetReviewSessionRepository.list_sessions_for_user\"):\n log.reason(\"Listing dataset review sessions for owner scope\", payload={\"user_id\": user_id})\n sessions = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.user_id == user_id)\n .order_by(DatasetReviewSession.updated_at.desc())\n .all()\n )\n log.reflect(\"Session list assembled\", payload={\"user_id\": user_id, \"session_count\": len(sessions)})\n return sessions\n\n # [/DEF:list_user_sess:Function]\n\n\n# [/DEF:DatasetReviewSessionRepository:Class]\n\n# [/DEF:DatasetReviewSessionRepository:Module]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewSessionRepository [C:5] [TYPE Module] [SEMANTICS dataset, repository, session, aggregate, persistence]\n# @BRIEF Persist and retrieve dataset review session aggregates, including readiness, findings, semantic decisions, clarification state, previews, and run contexts.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DISPATCHES -> [SessionRepositoryMutations:Module]\n# @PRE: repository operations execute within authenticated request or task scope.\n# @POST: session aggregate reads are structurally consistent and writes preserve ownership and version semantics.\n# @SIDE_EFFECT: reads and writes SQLAlchemy-backed session aggregates.\n# @DATA_CONTRACT: Input[SessionMutation] -> Output[PersistedSessionAggregate]\n# @INVARIANT: answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.\n# @RATIONALE: Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module.\n# @REJECTED: Keeping all repository operations in one file because it exceeded the fractal limit.\n\nfrom datetime import datetime\nfrom typing import Any, cast\n\nfrom sqlalchemy import or_\nfrom sqlalchemy.orm import Session, joinedload\nfrom sqlalchemy.orm.exc import StaleDataError\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n ClarificationQuestion,\n ClarificationSession,\n CompiledPreview,\n DatasetProfile,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n ImportedFilter,\n SemanticFieldEntry,\n SessionCollaborator,\n TemplateVariable,\n ValidationFinding,\n)\nfrom src.services.dataset_review.event_logger import SessionEventLogger\n\nlogger = cast(Any, logger)\n\n\n# #region DatasetReviewSessionVersionConflictError [C:2] [TYPE Class]\n# @BRIEF Signal optimistic-lock conflicts for dataset review session mutations.\nclass DatasetReviewSessionVersionConflictError(ValueError):\n def __init__(self, session_id: str, expected_version: int, actual_version: int):\n self.session_id = session_id\n self.expected_version = expected_version\n self.actual_version = actual_version\n super().__init__(\n f\"Session version conflict: expected {expected_version}, actual {actual_version}\"\n )\n\n\n# #endregion DatasetReviewSessionVersionConflictError\n\n\n# #region DatasetReviewSessionRepository [C:4] [TYPE Class]\n# @BRIEF Enforce ownership-scoped persistence and retrieval for dataset review session aggregates.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SessionEventLogger]\n# @PRE: constructor receives a live SQLAlchemy session and callers provide authenticated user scope.\n# @POST: repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning.\n# @SIDE_EFFECT: mutates and queries the persistence layer through the injected database session.\nclass DatasetReviewSessionRepository:\n # region init_repo [TYPE Function]\n # @PURPOSE: Bind one live SQLAlchemy session to the repository instance.\n # @PRE: db_session is not None\n # @POST: Repository instance initialized with valid session\n def __init__(self, db: Session):\n self.db = db\n self.event_logger = SessionEventLogger(db)\n\n # endregion init_repo\n\n # region get_owned_session [TYPE Function]\n # @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.\n # @PRE: session_id and user_id are non-empty identifiers from the authenticated ownership scope.\n # @POST: returns the owned session or raises a deterministic access error.\n def _get_owned_session(self, session_id: str, user_id: str) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.get_owned_session\"):\n logger.reason(\"Resolving owner-scoped dataset review session\", extra={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.session_id == session_id, DatasetReviewSession.user_id == user_id)\n .first()\n )\n if not session:\n logger.explore(\"Owner-scoped dataset review session lookup failed\", extra={\"session_id\": session_id, \"user_id\": user_id})\n raise ValueError(\"Session not found or access denied\")\n logger.reflect(\"Owner-scoped dataset review session resolved\", extra={\"session_id\": session.session_id})\n return session\n\n # endregion get_owned_session\n\n # region create_sess [TYPE Function]\n # @PURPOSE: Persist an initial dataset review session shell.\n # @POST: session is committed, refreshed, and returned with persisted identifiers.\n def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.create_session\"):\n logger.reason(\"Persisting dataset review session shell\", extra={\"user_id\": session.user_id, \"environment_id\": session.environment_id})\n self.db.add(session)\n self.db.commit()\n self.db.refresh(session)\n logger.reflect(\"Dataset review session shell persisted\", extra={\"session_id\": session.session_id})\n return session\n\n # endregion create_sess\n\n # region require_session_version [TYPE Function]\n # @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.\n # @POST: returns the same session when versions match; otherwise raises deterministic conflict error.\n def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.require_session_version\"):\n actual_version = int(getattr(session, \"version\", 0) or 0)\n logger.reason(\"Checking optimistic-lock version\", extra={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version})\n if actual_version != expected_version:\n logger.explore(\"Rejected mutation due to stale session version\", extra={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version})\n raise DatasetReviewSessionVersionConflictError(str(session.session_id), expected_version, actual_version)\n logger.reflect(\"Optimistic-lock version accepted\", extra={\"session_id\": session.session_id, \"version\": actual_version})\n return session\n\n # endregion require_session_version\n\n # region bump_session_version [TYPE Function]\n # @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.\n # @POST: session version increments monotonically.\n def bump_session_version(self, session: DatasetReviewSession) -> int:\n with belief_scope(\"DatasetReviewSessionRepository.bump_session_version\"):\n next_version = int(getattr(session, \"version\", 0) or 0) + 1\n session.version = next_version\n session.last_activity_at = datetime.utcnow()\n logger.reflect(\"Prepared incremented session version\", extra={\"session_id\": session.session_id, \"version\": next_version})\n return next_version\n\n # endregion bump_session_version\n\n # region commit_session_mutation [TYPE Function]\n # @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.\n # @POST: session mutation is committed with one version increment or a deterministic conflict error is raised.\n def commit_session_mutation(\n self, session: DatasetReviewSession, *, refresh_targets: list[Any] | None = None, expected_version: int | None = None,\n ) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.commit_session_mutation\"):\n observed_version = int(expected_version if expected_version is not None else getattr(session, \"version\", 0) or 0)\n logger.reason(\"Committing session mutation with optimistic lock\", extra={\"session_id\": session.session_id, \"observed_version\": observed_version})\n self.bump_session_version(session)\n try:\n self.db.commit()\n except StaleDataError as exc:\n self.db.rollback()\n actual_version_row = self.db.query(DatasetReviewSession.version).filter(DatasetReviewSession.session_id == session.session_id).first()\n actual_version = int(actual_version_row[0] or 0) if actual_version_row else 0\n logger.explore(\"Session commit rejected by optimistic lock\", extra={\"session_id\": session.session_id, \"expected_version\": observed_version, \"actual_version\": actual_version})\n raise DatasetReviewSessionVersionConflictError(session.session_id, observed_version, actual_version) from exc\n self.db.refresh(session)\n for target in refresh_targets or []:\n self.db.refresh(target)\n logger.reflect(\"Session mutation committed\", extra={\"session_id\": session.session_id, \"version\": getattr(session, \"version\", None)})\n return session\n\n # endregion commit_session_mutation\n\n # region load_detail [TYPE Function]\n # @PURPOSE: Return the full session aggregate for API and frontend resume flows.\n # @POST: Returns SessionDetail with all fields populated or None.\n def load_session_detail(self, session_id: str, user_id: str) -> DatasetReviewSession | None:\n with belief_scope(\"DatasetReviewSessionRepository.load_session_detail\"):\n logger.reason(\"Loading dataset review session detail\", extra={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .outerjoin(SessionCollaborator, DatasetReviewSession.session_id == SessionCollaborator.session_id)\n .options(\n joinedload(DatasetReviewSession.profile),\n joinedload(DatasetReviewSession.findings),\n joinedload(DatasetReviewSession.collaborators),\n joinedload(DatasetReviewSession.semantic_sources),\n joinedload(DatasetReviewSession.semantic_fields).joinedload(SemanticFieldEntry.candidates),\n joinedload(DatasetReviewSession.imported_filters),\n joinedload(DatasetReviewSession.template_variables),\n joinedload(DatasetReviewSession.execution_mappings),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.options),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.answer),\n joinedload(DatasetReviewSession.previews),\n joinedload(DatasetReviewSession.run_contexts),\n joinedload(DatasetReviewSession.events),\n )\n .filter(DatasetReviewSession.session_id == session_id)\n .filter(or_(DatasetReviewSession.user_id == user_id, SessionCollaborator.user_id == user_id))\n .first()\n )\n logger.reflect(\"Session detail lookup completed\", extra={\"session_id\": session_id, \"found\": bool(session)})\n return session\n\n # endregion load_detail\n\n # region save_profile_and_findings [TYPE Function]\n # @PURPOSE: Persist profile state and replace validation findings for an owned session.\n # @POST: stored profile matches the current session and findings are replaced.\n def save_profile_and_findings(\n self, session_id: str, user_id: str, profile: DatasetProfile, findings: list[ValidationFinding], expected_version: int | None = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_profile_and_findings as _save\n return _save(\n self.db, self.event_logger, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, profile, findings, expected_version,\n )\n\n # endregion save_profile_and_findings\n\n # region save_recovery_state [TYPE Function]\n # @PURPOSE: Persist imported filters, template variables, and initial execution mappings.\n def save_recovery_state(\n self, session_id: str, user_id: str, imported_filters: list[ImportedFilter],\n template_variables: list[TemplateVariable], execution_mappings: list[ExecutionMapping],\n expected_version: int | None = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_recovery_state as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, self.load_session_detail,\n session_id, user_id, imported_filters, template_variables, execution_mappings, expected_version,\n )\n\n # endregion save_recovery_state\n\n # region save_preview [TYPE Function]\n # @PURPOSE: Persist a preview snapshot and mark prior session previews stale.\n def save_preview(\n self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: int | None = None,\n ) -> CompiledPreview:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_preview as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, preview, expected_version,\n )\n\n # endregion save_preview\n\n # region save_run_context [TYPE Function]\n # @PURPOSE: Persist an immutable launch audit snapshot for an owned session.\n def save_run_context(\n self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: int | None = None,\n ) -> DatasetRunContext:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_run_context as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, run_context, expected_version,\n )\n\n # endregion save_run_context\n\n # region list_user_sess [TYPE Function]\n # @PURPOSE: List review sessions owned by a specific user ordered by most recent update.\n def list_sessions_for_user(self, user_id: str) -> list[DatasetReviewSession]:\n with belief_scope(\"DatasetReviewSessionRepository.list_sessions_for_user\"):\n logger.reason(\"Listing dataset review sessions for owner scope\", extra={\"user_id\": user_id})\n sessions = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.user_id == user_id)\n .order_by(DatasetReviewSession.updated_at.desc())\n .all()\n )\n logger.reflect(\"Session list assembled\", extra={\"user_id\": user_id, \"session_count\": len(sessions)})\n return sessions\n\n # endregion list_user_sess\n\n\n# #endregion DatasetReviewSessionRepository\n\n# #endregion DatasetReviewSessionRepository\n" }, { "contract_id": "DatasetReviewSessionVersionConflictError", "contract_type": "Class", "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 45, - "end_line": 58, + "start_line": 44, + "end_line": 56, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Signal optimistic-lock conflicts for dataset review session mutations." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DatasetReviewSessionVersionConflictError:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Signal optimistic-lock conflicts for dataset review session mutations.\nclass DatasetReviewSessionVersionConflictError(ValueError):\n def __init__(self, session_id: str, expected_version: int, actual_version: int):\n self.session_id = session_id\n self.expected_version = expected_version\n self.actual_version = actual_version\n super().__init__(\n f\"Session version conflict: expected {expected_version}, actual {actual_version}\"\n )\n\n\n# [/DEF:DatasetReviewSessionVersionConflictError:Class]\n" - }, - { - "contract_id": "init_repo", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 70, - "end_line": 79, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "POST": "Repository instance initialized with valid session", - "PRE": "db_session is not None", - "PURPOSE": "Bind one live SQLAlchemy session to the repository instance." - }, - "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": "def", - "body": " # [DEF:init_repo:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Bind one live SQLAlchemy session to the repository instance.\n # @PRE: db_session is not None\n # @POST: Repository instance initialized with valid session\n def __init__(self, db: Session):\n self.db = db\n self.event_logger = SessionEventLogger(db)\n\n # [/DEF:init_repo:Function]\n" - }, - { - "contract_id": "get_owned_session", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 81, - "end_line": 100, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "returns the owned session or raises a deterministic access error.", - "PRE": "session_id and user_id are non-empty identifiers from the authenticated ownership scope.", - "PURPOSE": "Resolve one owner-scoped dataset review session for mutation paths." - }, - "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": "def", - "body": " # [DEF:get_owned_session:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.\n # @PRE: session_id and user_id are non-empty identifiers from the authenticated ownership scope.\n # @POST: returns the owned session or raises a deterministic access error.\n def _get_owned_session(self, session_id: str, user_id: str) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.get_owned_session\"):\n log.reason(\"Resolving owner-scoped dataset review session\", payload={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.session_id == session_id, DatasetReviewSession.user_id == user_id)\n .first()\n )\n if not session:\n log.explore(\"Owner-scoped dataset review session lookup failed\", payload={\"session_id\": session_id, \"user_id\": user_id}, error=\"Session not found or access denied\")\n raise ValueError(\"Session not found or access denied\")\n log.reflect(\"Owner-scoped dataset review session resolved\", payload={\"session_id\": session.session_id})\n return session\n\n # [/DEF:get_owned_session:Function]\n" - }, - { - "contract_id": "create_sess", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 102, - "end_line": 115, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "session is committed, refreshed, and returned with persisted identifiers.", - "PURPOSE": "Persist an initial dataset review session shell." - }, - "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": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:create_sess:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist an initial dataset review session shell.\n # @POST: session is committed, refreshed, and returned with persisted identifiers.\n def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.create_session\"):\n log.reason(\"Persisting dataset review session shell\", payload={\"user_id\": session.user_id, \"environment_id\": session.environment_id})\n self.db.add(session)\n self.db.commit()\n self.db.refresh(session)\n log.reflect(\"Dataset review session shell persisted\", payload={\"session_id\": session.session_id})\n return session\n\n # [/DEF:create_sess:Function]\n" - }, - { - "contract_id": "require_session_version", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 117, - "end_line": 131, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "returns the same session when versions match; otherwise raises deterministic conflict error.", - "PURPOSE": "Enforce optimistic-lock version matching before a session mutation is persisted." - }, - "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": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:require_session_version:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.\n # @POST: returns the same session when versions match; otherwise raises deterministic conflict error.\n def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.require_session_version\"):\n actual_version = int(getattr(session, \"version\", 0) or 0)\n log.reason(\"Checking optimistic-lock version\", payload={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version})\n if actual_version != expected_version:\n log.explore(\"Rejected mutation due to stale session version\", payload={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version}, error=\"Optimistic lock version mismatch\")\n raise DatasetReviewSessionVersionConflictError(str(session.session_id), expected_version, actual_version)\n log.reflect(\"Optimistic-lock version accepted\", payload={\"session_id\": session.session_id, \"version\": actual_version})\n return session\n\n # [/DEF:require_session_version:Function]\n" - }, - { - "contract_id": "bump_session_version", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 133, - "end_line": 145, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "POST": "session version increments monotonically.", - "PURPOSE": "Increment optimistic-lock version after a successful session mutation is assembled." - }, - "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": "def", - "body": " # [DEF:bump_session_version:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.\n # @POST: session version increments monotonically.\n def bump_session_version(self, session: DatasetReviewSession) -> int:\n with belief_scope(\"DatasetReviewSessionRepository.bump_session_version\"):\n next_version = int(getattr(session, \"version\", 0) or 0) + 1\n setattr(session, \"version\", next_version)\n session.last_activity_at = datetime.utcnow()\n log.reflect(\"Prepared incremented session version\", payload={\"session_id\": session.session_id, \"version\": next_version})\n return next_version\n\n # [/DEF:bump_session_version:Function]\n" - }, - { - "contract_id": "commit_session_mutation", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 147, - "end_line": 172, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "POST": "session mutation is committed with one version increment or a deterministic conflict error is raised.", - "PURPOSE": "Commit one prepared session mutation and translate stale writes into deterministic conflicts." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "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": "def", - "body": " # [DEF:commit_session_mutation:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.\n # @POST: session mutation is committed with one version increment or a deterministic conflict error is raised.\n def commit_session_mutation(\n self, session: DatasetReviewSession, *, refresh_targets: Optional[List[Any]] = None, expected_version: Optional[int] = None,\n ) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.commit_session_mutation\"):\n observed_version = int(expected_version if expected_version is not None else getattr(session, \"version\", 0) or 0)\n log.reason(\"Committing session mutation with optimistic lock\", payload={\"session_id\": session.session_id, \"observed_version\": observed_version})\n self.bump_session_version(session)\n try:\n self.db.commit()\n except StaleDataError as exc:\n self.db.rollback()\n actual_version_row = self.db.query(DatasetReviewSession.version).filter(DatasetReviewSession.session_id == session.session_id).first()\n actual_version = int(actual_version_row[0] or 0) if actual_version_row else 0\n log.explore(\"Session commit rejected by optimistic lock\", payload={\"session_id\": session.session_id, \"expected_version\": observed_version, \"actual_version\": actual_version}, error=\"StaleDataError on session commit\")\n raise DatasetReviewSessionVersionConflictError(session.session_id, observed_version, actual_version) from exc\n self.db.refresh(session)\n for target in refresh_targets or []:\n self.db.refresh(target)\n log.reflect(\"Session mutation committed\", payload={\"session_id\": session.session_id, \"version\": getattr(session, \"version\", None)})\n return session\n\n # [/DEF:commit_session_mutation:Function]\n" - }, - { - "contract_id": "load_detail", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 174, - "end_line": 206, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns SessionDetail with all fields populated or None.", - "PURPOSE": "Return the full session aggregate for API and frontend resume flows." - }, - "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": "missing_required_tag", - "tag": "RELATION", - "message": "@RELATION is required for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:load_detail:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Return the full session aggregate for API and frontend resume flows.\n # @POST: Returns SessionDetail with all fields populated or None.\n def load_session_detail(self, session_id: str, user_id: str) -> Optional[DatasetReviewSession]:\n with belief_scope(\"DatasetReviewSessionRepository.load_session_detail\"):\n log.reason(\"Loading dataset review session detail\", payload={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .outerjoin(SessionCollaborator, DatasetReviewSession.session_id == SessionCollaborator.session_id)\n .options(\n joinedload(DatasetReviewSession.profile),\n joinedload(DatasetReviewSession.findings),\n joinedload(DatasetReviewSession.collaborators),\n joinedload(DatasetReviewSession.semantic_sources),\n joinedload(DatasetReviewSession.semantic_fields).joinedload(SemanticFieldEntry.candidates),\n joinedload(DatasetReviewSession.imported_filters),\n joinedload(DatasetReviewSession.template_variables),\n joinedload(DatasetReviewSession.execution_mappings),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.options),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.answer),\n joinedload(DatasetReviewSession.previews),\n joinedload(DatasetReviewSession.run_contexts),\n joinedload(DatasetReviewSession.events),\n )\n .filter(DatasetReviewSession.session_id == session_id)\n .filter(or_(DatasetReviewSession.user_id == user_id, SessionCollaborator.user_id == user_id))\n .first()\n )\n log.reflect(\"Session detail lookup completed\", payload={\"session_id\": session_id, \"found\": bool(session)})\n return session\n\n # [/DEF:load_detail:Function]\n" - }, - { - "contract_id": "save_profile_and_findings", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 208, - "end_line": 221, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "POST": "stored profile matches the current session and findings are replaced.", - "PURPOSE": "Persist profile state and replace validation findings for an owned session." - }, - "relations": [], - "schema_warnings": [ - { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "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": "def", - "body": " # [DEF:save_profile_and_findings:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Persist profile state and replace validation findings for an owned session.\n # @POST: stored profile matches the current session and findings are replaced.\n def save_profile_and_findings(\n self, session_id: str, user_id: str, profile: DatasetProfile, findings: List[ValidationFinding], expected_version: Optional[int] = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_profile_and_findings as _save\n return _save(\n self.db, self.event_logger, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, profile, findings, expected_version,\n )\n\n # [/DEF:save_profile_and_findings:Function]\n" - }, - { - "contract_id": "save_recovery_state", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 223, - "end_line": 238, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Persist imported filters, template variables, and initial execution mappings." - }, - "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": "def", - "body": " # [DEF:save_recovery_state:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist imported filters, template variables, and initial execution mappings.\n def save_recovery_state(\n self, session_id: str, user_id: str, imported_filters: List[ImportedFilter],\n template_variables: List[TemplateVariable], execution_mappings: List[ExecutionMapping],\n expected_version: Optional[int] = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_recovery_state as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, self.load_session_detail,\n session_id, user_id, imported_filters, template_variables, execution_mappings, expected_version,\n )\n\n # [/DEF:save_recovery_state:Function]\n" - }, - { - "contract_id": "save_preview", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 240, - "end_line": 252, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Persist a preview snapshot and mark prior session previews stale." - }, - "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": "def", - "body": " # [DEF:save_preview:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist a preview snapshot and mark prior session previews stale.\n def save_preview(\n self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: Optional[int] = None,\n ) -> CompiledPreview:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_preview as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, preview, expected_version,\n )\n\n # [/DEF:save_preview:Function]\n" - }, - { - "contract_id": "save_run_context", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 254, - "end_line": 266, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Persist an immutable launch audit snapshot for an owned session." - }, - "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": "def", - "body": " # [DEF:save_run_context:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Persist an immutable launch audit snapshot for an owned session.\n def save_run_context(\n self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: Optional[int] = None,\n ) -> DatasetRunContext:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_run_context as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, run_context, expected_version,\n )\n\n # [/DEF:save_run_context:Function]\n" - }, - { - "contract_id": "list_user_sess", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/repositories/session_repository.py", - "start_line": 268, - "end_line": 283, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "List review sessions owned by a specific user ordered by most recent update." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:list_user_sess:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: List review sessions owned by a specific user ordered by most recent update.\n def list_sessions_for_user(self, user_id: str) -> List[DatasetReviewSession]:\n with belief_scope(\"DatasetReviewSessionRepository.list_sessions_for_user\"):\n log.reason(\"Listing dataset review sessions for owner scope\", payload={\"user_id\": user_id})\n sessions = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.user_id == user_id)\n .order_by(DatasetReviewSession.updated_at.desc())\n .all()\n )\n log.reflect(\"Session list assembled\", payload={\"user_id\": user_id, \"session_count\": len(sessions)})\n return sessions\n\n # [/DEF:list_user_sess:Function]\n" + "anchor_syntax": "region", + "body": "# #region DatasetReviewSessionVersionConflictError [C:2] [TYPE Class]\n# @BRIEF Signal optimistic-lock conflicts for dataset review session mutations.\nclass DatasetReviewSessionVersionConflictError(ValueError):\n def __init__(self, session_id: str, expected_version: int, actual_version: int):\n self.session_id = session_id\n self.expected_version = expected_version\n self.actual_version = actual_version\n super().__init__(\n f\"Session version conflict: expected {expected_version}, actual {actual_version}\"\n )\n\n\n# #endregion DatasetReviewSessionVersionConflictError\n" }, { "contract_id": "SemanticSourceResolver", "contract_type": "Module", "file_path": "backend/src/services/dataset_review/semantic_resolver.py", "start_line": 1, - "end_line": 394, + "end_line": 387, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "INVARIANT": "Manual overrides are never silently replaced by imported, inferred, or AI-generated values.", "LAYER": "Domain", "POST": "candidate ranking follows the configured confidence hierarchy and unresolved fuzzy matches remain reviewable.", "PRE": "selected source and target field set must be known.", "PURPOSE": "Resolve and rank semantic candidates from trusted dictionary-like sources before any inferred fallback.", - "SEMANTICS": [ - "dataset_review", - "semantic_resolution", - "dictionary", - "trusted_sources", - "ranking" - ], "SIDE_EFFECT": "may create conflict findings and semantic candidate records." }, "relations": [ { "source_id": "SemanticSourceResolver", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LLMProviderService", "target_ref": "[LLMProviderService]" }, { "source_id": "SemanticSourceResolver", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SemanticSource", "target_ref": "[SemanticSource]" }, { "source_id": "SemanticSourceResolver", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SemanticFieldEntry", "target_ref": "[SemanticFieldEntry]" }, { "source_id": "SemanticSourceResolver", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SemanticCandidate", "target_ref": "[SemanticCandidate]" } @@ -86439,82 +62498,10 @@ "actual_complexity": 4, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:SemanticSourceResolver:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: dataset_review, semantic_resolution, dictionary, trusted_sources, ranking\n# @PURPOSE: Resolve and rank semantic candidates from trusted dictionary-like sources before any inferred fallback.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[LLMProviderService]\n# @RELATION: [DEPENDS_ON] ->[SemanticSource]\n# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]\n# @RELATION: [DEPENDS_ON] ->[SemanticCandidate]\n# @PRE: selected source and target field set must be known.\n# @POST: candidate ranking follows the configured confidence hierarchy and unresolved fuzzy matches remain reviewable.\n# @SIDE_EFFECT: may create conflict findings and semantic candidate records.\n# @INVARIANT: Manual overrides are never silently replaced by imported, inferred, or AI-generated values.\n\nfrom __future__ import annotations\n\n# [DEF:imports:Block]\nfrom dataclasses import dataclass, field\nfrom difflib import SequenceMatcher\nfrom typing import Any, Dict, Iterable, List, Mapping, Optional\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.dataset_review import (\n CandidateMatchType,\n CandidateStatus,\n FieldProvenance,\n SemanticSource,\n)\n# [/DEF:imports:Block]\n\nlog = MarkerLogger(\"SemanticSourceResolver\")\n\n\n# [DEF:DictionaryResolutionResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Carries field-level dictionary resolution output with explicit review and partial-recovery state.\n@dataclass\nclass DictionaryResolutionResult:\n source_ref: str\n resolved_fields: List[Dict[str, Any]] = field(default_factory=list)\n unresolved_fields: List[str] = field(default_factory=list)\n partial_recovery: bool = False\n# [/DEF:DictionaryResolutionResult:Class]\n\n\n# [DEF:SemanticSourceResolver:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Resolve semantic candidates from trusted sources while preserving manual locks and confidence ordering.\n# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]\n# @RELATION: [DEPENDS_ON] ->[SemanticCandidate]\n# @PRE: source payload and target field collection are provided by the caller.\n# @POST: result contains confidence-ranked candidates and does not overwrite manual locks implicitly.\n# @SIDE_EFFECT: emits semantic trace logs for ranking and fallback decisions.\nclass SemanticSourceResolver:\n # [DEF:resolve_from_file:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize uploaded semantic file records into field-level candidates.\n def resolve_from_file(self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]]) -> DictionaryResolutionResult:\n return DictionaryResolutionResult(source_ref=str(source_payload.get(\"source_ref\") or \"uploaded_file\"))\n # [/DEF:resolve_from_file:Function]\n\n # [DEF:resolve_from_dictionary:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Resolve candidates from connected tabular dictionary sources.\n # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]\n # @RELATION: [DEPENDS_ON] ->[SemanticCandidate]\n # @PRE: dictionary source exists and fields contain stable field_name values.\n # @POST: returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit.\n # @SIDE_EFFECT: emits belief-state logs describing trusted-match and partial-recovery outcomes.\n # @DATA_CONTRACT: Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult]\n def resolve_from_dictionary(\n self,\n source_payload: Mapping[str, Any],\n fields: Iterable[Mapping[str, Any]],\n ) -> DictionaryResolutionResult:\n with belief_scope(\"SemanticSourceResolver.resolve_from_dictionary\"):\n source_ref = str(source_payload.get(\"source_ref\") or \"\").strip()\n dictionary_rows = source_payload.get(\"rows\")\n\n if not source_ref:\n log.explore(\"Dictionary semantic source is missing source_ref\", error=\"No source_ref in dictionary source\")\n raise ValueError(\"Dictionary semantic source must include source_ref\")\n\n if not isinstance(dictionary_rows, list) or not dictionary_rows:\n log.explore(\"Dictionary semantic source has no usable rows\", error=\"Dictionary rows is empty or not a list\", payload={\"source_ref\": source_ref})\n raise ValueError(\"Dictionary semantic source must include non-empty rows\")\n\n log.reason(\n \"Resolving semantics from trusted dictionary source\",\n payload={\"source_ref\": source_ref, \"row_count\": len(dictionary_rows)},\n )\n\n normalized_rows = [self._normalize_dictionary_row(row) for row in dictionary_rows if isinstance(row, Mapping)]\n row_index = {\n row[\"field_key\"]: row\n for row in normalized_rows\n if row.get(\"field_key\")\n }\n\n resolved_fields: List[Dict[str, Any]] = []\n unresolved_fields: List[str] = []\n\n for raw_field in fields:\n field_name = str(raw_field.get(\"field_name\") or \"\").strip()\n if not field_name:\n continue\n\n is_locked = bool(raw_field.get(\"is_locked\"))\n if is_locked:\n log.reason(\n \"Preserving manual lock during dictionary resolution\",\n payload={\"field_name\": field_name},\n )\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": None,\n \"candidates\": [],\n \"provenance\": FieldProvenance.MANUAL_OVERRIDE.value,\n \"needs_review\": False,\n \"has_conflict\": False,\n \"is_locked\": True,\n \"status\": \"preserved_manual\",\n }\n )\n continue\n\n exact_match = row_index.get(self._normalize_key(field_name))\n candidates: List[Dict[str, Any]] = []\n\n if exact_match is not None:\n log.reason(\n \"Resolved exact dictionary match\",\n payload={\"field_name\": field_name, \"source_ref\": source_ref},\n )\n candidates.append(\n self._build_candidate_payload(\n rank=1,\n match_type=CandidateMatchType.EXACT,\n confidence_score=1.0,\n row=exact_match,\n )\n )\n else:\n fuzzy_matches = self._find_fuzzy_matches(field_name, normalized_rows)\n for rank_offset, fuzzy_match in enumerate(fuzzy_matches, start=1):\n candidates.append(\n self._build_candidate_payload(\n rank=rank_offset,\n match_type=CandidateMatchType.FUZZY,\n confidence_score=float(fuzzy_match[\"score\"]),\n row=fuzzy_match[\"row\"],\n )\n )\n\n if not candidates:\n unresolved_fields.append(field_name)\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": None,\n \"candidates\": [],\n \"provenance\": FieldProvenance.UNRESOLVED.value,\n \"needs_review\": True,\n \"has_conflict\": False,\n \"is_locked\": False,\n \"status\": \"unresolved\",\n }\n )\n log.explore(\"No trusted dictionary match found for field\", error=\"No dictionary match found for field\", payload={\"field_name\": field_name, \"source_ref\": source_ref})\n continue\n\n ranked_candidates = self.rank_candidates(candidates)\n applied_candidate = ranked_candidates[0]\n has_conflict = len(ranked_candidates) > 1\n provenance = (\n FieldProvenance.DICTIONARY_EXACT.value\n if applied_candidate[\"match_type\"] == CandidateMatchType.EXACT.value\n else FieldProvenance.FUZZY_INFERRED.value\n )\n needs_review = applied_candidate[\"match_type\"] != CandidateMatchType.EXACT.value\n\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": applied_candidate,\n \"candidates\": ranked_candidates,\n \"provenance\": provenance,\n \"needs_review\": needs_review,\n \"has_conflict\": has_conflict,\n \"is_locked\": False,\n \"status\": \"resolved\",\n }\n )\n\n result = DictionaryResolutionResult(\n source_ref=source_ref,\n resolved_fields=resolved_fields,\n unresolved_fields=unresolved_fields,\n partial_recovery=bool(unresolved_fields),\n )\n log.reflect(\n \"Dictionary resolution completed\",\n payload={\n \"source_ref\": source_ref,\n \"resolved_fields\": len(resolved_fields),\n \"unresolved_fields\": len(unresolved_fields),\n \"partial_recovery\": result.partial_recovery,\n },\n )\n return result\n # [/DEF:resolve_from_dictionary:Function]\n\n # [DEF:resolve_from_reference_dataset:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Reuse semantic metadata from trusted Superset datasets.\n def resolve_from_reference_dataset(\n self,\n source_payload: Mapping[str, Any],\n fields: Iterable[Mapping[str, Any]],\n ) -> DictionaryResolutionResult:\n return DictionaryResolutionResult(source_ref=str(source_payload.get(\"source_ref\") or \"reference_dataset\"))\n # [/DEF:resolve_from_reference_dataset:Function]\n\n # [DEF:rank_candidates:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Apply confidence ordering and determine best candidate per field.\n # @RELATION: [DEPENDS_ON] ->[SemanticCandidate]\n def rank_candidates(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n ranked = sorted(\n candidates,\n key=lambda candidate: (\n self._match_priority(candidate.get(\"match_type\")),\n -float(candidate.get(\"confidence_score\", 0.0)),\n int(candidate.get(\"candidate_rank\", 999)),\n ),\n )\n for index, candidate in enumerate(ranked, start=1):\n candidate[\"candidate_rank\"] = index\n return ranked\n # [/DEF:rank_candidates:Function]\n\n # [DEF:detect_conflicts:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Mark competing candidate sets that require explicit user review.\n def detect_conflicts(self, candidates: List[Dict[str, Any]]) -> bool:\n return len(candidates) > 1\n # [/DEF:detect_conflicts:Function]\n\n # [DEF:apply_field_decision:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Accept, reject, or manually override a field-level semantic value.\n def apply_field_decision(self, field_state: Mapping[str, Any], decision: Mapping[str, Any]) -> Dict[str, Any]:\n merged = dict(field_state)\n merged.update(decision)\n return merged\n # [/DEF:apply_field_decision:Function]\n\n # [DEF:propagate_source_version_update:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values.\n # @RELATION: [DEPENDS_ON] ->[SemanticSource]\n # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]\n # @PRE: source is persisted and fields belong to the same session aggregate.\n # @POST: unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched.\n # @SIDE_EFFECT: mutates in-memory field state for the caller to persist.\n # @DATA_CONTRACT: Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]]\n def propagate_source_version_update(\n self,\n source: SemanticSource,\n fields: Iterable[Any],\n ) -> Dict[str, int]:\n with belief_scope(\"SemanticSourceResolver.propagate_source_version_update\"):\n source_id = str(source.source_id or \"\").strip()\n source_version = str(source.source_version or \"\").strip()\n if not source_id or not source_version:\n log.explore(\"Semantic source version propagation rejected due to incomplete source metadata\", error=\"Source metadata incomplete (missing source_id or source_version)\", payload={\"source_id\": source_id, \"source_version\": source_version})\n raise ValueError(\"Semantic source must provide source_id and source_version\")\n\n propagated = 0\n preserved_locked = 0\n untouched = 0\n for field in fields:\n if str(getattr(field, \"source_id\", \"\") or \"\").strip() != source_id:\n untouched += 1\n continue\n if bool(getattr(field, \"is_locked\", False)) or getattr(field, \"provenance\", None) == FieldProvenance.MANUAL_OVERRIDE:\n preserved_locked += 1\n continue\n\n field.source_version = source_version\n field.needs_review = True\n field.has_conflict = bool(getattr(field, \"has_conflict\", False))\n propagated += 1\n\n log.reflect(\n \"Semantic source version propagation completed\",\n payload={\n \"source_id\": source_id,\n \"source_version\": source_version,\n \"propagated\": propagated,\n \"preserved_locked\": preserved_locked,\n \"untouched\": untouched,\n },\n )\n return {\n \"propagated\": propagated,\n \"preserved_locked\": preserved_locked,\n \"untouched\": untouched,\n }\n # [/DEF:propagate_source_version_update:Function]\n\n # [DEF:_normalize_dictionary_row:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize one dictionary row into a consistent lookup structure.\n def _normalize_dictionary_row(self, row: Mapping[str, Any]) -> Dict[str, Any]:\n field_name = (\n row.get(\"field_name\")\n or row.get(\"column_name\")\n or row.get(\"name\")\n or row.get(\"field\")\n )\n normalized_name = str(field_name or \"\").strip()\n return {\n \"field_name\": normalized_name,\n \"field_key\": self._normalize_key(normalized_name),\n \"verbose_name\": row.get(\"verbose_name\") or row.get(\"label\"),\n \"description\": row.get(\"description\"),\n \"display_format\": row.get(\"display_format\") or row.get(\"format\"),\n }\n # [/DEF:_normalize_dictionary_row:Function]\n\n # [DEF:_find_fuzzy_matches:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Produce confidence-scored fuzzy matches while keeping them reviewable.\n def _find_fuzzy_matches(self, field_name: str, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n normalized_target = self._normalize_key(field_name)\n fuzzy_matches: List[Dict[str, Any]] = []\n for row in rows:\n candidate_key = str(row.get(\"field_key\") or \"\")\n if not candidate_key:\n continue\n score = SequenceMatcher(None, normalized_target, candidate_key).ratio()\n if score < 0.72:\n continue\n fuzzy_matches.append({\"row\": row, \"score\": round(score, 3)})\n fuzzy_matches.sort(key=lambda item: item[\"score\"], reverse=True)\n return fuzzy_matches[:3]\n # [/DEF:_find_fuzzy_matches:Function]\n\n # [DEF:_build_candidate_payload:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Project normalized dictionary rows into semantic candidate payloads.\n def _build_candidate_payload(\n self,\n rank: int,\n match_type: CandidateMatchType,\n confidence_score: float,\n row: Mapping[str, Any],\n ) -> Dict[str, Any]:\n return {\n \"candidate_rank\": rank,\n \"match_type\": match_type.value,\n \"confidence_score\": confidence_score,\n \"proposed_verbose_name\": row.get(\"verbose_name\"),\n \"proposed_description\": row.get(\"description\"),\n \"proposed_display_format\": row.get(\"display_format\"),\n \"status\": CandidateStatus.PROPOSED.value,\n }\n # [/DEF:_build_candidate_payload:Function]\n\n # [DEF:_match_priority:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention.\n def _match_priority(self, match_type: Optional[str]) -> int:\n priority = {\n CandidateMatchType.EXACT.value: 0,\n CandidateMatchType.REFERENCE.value: 1,\n CandidateMatchType.FUZZY.value: 2,\n CandidateMatchType.GENERATED.value: 3,\n }\n return priority.get(str(match_type or \"\"), 99)\n # [/DEF:_match_priority:Function]\n\n # [DEF:_normalize_key:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize field identifiers for stable exact/fuzzy comparisons.\n def _normalize_key(self, value: str) -> str:\n return \"\".join(ch for ch in str(value or \"\").strip().lower() if ch.isalnum() or ch == \"_\")\n # [/DEF:_normalize_key:Function]\n# [/DEF:SemanticSourceResolver:Class]\n\n# [/DEF:SemanticSourceResolver:Module]\n" + "anchor_syntax": "region", + "body": "# #region SemanticSourceResolver [C:4] [TYPE Module] [SEMANTICS pydantic, dataset, semantic, resolver, mapping]\n# @BRIEF Resolve and rank semantic candidates from trusted dictionary-like sources before any inferred fallback.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [SemanticSource]\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @PRE: selected source and target field set must be known.\n# @POST: candidate ranking follows the configured confidence hierarchy and unresolved fuzzy matches remain reviewable.\n# @SIDE_EFFECT: may create conflict findings and semantic candidate records.\n# @INVARIANT: Manual overrides are never silently replaced by imported, inferred, or AI-generated values.\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable, Mapping\n\n# #region imports [TYPE Block]\nfrom dataclasses import dataclass, field\nfrom difflib import SequenceMatcher\nfrom typing import Any\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n CandidateMatchType,\n CandidateStatus,\n FieldProvenance,\n SemanticSource,\n)\n\n# #endregion imports\n\n\n# #region DictionaryResolutionResult [C:2] [TYPE Class]\n# @BRIEF Carries field-level dictionary resolution output with explicit review and partial-recovery state.\n@dataclass\nclass DictionaryResolutionResult:\n source_ref: str\n resolved_fields: list[dict[str, Any]] = field(default_factory=list)\n unresolved_fields: list[str] = field(default_factory=list)\n partial_recovery: bool = False\n# #endregion DictionaryResolutionResult\n\n\n# #region SemanticSourceResolver [C:4] [TYPE Class]\n# @BRIEF Resolve semantic candidates from trusted sources while preserving manual locks and confidence ordering.\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @PRE: source payload and target field collection are provided by the caller.\n# @POST: result contains confidence-ranked candidates and does not overwrite manual locks implicitly.\n# @SIDE_EFFECT: emits semantic trace logs for ranking and fallback decisions.\nclass SemanticSourceResolver:\n # region resolve_from_file [TYPE Function]\n # @PURPOSE: Normalize uploaded semantic file records into field-level candidates.\n def resolve_from_file(self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]]) -> DictionaryResolutionResult:\n return DictionaryResolutionResult(source_ref=str(source_payload.get(\"source_ref\") or \"uploaded_file\"))\n # endregion resolve_from_file\n\n # region resolve_from_dictionary [TYPE Function]\n # @PURPOSE: Resolve candidates from connected tabular dictionary sources.\n # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]\n # @RELATION: [DEPENDS_ON] ->[SemanticCandidate]\n # @PRE: dictionary source exists and fields contain stable field_name values.\n # @POST: returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit.\n # @SIDE_EFFECT: emits belief-state logs describing trusted-match and partial-recovery outcomes.\n # @DATA_CONTRACT: Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult]\n def resolve_from_dictionary(\n self,\n source_payload: Mapping[str, Any],\n fields: Iterable[Mapping[str, Any]],\n ) -> DictionaryResolutionResult:\n with belief_scope(\"SemanticSourceResolver.resolve_from_dictionary\"):\n source_ref = str(source_payload.get(\"source_ref\") or \"\").strip()\n dictionary_rows = source_payload.get(\"rows\")\n\n if not source_ref:\n logger.explore(\"Dictionary semantic source is missing source_ref\")\n raise ValueError(\"Dictionary semantic source must include source_ref\")\n\n if not isinstance(dictionary_rows, list) or not dictionary_rows:\n logger.explore(\n \"Dictionary semantic source has no usable rows\",\n extra={\"source_ref\": source_ref},\n )\n raise ValueError(\"Dictionary semantic source must include non-empty rows\")\n\n logger.reason(\n \"Resolving semantics from trusted dictionary source\",\n extra={\"source_ref\": source_ref, \"row_count\": len(dictionary_rows)},\n )\n\n normalized_rows = [self._normalize_dictionary_row(row) for row in dictionary_rows if isinstance(row, Mapping)]\n row_index = {\n row[\"field_key\"]: row\n for row in normalized_rows\n if row.get(\"field_key\")\n }\n\n resolved_fields: list[dict[str, Any]] = []\n unresolved_fields: list[str] = []\n\n for raw_field in fields:\n field_name = str(raw_field.get(\"field_name\") or \"\").strip()\n if not field_name:\n continue\n\n is_locked = bool(raw_field.get(\"is_locked\"))\n if is_locked:\n logger.reason(\n \"Preserving manual lock during dictionary resolution\",\n extra={\"field_name\": field_name},\n )\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": None,\n \"candidates\": [],\n \"provenance\": FieldProvenance.MANUAL_OVERRIDE.value,\n \"needs_review\": False,\n \"has_conflict\": False,\n \"is_locked\": True,\n \"status\": \"preserved_manual\",\n }\n )\n continue\n\n exact_match = row_index.get(self._normalize_key(field_name))\n candidates: list[dict[str, Any]] = []\n\n if exact_match is not None:\n logger.reason(\n \"Resolved exact dictionary match\",\n extra={\"field_name\": field_name, \"source_ref\": source_ref},\n )\n candidates.append(\n self._build_candidate_payload(\n rank=1,\n match_type=CandidateMatchType.EXACT,\n confidence_score=1.0,\n row=exact_match,\n )\n )\n else:\n fuzzy_matches = self._find_fuzzy_matches(field_name, normalized_rows)\n for rank_offset, fuzzy_match in enumerate(fuzzy_matches, start=1):\n candidates.append(\n self._build_candidate_payload(\n rank=rank_offset,\n match_type=CandidateMatchType.FUZZY,\n confidence_score=float(fuzzy_match[\"score\"]),\n row=fuzzy_match[\"row\"],\n )\n )\n\n if not candidates:\n unresolved_fields.append(field_name)\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": None,\n \"candidates\": [],\n \"provenance\": FieldProvenance.UNRESOLVED.value,\n \"needs_review\": True,\n \"has_conflict\": False,\n \"is_locked\": False,\n \"status\": \"unresolved\",\n }\n )\n logger.explore(\n \"No trusted dictionary match found for field\",\n extra={\"field_name\": field_name, \"source_ref\": source_ref},\n )\n continue\n\n ranked_candidates = self.rank_candidates(candidates)\n applied_candidate = ranked_candidates[0]\n has_conflict = len(ranked_candidates) > 1\n provenance = (\n FieldProvenance.DICTIONARY_EXACT.value\n if applied_candidate[\"match_type\"] == CandidateMatchType.EXACT.value\n else FieldProvenance.FUZZY_INFERRED.value\n )\n needs_review = applied_candidate[\"match_type\"] != CandidateMatchType.EXACT.value\n\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": applied_candidate,\n \"candidates\": ranked_candidates,\n \"provenance\": provenance,\n \"needs_review\": needs_review,\n \"has_conflict\": has_conflict,\n \"is_locked\": False,\n \"status\": \"resolved\",\n }\n )\n\n result = DictionaryResolutionResult(\n source_ref=source_ref,\n resolved_fields=resolved_fields,\n unresolved_fields=unresolved_fields,\n partial_recovery=bool(unresolved_fields),\n )\n logger.reflect(\n \"Dictionary resolution completed\",\n extra={\n \"source_ref\": source_ref,\n \"resolved_fields\": len(resolved_fields),\n \"unresolved_fields\": len(unresolved_fields),\n \"partial_recovery\": result.partial_recovery,\n },\n )\n return result\n # endregion resolve_from_dictionary\n\n # region resolve_from_reference_dataset [TYPE Function]\n # @PURPOSE: Reuse semantic metadata from trusted Superset datasets.\n def resolve_from_reference_dataset(\n self,\n source_payload: Mapping[str, Any],\n fields: Iterable[Mapping[str, Any]],\n ) -> DictionaryResolutionResult:\n return DictionaryResolutionResult(source_ref=str(source_payload.get(\"source_ref\") or \"reference_dataset\"))\n # endregion resolve_from_reference_dataset\n\n # region rank_candidates [TYPE Function]\n # @PURPOSE: Apply confidence ordering and determine best candidate per field.\n # @RELATION: [DEPENDS_ON] ->[SemanticCandidate]\n def rank_candidates(self, candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:\n ranked = sorted(\n candidates,\n key=lambda candidate: (\n self._match_priority(candidate.get(\"match_type\")),\n -float(candidate.get(\"confidence_score\", 0.0)),\n int(candidate.get(\"candidate_rank\", 999)),\n ),\n )\n for index, candidate in enumerate(ranked, start=1):\n candidate[\"candidate_rank\"] = index\n return ranked\n # endregion rank_candidates\n\n # region detect_conflicts [TYPE Function]\n # @PURPOSE: Mark competing candidate sets that require explicit user review.\n def detect_conflicts(self, candidates: list[dict[str, Any]]) -> bool:\n return len(candidates) > 1\n # endregion detect_conflicts\n\n # region apply_field_decision [TYPE Function]\n # @PURPOSE: Accept, reject, or manually override a field-level semantic value.\n def apply_field_decision(self, field_state: Mapping[str, Any], decision: Mapping[str, Any]) -> dict[str, Any]:\n merged = dict(field_state)\n merged.update(decision)\n return merged\n # endregion apply_field_decision\n\n # region propagate_source_version_update [TYPE Function]\n # @PURPOSE: Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values.\n # @RELATION: [DEPENDS_ON] ->[SemanticSource]\n # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]\n # @PRE: source is persisted and fields belong to the same session aggregate.\n # @POST: unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched.\n # @SIDE_EFFECT: mutates in-memory field state for the caller to persist.\n # @DATA_CONTRACT: Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]]\n def propagate_source_version_update(\n self,\n source: SemanticSource,\n fields: Iterable[Any],\n ) -> dict[str, int]:\n with belief_scope(\"SemanticSourceResolver.propagate_source_version_update\"):\n source_id = str(source.source_id or \"\").strip()\n source_version = str(source.source_version or \"\").strip()\n if not source_id or not source_version:\n logger.explore(\n \"Semantic source version propagation rejected due to incomplete source metadata\",\n extra={\"source_id\": source_id, \"source_version\": source_version},\n )\n raise ValueError(\"Semantic source must provide source_id and source_version\")\n\n propagated = 0\n preserved_locked = 0\n untouched = 0\n for field in fields:\n if str(getattr(field, \"source_id\", \"\") or \"\").strip() != source_id:\n untouched += 1\n continue\n if bool(getattr(field, \"is_locked\", False)) or getattr(field, \"provenance\", None) == FieldProvenance.MANUAL_OVERRIDE:\n preserved_locked += 1\n continue\n\n field.source_version = source_version\n field.needs_review = True\n field.has_conflict = bool(getattr(field, \"has_conflict\", False))\n propagated += 1\n\n logger.reflect(\n \"Semantic source version propagation completed\",\n extra={\n \"source_id\": source_id,\n \"source_version\": source_version,\n \"propagated\": propagated,\n \"preserved_locked\": preserved_locked,\n \"untouched\": untouched,\n },\n )\n return {\n \"propagated\": propagated,\n \"preserved_locked\": preserved_locked,\n \"untouched\": untouched,\n }\n # endregion propagate_source_version_update\n\n # region _normalize_dictionary_row [TYPE Function]\n # @PURPOSE: Normalize one dictionary row into a consistent lookup structure.\n def _normalize_dictionary_row(self, row: Mapping[str, Any]) -> dict[str, Any]:\n field_name = (\n row.get(\"field_name\")\n or row.get(\"column_name\")\n or row.get(\"name\")\n or row.get(\"field\")\n )\n normalized_name = str(field_name or \"\").strip()\n return {\n \"field_name\": normalized_name,\n \"field_key\": self._normalize_key(normalized_name),\n \"verbose_name\": row.get(\"verbose_name\") or row.get(\"label\"),\n \"description\": row.get(\"description\"),\n \"display_format\": row.get(\"display_format\") or row.get(\"format\"),\n }\n # endregion _normalize_dictionary_row\n\n # region _find_fuzzy_matches [TYPE Function]\n # @PURPOSE: Produce confidence-scored fuzzy matches while keeping them reviewable.\n def _find_fuzzy_matches(self, field_name: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:\n normalized_target = self._normalize_key(field_name)\n fuzzy_matches: list[dict[str, Any]] = []\n for row in rows:\n candidate_key = str(row.get(\"field_key\") or \"\")\n if not candidate_key:\n continue\n score = SequenceMatcher(None, normalized_target, candidate_key).ratio()\n if score < 0.72:\n continue\n fuzzy_matches.append({\"row\": row, \"score\": round(score, 3)})\n fuzzy_matches.sort(key=lambda item: item[\"score\"], reverse=True)\n return fuzzy_matches[:3]\n # endregion _find_fuzzy_matches\n\n # region _build_candidate_payload [TYPE Function]\n # @PURPOSE: Project normalized dictionary rows into semantic candidate payloads.\n def _build_candidate_payload(\n self,\n rank: int,\n match_type: CandidateMatchType,\n confidence_score: float,\n row: Mapping[str, Any],\n ) -> dict[str, Any]:\n return {\n \"candidate_rank\": rank,\n \"match_type\": match_type.value,\n \"confidence_score\": confidence_score,\n \"proposed_verbose_name\": row.get(\"verbose_name\"),\n \"proposed_description\": row.get(\"description\"),\n \"proposed_display_format\": row.get(\"display_format\"),\n \"status\": CandidateStatus.PROPOSED.value,\n }\n # endregion _build_candidate_payload\n\n # region _match_priority [TYPE Function]\n # @PURPOSE: Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention.\n def _match_priority(self, match_type: str | None) -> int:\n priority = {\n CandidateMatchType.EXACT.value: 0,\n CandidateMatchType.REFERENCE.value: 1,\n CandidateMatchType.FUZZY.value: 2,\n CandidateMatchType.GENERATED.value: 3,\n }\n return priority.get(str(match_type or \"\"), 99)\n # endregion _match_priority\n\n # region _normalize_key [TYPE Function]\n # @PURPOSE: Normalize field identifiers for stable exact/fuzzy comparisons.\n def _normalize_key(self, value: str) -> str:\n return \"\".join(ch for ch in str(value or \"\").strip().lower() if ch.isalnum() or ch == \"_\")\n # endregion _normalize_key\n# #endregion SemanticSourceResolver\n\n# #endregion SemanticSourceResolver\n" }, { "contract_id": "imports", @@ -86527,411 +62514,39 @@ "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:imports:Block]\nfrom dataclasses import dataclass, field\nfrom difflib import SequenceMatcher\nfrom typing import Any, Dict, Iterable, List, Mapping, Optional\n\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\nfrom src.models.dataset_review import (\n CandidateMatchType,\n CandidateStatus,\n FieldProvenance,\n SemanticSource,\n)\n# [/DEF:imports:Block]\n" + "anchor_syntax": "region", + "body": "# #region imports [TYPE Block]\nfrom dataclasses import dataclass, field\nfrom difflib import SequenceMatcher\nfrom typing import Any\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n CandidateMatchType,\n CandidateStatus,\n FieldProvenance,\n SemanticSource,\n)\n\n# #endregion imports\n" }, { "contract_id": "DictionaryResolutionResult", "contract_type": "Class", "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 35, - "end_line": 44, + "start_line": 33, + "end_line": 41, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Carries field-level dictionary resolution output with explicit review and partial-recovery state." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:DictionaryResolutionResult:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Carries field-level dictionary resolution output with explicit review and partial-recovery state.\n@dataclass\nclass DictionaryResolutionResult:\n source_ref: str\n resolved_fields: List[Dict[str, Any]] = field(default_factory=list)\n unresolved_fields: List[str] = field(default_factory=list)\n partial_recovery: bool = False\n# [/DEF:DictionaryResolutionResult:Class]\n" - }, - { - "contract_id": "resolve_from_file", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 56, - "end_line": 61, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Normalize uploaded semantic file records into field-level candidates." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:resolve_from_file:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize uploaded semantic file records into field-level candidates.\n def resolve_from_file(self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]]) -> DictionaryResolutionResult:\n return DictionaryResolutionResult(source_ref=str(source_payload.get(\"source_ref\") or \"uploaded_file\"))\n # [/DEF:resolve_from_file:Function]\n" - }, - { - "contract_id": "resolve_from_dictionary", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 63, - "end_line": 213, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult]", - "POST": "returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit.", - "PRE": "dictionary source exists and fields contain stable field_name values.", - "PURPOSE": "Resolve candidates from connected tabular dictionary sources.", - "SIDE_EFFECT": "emits belief-state logs describing trusted-match and partial-recovery outcomes." - }, - "relations": [ - { - "source_id": "resolve_from_dictionary", - "relation_type": "[DEPENDS_ON]", - "target_id": "SemanticFieldEntry", - "target_ref": "[SemanticFieldEntry]" - }, - { - "source_id": "resolve_from_dictionary", - "relation_type": "[DEPENDS_ON]", - "target_id": "SemanticCandidate", - "target_ref": "[SemanticCandidate]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:resolve_from_dictionary:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Resolve candidates from connected tabular dictionary sources.\n # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]\n # @RELATION: [DEPENDS_ON] ->[SemanticCandidate]\n # @PRE: dictionary source exists and fields contain stable field_name values.\n # @POST: returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit.\n # @SIDE_EFFECT: emits belief-state logs describing trusted-match and partial-recovery outcomes.\n # @DATA_CONTRACT: Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult]\n def resolve_from_dictionary(\n self,\n source_payload: Mapping[str, Any],\n fields: Iterable[Mapping[str, Any]],\n ) -> DictionaryResolutionResult:\n with belief_scope(\"SemanticSourceResolver.resolve_from_dictionary\"):\n source_ref = str(source_payload.get(\"source_ref\") or \"\").strip()\n dictionary_rows = source_payload.get(\"rows\")\n\n if not source_ref:\n log.explore(\"Dictionary semantic source is missing source_ref\", error=\"No source_ref in dictionary source\")\n raise ValueError(\"Dictionary semantic source must include source_ref\")\n\n if not isinstance(dictionary_rows, list) or not dictionary_rows:\n log.explore(\"Dictionary semantic source has no usable rows\", error=\"Dictionary rows is empty or not a list\", payload={\"source_ref\": source_ref})\n raise ValueError(\"Dictionary semantic source must include non-empty rows\")\n\n log.reason(\n \"Resolving semantics from trusted dictionary source\",\n payload={\"source_ref\": source_ref, \"row_count\": len(dictionary_rows)},\n )\n\n normalized_rows = [self._normalize_dictionary_row(row) for row in dictionary_rows if isinstance(row, Mapping)]\n row_index = {\n row[\"field_key\"]: row\n for row in normalized_rows\n if row.get(\"field_key\")\n }\n\n resolved_fields: List[Dict[str, Any]] = []\n unresolved_fields: List[str] = []\n\n for raw_field in fields:\n field_name = str(raw_field.get(\"field_name\") or \"\").strip()\n if not field_name:\n continue\n\n is_locked = bool(raw_field.get(\"is_locked\"))\n if is_locked:\n log.reason(\n \"Preserving manual lock during dictionary resolution\",\n payload={\"field_name\": field_name},\n )\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": None,\n \"candidates\": [],\n \"provenance\": FieldProvenance.MANUAL_OVERRIDE.value,\n \"needs_review\": False,\n \"has_conflict\": False,\n \"is_locked\": True,\n \"status\": \"preserved_manual\",\n }\n )\n continue\n\n exact_match = row_index.get(self._normalize_key(field_name))\n candidates: List[Dict[str, Any]] = []\n\n if exact_match is not None:\n log.reason(\n \"Resolved exact dictionary match\",\n payload={\"field_name\": field_name, \"source_ref\": source_ref},\n )\n candidates.append(\n self._build_candidate_payload(\n rank=1,\n match_type=CandidateMatchType.EXACT,\n confidence_score=1.0,\n row=exact_match,\n )\n )\n else:\n fuzzy_matches = self._find_fuzzy_matches(field_name, normalized_rows)\n for rank_offset, fuzzy_match in enumerate(fuzzy_matches, start=1):\n candidates.append(\n self._build_candidate_payload(\n rank=rank_offset,\n match_type=CandidateMatchType.FUZZY,\n confidence_score=float(fuzzy_match[\"score\"]),\n row=fuzzy_match[\"row\"],\n )\n )\n\n if not candidates:\n unresolved_fields.append(field_name)\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": None,\n \"candidates\": [],\n \"provenance\": FieldProvenance.UNRESOLVED.value,\n \"needs_review\": True,\n \"has_conflict\": False,\n \"is_locked\": False,\n \"status\": \"unresolved\",\n }\n )\n log.explore(\"No trusted dictionary match found for field\", error=\"No dictionary match found for field\", payload={\"field_name\": field_name, \"source_ref\": source_ref})\n continue\n\n ranked_candidates = self.rank_candidates(candidates)\n applied_candidate = ranked_candidates[0]\n has_conflict = len(ranked_candidates) > 1\n provenance = (\n FieldProvenance.DICTIONARY_EXACT.value\n if applied_candidate[\"match_type\"] == CandidateMatchType.EXACT.value\n else FieldProvenance.FUZZY_INFERRED.value\n )\n needs_review = applied_candidate[\"match_type\"] != CandidateMatchType.EXACT.value\n\n resolved_fields.append(\n {\n \"field_name\": field_name,\n \"applied_candidate\": applied_candidate,\n \"candidates\": ranked_candidates,\n \"provenance\": provenance,\n \"needs_review\": needs_review,\n \"has_conflict\": has_conflict,\n \"is_locked\": False,\n \"status\": \"resolved\",\n }\n )\n\n result = DictionaryResolutionResult(\n source_ref=source_ref,\n resolved_fields=resolved_fields,\n unresolved_fields=unresolved_fields,\n partial_recovery=bool(unresolved_fields),\n )\n log.reflect(\n \"Dictionary resolution completed\",\n payload={\n \"source_ref\": source_ref,\n \"resolved_fields\": len(resolved_fields),\n \"unresolved_fields\": len(unresolved_fields),\n \"partial_recovery\": result.partial_recovery,\n },\n )\n return result\n # [/DEF:resolve_from_dictionary:Function]\n" - }, - { - "contract_id": "resolve_from_reference_dataset", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 215, - "end_line": 224, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Reuse semantic metadata from trusted Superset datasets." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:resolve_from_reference_dataset:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Reuse semantic metadata from trusted Superset datasets.\n def resolve_from_reference_dataset(\n self,\n source_payload: Mapping[str, Any],\n fields: Iterable[Mapping[str, Any]],\n ) -> DictionaryResolutionResult:\n return DictionaryResolutionResult(source_ref=str(source_payload.get(\"source_ref\") or \"reference_dataset\"))\n # [/DEF:resolve_from_reference_dataset:Function]\n" - }, - { - "contract_id": "rank_candidates", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 226, - "end_line": 242, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Apply confidence ordering and determine best candidate per field." - }, - "relations": [ - { - "source_id": "rank_candidates", - "relation_type": "[DEPENDS_ON]", - "target_id": "SemanticCandidate", - "target_ref": "[SemanticCandidate]" - } - ], - "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" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:rank_candidates:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Apply confidence ordering and determine best candidate per field.\n # @RELATION: [DEPENDS_ON] ->[SemanticCandidate]\n def rank_candidates(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n ranked = sorted(\n candidates,\n key=lambda candidate: (\n self._match_priority(candidate.get(\"match_type\")),\n -float(candidate.get(\"confidence_score\", 0.0)),\n int(candidate.get(\"candidate_rank\", 999)),\n ),\n )\n for index, candidate in enumerate(ranked, start=1):\n candidate[\"candidate_rank\"] = index\n return ranked\n # [/DEF:rank_candidates:Function]\n" - }, - { - "contract_id": "detect_conflicts", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 244, - "end_line": 249, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Mark competing candidate sets that require explicit user review." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:detect_conflicts:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Mark competing candidate sets that require explicit user review.\n def detect_conflicts(self, candidates: List[Dict[str, Any]]) -> bool:\n return len(candidates) > 1\n # [/DEF:detect_conflicts:Function]\n" - }, - { - "contract_id": "apply_field_decision", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 251, - "end_line": 258, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Accept, reject, or manually override a field-level semantic value." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:apply_field_decision:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Accept, reject, or manually override a field-level semantic value.\n def apply_field_decision(self, field_state: Mapping[str, Any], decision: Mapping[str, Any]) -> Dict[str, Any]:\n merged = dict(field_state)\n merged.update(decision)\n return merged\n # [/DEF:apply_field_decision:Function]\n" - }, - { - "contract_id": "propagate_source_version_update", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 260, - "end_line": 312, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]]", - "POST": "unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched.", - "PRE": "source is persisted and fields belong to the same session aggregate.", - "PURPOSE": "Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values.", - "SIDE_EFFECT": "mutates in-memory field state for the caller to persist." - }, - "relations": [ - { - "source_id": "propagate_source_version_update", - "relation_type": "[DEPENDS_ON]", - "target_id": "SemanticSource", - "target_ref": "[SemanticSource]" - }, - { - "source_id": "propagate_source_version_update", - "relation_type": "[DEPENDS_ON]", - "target_id": "SemanticFieldEntry", - "target_ref": "[SemanticFieldEntry]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:propagate_source_version_update:Function]\n # @COMPLEXITY: 4\n # @PURPOSE: Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values.\n # @RELATION: [DEPENDS_ON] ->[SemanticSource]\n # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]\n # @PRE: source is persisted and fields belong to the same session aggregate.\n # @POST: unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched.\n # @SIDE_EFFECT: mutates in-memory field state for the caller to persist.\n # @DATA_CONTRACT: Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]]\n def propagate_source_version_update(\n self,\n source: SemanticSource,\n fields: Iterable[Any],\n ) -> Dict[str, int]:\n with belief_scope(\"SemanticSourceResolver.propagate_source_version_update\"):\n source_id = str(source.source_id or \"\").strip()\n source_version = str(source.source_version or \"\").strip()\n if not source_id or not source_version:\n log.explore(\"Semantic source version propagation rejected due to incomplete source metadata\", error=\"Source metadata incomplete (missing source_id or source_version)\", payload={\"source_id\": source_id, \"source_version\": source_version})\n raise ValueError(\"Semantic source must provide source_id and source_version\")\n\n propagated = 0\n preserved_locked = 0\n untouched = 0\n for field in fields:\n if str(getattr(field, \"source_id\", \"\") or \"\").strip() != source_id:\n untouched += 1\n continue\n if bool(getattr(field, \"is_locked\", False)) or getattr(field, \"provenance\", None) == FieldProvenance.MANUAL_OVERRIDE:\n preserved_locked += 1\n continue\n\n field.source_version = source_version\n field.needs_review = True\n field.has_conflict = bool(getattr(field, \"has_conflict\", False))\n propagated += 1\n\n log.reflect(\n \"Semantic source version propagation completed\",\n payload={\n \"source_id\": source_id,\n \"source_version\": source_version,\n \"propagated\": propagated,\n \"preserved_locked\": preserved_locked,\n \"untouched\": untouched,\n },\n )\n return {\n \"propagated\": propagated,\n \"preserved_locked\": preserved_locked,\n \"untouched\": untouched,\n }\n # [/DEF:propagate_source_version_update:Function]\n" - }, - { - "contract_id": "_normalize_dictionary_row", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 314, - "end_line": 332, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Normalize one dictionary row into a consistent lookup structure." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_dictionary_row:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize one dictionary row into a consistent lookup structure.\n def _normalize_dictionary_row(self, row: Mapping[str, Any]) -> Dict[str, Any]:\n field_name = (\n row.get(\"field_name\")\n or row.get(\"column_name\")\n or row.get(\"name\")\n or row.get(\"field\")\n )\n normalized_name = str(field_name or \"\").strip()\n return {\n \"field_name\": normalized_name,\n \"field_key\": self._normalize_key(normalized_name),\n \"verbose_name\": row.get(\"verbose_name\") or row.get(\"label\"),\n \"description\": row.get(\"description\"),\n \"display_format\": row.get(\"display_format\") or row.get(\"format\"),\n }\n # [/DEF:_normalize_dictionary_row:Function]\n" - }, - { - "contract_id": "_find_fuzzy_matches", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 334, - "end_line": 350, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Produce confidence-scored fuzzy matches while keeping them reviewable." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:_find_fuzzy_matches:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Produce confidence-scored fuzzy matches while keeping them reviewable.\n def _find_fuzzy_matches(self, field_name: str, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:\n normalized_target = self._normalize_key(field_name)\n fuzzy_matches: List[Dict[str, Any]] = []\n for row in rows:\n candidate_key = str(row.get(\"field_key\") or \"\")\n if not candidate_key:\n continue\n score = SequenceMatcher(None, normalized_target, candidate_key).ratio()\n if score < 0.72:\n continue\n fuzzy_matches.append({\"row\": row, \"score\": round(score, 3)})\n fuzzy_matches.sort(key=lambda item: item[\"score\"], reverse=True)\n return fuzzy_matches[:3]\n # [/DEF:_find_fuzzy_matches:Function]\n" - }, - { - "contract_id": "_build_candidate_payload", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 352, - "end_line": 371, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Project normalized dictionary rows into semantic candidate payloads." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:_build_candidate_payload:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Project normalized dictionary rows into semantic candidate payloads.\n def _build_candidate_payload(\n self,\n rank: int,\n match_type: CandidateMatchType,\n confidence_score: float,\n row: Mapping[str, Any],\n ) -> Dict[str, Any]:\n return {\n \"candidate_rank\": rank,\n \"match_type\": match_type.value,\n \"confidence_score\": confidence_score,\n \"proposed_verbose_name\": row.get(\"verbose_name\"),\n \"proposed_description\": row.get(\"description\"),\n \"proposed_display_format\": row.get(\"display_format\"),\n \"status\": CandidateStatus.PROPOSED.value,\n }\n # [/DEF:_build_candidate_payload:Function]\n" - }, - { - "contract_id": "_match_priority", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 373, - "end_line": 384, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:_match_priority:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention.\n def _match_priority(self, match_type: Optional[str]) -> int:\n priority = {\n CandidateMatchType.EXACT.value: 0,\n CandidateMatchType.REFERENCE.value: 1,\n CandidateMatchType.FUZZY.value: 2,\n CandidateMatchType.GENERATED.value: 3,\n }\n return priority.get(str(match_type or \"\"), 99)\n # [/DEF:_match_priority:Function]\n" - }, - { - "contract_id": "_normalize_key", - "contract_type": "Function", - "file_path": "backend/src/services/dataset_review/semantic_resolver.py", - "start_line": 386, - "end_line": 391, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Normalize field identifiers for stable exact/fuzzy comparisons." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_key:Function]\n # @COMPLEXITY: 2\n # @PURPOSE: Normalize field identifiers for stable exact/fuzzy comparisons.\n def _normalize_key(self, value: str) -> str:\n return \"\".join(ch for ch in str(value or \"\").strip().lower() if ch.isalnum() or ch == \"_\")\n # [/DEF:_normalize_key:Function]\n" + "anchor_syntax": "region", + "body": "# #region DictionaryResolutionResult [C:2] [TYPE Class]\n# @BRIEF Carries field-level dictionary resolution output with explicit review and partial-recovery state.\n@dataclass\nclass DictionaryResolutionResult:\n source_ref: str\n resolved_fields: list[dict[str, Any]] = field(default_factory=list)\n unresolved_fields: list[str] = field(default_factory=list)\n partial_recovery: bool = False\n# #endregion DictionaryResolutionResult\n" }, { "contract_id": "GitServiceModule", "contract_type": "Module", "file_path": "backend/src/services/git/__init__.py", "start_line": 1, - "end_line": 66, + "end_line": 63, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", "PURPOSE": "Composed GitService via multiple inheritance from domain-specific mixins.", - "RATIONALE": "Decomposed from monolithic git_service.py (2101 lines) into", - "SEMANTICS": [ - "git", - "service", - "decomposition", - "mixin", - "composition" - ] + "RATIONALE": "Decomposed from monolithic git_service.py (2101 lines) into" }, "relations": [ { @@ -87000,19 +62615,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceModule:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, service, decomposition, mixin, composition\n# @PURPOSE: Composed GitService via multiple inheritance from domain-specific mixins.\n# @RELATION: DEPENDS_ON -> [GitServiceBase]\n# @RELATION: DEPENDS_ON -> [GitServiceBranchMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceSyncMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceStatusMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceMergeMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceUrlMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceGiteaMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceGithubMixin]\n# @RELATION: DEPENDS_ON -> [GitServiceGitlabMixin]\n#\n# @RATIONALE: Decomposed from monolithic git_service.py (2101 lines) into\n# domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class\n# preserves the original public API surface — all consumers continue to import\n# `from src.services.git_service import GitService` without changes.\n# @REJECTED: Keeping a single 2101-line file — violates fractal limit INV_7.\n\nfrom ._base import GitServiceBase\nfrom ._branch import GitServiceBranchMixin\nfrom ._sync import GitServiceSyncMixin\nfrom ._status import GitServiceStatusMixin\nfrom ._merge import GitServiceMergeMixin\nfrom ._url import GitServiceUrlMixin\nfrom ._gitea import GitServiceGiteaMixin\nfrom ._remote_providers import GitServiceGithubMixin, GitServiceGitlabMixin\n\n__all__ = [\"GitService\"]\n\n\n# [DEF:GitService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull,\n# merge, status, diff, history, and provider API operations (Gitea, GitHub, GitLab).\n# @RELATION: INHERITS -> [GitServiceBase]\n# @RELATION: INHERITS -> [GitServiceBranchMixin]\n# @RELATION: INHERITS -> [GitServiceSyncMixin]\n# @RELATION: INHERITS -> [GitServiceStatusMixin]\n# @RELATION: INHERITS -> [GitServiceMergeMixin]\n# @RELATION: INHERITS -> [GitServiceUrlMixin]\n# @RELATION: INHERITS -> [GitServiceGiteaMixin]\n# @RELATION: INHERITS -> [GitServiceGithubMixin]\n# @RELATION: INHERITS -> [GitServiceGitlabMixin]\nclass GitService(\n GitServiceGiteaMixin,\n GitServiceGithubMixin,\n GitServiceGitlabMixin,\n GitServiceMergeMixin,\n GitServiceSyncMixin,\n GitServiceStatusMixin,\n GitServiceBranchMixin,\n GitServiceUrlMixin,\n GitServiceBase,\n):\n \"\"\"\n Composed GitService — all Git operations via domain-specific mixins.\n\n MRO ensures provider mixins and domain mixins resolve before the base class.\n All consumers continue to use: from src.services.git_service import GitService\n \"\"\"\n pass\n# [/DEF:GitService:Class]\n# [/DEF:GitServiceModule:Module]\n" + "anchor_syntax": "region", + "body": "# #region GitServiceModule [C:3] [TYPE Module] [SEMANTICS git, package, export, mixin, decomposition]\n# @LAYER: Infra\n# @BRIEF Composed GitService via multiple inheritance from domain-specific mixins.\n# @RELATION DEPENDS_ON -> [GitServiceBase]\n# @RELATION DEPENDS_ON -> [GitServiceBranchMixin]\n# @RELATION DEPENDS_ON -> [GitServiceSyncMixin]\n# @RELATION DEPENDS_ON -> [GitServiceStatusMixin]\n# @RELATION DEPENDS_ON -> [GitServiceMergeMixin]\n# @RELATION DEPENDS_ON -> [GitServiceUrlMixin]\n# @RELATION DEPENDS_ON -> [GitServiceGiteaMixin]\n# @RELATION DEPENDS_ON -> [GitServiceGithubMixin]\n# @RELATION DEPENDS_ON -> [GitServiceGitlabMixin]\n#\n# @RATIONALE: Decomposed from monolithic git_service.py (2101 lines) into\n# domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class\n# preserves the original public API surface — all consumers continue to import\n# `from src.services.git_service import GitService` without changes.\n# @REJECTED: Keeping a single 2101-line file — violates fractal limit INV_7.\n\nfrom ._base import GitServiceBase\nfrom ._branch import GitServiceBranchMixin\nfrom ._gitea import GitServiceGiteaMixin\nfrom ._merge import GitServiceMergeMixin\nfrom ._remote_providers import GitServiceGithubMixin, GitServiceGitlabMixin\nfrom ._status import GitServiceStatusMixin\nfrom ._sync import GitServiceSyncMixin\nfrom ._url import GitServiceUrlMixin\n\n__all__ = [\"GitService\"]\n\n\n# #region GitService [C:3] [TYPE Class]\n# @BRIEF Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull,\n# merge, status, diff, history, and provider API operations (Gitea, GitHub, GitLab).\n# @RELATION INHERITS -> [GitServiceBase]\n# @RELATION INHERITS -> [GitServiceBranchMixin]\n# @RELATION INHERITS -> [GitServiceSyncMixin]\n# @RELATION INHERITS -> [GitServiceStatusMixin]\n# @RELATION INHERITS -> [GitServiceMergeMixin]\n# @RELATION INHERITS -> [GitServiceUrlMixin]\n# @RELATION INHERITS -> [GitServiceGiteaMixin]\n# @RELATION INHERITS -> [GitServiceGithubMixin]\n# @RELATION INHERITS -> [GitServiceGitlabMixin]\nclass GitService(\n GitServiceGiteaMixin,\n GitServiceGithubMixin,\n GitServiceGitlabMixin,\n GitServiceMergeMixin,\n GitServiceSyncMixin,\n GitServiceStatusMixin,\n GitServiceBranchMixin,\n GitServiceUrlMixin,\n GitServiceBase,\n):\n \"\"\"\n Composed GitService — all Git operations via domain-specific mixins.\n\n MRO ensures provider mixins and domain mixins resolve before the base class.\n All consumers continue to use: from src.services.git_service import GitService\n \"\"\"\n pass\n# #endregion GitService\n# #endregion GitServiceModule\n" }, { "contract_id": "GitService", "contract_type": "Class", "file_path": "backend/src/services/git/__init__.py", - "start_line": 34, - "end_line": 65, + "start_line": 32, + "end_line": 62, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull," }, "relations": [], @@ -87027,28 +62642,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull,\n# merge, status, diff, history, and provider API operations (Gitea, GitHub, GitLab).\n# @RELATION: INHERITS -> [GitServiceBase]\n# @RELATION: INHERITS -> [GitServiceBranchMixin]\n# @RELATION: INHERITS -> [GitServiceSyncMixin]\n# @RELATION: INHERITS -> [GitServiceStatusMixin]\n# @RELATION: INHERITS -> [GitServiceMergeMixin]\n# @RELATION: INHERITS -> [GitServiceUrlMixin]\n# @RELATION: INHERITS -> [GitServiceGiteaMixin]\n# @RELATION: INHERITS -> [GitServiceGithubMixin]\n# @RELATION: INHERITS -> [GitServiceGitlabMixin]\nclass GitService(\n GitServiceGiteaMixin,\n GitServiceGithubMixin,\n GitServiceGitlabMixin,\n GitServiceMergeMixin,\n GitServiceSyncMixin,\n GitServiceStatusMixin,\n GitServiceBranchMixin,\n GitServiceUrlMixin,\n GitServiceBase,\n):\n \"\"\"\n Composed GitService — all Git operations via domain-specific mixins.\n\n MRO ensures provider mixins and domain mixins resolve before the base class.\n All consumers continue to use: from src.services.git_service import GitService\n \"\"\"\n pass\n# [/DEF:GitService:Class]\n" + "anchor_syntax": "region", + "body": "# #region GitService [C:3] [TYPE Class]\n# @BRIEF Composed GitService class providing all Git operations — init, clone, branches, commits, push, pull,\n# merge, status, diff, history, and provider API operations (Gitea, GitHub, GitLab).\n# @RELATION INHERITS -> [GitServiceBase]\n# @RELATION INHERITS -> [GitServiceBranchMixin]\n# @RELATION INHERITS -> [GitServiceSyncMixin]\n# @RELATION INHERITS -> [GitServiceStatusMixin]\n# @RELATION INHERITS -> [GitServiceMergeMixin]\n# @RELATION INHERITS -> [GitServiceUrlMixin]\n# @RELATION INHERITS -> [GitServiceGiteaMixin]\n# @RELATION INHERITS -> [GitServiceGithubMixin]\n# @RELATION INHERITS -> [GitServiceGitlabMixin]\nclass GitService(\n GitServiceGiteaMixin,\n GitServiceGithubMixin,\n GitServiceGitlabMixin,\n GitServiceMergeMixin,\n GitServiceSyncMixin,\n GitServiceStatusMixin,\n GitServiceBranchMixin,\n GitServiceUrlMixin,\n GitServiceBase,\n):\n \"\"\"\n Composed GitService — all Git operations via domain-specific mixins.\n\n MRO ensures provider mixins and domain mixins resolve before the base class.\n All consumers continue to use: from src.services.git_service import GitService\n \"\"\"\n pass\n# #endregion GitService\n" }, { "contract_id": "GitServiceBase", "contract_type": "Module", "file_path": "backend/src/services/git/_base.py", "start_line": 1, - "end_line": 318, + "end_line": 317, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), and identity configuration.", - "SEMANTICS": [ - "git", - "service", - "repository", - "version_control", - "initialization" - ] + "PURPOSE": "Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), and identity configuration." }, "relations": [ { @@ -87096,537 +62704,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceBase:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, service, repository, version_control, initialization\n# @PURPOSE: Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), and identity configuration.\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\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional\nfrom git import Repo\nfrom git.exc import InvalidGitRepositoryError, NoSuchPathError\nfrom fastapi import HTTPException\nfrom src.core.logger import belief_scope\nfrom src.core.cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"GitBase\")\nfrom src.models.git import GitRepository\nfrom src.models.config import AppConfigRecord\nfrom src.core.database import SessionLocal\n\n\n# [DEF:GitServiceBase:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Base class for GitService providing initialization, path resolution, repository lifecycle, and identity.\nclass GitServiceBase:\n # [DEF:GitService_init:Function]\n # @PURPOSE: Initializes the GitService with a base path for repositories.\n # @PARAM: base_path (str) - Root directory for all Git clones.\n # @PRE: base_path is a valid string path.\n # @POST: GitService is initialized; base_path directory exists.\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 # [/DEF:GitService_init:Function]\n\n # [DEF:_ensure_base_path_exists: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 log.explore(\"Cannot create Git repositories base path\", payload={\"base_path\": self.base_path}, error=str(e))\n raise ValueError(f\"Cannot create Git repositories base path: {self.base_path}. {e}\")\n # [/DEF:_ensure_base_path_exists:Function]\n\n # [DEF:_resolve_base_path: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 log.explore(\"Falling back to default base path\", error=str(e))\n return fallback_path\n # [/DEF:_resolve_base_path:Function]\n\n # [DEF:_normalize_repo_key: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: Optional[str]) -> 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 # [/DEF:_normalize_repo_key:Function]\n\n # [DEF:_update_repo_local_path: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 log.explore(\"Failed to update repo local path\", error=str(e))\n # [/DEF:_update_repo_local_path:Function]\n\n # [DEF:_migrate_repo_directory: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 log.explore(f\"Target already exists, keeping source path: {target_abs}\", error=\"Target path exists\")\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 log.reflect(f\"Repository migrated for dashboard {dashboard_id}: {source_abs} -> {target_abs}\")\n return target_abs\n # [/DEF:_migrate_repo_directory:Function]\n\n # [DEF:_get_repo_path: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: Optional[str] = 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 log.explore(\"Could not resolve local_path from DB\", error=str(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 # [/DEF:_get_repo_path:Function]\n\n # [DEF:init_repo:Function]\n # @PURPOSE: Initialize or clone a repository for a dashboard.\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.\n # @RETURN: Repo\n def init_repo(self, dashboard_id: int, remote_url: str, pat: str, repo_key: Optional[str] = None) -> Repo:\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 pat and \"://\" in remote_url:\n proto, rest = remote_url.split(\"://\", 1)\n auth_url = f\"{proto}://oauth2:{pat}@{rest}\"\n else:\n auth_url = remote_url\n if os.path.exists(repo_path):\n log.reason(f\"Opening existing repo at {repo_path}\")\n try:\n repo = Repo(repo_path)\n except (InvalidGitRepositoryError, NoSuchPathError):\n log.explore(f\"Existing path is not a Git repository, recreating: {repo_path}\", error=\"Not a git repository\")\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 = Repo.clone_from(auth_url, repo_path)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n log.reason(f\"Cloning {remote_url} to {repo_path}\")\n repo = Repo.clone_from(auth_url, repo_path)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n # [/DEF:init_repo:Function]\n\n # [DEF:delete_repo:Function]\n # @PURPOSE: Remove local repository and DB binding for a dashboard.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Local path is deleted when present and GitRepository row is removed.\n def delete_repo(self, dashboard_id: int) -> None:\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 log.explore(f\"Failed to delete repository for dashboard {dashboard_id}\", error=str(e))\n raise HTTPException(status_code=500, detail=f\"Failed to delete repository: {str(e)}\")\n finally:\n session.close()\n # [/DEF:delete_repo:Function]\n\n # [DEF:get_repo:Function]\n # @PURPOSE: Get Repo object for a dashboard.\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 belief_scope(\"GitService.get_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n log.explore(f\"Repository for dashboard {dashboard_id} does not exist\", error=\"Repository not found\")\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 log.explore(f\"Failed to open repository at {repo_path}\", error=str(e))\n raise HTTPException(status_code=500, detail=\"Failed to open local Git repository\")\n # [/DEF:get_repo:Function]\n\n # [DEF:configure_identity:Function]\n # @PURPOSE: Configure repository-local Git committer identity for user-scoped operations.\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 def configure_identity(self, dashboard_id: int, git_username: Optional[str], git_email: Optional[str]) -> None:\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 log.reason(f\"Applied repository-local git identity for dashboard {dashboard_id}\")\n except Exception as e:\n log.explore(\"Failed to configure git identity\", error=str(e))\n raise HTTPException(status_code=500, detail=f\"Failed to configure git identity: {str(e)}\")\n # [/DEF:configure_identity:Function]\n# [/DEF:GitServiceBase:Class]\n# [/DEF:GitServiceBase:Module]\n" - }, - { - "contract_id": "GitService_init", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 32, - "end_line": 44, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "base_path (str) - Root directory for all Git clones.", - "POST": "GitService is initialized; base_path directory exists.", - "PRE": "base_path is a valid string path.", - "PURPOSE": "Initializes the GitService with a base path for repositories." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:GitService_init:Function]\n # @PURPOSE: Initializes the GitService with a base path for repositories.\n # @PARAM: base_path (str) - Root directory for all Git clones.\n # @PRE: base_path is a valid string path.\n # @POST: GitService is initialized; base_path directory exists.\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 # [/DEF:GitService_init:Function]\n" - }, - { - "contract_id": "_ensure_base_path_exists", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 46, - "end_line": 59, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "self.base_path exists as directory or raises ValueError.", - "PRE": "self.base_path is resolved to filesystem path.", - "PURPOSE": "Ensure the repositories root directory exists and is a directory." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_ensure_base_path_exists: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 log.explore(\"Cannot create Git repositories base path\", payload={\"base_path\": self.base_path}, error=str(e))\n raise ValueError(f\"Cannot create Git repositories base path: {self.base_path}. {e}\")\n # [/DEF:_ensure_base_path_exists:Function]\n" - }, - { - "contract_id": "_resolve_base_path", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 61, - "end_line": 94, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns absolute path for Git repositories root.", - "PRE": "base_path is a string path.", - "PURPOSE": "Resolve base repository directory from explicit argument or global storage settings.", - "RETURN": "str" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_resolve_base_path: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 log.explore(\"Falling back to default base path\", error=str(e))\n return fallback_path\n # [/DEF:_resolve_base_path:Function]\n" - }, - { - "contract_id": "_normalize_repo_key", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 96, - "end_line": 105, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns normalized non-empty key.", - "PRE": "repo_key can be None/empty.", - "PURPOSE": "Convert user/dashboard-provided key to safe filesystem directory name.", - "RETURN": "str" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_repo_key: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: Optional[str]) -> 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 # [/DEF:_normalize_repo_key:Function]\n" - }, - { - "contract_id": "_update_repo_local_path", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 107, - "end_line": 127, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "local_path is updated for existing record.", - "PRE": "dashboard_id is valid integer.", - "PURPOSE": "Persist repository local_path in GitRepository table when record exists." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_update_repo_local_path: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 log.explore(\"Failed to update repo local path\", error=str(e))\n # [/DEF:_update_repo_local_path:Function]\n" - }, - { - "contract_id": "_migrate_repo_directory", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 129, - "end_line": 150, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Repository content available at target_path.", - "PRE": "source_path exists.", - "PURPOSE": "Move legacy repository directory to target path and sync DB metadata.", - "RETURN": "str" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_migrate_repo_directory: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 log.explore(f\"Target already exists, keeping source path: {target_abs}\", error=\"Target path exists\")\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 log.reflect(f\"Repository migrated for dashboard {dashboard_id}: {source_abs} -> {target_abs}\")\n return target_abs\n # [/DEF:_migrate_repo_directory:Function]\n" - }, - { - "contract_id": "_get_repo_path", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 152, - "end_line": 196, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "repo_key (Optional[str]) - Slug-like key used when DB local_path is absent.", - "POST": "Returns DB-local_path when present, otherwise base_path/.", - "PRE": "dashboard_id is an integer.", - "PURPOSE": "Resolves the local filesystem path for a dashboard's repository.", - "RETURN": "str" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_repo_path: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: Optional[str] = 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 log.explore(\"Could not resolve local_path from DB\", error=str(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 # [/DEF:_get_repo_path:Function]\n" - }, - { - "contract_id": "delete_repo", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 237, - "end_line": 277, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Local path is deleted when present and GitRepository row is removed.", - "PRE": "dashboard_id is a valid integer.", - "PURPOSE": "Remove local repository and DB binding for a 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" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:delete_repo:Function]\n # @PURPOSE: Remove local repository and DB binding for a dashboard.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Local path is deleted when present and GitRepository row is removed.\n def delete_repo(self, dashboard_id: int) -> None:\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 log.explore(f\"Failed to delete repository for dashboard {dashboard_id}\", error=str(e))\n raise HTTPException(status_code=500, detail=f\"Failed to delete repository: {str(e)}\")\n finally:\n session.close()\n # [/DEF:delete_repo:Function]\n" - }, - { - "contract_id": "get_repo", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 279, - "end_line": 295, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns a GitPython Repo instance for the dashboard.", - "PRE": "Repository must exist on disk for the given dashboard_id.", - "PURPOSE": "Get Repo object for a dashboard.", - "RETURN": "Repo" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_repo:Function]\n # @PURPOSE: Get Repo object for a dashboard.\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 belief_scope(\"GitService.get_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n log.explore(f\"Repository for dashboard {dashboard_id} does not exist\", error=\"Repository not found\")\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 log.explore(f\"Failed to open repository at {repo_path}\", error=str(e))\n raise HTTPException(status_code=500, detail=\"Failed to open local Git repository\")\n # [/DEF:get_repo:Function]\n" - }, - { - "contract_id": "configure_identity", - "contract_type": "Function", - "file_path": "backend/src/services/git/_base.py", - "start_line": 297, - "end_line": 316, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Repository config has user.name and user.email when both identity values are provided.", - "PRE": "dashboard_id repository exists; git_username/git_email may be empty.", - "PURPOSE": "Configure repository-local Git committer identity for user-scoped operations." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:configure_identity:Function]\n # @PURPOSE: Configure repository-local Git committer identity for user-scoped operations.\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 def configure_identity(self, dashboard_id: int, git_username: Optional[str], git_email: Optional[str]) -> None:\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 log.reason(f\"Applied repository-local git identity for dashboard {dashboard_id}\")\n except Exception as e:\n log.explore(\"Failed to configure git identity\", error=str(e))\n raise HTTPException(status_code=500, detail=f\"Failed to configure git identity: {str(e)}\")\n # [/DEF:configure_identity:Function]\n" + "anchor_syntax": "region", + "body": "# #region GitServiceBase [C:3] [TYPE Module] [SEMANTICS git, repository, clone, base, mixin]\n# @LAYER: Infra\n# @BRIEF Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), and identity configuration.\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\nfrom pathlib import Path\n\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:3] [TYPE Class]\n# @BRIEF Base class for GitService providing initialization, path resolution, repository lifecycle, and identity.\nclass GitServiceBase:\n # region GitService_init [TYPE Function]\n # @PURPOSE: Initializes the GitService with a base path for repositories.\n # @PARAM: base_path (str) - Root directory for all Git clones.\n # @PRE: base_path is a valid string path.\n # @POST: GitService is initialized; base_path directory exists.\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 # endregion GitService_init\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 [TYPE Function]\n # @PURPOSE: Initialize or clone a repository for a dashboard.\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.\n # @RETURN: Repo\n def init_repo(self, dashboard_id: int, remote_url: str, pat: str, repo_key: str | None = None) -> Repo:\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 pat and \"://\" in remote_url:\n proto, rest = remote_url.split(\"://\", 1)\n auth_url = f\"{proto}://oauth2:{pat}@{rest}\"\n else:\n auth_url = remote_url\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 = Repo.clone_from(auth_url, repo_path)\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 = Repo.clone_from(auth_url, repo_path)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n # endregion init_repo\n\n # region delete_repo [TYPE Function]\n # @PURPOSE: Remove local repository and DB binding for a dashboard.\n # @PRE: dashboard_id is a valid integer.\n # @POST: Local path is deleted when present and GitRepository row is removed.\n def delete_repo(self, dashboard_id: int) -> None:\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 [TYPE Function]\n # @PURPOSE: Get Repo object for a dashboard.\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 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 [TYPE Function]\n # @PURPOSE: Configure repository-local Git committer identity for user-scoped operations.\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 def configure_identity(self, dashboard_id: int, git_username: str | None, git_email: str | None) -> None:\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# #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": 187, + "end_line": 193, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes.", - "SEMANTICS": [ - "git", - "branches", - "commits", - "checkout", - "gitflow" - ] + "PURPOSE": "Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes." }, "relations": [ { @@ -87656,127 +62748,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceBranchMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, branches, commits, checkout, gitflow\n# @PURPOSE: Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes.\n# @RELATION: USED_BY -> [GitService]\n\nimport os\nfrom typing import Any, Dict, List, Optional\nfrom datetime import datetime\nfrom git import Repo\nfrom git.exc import GitCommandError\nfrom fastapi import HTTPException\nfrom src.core.logger import belief_scope\nfrom src.core.cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"GitBranch\")\n\n\n# [DEF:GitServiceBranchMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing branch and commit operations for GitService.\nclass GitServiceBranchMixin:\n # [DEF:_ensure_gitflow_branches: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 log.explore(\"Branch bootstrap skipped - no commits\", payload={\"dashboard_id\": dashboard_id}, error=\"Repository has no initial commit to branch from\")\n return\n if \"main\" not in local_heads:\n local_heads[\"main\"] = repo.create_head(\"main\", base_commit)\n log.reason(\"Created local branch main\", payload={\"dashboard_id\": dashboard_id})\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 log.reason(\"Created local branch\", payload={\"branch_name\": branch_name, \"dashboard_id\": dashboard_id})\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n log.reason(\"Remote origin not configured; skipping remote branch creation\", payload={\"dashboard_id\": dashboard_id})\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 log.explore(\"Failed to fetch origin refs\", error=str(e))\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 log.reason(\"Pushed branch to origin\", payload={\"branch_name\": branch_name, \"dashboard_id\": dashboard_id})\n except Exception as e:\n log.explore(\"Failed to push branch to origin\", error=str(e))\n raise HTTPException(\n status_code=500,\n detail=f\"Failed to create default branch '{branch_name}' on remote: {str(e)}\",\n )\n try:\n repo.git.checkout(\"dev\")\n log.reason(\"Checked out default branch dev\", payload={\"dashboard_id\": dashboard_id})\n except Exception as e:\n log.explore(\"Could not checkout dev branch\", payload={\"dashboard_id\": dashboard_id}, error=str(e))\n # [/DEF:_ensure_gitflow_branches:Function]\n\n # [DEF:list_branches: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 log.reason(\"Listing branches\", payload={\"dashboard_id\": dashboard_id})\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 log.explore(\"Skipping ref\", payload={\"ref\": str(ref)}, error=str(e))\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 log.explore(\"Could not determine active branch\", error=str(e))\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 # [/DEF:list_branches:Function]\n\n # [DEF:create_branch: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 log.reason(\"Creating branch\", payload={\"name\": name, \"from_branch\": from_branch})\n if not repo.heads and not repo.remotes:\n log.explore(\"Repository is empty; creating initial commit to enable branching\", error=\"No branches or remotes exist; initializing from scratch\")\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 log.explore(\"Source branch not found, using HEAD\", payload={\"from_branch\": from_branch}, error=f\"Branch '{from_branch}' does not exist, falling back to HEAD\")\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 log.explore(\"Failed to create branch\", error=str(e))\n raise\n # [/DEF:create_branch:Function]\n\n # [DEF:checkout_branch: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 log.reason(\"Checking out branch\", payload={\"name\": name})\n repo.git.checkout(name)\n # [/DEF:checkout_branch:Function]\n\n # [DEF:commit_changes: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 log.reason(\"No changes to commit\", payload={\"dashboard_id\": dashboard_id})\n return\n if files:\n log.reason(\"Staging files\", payload={\"files\": files})\n repo.index.add(files)\n else:\n log.reason(\"Staging all changes\")\n repo.git.add(A=True)\n repo.index.commit(message)\n log.reflect(\"Committed changes\", payload={\"message\": message})\n # [/DEF:commit_changes:Function]\n# [/DEF:GitServiceBranchMixin:Class]\n# [/DEF:GitServiceBranchMixin:Module]\n" - }, - { - "contract_id": "_ensure_gitflow_branches", - "contract_type": "Function", - "file_path": "backend/src/services/git/_branch.py", - "start_line": 24, - "end_line": 81, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "main, dev, preprod are available in local repository and pushed to origin when available.", - "PRE": "repo is a valid GitPython Repo instance.", - "PURPOSE": "Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_ensure_gitflow_branches: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 log.explore(\"Branch bootstrap skipped - no commits\", payload={\"dashboard_id\": dashboard_id}, error=\"Repository has no initial commit to branch from\")\n return\n if \"main\" not in local_heads:\n local_heads[\"main\"] = repo.create_head(\"main\", base_commit)\n log.reason(\"Created local branch main\", payload={\"dashboard_id\": dashboard_id})\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 log.reason(\"Created local branch\", payload={\"branch_name\": branch_name, \"dashboard_id\": dashboard_id})\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n log.reason(\"Remote origin not configured; skipping remote branch creation\", payload={\"dashboard_id\": dashboard_id})\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 log.explore(\"Failed to fetch origin refs\", error=str(e))\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 log.reason(\"Pushed branch to origin\", payload={\"branch_name\": branch_name, \"dashboard_id\": dashboard_id})\n except Exception as e:\n log.explore(\"Failed to push branch to origin\", error=str(e))\n raise HTTPException(\n status_code=500,\n detail=f\"Failed to create default branch '{branch_name}' on remote: {str(e)}\",\n )\n try:\n repo.git.checkout(\"dev\")\n log.reason(\"Checked out default branch dev\", payload={\"dashboard_id\": dashboard_id})\n except Exception as e:\n log.explore(\"Could not checkout dev branch\", payload={\"dashboard_id\": dashboard_id}, error=str(e))\n # [/DEF:_ensure_gitflow_branches:Function]\n" - }, - { - "contract_id": "list_branches", - "contract_type": "Function", - "file_path": "backend/src/services/git/_branch.py", - "start_line": 83, - "end_line": 121, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns a list of branch metadata dictionaries.", - "PRE": "Repository for dashboard_id exists.", - "PURPOSE": "List all branches for a dashboard's repository.", - "RETURN": "List[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" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:list_branches: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 log.reason(\"Listing branches\", payload={\"dashboard_id\": dashboard_id})\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 log.explore(\"Skipping ref\", payload={\"ref\": str(ref)}, error=str(e))\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 log.explore(\"Could not determine active branch\", error=str(e))\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 # [/DEF:list_branches:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GitServiceGiteaMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_gitea.py", "start_line": 1, - "end_line": 262, + "end_line": 261, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation.", - "SEMANTICS": [ - "git", - "gitea", - "api", - "provider", - "connection_test" - ] + "PURPOSE": "Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation." }, "relations": [ { @@ -87806,346 +62792,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceGiteaMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, gitea, api, provider, connection_test\n# @PURPOSE: Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation.\n# @RELATION: USED_BY -> [GitService]\n\nimport httpx\nfrom typing import Any, Dict, List, Optional\nfrom urllib.parse import quote\nfrom fastapi import HTTPException\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\n\nlog = MarkerLogger(\"GitGitea\")\nfrom src.models.git import GitProvider\n\n\n# [DEF:GitServiceGiteaMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing Gitea API operations for GitService.\nclass GitServiceGiteaMixin:\n # [DEF:test_connection: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 log.reason(\"Local/Offline mode detected for URL\")\n return True\n if not url.startswith(('http://', 'https://')):\n log.explore(f\"Invalid URL protocol: {url}\", error=\"Invalid URL protocol\")\n return False\n if not pat or not pat.strip():\n log.explore(\"Git PAT is missing or empty\", error=\"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 log.explore(f\"Git connection test failed for {provider} at {api_url}. Status: {resp.status_code}\", error=\"Git connection test failed\")\n return resp.status_code == 200\n except Exception as e:\n log.explore(f\"Error testing git connection: {e}\", error=str(e))\n return False\n # [/DEF:test_connection:Function]\n\n # [DEF:_gitea_headers: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 # [/DEF:_gitea_headers:Function]\n\n # [DEF:_gitea_request: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: Optional[Dict[str, Any]] = 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 log.explore(f\"Network error: {e}\", error=str(e))\n raise HTTPException(status_code=503, detail=f\"Gitea API is unavailable: {str(e)}\")\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 log.explore(f\"Gitea API error ({endpoint})\", payload={\"method\": method, \"status\": response.status_code}, error=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 # [/DEF:_gitea_request:Function]\n\n # [DEF:get_gitea_current_user: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 # [/DEF:get_gitea_current_user:Function]\n\n # [DEF:list_gitea_repositories: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 # [/DEF:list_gitea_repositories:Function]\n\n # [DEF:create_gitea_repository: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: Optional[str] = None, auto_init: bool = True, default_branch: Optional[str] = \"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 # [/DEF:create_gitea_repository:Function]\n\n # [DEF:delete_gitea_repository: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 # [/DEF:delete_gitea_repository:Function]\n\n # [DEF:_gitea_branch_exists: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 # [/DEF:_gitea_branch_exists:Function]\n\n # [DEF:_build_gitea_pr_404_detail: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 ) -> Optional[str]:\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 # [/DEF:_build_gitea_pr_404_detail:Function]\n\n # [DEF:create_gitea_pull_request: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: Optional[str] = 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 log.explore(f\"Primary Gitea URL not found, retrying with remote host: {fallback_url}\", error=fallback_url)\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 # [/DEF:create_gitea_pull_request:Function]\n# [/DEF:GitServiceGiteaMixin:Class]\n# [/DEF:GitServiceGiteaMixin:Module]\n" - }, - { - "contract_id": "_gitea_headers", - "contract_type": "Function", - "file_path": "backend/src/services/git/_gitea.py", - "start_line": 65, - "end_line": 79, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns headers with token auth.", - "PRE": "pat is provided.", - "PURPOSE": "Build Gitea API authorization headers.", - "RETURN": "Dict[str, str]" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_gitea_headers: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 # [/DEF:_gitea_headers:Function]\n" - }, - { - "contract_id": "_gitea_request", - "contract_type": "Function", - "file_path": "backend/src/services/git/_gitea.py", - "start_line": 81, - "end_line": 111, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns decoded JSON payload.", - "PRE": "method and endpoint are valid.", - "PURPOSE": "Execute HTTP request against Gitea API with stable error mapping.", - "RETURN": "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_gitea_request: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: Optional[Dict[str, Any]] = 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 log.explore(f\"Network error: {e}\", error=str(e))\n raise HTTPException(status_code=503, detail=f\"Gitea API is unavailable: {str(e)}\")\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 log.explore(f\"Gitea API error ({endpoint})\", payload={\"method\": method, \"status\": response.status_code}, error=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 # [/DEF:_gitea_request:Function]\n" - }, - { - "contract_id": "get_gitea_current_user", - "contract_type": "Function", - "file_path": "backend/src/services/git/_gitea.py", - "start_line": 113, - "end_line": 124, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns current username.", - "PRE": "server_url and pat are valid.", - "PURPOSE": "Resolve current Gitea user for PAT.", - "RETURN": "str" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_gitea_current_user: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 # [/DEF:get_gitea_current_user:Function]\n" - }, - { - "contract_id": "_gitea_branch_exists", - "contract_type": "Function", - "file_path": "backend/src/services/git/_gitea.py", - "start_line": 168, - "end_line": 184, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns True when branch exists, False when 404.", - "PRE": "owner/repo/branch are non-empty.", - "PURPOSE": "Check whether a branch exists in Gitea repository.", - "RETURN": "bool" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_gitea_branch_exists: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 # [/DEF:_gitea_branch_exists:Function]\n" - }, - { - "contract_id": "_build_gitea_pr_404_detail", - "contract_type": "Function", - "file_path": "backend/src/services/git/_gitea.py", - "start_line": 186, - "end_line": 205, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns specific branch-missing message when detected.", - "PRE": "owner/repo/from_branch/to_branch are provided.", - "PURPOSE": "Build actionable error detail for Gitea PR 404 responses.", - "RETURN": "Optional[str]" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_build_gitea_pr_404_detail: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 ) -> Optional[str]:\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 # [/DEF:_build_gitea_pr_404_detail:Function]\n" - }, - { - "contract_id": "create_gitea_pull_request", - "contract_type": "Function", - "file_path": "backend/src/services/git/_gitea.py", - "start_line": 207, - "end_line": 260, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns normalized PR metadata.", - "PRE": "Config and remote URL are valid.", - "PURPOSE": "Create pull request in Gitea.", - "RETURN": "Dict[str, 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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:create_gitea_pull_request: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: Optional[str] = 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 log.explore(f\"Primary Gitea URL not found, retrying with remote host: {fallback_url}\", error=fallback_url)\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 # [/DEF:create_gitea_pull_request:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GitServiceMergeMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_merge.py", "start_line": 1, - "end_line": 278, + "end_line": 277, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote.", - "SEMANTICS": [ - "git", - "merge", - "conflicts", - "resolution", - "promote" - ] + "PURPOSE": "Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote." }, "relations": [ { @@ -88175,256 +62836,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceMergeMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, merge, conflicts, resolution, promote\n# @PURPOSE: Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote.\n# @RELATION: USED_BY -> [GitService]\n\nimport os\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional\nfrom git import Repo\nfrom git.exc import GitCommandError\nfrom git.objects.blob import Blob\nfrom fastapi import HTTPException\nfrom src.core.logger import logger, belief_scope\n\n\n# [DEF:GitServiceMergeMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing merge operations for GitService.\nclass GitServiceMergeMixin:\n # [DEF:_read_blob_text:Function]\n # @PURPOSE: Read text from a Git blob.\n def _read_blob_text(self, blob: Blob) -> str:\n with belief_scope(\"GitService._read_blob_text\"):\n if blob is None:\n return \"\"\n try:\n return blob.data_stream.read().decode(\"utf-8\", errors=\"replace\")\n except Exception:\n return \"\"\n # [/DEF:_read_blob_text:Function]\n\n # [DEF:_get_unmerged_file_paths:Function]\n # @PURPOSE: List files with merge conflicts.\n def _get_unmerged_file_paths(self, repo: Repo) -> List[str]:\n with belief_scope(\"GitService._get_unmerged_file_paths\"):\n try:\n return sorted(list(repo.index.unmerged_blobs().keys()))\n except Exception:\n return []\n # [/DEF:_get_unmerged_file_paths:Function]\n\n # [DEF:_build_unfinished_merge_payload:Function]\n # @PURPOSE: Build payload for unfinished merge state.\n def _build_unfinished_merge_payload(self, repo: Repo) -> Dict[str, Any]:\n with belief_scope(\"GitService._build_unfinished_merge_payload\"):\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n merge_head_value = \"\"\n merge_msg_preview = \"\"\n current_branch = \"unknown\"\n try:\n merge_head_value = Path(merge_head_path).read_text(encoding=\"utf-8\").strip()\n except Exception:\n merge_head_value = \"\"\n try:\n merge_msg_path = os.path.join(repo.git_dir, \"MERGE_MSG\")\n if os.path.exists(merge_msg_path):\n merge_msg_preview = (\n Path(merge_msg_path).read_text(encoding=\"utf-8\").strip().splitlines()[:1] or [\"\"]\n )[0]\n except Exception:\n merge_msg_preview = \"\"\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = \"detached_or_unknown\"\n conflicts_count = len(self._get_unmerged_file_paths(repo))\n return {\n \"error_code\": \"GIT_UNFINISHED_MERGE\",\n \"message\": (\n \"\\u0412 \\u0440\\u0435\\u043f\\u043e\\u0437\\u0438\\u0442\\u043e\\u0440\\u0438\\u0438 \\u0435\\u0441\\u0442\\u044c \\u043d\\u0435\\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0451\\u043d\\u043d\\u043e\\u0435 \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u0435. \"\n \"\\u0417\\u0430\\u0432\\u0435\\u0440\\u0448\\u0438\\u0442\\u0435 \\u0438\\u043b\\u0438 \\u043e\\u0442\\u043c\\u0435\\u043d\\u0438\\u0442\\u0435 \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u0435 \\u0432\\u0440\\u0443\\u0447\\u043d\\u0443\\u044e.\"\n ),\n \"repository_path\": repo.working_tree_dir,\n \"git_dir\": repo.git_dir,\n \"current_branch\": current_branch,\n \"merge_head\": merge_head_value,\n \"merge_message_preview\": merge_msg_preview,\n \"conflicts_count\": conflicts_count,\n \"next_steps\": [\n \"\\u041e\\u0442\\u043a\\u0440\\u043e\\u0439\\u0442\\u0435 \\u043b\\u043e\\u043a\\u0430\\u043b\\u044c\\u043d\\u044b\\u0439 \\u0440\\u0435\\u043f\\u043e\\u0437\\u0438\\u0442\\u043e\\u0440\\u0438\\u0439 \\u043f\\u043e \\u043f\\u0443\\u0442\\u0438 repository_path\",\n \"\\u041f\\u0440\\u043e\\u0432\\u0435\\u0440\\u044c\\u0442\\u0435 \\u0441\\u043e\\u0441\\u0442\\u043e\\u044f\\u043d\\u0438\\u0435: git status\",\n \"\\u0420\\u0430\\u0437\\u0440\\u0435\\u0448\\u0438\\u0442\\u0435 \\u043a\\u043e\\u043d\\u0444\\u043b\\u0438\\u043a\\u0442\\u044b \\u0438 \\u0432\\u044b\\u043f\\u043e\\u043b\\u043d\\u0438\\u0442\\u0435 commit, \\u043b\\u0438\\u0431\\u043e \\u043e\\u0442\\u043c\\u0435\\u043d\\u0438\\u0442\\u0435: git merge --abort\",\n \"\\u041f\\u043e\\u0441\\u043b\\u0435 \\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0435\\u043d\\u0438\\u044f/\\u043e\\u0442\\u043c\\u0435\\u043d\\u044b \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u044f \\u043f\\u043e\\u0432\\u0442\\u043e\\u0440\\u0438\\u0442\\u0435 Pull \\u0438\\u0437 \\u0438\\u043d\\u0442\\u0435\\u0440\\u0444\\u0435\\u0439\\u0441\\u0430\",\n ],\n \"manual_commands\": [\"git status\", \"git add \", 'git commit -m \"resolve merge conflicts\"', \"git merge --abort\"],\n }\n # [/DEF:_build_unfinished_merge_payload:Function]\n\n # [DEF:get_merge_status:Function]\n # @PURPOSE: Get current merge status for a dashboard repository.\n def get_merge_status(self, dashboard_id: int) -> Dict[str, Any]:\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 # [/DEF:get_merge_status:Function]\n\n # [DEF:get_merge_conflicts:Function]\n # @PURPOSE: List all files with conflicts and their contents.\n def get_merge_conflicts(self, dashboard_id: int) -> List[Dict[str, Any]]:\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 # [/DEF:get_merge_conflicts:Function]\n\n # [DEF:resolve_merge_conflicts:Function]\n # @PURPOSE: Resolve conflicts using specified strategy.\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions: List[Dict[str, Any]]) -> List[str]:\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 # [/DEF:resolve_merge_conflicts:Function]\n\n # [DEF:abort_merge:Function]\n # @PURPOSE: Abort ongoing merge.\n def abort_merge(self, dashboard_id: int) -> Dict[str, Any]:\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 # [/DEF:abort_merge:Function]\n\n # [DEF:continue_merge:Function]\n # @PURPOSE: Finalize merge after conflict resolution.\n def continue_merge(self, dashboard_id: int, message: Optional[str] = None) -> Dict[str, Any]:\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 # [/DEF:continue_merge:Function]\n\n # [DEF:promote_direct_merge:Function]\n # @PURPOSE: Perform direct merge between branches in local repo and push target branch.\n # @PRE: Repository exists and both branches are valid.\n # @POST: Target branch contains merged changes from source branch.\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 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 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 return {\"mode\": \"direct\", \"from_branch\": source, \"to_branch\": target, \"status\": \"merged\"}\n # [/DEF:promote_direct_merge:Function]\n# [/DEF:GitServiceMergeMixin:Class]\n# [/DEF:GitServiceMergeMixin:Module]\n" - }, - { - "contract_id": "_read_blob_text", - "contract_type": "Function", - "file_path": "backend/src/services/git/_merge.py", - "start_line": 22, - "end_line": 32, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Read text from a Git blob." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_read_blob_text:Function]\n # @PURPOSE: Read text from a Git blob.\n def _read_blob_text(self, blob: Blob) -> str:\n with belief_scope(\"GitService._read_blob_text\"):\n if blob is None:\n return \"\"\n try:\n return blob.data_stream.read().decode(\"utf-8\", errors=\"replace\")\n except Exception:\n return \"\"\n # [/DEF:_read_blob_text:Function]\n" - }, - { - "contract_id": "_get_unmerged_file_paths", - "contract_type": "Function", - "file_path": "backend/src/services/git/_merge.py", - "start_line": 34, - "end_line": 42, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "List files with merge conflicts." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_unmerged_file_paths:Function]\n # @PURPOSE: List files with merge conflicts.\n def _get_unmerged_file_paths(self, repo: Repo) -> List[str]:\n with belief_scope(\"GitService._get_unmerged_file_paths\"):\n try:\n return sorted(list(repo.index.unmerged_blobs().keys()))\n except Exception:\n return []\n # [/DEF:_get_unmerged_file_paths:Function]\n" - }, - { - "contract_id": "_build_unfinished_merge_payload", - "contract_type": "Function", - "file_path": "backend/src/services/git/_merge.py", - "start_line": 44, - "end_line": 89, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Build payload for unfinished merge state." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_build_unfinished_merge_payload:Function]\n # @PURPOSE: Build payload for unfinished merge state.\n def _build_unfinished_merge_payload(self, repo: Repo) -> Dict[str, Any]:\n with belief_scope(\"GitService._build_unfinished_merge_payload\"):\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n merge_head_value = \"\"\n merge_msg_preview = \"\"\n current_branch = \"unknown\"\n try:\n merge_head_value = Path(merge_head_path).read_text(encoding=\"utf-8\").strip()\n except Exception:\n merge_head_value = \"\"\n try:\n merge_msg_path = os.path.join(repo.git_dir, \"MERGE_MSG\")\n if os.path.exists(merge_msg_path):\n merge_msg_preview = (\n Path(merge_msg_path).read_text(encoding=\"utf-8\").strip().splitlines()[:1] or [\"\"]\n )[0]\n except Exception:\n merge_msg_preview = \"\"\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = \"detached_or_unknown\"\n conflicts_count = len(self._get_unmerged_file_paths(repo))\n return {\n \"error_code\": \"GIT_UNFINISHED_MERGE\",\n \"message\": (\n \"\\u0412 \\u0440\\u0435\\u043f\\u043e\\u0437\\u0438\\u0442\\u043e\\u0440\\u0438\\u0438 \\u0435\\u0441\\u0442\\u044c \\u043d\\u0435\\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0451\\u043d\\u043d\\u043e\\u0435 \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u0435. \"\n \"\\u0417\\u0430\\u0432\\u0435\\u0440\\u0448\\u0438\\u0442\\u0435 \\u0438\\u043b\\u0438 \\u043e\\u0442\\u043c\\u0435\\u043d\\u0438\\u0442\\u0435 \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u0435 \\u0432\\u0440\\u0443\\u0447\\u043d\\u0443\\u044e.\"\n ),\n \"repository_path\": repo.working_tree_dir,\n \"git_dir\": repo.git_dir,\n \"current_branch\": current_branch,\n \"merge_head\": merge_head_value,\n \"merge_message_preview\": merge_msg_preview,\n \"conflicts_count\": conflicts_count,\n \"next_steps\": [\n \"\\u041e\\u0442\\u043a\\u0440\\u043e\\u0439\\u0442\\u0435 \\u043b\\u043e\\u043a\\u0430\\u043b\\u044c\\u043d\\u044b\\u0439 \\u0440\\u0435\\u043f\\u043e\\u0437\\u0438\\u0442\\u043e\\u0440\\u0438\\u0439 \\u043f\\u043e \\u043f\\u0443\\u0442\\u0438 repository_path\",\n \"\\u041f\\u0440\\u043e\\u0432\\u0435\\u0440\\u044c\\u0442\\u0435 \\u0441\\u043e\\u0441\\u0442\\u043e\\u044f\\u043d\\u0438\\u0435: git status\",\n \"\\u0420\\u0430\\u0437\\u0440\\u0435\\u0448\\u0438\\u0442\\u0435 \\u043a\\u043e\\u043d\\u0444\\u043b\\u0438\\u043a\\u0442\\u044b \\u0438 \\u0432\\u044b\\u043f\\u043e\\u043b\\u043d\\u0438\\u0442\\u0435 commit, \\u043b\\u0438\\u0431\\u043e \\u043e\\u0442\\u043c\\u0435\\u043d\\u0438\\u0442\\u0435: git merge --abort\",\n \"\\u041f\\u043e\\u0441\\u043b\\u0435 \\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0435\\u043d\\u0438\\u044f/\\u043e\\u0442\\u043c\\u0435\\u043d\\u044b \\u0441\\u043b\\u0438\\u044f\\u043d\\u0438\\u044f \\u043f\\u043e\\u0432\\u0442\\u043e\\u0440\\u0438\\u0442\\u0435 Pull \\u0438\\u0437 \\u0438\\u043d\\u0442\\u0435\\u0440\\u0444\\u0435\\u0439\\u0441\\u0430\",\n ],\n \"manual_commands\": [\"git status\", \"git add \", 'git commit -m \"resolve merge conflicts\"', \"git merge --abort\"],\n }\n # [/DEF:_build_unfinished_merge_payload:Function]\n" - }, - { - "contract_id": "promote_direct_merge", - "contract_type": "Function", - "file_path": "backend/src/services/git/_merge.py", - "start_line": 231, - "end_line": 276, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Target branch contains merged changes from source branch.", - "PRE": "Repository exists and both branches are valid.", - "PURPOSE": "Perform direct merge between branches in local repo and push target branch.", - "RETURN": "Dict[str, 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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:promote_direct_merge:Function]\n # @PURPOSE: Perform direct merge between branches in local repo and push target branch.\n # @PRE: Repository exists and both branches are valid.\n # @POST: Target branch contains merged changes from source branch.\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 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 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 return {\"mode\": \"direct\", \"from_branch\": source, \"to_branch\": target, \"status\": \"merged\"}\n # [/DEF:promote_direct_merge:Function]\n" - }, - { - "contract_id": "create_github_repository", - "contract_type": "Function", - "file_path": "backend/src/services/git/_remote_providers.py", - "start_line": 19, - "end_line": 57, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns created repository payload.", - "PRE": "PAT has repository create permission.", - "PURPOSE": "Create repository in GitHub or GitHub Enterprise.", - "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" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:create_github_repository: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: Optional[str] = None, auto_init: bool = True, default_branch: Optional[str] = \"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 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\"GitHub API is unavailable: {str(e)}\")\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 # [/DEF:create_github_repository:Function]\n" - }, - { - "contract_id": "create_github_pull_request", - "contract_type": "Function", - "file_path": "backend/src/services/git/_remote_providers.py", - "start_line": 59, - "end_line": 97, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns normalized PR metadata.", - "PRE": "Config and remote URL are valid.", - "PURPOSE": "Create pull request in GitHub or GitHub Enterprise.", - "RETURN": "Dict[str, 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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:create_github_pull_request:Function]\n # @PURPOSE: Create pull request in GitHub or GitHub Enterprise.\n # @PRE: Config and remote URL are valid.\n # @POST: Returns normalized PR metadata.\n # @RETURN: Dict[str, Any]\n async def create_github_pull_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: Optional[str] = None, draft: 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 if \"github.com\" in base_url:\n api_url = f\"https://api.github.com/repos/{identity['namespace']}/{identity['repo']}/pulls\"\n else:\n api_url = f\"{base_url}/api/v3/repos/{identity['namespace']}/{identity['repo']}/pulls\"\n headers = {\n \"Authorization\": f\"token {pat.strip()}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/vnd.github+json\",\n }\n payload = {\n \"title\": title, \"head\": from_branch, \"base\": to_branch,\n \"body\": description or \"\", \"draft\": bool(draft),\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\"GitHub API is unavailable: {str(e)}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n detail = response.json().get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitHub API error: {detail}\")\n data = response.json()\n return {\"id\": data.get(\"number\") or data.get(\"id\"), \"url\": data.get(\"html_url\") or data.get(\"url\"), \"status\": data.get(\"state\") or \"open\"}\n # [/DEF:create_github_pull_request:Function]\n" + "anchor_syntax": "region", + "body": "# #region GitServiceMergeMixin [C:3] [TYPE Module] [SEMANTICS git, merge, branch, conflict, resolution]\n# @LAYER: Infra\n# @BRIEF Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote.\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 [TYPE Function]\n # @PURPOSE: Get current merge status for a dashboard repository.\n def get_merge_status(self, dashboard_id: int) -> dict[str, Any]:\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 [TYPE Function]\n # @PURPOSE: List all files with conflicts and their contents.\n def get_merge_conflicts(self, dashboard_id: int) -> list[dict[str, Any]]:\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 [TYPE Function]\n # @PURPOSE: Resolve conflicts using specified strategy.\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions: list[dict[str, Any]]) -> list[str]:\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 [TYPE Function]\n # @PURPOSE: Abort ongoing merge.\n def abort_merge(self, dashboard_id: int) -> dict[str, Any]:\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 [TYPE Function]\n # @PURPOSE: Finalize merge after conflict resolution.\n def continue_merge(self, dashboard_id: int, message: str | None = None) -> dict[str, Any]:\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 [TYPE Function]\n # @PURPOSE: Perform direct merge between branches in local repo and push target branch.\n # @PRE: Repository exists and both branches are valid.\n # @POST: Target branch contains merged changes from source branch.\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 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 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 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": "GitServiceGitlabMixin", "contract_type": "Class", "file_path": "backend/src/services/git/_remote_providers.py", - "start_line": 100, - "end_line": 185, + "start_line": 97, + "end_line": 181, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Mixin providing GitLab API operations for GitService." }, "relations": [], @@ -88439,134 +62863,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceGitlabMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing GitLab API operations for GitService.\nclass GitServiceGitlabMixin:\n # [DEF:create_gitlab_repository: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: Optional[str] = None, auto_init: bool = True, default_branch: Optional[str] = \"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: {str(e)}\")\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 # [/DEF:create_gitlab_repository:Function]\n\n # [DEF:create_gitlab_merge_request: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: Optional[str] = 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: {str(e)}\")\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 # [/DEF:create_gitlab_merge_request:Function]\n# [/DEF:GitServiceGitlabMixin:Class]\n" - }, - { - "contract_id": "create_gitlab_repository", - "contract_type": "Function", - "file_path": "backend/src/services/git/_remote_providers.py", - "start_line": 104, - "end_line": 148, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns created repository payload.", - "PRE": "PAT has api scope.", - "PURPOSE": "Create repository(project) in GitLab.", - "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" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:create_gitlab_repository: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: Optional[str] = None, auto_init: bool = True, default_branch: Optional[str] = \"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: {str(e)}\")\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 # [/DEF:create_gitlab_repository:Function]\n" - }, - { - "contract_id": "create_gitlab_merge_request", - "contract_type": "Function", - "file_path": "backend/src/services/git/_remote_providers.py", - "start_line": 150, - "end_line": 184, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns normalized MR metadata.", - "PRE": "Config and remote URL are valid.", - "PURPOSE": "Create merge request in GitLab.", - "RETURN": "Dict[str, 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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:create_gitlab_merge_request: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: Optional[str] = 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: {str(e)}\")\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 # [/DEF:create_gitlab_merge_request:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GitServiceStatusMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_status.py", "start_line": 1, - "end_line": 167, + "end_line": 159, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Status, diff, and commit history operations for GitService using git status --porcelain to avoid --cached flag issues.", - "SEMANTICS": [ - "git", - "status", - "diff", - "history", - "porcelain" - ] + "PURPOSE": "Status, diff, and commit history operations for GitService using git status --porcelain to avoid --cached flag issues." }, "relations": [ { @@ -88596,249 +62907,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceStatusMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, status, diff, history, porcelain\n# @PURPOSE: Status, diff, and commit history operations for GitService using git status --porcelain to avoid --cached flag issues.\n# @RELATION: USED_BY -> [GitService]\n\nfrom typing import Any, Dict, List\nfrom datetime import datetime\nfrom git import Repo\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\n\nlog = MarkerLogger(\"GitStatus\")\n\n\n# [DEF:GitServiceStatusMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing repository status, diff, and commit history for GitService.\nclass GitServiceStatusMixin:\n # [DEF:_parse_status_porcelain:Function]\n # @COMPLEXITY: 2\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 log.explore(\"git status --porcelain failed\", error=\"git status --porcelain command 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 # [/DEF:_parse_status_porcelain:Function]\n\n # [DEF:get_status: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 # [/DEF:get_status:Function]\n\n # [DEF:get_diff: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 # [/DEF:get_diff:Function]\n\n # [DEF:get_commit_history: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 log.explore(f\"Could not retrieve commit history for dashboard {dashboard_id}: {e}\", error=str(e))\n return []\n return commits\n # [/DEF:get_commit_history:Function]\n# [/DEF:GitServiceStatusMixin:Class]\n# [/DEF:GitServiceStatusMixin:Module]\n" - }, - { - "contract_id": "_parse_status_porcelain", - "contract_type": "Function", - "file_path": "backend/src/services/git/_status.py", - "start_line": 21, - "end_line": 59, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "POST": "Returns (staged, modified, untracked) tuple of file path lists.", - "PRE": "`repo` is an open GitPython Repo instance.", - "PURPOSE": "Parse git status --porcelain output into staged, modified, and untracked file lists.", - "RATIONALE": "Avoids repo.is_dirty() / repo.index.diff(\"HEAD\") which internally" - }, - "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": "RATIONALE", - "message": "@RATIONALE is forbidden for contract type 'Function' at C2", - "detail": { - "actual_complexity": 2, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_parse_status_porcelain:Function]\n # @COMPLEXITY: 2\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 log.explore(\"git status --porcelain failed\", error=\"git status --porcelain command 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 # [/DEF:_parse_status_porcelain:Function]\n" - }, - { - "contract_id": "get_status", - "contract_type": "Function", - "file_path": "backend/src/services/git/_status.py", - "start_line": 61, - "end_line": 119, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns a dictionary representing the Git status.", - "PRE": "Repository for dashboard_id exists.", - "PURPOSE": "Get current repository status (dirty files, untracked, etc.)", - "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" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_status: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 # [/DEF:get_status:Function]\n" - }, - { - "contract_id": "get_diff", - "contract_type": "Function", - "file_path": "backend/src/services/git/_status.py", - "start_line": 121, - "end_line": 137, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "staged (bool) - Whether to show staged changes.", - "POST": "Returns the diff text as a string.", - "PRE": "Repository for dashboard_id exists.", - "PURPOSE": "Generate diff for a file or the whole repository.", - "RETURN": "str" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_diff: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 # [/DEF:get_diff:Function]\n" - }, - { - "contract_id": "get_commit_history", - "contract_type": "Function", - "file_path": "backend/src/services/git/_status.py", - "start_line": 139, - "end_line": 165, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PARAM": "limit (int) - Max number of commits to return.", - "POST": "Returns a list of dictionaries for each commit in history.", - "PRE": "Repository for dashboard_id exists.", - "PURPOSE": "Retrieve commit history for a repository.", - "RETURN": "List[dict]" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_commit_history: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 log.explore(f\"Could not retrieve commit history for dashboard {dashboard_id}: {e}\", error=str(e))\n return []\n return commits\n # [/DEF:get_commit_history:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GitServiceSyncMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_sync.py", "start_line": 1, - "end_line": 194, + "end_line": 173, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "Push and pull operations for GitService with origin host auto-alignment.", - "SEMANTICS": [ - "git", - "push", - "pull", - "sync", - "remote" - ] + "PURPOSE": "Push and pull operations for GitService with origin host auto-alignment." }, "relations": [ { @@ -88874,28 +62957,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceSyncMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, push, pull, sync, remote\n# @PURPOSE: Push and pull operations for GitService with origin host auto-alignment.\n# @RELATION: USED_BY -> [GitService]\n# @RELATION: DEPENDS_ON -> [GitServiceUrlMixin]\n\nimport os\nfrom typing import Optional\nfrom git import Repo\nfrom git.exc import GitCommandError\nfrom fastapi import HTTPException\nfrom src.core.database import SessionLocal\nfrom src.core.logger import belief_scope\nfrom src.core.cot_logger import MarkerLogger\n\nlog = MarkerLogger(\"GitSync\")\nfrom src.models.git import GitRepository, GitServerConfig\n\n\n# [DEF:GitServiceSyncMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Mixin providing push and pull operations with origin host alignment.\nclass GitServiceSyncMixin:\n # [DEF:push_changes: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 log.explore(\"No local branches to push\", payload={\"dashboard_id\": dashboard_id}, error=\"Repository has no local branch heads to push\")\n return\n try:\n origin = repo.remote(name='origin')\n except ValueError:\n log.explore(\"Remote 'origin' not found\", payload={\"dashboard_id\": dashboard_id}, error=\"Git remote 'origin' is not configured for this repository\")\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 log.explore(\"Failed to load repository binding diagnostics\", payload={\"dashboard_id\": dashboard_id}, error=str(diag_error))\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 log.reason(\n \"Push diagnostics\",\n payload={\n \"dashboard_id\": dashboard_id, \"config_id\": binding_config_id,\n \"config_url\": binding_config_url, \"binding_remote_url\": binding_remote_url,\n \"origin_urls\": origin_urls, \"origin_realigned\": bool(realigned_origin_url),\n },\n )\n try:\n current_branch = repo.active_branch\n log.reason(\"Pushing branch\", payload={\"branch\": current_branch.name, \"origin\": True})\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 log.explore(\"Error pushing ref\", payload={\"ref\": info.remote_ref_string, \"summary\": info.summary}, error=f\"Git push error: {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 log.explore(\"Failed to push changes\", error=str(e))\n raise HTTPException(status_code=500, detail=f\"Git push failed: {details}\")\n except Exception as e:\n log.explore(\"Failed to push changes\", error=str(e))\n raise HTTPException(status_code=500, detail=f\"Git push failed: {str(e)}\")\n # [/DEF:push_changes:Function]\n\n # [DEF:pull_changes: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 log.explore(\"Unfinished merge detected\", error=\"Unfinished merge state found\",\n payload={\n \"dashboard_id\": dashboard_id,\n \"repo_path\": payload[\"repository_path\"],\n \"git_dir\": payload[\"git_dir\"],\n \"branch\": payload[\"current_branch\"],\n \"merge_head\": payload[\"merge_head\"],\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 log.reason(\n \"Pull diagnostics\",\n payload={\n \"dashboard_id\": dashboard_id,\n \"repo_path\": repo.working_tree_dir,\n \"branch\": current_branch,\n \"origin_urls\": origin_urls,\n },\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 log.reason(\n \"Pull remote branch check\",\n payload={\n \"dashboard_id\": dashboard_id,\n \"branch\": current_branch,\n \"remote_ref\": remote_ref,\n \"exists\": has_remote_branch,\n },\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 log.reason(\"Pulling changes\", payload={\"branch\": current_branch})\n repo.git.pull(\"--no-rebase\", \"origin\", current_branch)\n except ValueError:\n log.explore(\"Remote 'origin' not found\", payload={\"dashboard_id\": dashboard_id}, error=\"Git remote 'origin' is not configured for this repository\")\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 log.explore(\"Failed to pull changes\", error=str(e))\n raise HTTPException(status_code=500, detail=f\"Git pull failed: {details}\")\n except HTTPException:\n raise\n except Exception as e:\n log.explore(\"Failed to pull changes\", error=str(e))\n raise HTTPException(status_code=500, detail=f\"Git pull failed: {str(e)}\")\n # [/DEF:pull_changes:Function]\n# [/DEF:GitServiceSyncMixin:Class]\n# [/DEF:GitServiceSyncMixin:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "GitServiceUrlMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_url.py", "start_line": 1, - "end_line": 206, + "end_line": 210, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Infra", - "PURPOSE": "URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs.", - "SEMANTICS": [ - "git", - "url", - "parsing", - "normalization", - "host_alignment" - ] + "PURPOSE": "URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs." }, "relations": [ { @@ -88973,582 +63049,62 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:GitServiceUrlMixin:Module]\n# @COMPLEXITY: 3\n# @LAYER: Infra\n# @SEMANTICS: git, url, parsing, normalization, host_alignment\n# @PURPOSE: URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs.\n# @RELATION: USED_BY -> [GitServiceSyncMixin]\n# @RELATION: USED_BY -> [GitServiceGiteaMixin]\n# @RELATION: USED_BY -> [GitServiceRemoteMixin]\n\nimport os\nfrom typing import Any, Dict, List, Optional\nfrom urllib.parse import quote, urlparse\nfrom fastapi import HTTPException\nfrom src.core.database import SessionLocal\nfrom src.core.cot_logger import MarkerLogger\nfrom src.core.logger import belief_scope\n\nlog = MarkerLogger(\"GitUrl\")\nfrom src.models.git import GitRepository, GitServerConfig\n\n# [DEF:GitServiceUrlMixin:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: URL helper methods for GitService — extract host, strip credentials, replace host, align origin, parse remote identity, derive server URL, normalize server URL.\nclass GitServiceUrlMixin:\n # [DEF:_extract_http_host:Function]\n # @PURPOSE: Extract normalized host[:port] from HTTP(S) URL.\n # @PRE: url_value may be empty.\n # @POST: Returns lowercase host token or None.\n # @RETURN: Optional[str]\n def _extract_http_host(self, url_value: Optional[str]) -> Optional[str]:\n normalized = str(url_value or \"\").strip()\n if not normalized:\n return None\n try:\n parsed = urlparse(normalized)\n except Exception:\n return None\n if parsed.scheme not in {\"http\", \"https\"}:\n return None\n host = parsed.hostname\n if not host:\n return None\n if parsed.port:\n return f\"{host.lower()}:{parsed.port}\"\n return host.lower()\n # [/DEF:_extract_http_host:Function]\n\n # [DEF:_strip_url_credentials:Function]\n # @PURPOSE: Remove credentials from URL while preserving scheme/host/path.\n # @PRE: url_value may contain credentials.\n # @POST: Returns URL without username/password.\n # @RETURN: str\n def _strip_url_credentials(self, url_value: str) -> str:\n normalized = str(url_value or \"\").strip()\n if not normalized:\n return normalized\n try:\n parsed = urlparse(normalized)\n except Exception:\n return normalized\n if parsed.scheme not in {\"http\", \"https\"} or not parsed.hostname:\n return normalized\n host = parsed.hostname\n if parsed.port:\n host = f\"{host}:{parsed.port}\"\n return parsed._replace(netloc=host).geturl()\n # [/DEF:_strip_url_credentials:Function]\n\n # [DEF:_replace_host_in_url:Function]\n # @PURPOSE: Replace source URL host with host from configured server URL.\n # @PRE: source_url and config_url are HTTP(S) URLs.\n # @POST: Returns source URL with updated host (credentials preserved) or None.\n # @RETURN: Optional[str]\n def _replace_host_in_url(self, source_url: Optional[str], config_url: Optional[str]) -> Optional[str]:\n source = str(source_url or \"\").strip()\n config = str(config_url or \"\").strip()\n if not source or not config:\n return None\n try:\n source_parsed = urlparse(source)\n config_parsed = urlparse(config)\n except Exception:\n return None\n if source_parsed.scheme not in {\"http\", \"https\"} or config_parsed.scheme not in {\"http\", \"https\"}:\n return None\n if not source_parsed.hostname or not config_parsed.hostname:\n return None\n target_host = config_parsed.hostname\n if config_parsed.port:\n target_host = f\"{target_host}:{config_parsed.port}\"\n auth_part = \"\"\n if source_parsed.username:\n auth_part = quote(source_parsed.username, safe=\"\")\n if source_parsed.password is not None:\n auth_part = f\"{auth_part}:{quote(source_parsed.password, safe='')}\"\n auth_part = f\"{auth_part}@\"\n new_netloc = f\"{auth_part}{target_host}\"\n return source_parsed._replace(netloc=new_netloc).geturl()\n # [/DEF:_replace_host_in_url:Function]\n\n # [DEF:_align_origin_host_with_config:Function]\n # @PURPOSE: Auto-align local origin host to configured Git server host when they drift.\n # @PRE: origin remote exists.\n # @POST: origin URL host updated and DB binding normalized when mismatch detected.\n # @RETURN: Optional[str]\n def _align_origin_host_with_config(\n self,\n dashboard_id: int,\n origin,\n config_url: Optional[str],\n current_origin_url: Optional[str],\n binding_remote_url: Optional[str],\n ) -> Optional[str]:\n config_host = self._extract_http_host(config_url)\n source_origin_url = str(current_origin_url or \"\").strip() or str(binding_remote_url or \"\").strip()\n origin_host = self._extract_http_host(source_origin_url)\n if not config_host or not origin_host:\n return None\n if config_host == origin_host:\n return None\n aligned_url = self._replace_host_in_url(source_origin_url, config_url)\n if not aligned_url:\n return None\n log.explore(f\"Host mismatch for dashboard {dashboard_id}: config_host={config_host} origin_host={origin_host}, applying origin.set_url\", error=f\"Host mismatch for dashboard {dashboard_id}\")\n try:\n origin.set_url(aligned_url)\n except Exception as e:\n log.explore(f\"Failed to set origin URL for dashboard {dashboard_id}: {e}\", error=str(e))\n return 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.remote_url = self._strip_url_credentials(aligned_url)\n session.commit()\n finally:\n session.close()\n except Exception as e:\n log.explore(f\"Failed to persist aligned remote_url for dashboard {dashboard_id}: {e}\", error=str(e))\n return aligned_url\n # [/DEF:_align_origin_host_with_config:Function]\n\n # [DEF:_parse_remote_repo_identity:Function]\n # @PURPOSE: Parse owner/repo from remote URL for Git server API operations.\n # @PRE: remote_url is a valid git URL.\n # @POST: Returns owner/repo tokens.\n # @RETURN: Dict[str, str]\n def _parse_remote_repo_identity(self, remote_url: str) -> Dict[str, str]:\n normalized = str(remote_url or \"\").strip()\n if not normalized:\n raise HTTPException(status_code=400, detail=\"Repository remote_url is empty\")\n if normalized.startswith(\"git@\"):\n path = normalized.split(\":\", 1)[1] if \":\" in normalized else \"\"\n else:\n parsed = urlparse(normalized)\n path = parsed.path or \"\"\n path = path.strip(\"/\")\n if path.endswith(\".git\"):\n path = path[:-4]\n parts = [segment for segment in path.split(\"/\") if segment]\n if len(parts) < 2:\n raise HTTPException(status_code=400, detail=f\"Cannot parse repository owner/name from remote URL: {remote_url}\")\n owner = parts[0]\n repo = parts[-1]\n namespace = \"/\".join(parts[:-1])\n return {\"owner\": owner, \"repo\": repo, \"namespace\": namespace, \"full_name\": f\"{namespace}/{repo}\"}\n # [/DEF:_parse_remote_repo_identity:Function]\n\n # [DEF:_derive_server_url_from_remote:Function]\n # @PURPOSE: Build API base URL from remote repository URL without credentials.\n # @PRE: remote_url may be any git URL.\n # @POST: Returns normalized http(s) base URL or None when derivation is impossible.\n # @RETURN: Optional[str]\n def _derive_server_url_from_remote(self, remote_url: str) -> Optional[str]:\n normalized = str(remote_url or \"\").strip()\n if not normalized or normalized.startswith(\"git@\"):\n return None\n parsed = urlparse(normalized)\n if parsed.scheme not in {\"http\", \"https\"}:\n return None\n if not parsed.hostname:\n return None\n netloc = parsed.hostname\n if parsed.port:\n netloc = f\"{netloc}:{parsed.port}\"\n return f\"{parsed.scheme}://{netloc}\".rstrip(\"/\")\n # [/DEF:_derive_server_url_from_remote:Function]\n\n # [DEF:_normalize_git_server_url:Function]\n # @PURPOSE: Normalize Git server URL for provider API calls.\n # @PRE: raw_url is non-empty.\n # @POST: Returns URL without trailing slash.\n # @RETURN: str\n def _normalize_git_server_url(self, raw_url: str) -> str:\n normalized = (raw_url or \"\").strip()\n if not normalized:\n raise HTTPException(status_code=400, detail=\"Git server URL is required\")\n return normalized.rstrip(\"/\")\n # [/DEF:_normalize_git_server_url:Function]\n# [/DEF:GitServiceUrlMixin:Class]\n# [/DEF:GitServiceUrlMixin:Module]\n" - }, - { - "contract_id": "_extract_http_host", - "contract_type": "Function", - "file_path": "backend/src/services/git/_url.py", - "start_line": 25, - "end_line": 46, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns lowercase host token or None.", - "PRE": "url_value may be empty.", - "PURPOSE": "Extract normalized host[:port] from HTTP(S) URL.", - "RETURN": "Optional[str]" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_extract_http_host:Function]\n # @PURPOSE: Extract normalized host[:port] from HTTP(S) URL.\n # @PRE: url_value may be empty.\n # @POST: Returns lowercase host token or None.\n # @RETURN: Optional[str]\n def _extract_http_host(self, url_value: Optional[str]) -> Optional[str]:\n normalized = str(url_value or \"\").strip()\n if not normalized:\n return None\n try:\n parsed = urlparse(normalized)\n except Exception:\n return None\n if parsed.scheme not in {\"http\", \"https\"}:\n return None\n host = parsed.hostname\n if not host:\n return None\n if parsed.port:\n return f\"{host.lower()}:{parsed.port}\"\n return host.lower()\n # [/DEF:_extract_http_host:Function]\n" - }, - { - "contract_id": "_strip_url_credentials", - "contract_type": "Function", - "file_path": "backend/src/services/git/_url.py", - "start_line": 48, - "end_line": 67, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns URL without username/password.", - "PRE": "url_value may contain credentials.", - "PURPOSE": "Remove credentials from URL while preserving scheme/host/path.", - "RETURN": "str" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_strip_url_credentials:Function]\n # @PURPOSE: Remove credentials from URL while preserving scheme/host/path.\n # @PRE: url_value may contain credentials.\n # @POST: Returns URL without username/password.\n # @RETURN: str\n def _strip_url_credentials(self, url_value: str) -> str:\n normalized = str(url_value or \"\").strip()\n if not normalized:\n return normalized\n try:\n parsed = urlparse(normalized)\n except Exception:\n return normalized\n if parsed.scheme not in {\"http\", \"https\"} or not parsed.hostname:\n return normalized\n host = parsed.hostname\n if parsed.port:\n host = f\"{host}:{parsed.port}\"\n return parsed._replace(netloc=host).geturl()\n # [/DEF:_strip_url_credentials:Function]\n" - }, - { - "contract_id": "_replace_host_in_url", - "contract_type": "Function", - "file_path": "backend/src/services/git/_url.py", - "start_line": 69, - "end_line": 99, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns source URL with updated host (credentials preserved) or None.", - "PRE": "source_url and config_url are HTTP(S) URLs.", - "PURPOSE": "Replace source URL host with host from configured server URL.", - "RETURN": "Optional[str]" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_replace_host_in_url:Function]\n # @PURPOSE: Replace source URL host with host from configured server URL.\n # @PRE: source_url and config_url are HTTP(S) URLs.\n # @POST: Returns source URL with updated host (credentials preserved) or None.\n # @RETURN: Optional[str]\n def _replace_host_in_url(self, source_url: Optional[str], config_url: Optional[str]) -> Optional[str]:\n source = str(source_url or \"\").strip()\n config = str(config_url or \"\").strip()\n if not source or not config:\n return None\n try:\n source_parsed = urlparse(source)\n config_parsed = urlparse(config)\n except Exception:\n return None\n if source_parsed.scheme not in {\"http\", \"https\"} or config_parsed.scheme not in {\"http\", \"https\"}:\n return None\n if not source_parsed.hostname or not config_parsed.hostname:\n return None\n target_host = config_parsed.hostname\n if config_parsed.port:\n target_host = f\"{target_host}:{config_parsed.port}\"\n auth_part = \"\"\n if source_parsed.username:\n auth_part = quote(source_parsed.username, safe=\"\")\n if source_parsed.password is not None:\n auth_part = f\"{auth_part}:{quote(source_parsed.password, safe='')}\"\n auth_part = f\"{auth_part}@\"\n new_netloc = f\"{auth_part}{target_host}\"\n return source_parsed._replace(netloc=new_netloc).geturl()\n # [/DEF:_replace_host_in_url:Function]\n" - }, - { - "contract_id": "_align_origin_host_with_config", - "contract_type": "Function", - "file_path": "backend/src/services/git/_url.py", - "start_line": 101, - "end_line": 146, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "origin URL host updated and DB binding normalized when mismatch detected.", - "PRE": "origin remote exists.", - "PURPOSE": "Auto-align local origin host to configured Git server host when they drift.", - "RETURN": "Optional[str]" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_align_origin_host_with_config:Function]\n # @PURPOSE: Auto-align local origin host to configured Git server host when they drift.\n # @PRE: origin remote exists.\n # @POST: origin URL host updated and DB binding normalized when mismatch detected.\n # @RETURN: Optional[str]\n def _align_origin_host_with_config(\n self,\n dashboard_id: int,\n origin,\n config_url: Optional[str],\n current_origin_url: Optional[str],\n binding_remote_url: Optional[str],\n ) -> Optional[str]:\n config_host = self._extract_http_host(config_url)\n source_origin_url = str(current_origin_url or \"\").strip() or str(binding_remote_url or \"\").strip()\n origin_host = self._extract_http_host(source_origin_url)\n if not config_host or not origin_host:\n return None\n if config_host == origin_host:\n return None\n aligned_url = self._replace_host_in_url(source_origin_url, config_url)\n if not aligned_url:\n return None\n log.explore(f\"Host mismatch for dashboard {dashboard_id}: config_host={config_host} origin_host={origin_host}, applying origin.set_url\", error=f\"Host mismatch for dashboard {dashboard_id}\")\n try:\n origin.set_url(aligned_url)\n except Exception as e:\n log.explore(f\"Failed to set origin URL for dashboard {dashboard_id}: {e}\", error=str(e))\n return 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.remote_url = self._strip_url_credentials(aligned_url)\n session.commit()\n finally:\n session.close()\n except Exception as e:\n log.explore(f\"Failed to persist aligned remote_url for dashboard {dashboard_id}: {e}\", error=str(e))\n return aligned_url\n # [/DEF:_align_origin_host_with_config:Function]\n" - }, - { - "contract_id": "_parse_remote_repo_identity", - "contract_type": "Function", - "file_path": "backend/src/services/git/_url.py", - "start_line": 148, - "end_line": 172, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns owner/repo tokens.", - "PRE": "remote_url is a valid git URL.", - "PURPOSE": "Parse owner/repo from remote URL for Git server API operations.", - "RETURN": "Dict[str, str]" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_parse_remote_repo_identity:Function]\n # @PURPOSE: Parse owner/repo from remote URL for Git server API operations.\n # @PRE: remote_url is a valid git URL.\n # @POST: Returns owner/repo tokens.\n # @RETURN: Dict[str, str]\n def _parse_remote_repo_identity(self, remote_url: str) -> Dict[str, str]:\n normalized = str(remote_url or \"\").strip()\n if not normalized:\n raise HTTPException(status_code=400, detail=\"Repository remote_url is empty\")\n if normalized.startswith(\"git@\"):\n path = normalized.split(\":\", 1)[1] if \":\" in normalized else \"\"\n else:\n parsed = urlparse(normalized)\n path = parsed.path or \"\"\n path = path.strip(\"/\")\n if path.endswith(\".git\"):\n path = path[:-4]\n parts = [segment for segment in path.split(\"/\") if segment]\n if len(parts) < 2:\n raise HTTPException(status_code=400, detail=f\"Cannot parse repository owner/name from remote URL: {remote_url}\")\n owner = parts[0]\n repo = parts[-1]\n namespace = \"/\".join(parts[:-1])\n return {\"owner\": owner, \"repo\": repo, \"namespace\": namespace, \"full_name\": f\"{namespace}/{repo}\"}\n # [/DEF:_parse_remote_repo_identity:Function]\n" - }, - { - "contract_id": "_derive_server_url_from_remote", - "contract_type": "Function", - "file_path": "backend/src/services/git/_url.py", - "start_line": 174, - "end_line": 192, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns normalized http(s) base URL or None when derivation is impossible.", - "PRE": "remote_url may be any git URL.", - "PURPOSE": "Build API base URL from remote repository URL without credentials.", - "RETURN": "Optional[str]" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_derive_server_url_from_remote:Function]\n # @PURPOSE: Build API base URL from remote repository URL without credentials.\n # @PRE: remote_url may be any git URL.\n # @POST: Returns normalized http(s) base URL or None when derivation is impossible.\n # @RETURN: Optional[str]\n def _derive_server_url_from_remote(self, remote_url: str) -> Optional[str]:\n normalized = str(remote_url or \"\").strip()\n if not normalized or normalized.startswith(\"git@\"):\n return None\n parsed = urlparse(normalized)\n if parsed.scheme not in {\"http\", \"https\"}:\n return None\n if not parsed.hostname:\n return None\n netloc = parsed.hostname\n if parsed.port:\n netloc = f\"{netloc}:{parsed.port}\"\n return f\"{parsed.scheme}://{netloc}\".rstrip(\"/\")\n # [/DEF:_derive_server_url_from_remote:Function]\n" - }, - { - "contract_id": "_normalize_git_server_url", - "contract_type": "Function", - "file_path": "backend/src/services/git/_url.py", - "start_line": 194, - "end_line": 204, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns URL without trailing slash.", - "PRE": "raw_url is non-empty.", - "PURPOSE": "Normalize Git server URL for provider API calls.", - "RETURN": "str" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_git_server_url:Function]\n # @PURPOSE: Normalize Git server URL for provider API calls.\n # @PRE: raw_url is non-empty.\n # @POST: Returns URL without trailing slash.\n # @RETURN: str\n def _normalize_git_server_url(self, raw_url: str) -> str:\n normalized = (raw_url or \"\").strip()\n if not normalized:\n raise HTTPException(status_code=400, detail=\"Git server URL is required\")\n return normalized.rstrip(\"/\")\n # [/DEF:_normalize_git_server_url:Function]\n" - }, - { - "contract_id": "git_service:Module", - "contract_type": "Tombstone", - "file_path": "backend/src/services/git_service.py", - "start_line": 1, - "end_line": 11, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "PURPOSE": "Re-export shim — GitService has been decomposed into services/git/ package." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is not allowed for contract type 'Tombstone'", - "detail": { - "actual_type": "Tombstone", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "COMPLEXITY", - "message": "@COMPLEXITY is forbidden for contract type 'Tombstone' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Tombstone" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Tombstone'", - "detail": { - "actual_type": "Tombstone", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Tombstone' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Tombstone" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:git_service:Module:Tombstone]\n# @COMPLEXITY: 1\n# @PURPOSE: Re-export shim — GitService has been decomposed into services/git/ package.\n# All consumers continue to import from this same path without changes.\n# @RELATION: REDIRECTS_TO -> [GitServiceModule]\n# @RATIONALE: Monolithic GitService (2101 lines) was decomposed into 8 domain-specific mixins\n# under services/git/ to satisfy INV_7 (< 400 lines per module). This shim preserves\n# the original import path for all 5 consumers.\n# @REJECTED: Breaking 5 consumer imports to remove this shim — unacceptable migration cost.\nfrom src.services.git import GitService # noqa: F401\n# [/DEF:git_service:Module:Tombstone]\n" + "anchor_syntax": "region", + "body": "# #region GitServiceUrlMixin [C:3] [TYPE Module] [SEMANTICS git, url, parse, remote, endpoint]\n# @LAYER: Infra\n# @BRIEF URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs.\n# @RELATION USED_BY -> [GitServiceSyncMixin]\n# @RELATION USED_BY -> [GitServiceGiteaMixin]\n# @RELATION USED_BY -> [GitServiceRemoteMixin]\n\nfrom urllib.parse import quote, urlparse\n\nfrom fastapi import HTTPException\n\nfrom src.core.database import SessionLocal\nfrom src.core.logger import logger\nfrom src.models.git import GitRepository\n\n\n# #region GitServiceUrlMixin [C:3] [TYPE Class]\n# @BRIEF URL helper methods for GitService — extract host, strip credentials, replace host, align origin, parse remote identity, derive server URL, normalize server URL.\nclass GitServiceUrlMixin:\n # region _extract_http_host [TYPE Function]\n # @PURPOSE: Extract normalized host[:port] from HTTP(S) URL.\n # @PRE: url_value may be empty.\n # @POST: Returns lowercase host token or None.\n # @RETURN: Optional[str]\n def _extract_http_host(self, url_value: str | None) -> str | None:\n normalized = str(url_value or \"\").strip()\n if not normalized:\n return None\n try:\n parsed = urlparse(normalized)\n except Exception:\n return None\n if parsed.scheme not in {\"http\", \"https\"}:\n return None\n host = parsed.hostname\n if not host:\n return None\n if parsed.port:\n return f\"{host.lower()}:{parsed.port}\"\n return host.lower()\n # endregion _extract_http_host\n\n # region _strip_url_credentials [TYPE Function]\n # @PURPOSE: Remove credentials from URL while preserving scheme/host/path.\n # @PRE: url_value may contain credentials.\n # @POST: Returns URL without username/password.\n # @RETURN: str\n def _strip_url_credentials(self, url_value: str) -> str:\n normalized = str(url_value or \"\").strip()\n if not normalized:\n return normalized\n try:\n parsed = urlparse(normalized)\n except Exception:\n return normalized\n if parsed.scheme not in {\"http\", \"https\"} or not parsed.hostname:\n return normalized\n host = parsed.hostname\n if parsed.port:\n host = f\"{host}:{parsed.port}\"\n return parsed._replace(netloc=host).geturl()\n # endregion _strip_url_credentials\n\n # region _replace_host_in_url [TYPE Function]\n # @PURPOSE: Replace source URL host with host from configured server URL.\n # @PRE: source_url and config_url are HTTP(S) URLs.\n # @POST: Returns source URL with updated host (credentials preserved) or None.\n # @RETURN: Optional[str]\n def _replace_host_in_url(self, source_url: str | None, config_url: str | None) -> str | None:\n source = str(source_url or \"\").strip()\n config = str(config_url or \"\").strip()\n if not source or not config:\n return None\n try:\n source_parsed = urlparse(source)\n config_parsed = urlparse(config)\n except Exception:\n return None\n if source_parsed.scheme not in {\"http\", \"https\"} or config_parsed.scheme not in {\"http\", \"https\"}:\n return None\n if not source_parsed.hostname or not config_parsed.hostname:\n return None\n target_host = config_parsed.hostname\n if config_parsed.port:\n target_host = f\"{target_host}:{config_parsed.port}\"\n auth_part = \"\"\n if source_parsed.username:\n auth_part = quote(source_parsed.username, safe=\"\")\n if source_parsed.password is not None:\n auth_part = f\"{auth_part}:{quote(source_parsed.password, safe='')}\"\n auth_part = f\"{auth_part}@\"\n new_netloc = f\"{auth_part}{target_host}\"\n return source_parsed._replace(netloc=new_netloc).geturl()\n # endregion _replace_host_in_url\n\n # region _align_origin_host_with_config [TYPE Function]\n # @PURPOSE: Auto-align local origin host to configured Git server host when they drift.\n # @PRE: origin remote exists.\n # @POST: origin URL host updated and DB binding normalized when mismatch detected.\n # @RETURN: Optional[str]\n def _align_origin_host_with_config(\n self,\n dashboard_id: int,\n origin,\n config_url: str | None,\n current_origin_url: str | None,\n binding_remote_url: str | None,\n ) -> str | None:\n config_host = self._extract_http_host(config_url)\n source_origin_url = str(current_origin_url or \"\").strip() or str(binding_remote_url or \"\").strip()\n origin_host = self._extract_http_host(source_origin_url)\n if not config_host or not origin_host:\n return None\n if config_host == origin_host:\n return None\n aligned_url = self._replace_host_in_url(source_origin_url, config_url)\n if not aligned_url:\n return None\n logger.reason(\n f\"Host mismatch for dashboard {dashboard_id}: config_host={config_host} origin_host={origin_host}, applying origin.set_url\",\n extra={\"src\": \"_align_origin_host_with_config\"},\n )\n try:\n origin.set_url(aligned_url)\n except Exception as e:\n logger.warning(\n \"[_align_origin_host_with_config][Coherence:Failed] Failed to set origin URL for dashboard %s: %s\",\n dashboard_id, e,\n )\n return 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.remote_url = self._strip_url_credentials(aligned_url)\n session.commit()\n finally:\n session.close()\n except Exception as e:\n logger.explore(\n f\"Failed to persist aligned remote_url for dashboard {dashboard_id}: {e}\",\n extra={\"src\": \"_align_origin_host_with_config\"},\n )\n return aligned_url\n # endregion _align_origin_host_with_config\n\n # region _parse_remote_repo_identity [TYPE Function]\n # @PURPOSE: Parse owner/repo from remote URL for Git server API operations.\n # @PRE: remote_url is a valid git URL.\n # @POST: Returns owner/repo tokens.\n # @RETURN: Dict[str, str]\n def _parse_remote_repo_identity(self, remote_url: str) -> dict[str, str]:\n normalized = str(remote_url or \"\").strip()\n if not normalized:\n raise HTTPException(status_code=400, detail=\"Repository remote_url is empty\")\n if normalized.startswith(\"git@\"):\n path = normalized.split(\":\", 1)[1] if \":\" in normalized else \"\"\n else:\n parsed = urlparse(normalized)\n path = parsed.path or \"\"\n path = path.strip(\"/\")\n if path.endswith(\".git\"):\n path = path[:-4]\n parts = [segment for segment in path.split(\"/\") if segment]\n if len(parts) < 2:\n raise HTTPException(status_code=400, detail=f\"Cannot parse repository owner/name from remote URL: {remote_url}\")\n owner = parts[0]\n repo = parts[-1]\n namespace = \"/\".join(parts[:-1])\n return {\"owner\": owner, \"repo\": repo, \"namespace\": namespace, \"full_name\": f\"{namespace}/{repo}\"}\n # endregion _parse_remote_repo_identity\n\n # region _derive_server_url_from_remote [TYPE Function]\n # @PURPOSE: Build API base URL from remote repository URL without credentials.\n # @PRE: remote_url may be any git URL.\n # @POST: Returns normalized http(s) base URL or None when derivation is impossible.\n # @RETURN: Optional[str]\n def _derive_server_url_from_remote(self, remote_url: str) -> str | None:\n normalized = str(remote_url or \"\").strip()\n if not normalized or normalized.startswith(\"git@\"):\n return None\n parsed = urlparse(normalized)\n if parsed.scheme not in {\"http\", \"https\"}:\n return None\n if not parsed.hostname:\n return None\n netloc = parsed.hostname\n if parsed.port:\n netloc = f\"{netloc}:{parsed.port}\"\n return f\"{parsed.scheme}://{netloc}\".rstrip(\"/\")\n # endregion _derive_server_url_from_remote\n\n # region _normalize_git_server_url [TYPE Function]\n # @PURPOSE: Normalize Git server URL for provider API calls.\n # @PRE: raw_url is non-empty.\n # @POST: Returns URL without trailing slash.\n # @RETURN: str\n def _normalize_git_server_url(self, raw_url: str) -> str:\n normalized = (raw_url or \"\").strip()\n if not normalized:\n raise HTTPException(status_code=400, detail=\"Git server URL is required\")\n return normalized.rstrip(\"/\")\n # endregion _normalize_git_server_url\n# #endregion GitServiceUrlMixin\n# #endregion GitServiceUrlMixin\n" }, { "contract_id": "health_service", "contract_type": "Module", "file_path": "backend/src/services/health_service.py", "start_line": 1, - "end_line": 384, + "end_line": 378, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain/Service", - "PURPOSE": "Business logic for aggregating dashboard health status from validation records.", - "SEMANTICS": [ - "health", - "aggregation", - "dashboards" - ] + "PURPOSE": "Business logic for aggregating dashboard health status from validation records." }, "relations": [ { "source_id": "health_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ValidationRecord", "target_ref": "[ValidationRecord]" }, { "source_id": "health_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetClient", "target_ref": "[SupersetClient]" }, { "source_id": "health_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskCleanupService", "target_ref": "[TaskCleanupService]" }, { "source_id": "health_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:health_service:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: health, aggregation, dashboards\n# @PURPOSE: Business logic for aggregating dashboard health status from validation records.\n# @LAYER: Domain/Service\n# @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n# @RELATION: [DEPENDS_ON] ->[SupersetClient]\n# @RELATION: [DEPENDS_ON] ->[TaskCleanupService]\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n\nfrom typing import List, Dict, Any, Optional, Tuple, cast\nimport time\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import func\nimport os\nfrom ..models.llm import ValidationRecord\nfrom ..schemas.health import DashboardHealthItem, HealthSummaryResponse\nfrom ..core.logger import logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager.cleanup import TaskCleanupService\nfrom ..core.task_manager import TaskManager\n\n\ndef _empty_dashboard_meta() -> Dict[str, Optional[str]]:\n return cast(Dict[str, Optional[str]], {\"slug\": None, \"title\": None})\n\n\n# [DEF:HealthService:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Aggregate latest dashboard validation state and manage persisted health report lifecycle.\n# @PRE: Service is constructed with a live SQLAlchemy session and optional config manager.\n# @POST: Exposes health summary aggregation and validation report deletion operations.\n# @SIDE_EFFECT: Maintains in-memory dashboard metadata caches and may coordinate cleanup through collaborators.\n# @DATA_CONTRACT: Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool]\n# @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n# @RELATION: [DEPENDS_ON] ->[DashboardHealthItem]\n# @RELATION: [DEPENDS_ON] ->[HealthSummaryResponse]\n# @RELATION: [DEPENDS_ON] ->[SupersetClient]\n# @RELATION: [DEPENDS_ON] ->[TaskCleanupService]\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\nclass HealthService:\n _dashboard_summary_cache: Dict[\n str, Tuple[float, Dict[str, Dict[str, Optional[str]]]]\n ] = {}\n _dashboard_summary_cache_ttl_seconds = 60.0\n\n \"\"\"\n @PURPOSE: Service for managing and querying dashboard health data.\n \"\"\"\n\n # [DEF:HealthService_init:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.\n # @PRE: db is a valid SQLAlchemy session.\n # @POST: Service is ready to aggregate summaries and delete health reports.\n # @SIDE_EFFECT: Initializes per-instance dashboard metadata cache.\n # @DATA_CONTRACT: Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]\n # @RELATION: [BINDS_TO] ->[HealthService]\n def __init__(self, db: Session, config_manager=None):\n self.db = db\n self.config_manager = config_manager\n self._dashboard_meta_cache: Dict[Tuple[str, str], Dict[str, Optional[str]]] = {}\n\n # [/DEF:HealthService_init:Function]\n\n # [DEF:_prime_dashboard_meta_cache:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.\n # @PRE: records may contain mixed numeric and slug dashboard identifiers.\n # @POST: Numeric dashboard ids for known environments are cached when discoverable.\n # @SIDE_EFFECT: May call Superset dashboard list API once per referenced environment.\n # @DATA_CONTRACT: Input[records: List[ValidationRecord]] -> Output[None]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[ConfigManager]\n # @RELATION: [DEPENDS_ON] ->[SupersetClient]\n def _prime_dashboard_meta_cache(self, records: List[ValidationRecord]) -> None:\n if not self.config_manager or not records:\n return\n\n numeric_ids_by_env: Dict[str, set[str]] = {}\n for record in records:\n environment_id = str(record.environment_id or \"\").strip()\n dashboard_id = str(record.dashboard_id or \"\").strip()\n if not environment_id or not dashboard_id or not dashboard_id.isdigit():\n continue\n cache_key = (environment_id, dashboard_id)\n if cache_key in self._dashboard_meta_cache:\n continue\n numeric_ids_by_env.setdefault(environment_id, set()).add(dashboard_id)\n\n if not numeric_ids_by_env:\n return\n\n environments = {\n str(getattr(env, \"id\", \"\")).strip(): env\n for env in self.config_manager.get_environments()\n if str(getattr(env, \"id\", \"\")).strip()\n }\n\n for environment_id, dashboard_ids in numeric_ids_by_env.items():\n env = environments.get(environment_id)\n if not env:\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n continue\n\n try:\n cached_meta = self.__class__._dashboard_summary_cache.get(\n environment_id\n )\n dashboard_meta_map: Dict[str, Dict[str, Optional[str]]]\n if (\n cached_meta is not None\n and (time.monotonic() - cached_meta[0])\n < self.__class__._dashboard_summary_cache_ttl_seconds\n ):\n cached_meta_data = cast(\n Tuple[float, Dict[str, Dict[str, Optional[str]]]],\n cached_meta,\n )\n dashboard_meta_map = cached_meta_data[1]\n else:\n dashboards = SupersetClient(env).get_dashboards_summary()\n dashboard_meta_map = {\n str(item.get(\"id\")): {\n \"slug\": item.get(\"slug\"),\n \"title\": item.get(\"title\"),\n }\n for item in dashboards\n if str(item.get(\"id\") or \"\").strip()\n }\n self.__class__._dashboard_summary_cache[environment_id] = (\n time.monotonic(),\n dashboard_meta_map,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n dashboard_meta_map.get(\n dashboard_id,\n _empty_dashboard_meta(),\n )\n )\n except Exception as exc:\n logger.warning(\n \"[HealthService][_prime_dashboard_meta_cache] Failed to preload dashboard metadata for env=%s: %s\",\n environment_id,\n exc,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n\n # [/DEF:_prime_dashboard_meta_cache:Function]\n\n # [DEF:_resolve_dashboard_meta:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record.\n # @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty.\n # @POST: Returns dict with `slug` and `title` keys, using cache when possible.\n # @SIDE_EFFECT: Writes default cache entries for unresolved numeric dashboard ids.\n def _resolve_dashboard_meta(\n self, dashboard_id: str, environment_id: Optional[str]\n ) -> Dict[str, Optional[str]]:\n normalized_dashboard_id = str(dashboard_id or \"\").strip()\n normalized_environment_id = str(environment_id or \"\").strip()\n if not normalized_dashboard_id:\n return _empty_dashboard_meta()\n\n if not normalized_dashboard_id.isdigit():\n return {\"slug\": normalized_dashboard_id, \"title\": None}\n\n if not self.config_manager or not normalized_environment_id:\n return _empty_dashboard_meta()\n\n cache_key = (normalized_environment_id, normalized_dashboard_id)\n cached = self._dashboard_meta_cache.get(cache_key)\n if cached is not None:\n return cached\n\n meta = _empty_dashboard_meta()\n self._dashboard_meta_cache[cache_key] = meta\n return meta\n\n # [/DEF:_resolve_dashboard_meta:Function]\n\n # [DEF:get_health_summary:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title.\n # @PRE: environment_id may be omitted to aggregate across all environments.\n # @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard.\n # @SIDE_EFFECT: May call Superset API to resolve dashboard metadata.\n # @DATA_CONTRACT: Input[environment_id: Optional[str]] -> Output[HealthSummaryResponse]\n # @RELATION: [CALLS] ->[_prime_dashboard_meta_cache]\n # @RELATION: [CALLS] ->[_resolve_dashboard_meta]\n async def get_health_summary(\n self, environment_id: str = \"\"\n ) -> HealthSummaryResponse:\n \"\"\"\n @PURPOSE: Aggregates the latest validation status for all dashboards.\n @PRE: environment_id (optional) to filter by environment.\n @POST: Returns a HealthSummaryResponse with aggregated status counts and items.\n \"\"\"\n # [REASON] We need the latest ValidationRecord for each unique dashboard_id.\n # We use a subquery to find the max timestamp per dashboard_id.\n\n subquery = self.db.query(\n ValidationRecord.dashboard_id,\n func.max(ValidationRecord.timestamp).label(\"max_ts\"),\n )\n if environment_id:\n subquery = subquery.filter(\n ValidationRecord.environment_id == environment_id\n )\n subquery = subquery.group_by(ValidationRecord.dashboard_id).subquery()\n\n query = self.db.query(ValidationRecord).join(\n subquery,\n (ValidationRecord.dashboard_id == subquery.c.dashboard_id)\n & (ValidationRecord.timestamp == subquery.c.max_ts),\n )\n\n records = query.all()\n\n self._prime_dashboard_meta_cache(records)\n\n items = []\n pass_count = 0\n warn_count = 0\n fail_count = 0\n unknown_count = 0\n\n for rec in records:\n record = cast(Any, rec)\n status = str(record.status or \"\").upper()\n if status == \"PASS\":\n pass_count += 1\n elif status == \"WARN\":\n warn_count += 1\n elif status == \"FAIL\":\n fail_count += 1\n else:\n unknown_count += 1\n status = \"UNKNOWN\"\n\n record_id = str(record.id or \"\")\n dashboard_id = str(record.dashboard_id or \"\")\n resolved_environment_id = (\n str(record.environment_id)\n if record.environment_id is not None\n else None\n )\n response_environment_id = (\n resolved_environment_id\n if resolved_environment_id is not None\n else \"unknown\"\n )\n task_id = str(record.task_id) if record.task_id is not None else None\n summary = str(record.summary) if record.summary is not None else None\n timestamp = cast(Any, record.timestamp)\n\n meta = self._resolve_dashboard_meta(dashboard_id, resolved_environment_id)\n items.append(\n DashboardHealthItem(\n record_id=record_id,\n dashboard_id=dashboard_id,\n dashboard_slug=meta.get(\"slug\"),\n dashboard_title=meta.get(\"title\"),\n environment_id=response_environment_id,\n status=status,\n last_check=timestamp,\n task_id=task_id,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[HealthService][get_health_summary] Aggregated {len(items)} dashboard health records.\"\n )\n\n return HealthSummaryResponse(\n items=items,\n pass_count=pass_count,\n warn_count=warn_count,\n fail_count=fail_count,\n unknown_count=unknown_count,\n )\n\n # [/DEF:get_health_summary:Function]\n\n # [DEF:delete_validation_report:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.\n # @PRE: record_id is a validation record identifier.\n # @POST: Returns True only when a matching record was deleted.\n # @SIDE_EFFECT: Deletes DB rows, optional screenshot file, and optional task/log persistence.\n # @DATA_CONTRACT: Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[TaskCleanupService]\n def delete_validation_report(\n self, record_id: str, task_manager: Optional[TaskManager] = None\n ) -> bool:\n record = (\n self.db.query(ValidationRecord)\n .filter(ValidationRecord.id == record_id)\n .first()\n )\n if not record:\n return False\n\n peer_query = self.db.query(ValidationRecord).filter(\n ValidationRecord.dashboard_id == record.dashboard_id\n )\n if record.environment_id is None:\n peer_query = peer_query.filter(ValidationRecord.environment_id.is_(None))\n else:\n peer_query = peer_query.filter(\n ValidationRecord.environment_id == record.environment_id\n )\n\n records_to_delete = peer_query.all()\n screenshot_paths = [\n str(item.screenshot_path or \"\").strip()\n for item in records_to_delete\n if str(item.screenshot_path or \"\").strip()\n ]\n task_ids = {\n str(item.task_id or \"\").strip()\n for item in records_to_delete\n if str(item.task_id or \"\").strip()\n }\n\n logger.info(\n \"[HealthService][delete_validation_report] Removing %s validation record(s) for dashboard=%s environment=%s triggered_by_record=%s\",\n len(records_to_delete),\n record.dashboard_id,\n record.environment_id,\n record_id,\n )\n\n for item in records_to_delete:\n self.db.delete(item)\n self.db.commit()\n\n for screenshot_path in screenshot_paths:\n try:\n if os.path.exists(screenshot_path):\n os.remove(screenshot_path)\n except OSError as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to remove screenshot %s: %s\",\n screenshot_path,\n exc,\n )\n\n if task_ids and task_manager and self.config_manager:\n try:\n cleanup_service = TaskCleanupService(\n task_manager.persistence_service,\n task_manager.log_persistence_service,\n self.config_manager,\n )\n for task_id in task_ids:\n task_manager.tasks.pop(task_id, None)\n cleanup_service.delete_task_with_logs(task_id)\n except Exception as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to cleanup linked task/logs for dashboard=%s environment=%s: %s\",\n record.dashboard_id,\n record.environment_id,\n exc,\n )\n\n return True\n\n # [/DEF:delete_validation_report:Function]\n\n\n# [/DEF:HealthService:Class]\n\n# [/DEF:health_service:Module]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region health_service [C:3] [TYPE Module] [SEMANTICS sqlalchemy, health, dashboard, validation, aggregate]\n# @BRIEF Business logic for aggregating dashboard health status from validation records.\n# @LAYER: Domain/Service\n# @RELATION DEPENDS_ON -> [ValidationRecord]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [TaskCleanupService]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nimport os\nimport time\nfrom typing import Any, cast\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Session\n\nfrom ..core.logger import logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager import TaskManager\nfrom ..core.task_manager.cleanup import TaskCleanupService\nfrom ..models.llm import ValidationRecord\nfrom ..schemas.health import DashboardHealthItem, HealthSummaryResponse\n\n\ndef _empty_dashboard_meta() -> dict[str, str | None]:\n return cast(dict[str, str | None], {\"slug\": None, \"title\": None})\n\n\n# #region HealthService [C:4] [TYPE Class]\n# @BRIEF Aggregate latest dashboard validation state and manage persisted health report lifecycle.\n# @PRE: Service is constructed with a live SQLAlchemy session and optional config manager.\n# @POST: Exposes health summary aggregation and validation report deletion operations.\n# @SIDE_EFFECT: Maintains in-memory dashboard metadata caches and may coordinate cleanup through collaborators.\n# @DATA_CONTRACT: Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool]\n# @RELATION DEPENDS_ON -> [ValidationRecord]\n# @RELATION DEPENDS_ON -> [DashboardHealthItem]\n# @RELATION DEPENDS_ON -> [HealthSummaryResponse]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [TaskCleanupService]\n# @RELATION DEPENDS_ON -> [TaskManager]\nclass HealthService:\n _dashboard_summary_cache: dict[\n str, tuple[float, dict[str, dict[str, str | None]]]\n ] = {}\n _dashboard_summary_cache_ttl_seconds = 60.0\n\n \"\"\"\n @PURPOSE: Service for managing and querying dashboard health data.\n \"\"\"\n\n # region HealthService_init [TYPE Function]\n # @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.\n # @PRE: db is a valid SQLAlchemy session.\n # @POST: Service is ready to aggregate summaries and delete health reports.\n # @SIDE_EFFECT: Initializes per-instance dashboard metadata cache.\n # @DATA_CONTRACT: Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]\n # @RELATION: [BINDS_TO] ->[HealthService]\n def __init__(self, db: Session, config_manager=None):\n self.db = db\n self.config_manager = config_manager\n self._dashboard_meta_cache: dict[tuple[str, str], dict[str, str | None]] = {}\n\n # endregion HealthService_init\n\n # region _prime_dashboard_meta_cache [TYPE Function]\n # @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.\n # @PRE: records may contain mixed numeric and slug dashboard identifiers.\n # @POST: Numeric dashboard ids for known environments are cached when discoverable.\n # @SIDE_EFFECT: May call Superset dashboard list API once per referenced environment.\n # @DATA_CONTRACT: Input[records: List[ValidationRecord]] -> Output[None]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[ConfigManager]\n # @RELATION: [DEPENDS_ON] ->[SupersetClient]\n def _prime_dashboard_meta_cache(self, records: list[ValidationRecord]) -> None:\n if not self.config_manager or not records:\n return\n\n numeric_ids_by_env: dict[str, set[str]] = {}\n for record in records:\n environment_id = str(record.environment_id or \"\").strip()\n dashboard_id = str(record.dashboard_id or \"\").strip()\n if not environment_id or not dashboard_id or not dashboard_id.isdigit():\n continue\n cache_key = (environment_id, dashboard_id)\n if cache_key in self._dashboard_meta_cache:\n continue\n numeric_ids_by_env.setdefault(environment_id, set()).add(dashboard_id)\n\n if not numeric_ids_by_env:\n return\n\n environments = {\n str(getattr(env, \"id\", \"\")).strip(): env\n for env in self.config_manager.get_environments()\n if str(getattr(env, \"id\", \"\")).strip()\n }\n\n for environment_id, dashboard_ids in numeric_ids_by_env.items():\n env = environments.get(environment_id)\n if not env:\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n continue\n\n try:\n cached_meta = self.__class__._dashboard_summary_cache.get(\n environment_id\n )\n dashboard_meta_map: dict[str, dict[str, str | None]]\n if (\n cached_meta is not None\n and (time.monotonic() - cached_meta[0])\n < self.__class__._dashboard_summary_cache_ttl_seconds\n ):\n cached_meta_data = cast(\n tuple[float, dict[str, dict[str, str | None]]],\n cached_meta,\n )\n dashboard_meta_map = cached_meta_data[1]\n else:\n dashboards = SupersetClient(env).get_dashboards_summary()\n dashboard_meta_map = {\n str(item.get(\"id\")): {\n \"slug\": item.get(\"slug\"),\n \"title\": item.get(\"title\"),\n }\n for item in dashboards\n if str(item.get(\"id\") or \"\").strip()\n }\n self.__class__._dashboard_summary_cache[environment_id] = (\n time.monotonic(),\n dashboard_meta_map,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n dashboard_meta_map.get(\n dashboard_id,\n _empty_dashboard_meta(),\n )\n )\n except Exception as exc:\n logger.warning(\n \"[HealthService][_prime_dashboard_meta_cache] Failed to preload dashboard metadata for env=%s: %s\",\n environment_id,\n exc,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n\n # endregion _prime_dashboard_meta_cache\n\n # region _resolve_dashboard_meta [TYPE Function]\n # @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record.\n # @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty.\n # @POST: Returns dict with `slug` and `title` keys, using cache when possible.\n # @SIDE_EFFECT: Writes default cache entries for unresolved numeric dashboard ids.\n def _resolve_dashboard_meta(\n self, dashboard_id: str, environment_id: str | None\n ) -> dict[str, str | None]:\n normalized_dashboard_id = str(dashboard_id or \"\").strip()\n normalized_environment_id = str(environment_id or \"\").strip()\n if not normalized_dashboard_id:\n return _empty_dashboard_meta()\n\n if not normalized_dashboard_id.isdigit():\n return {\"slug\": normalized_dashboard_id, \"title\": None}\n\n if not self.config_manager or not normalized_environment_id:\n return _empty_dashboard_meta()\n\n cache_key = (normalized_environment_id, normalized_dashboard_id)\n cached = self._dashboard_meta_cache.get(cache_key)\n if cached is not None:\n return cached\n\n meta = _empty_dashboard_meta()\n self._dashboard_meta_cache[cache_key] = meta\n return meta\n\n # endregion _resolve_dashboard_meta\n\n # region get_health_summary [TYPE Function]\n # @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title.\n # @PRE: environment_id may be omitted to aggregate across all environments.\n # @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard.\n # @SIDE_EFFECT: May call Superset API to resolve dashboard metadata.\n # @DATA_CONTRACT: Input[environment_id: Optional[str]] -> Output[HealthSummaryResponse]\n # @RELATION: [CALLS] ->[_prime_dashboard_meta_cache]\n # @RELATION: [CALLS] ->[_resolve_dashboard_meta]\n async def get_health_summary(\n self, environment_id: str = \"\"\n ) -> HealthSummaryResponse:\n \"\"\"\n @PURPOSE: Aggregates the latest validation status for all dashboards.\n @PRE: environment_id (optional) to filter by environment.\n @POST: Returns a HealthSummaryResponse with aggregated status counts and items.\n \"\"\"\n # [REASON] We need the latest ValidationRecord for each unique dashboard_id.\n # We use a subquery to find the max timestamp per dashboard_id.\n\n subquery = self.db.query(\n ValidationRecord.dashboard_id,\n func.max(ValidationRecord.timestamp).label(\"max_ts\"),\n )\n if environment_id:\n subquery = subquery.filter(\n ValidationRecord.environment_id == environment_id\n )\n subquery = subquery.group_by(ValidationRecord.dashboard_id).subquery()\n\n query = self.db.query(ValidationRecord).join(\n subquery,\n (ValidationRecord.dashboard_id == subquery.c.dashboard_id)\n & (ValidationRecord.timestamp == subquery.c.max_ts),\n )\n\n records = query.all()\n\n self._prime_dashboard_meta_cache(records)\n\n items = []\n pass_count = 0\n warn_count = 0\n fail_count = 0\n unknown_count = 0\n\n for rec in records:\n record = cast(Any, rec)\n status = str(record.status or \"\").upper()\n if status == \"PASS\":\n pass_count += 1\n elif status == \"WARN\":\n warn_count += 1\n elif status == \"FAIL\":\n fail_count += 1\n else:\n unknown_count += 1\n status = \"UNKNOWN\"\n\n record_id = str(record.id or \"\")\n dashboard_id = str(record.dashboard_id or \"\")\n resolved_environment_id = (\n str(record.environment_id)\n if record.environment_id is not None\n else None\n )\n response_environment_id = (\n resolved_environment_id\n if resolved_environment_id is not None\n else \"unknown\"\n )\n task_id = str(record.task_id) if record.task_id is not None else None\n summary = str(record.summary) if record.summary is not None else None\n timestamp = cast(Any, record.timestamp)\n\n meta = self._resolve_dashboard_meta(dashboard_id, resolved_environment_id)\n items.append(\n DashboardHealthItem(\n record_id=record_id,\n dashboard_id=dashboard_id,\n dashboard_slug=meta.get(\"slug\"),\n dashboard_title=meta.get(\"title\"),\n environment_id=response_environment_id,\n status=status,\n last_check=timestamp,\n task_id=task_id,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[HealthService][get_health_summary] Aggregated {len(items)} dashboard health records.\"\n )\n\n return HealthSummaryResponse(\n items=items,\n pass_count=pass_count,\n warn_count=warn_count,\n fail_count=fail_count,\n unknown_count=unknown_count,\n )\n\n # endregion get_health_summary\n\n # region delete_validation_report [TYPE Function]\n # @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.\n # @PRE: record_id is a validation record identifier.\n # @POST: Returns True only when a matching record was deleted.\n # @SIDE_EFFECT: Deletes DB rows, optional screenshot file, and optional task/log persistence.\n # @DATA_CONTRACT: Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[TaskCleanupService]\n def delete_validation_report(\n self, record_id: str, task_manager: TaskManager | None = None\n ) -> bool:\n record = (\n self.db.query(ValidationRecord)\n .filter(ValidationRecord.id == record_id)\n .first()\n )\n if not record:\n return False\n\n peer_query = self.db.query(ValidationRecord).filter(\n ValidationRecord.dashboard_id == record.dashboard_id\n )\n if record.environment_id is None:\n peer_query = peer_query.filter(ValidationRecord.environment_id.is_(None))\n else:\n peer_query = peer_query.filter(\n ValidationRecord.environment_id == record.environment_id\n )\n\n records_to_delete = peer_query.all()\n screenshot_paths = [\n str(item.screenshot_path or \"\").strip()\n for item in records_to_delete\n if str(item.screenshot_path or \"\").strip()\n ]\n task_ids = {\n str(item.task_id or \"\").strip()\n for item in records_to_delete\n if str(item.task_id or \"\").strip()\n }\n\n logger.info(\n \"[HealthService][delete_validation_report] Removing %s validation record(s) for dashboard=%s environment=%s triggered_by_record=%s\",\n len(records_to_delete),\n record.dashboard_id,\n record.environment_id,\n record_id,\n )\n\n for item in records_to_delete:\n self.db.delete(item)\n self.db.commit()\n\n for screenshot_path in screenshot_paths:\n try:\n if os.path.exists(screenshot_path):\n os.remove(screenshot_path)\n except OSError as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to remove screenshot %s: %s\",\n screenshot_path,\n exc,\n )\n\n if task_ids and task_manager and self.config_manager:\n try:\n cleanup_service = TaskCleanupService(\n task_manager.persistence_service,\n task_manager.log_persistence_service,\n self.config_manager,\n )\n for task_id in task_ids:\n task_manager.tasks.pop(task_id, None)\n cleanup_service.delete_task_with_logs(task_id)\n except Exception as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to cleanup linked task/logs for dashboard=%s environment=%s: %s\",\n record.dashboard_id,\n record.environment_id,\n exc,\n )\n\n return True\n\n # endregion delete_validation_report\n\n\n# #endregion HealthService\n\n# #endregion health_service\n" }, { "contract_id": "HealthService", "contract_type": "Class", "file_path": "backend/src/services/health_service.py", "start_line": 28, - "end_line": 382, + "end_line": 376, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "DATA_CONTRACT": "Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool]", "POST": "Exposes health summary aggregation and validation report deletion operations.", "PRE": "Service is constructed with a live SQLAlchemy session and optional config manager.", @@ -89558,37 +63114,37 @@ "relations": [ { "source_id": "HealthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ValidationRecord", "target_ref": "[ValidationRecord]" }, { "source_id": "HealthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "DashboardHealthItem", "target_ref": "[DashboardHealthItem]" }, { "source_id": "HealthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "HealthSummaryResponse", "target_ref": "[HealthSummaryResponse]" }, { "source_id": "HealthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetClient", "target_ref": "[SupersetClient]" }, { "source_id": "HealthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskCleanupService", "target_ref": "[TaskCleanupService]" }, { "source_id": "HealthService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" } @@ -89602,540 +63158,24 @@ "actual_complexity": 4, "contract_type": "Class" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:HealthService:Class]\n# @COMPLEXITY: 4\n# @PURPOSE: Aggregate latest dashboard validation state and manage persisted health report lifecycle.\n# @PRE: Service is constructed with a live SQLAlchemy session and optional config manager.\n# @POST: Exposes health summary aggregation and validation report deletion operations.\n# @SIDE_EFFECT: Maintains in-memory dashboard metadata caches and may coordinate cleanup through collaborators.\n# @DATA_CONTRACT: Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool]\n# @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n# @RELATION: [DEPENDS_ON] ->[DashboardHealthItem]\n# @RELATION: [DEPENDS_ON] ->[HealthSummaryResponse]\n# @RELATION: [DEPENDS_ON] ->[SupersetClient]\n# @RELATION: [DEPENDS_ON] ->[TaskCleanupService]\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\nclass HealthService:\n _dashboard_summary_cache: Dict[\n str, Tuple[float, Dict[str, Dict[str, Optional[str]]]]\n ] = {}\n _dashboard_summary_cache_ttl_seconds = 60.0\n\n \"\"\"\n @PURPOSE: Service for managing and querying dashboard health data.\n \"\"\"\n\n # [DEF:HealthService_init:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.\n # @PRE: db is a valid SQLAlchemy session.\n # @POST: Service is ready to aggregate summaries and delete health reports.\n # @SIDE_EFFECT: Initializes per-instance dashboard metadata cache.\n # @DATA_CONTRACT: Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]\n # @RELATION: [BINDS_TO] ->[HealthService]\n def __init__(self, db: Session, config_manager=None):\n self.db = db\n self.config_manager = config_manager\n self._dashboard_meta_cache: Dict[Tuple[str, str], Dict[str, Optional[str]]] = {}\n\n # [/DEF:HealthService_init:Function]\n\n # [DEF:_prime_dashboard_meta_cache:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.\n # @PRE: records may contain mixed numeric and slug dashboard identifiers.\n # @POST: Numeric dashboard ids for known environments are cached when discoverable.\n # @SIDE_EFFECT: May call Superset dashboard list API once per referenced environment.\n # @DATA_CONTRACT: Input[records: List[ValidationRecord]] -> Output[None]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[ConfigManager]\n # @RELATION: [DEPENDS_ON] ->[SupersetClient]\n def _prime_dashboard_meta_cache(self, records: List[ValidationRecord]) -> None:\n if not self.config_manager or not records:\n return\n\n numeric_ids_by_env: Dict[str, set[str]] = {}\n for record in records:\n environment_id = str(record.environment_id or \"\").strip()\n dashboard_id = str(record.dashboard_id or \"\").strip()\n if not environment_id or not dashboard_id or not dashboard_id.isdigit():\n continue\n cache_key = (environment_id, dashboard_id)\n if cache_key in self._dashboard_meta_cache:\n continue\n numeric_ids_by_env.setdefault(environment_id, set()).add(dashboard_id)\n\n if not numeric_ids_by_env:\n return\n\n environments = {\n str(getattr(env, \"id\", \"\")).strip(): env\n for env in self.config_manager.get_environments()\n if str(getattr(env, \"id\", \"\")).strip()\n }\n\n for environment_id, dashboard_ids in numeric_ids_by_env.items():\n env = environments.get(environment_id)\n if not env:\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n continue\n\n try:\n cached_meta = self.__class__._dashboard_summary_cache.get(\n environment_id\n )\n dashboard_meta_map: Dict[str, Dict[str, Optional[str]]]\n if (\n cached_meta is not None\n and (time.monotonic() - cached_meta[0])\n < self.__class__._dashboard_summary_cache_ttl_seconds\n ):\n cached_meta_data = cast(\n Tuple[float, Dict[str, Dict[str, Optional[str]]]],\n cached_meta,\n )\n dashboard_meta_map = cached_meta_data[1]\n else:\n dashboards = SupersetClient(env).get_dashboards_summary()\n dashboard_meta_map = {\n str(item.get(\"id\")): {\n \"slug\": item.get(\"slug\"),\n \"title\": item.get(\"title\"),\n }\n for item in dashboards\n if str(item.get(\"id\") or \"\").strip()\n }\n self.__class__._dashboard_summary_cache[environment_id] = (\n time.monotonic(),\n dashboard_meta_map,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n dashboard_meta_map.get(\n dashboard_id,\n _empty_dashboard_meta(),\n )\n )\n except Exception as exc:\n logger.warning(\n \"[HealthService][_prime_dashboard_meta_cache] Failed to preload dashboard metadata for env=%s: %s\",\n environment_id,\n exc,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n\n # [/DEF:_prime_dashboard_meta_cache:Function]\n\n # [DEF:_resolve_dashboard_meta:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record.\n # @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty.\n # @POST: Returns dict with `slug` and `title` keys, using cache when possible.\n # @SIDE_EFFECT: Writes default cache entries for unresolved numeric dashboard ids.\n def _resolve_dashboard_meta(\n self, dashboard_id: str, environment_id: Optional[str]\n ) -> Dict[str, Optional[str]]:\n normalized_dashboard_id = str(dashboard_id or \"\").strip()\n normalized_environment_id = str(environment_id or \"\").strip()\n if not normalized_dashboard_id:\n return _empty_dashboard_meta()\n\n if not normalized_dashboard_id.isdigit():\n return {\"slug\": normalized_dashboard_id, \"title\": None}\n\n if not self.config_manager or not normalized_environment_id:\n return _empty_dashboard_meta()\n\n cache_key = (normalized_environment_id, normalized_dashboard_id)\n cached = self._dashboard_meta_cache.get(cache_key)\n if cached is not None:\n return cached\n\n meta = _empty_dashboard_meta()\n self._dashboard_meta_cache[cache_key] = meta\n return meta\n\n # [/DEF:_resolve_dashboard_meta:Function]\n\n # [DEF:get_health_summary:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title.\n # @PRE: environment_id may be omitted to aggregate across all environments.\n # @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard.\n # @SIDE_EFFECT: May call Superset API to resolve dashboard metadata.\n # @DATA_CONTRACT: Input[environment_id: Optional[str]] -> Output[HealthSummaryResponse]\n # @RELATION: [CALLS] ->[_prime_dashboard_meta_cache]\n # @RELATION: [CALLS] ->[_resolve_dashboard_meta]\n async def get_health_summary(\n self, environment_id: str = \"\"\n ) -> HealthSummaryResponse:\n \"\"\"\n @PURPOSE: Aggregates the latest validation status for all dashboards.\n @PRE: environment_id (optional) to filter by environment.\n @POST: Returns a HealthSummaryResponse with aggregated status counts and items.\n \"\"\"\n # [REASON] We need the latest ValidationRecord for each unique dashboard_id.\n # We use a subquery to find the max timestamp per dashboard_id.\n\n subquery = self.db.query(\n ValidationRecord.dashboard_id,\n func.max(ValidationRecord.timestamp).label(\"max_ts\"),\n )\n if environment_id:\n subquery = subquery.filter(\n ValidationRecord.environment_id == environment_id\n )\n subquery = subquery.group_by(ValidationRecord.dashboard_id).subquery()\n\n query = self.db.query(ValidationRecord).join(\n subquery,\n (ValidationRecord.dashboard_id == subquery.c.dashboard_id)\n & (ValidationRecord.timestamp == subquery.c.max_ts),\n )\n\n records = query.all()\n\n self._prime_dashboard_meta_cache(records)\n\n items = []\n pass_count = 0\n warn_count = 0\n fail_count = 0\n unknown_count = 0\n\n for rec in records:\n record = cast(Any, rec)\n status = str(record.status or \"\").upper()\n if status == \"PASS\":\n pass_count += 1\n elif status == \"WARN\":\n warn_count += 1\n elif status == \"FAIL\":\n fail_count += 1\n else:\n unknown_count += 1\n status = \"UNKNOWN\"\n\n record_id = str(record.id or \"\")\n dashboard_id = str(record.dashboard_id or \"\")\n resolved_environment_id = (\n str(record.environment_id)\n if record.environment_id is not None\n else None\n )\n response_environment_id = (\n resolved_environment_id\n if resolved_environment_id is not None\n else \"unknown\"\n )\n task_id = str(record.task_id) if record.task_id is not None else None\n summary = str(record.summary) if record.summary is not None else None\n timestamp = cast(Any, record.timestamp)\n\n meta = self._resolve_dashboard_meta(dashboard_id, resolved_environment_id)\n items.append(\n DashboardHealthItem(\n record_id=record_id,\n dashboard_id=dashboard_id,\n dashboard_slug=meta.get(\"slug\"),\n dashboard_title=meta.get(\"title\"),\n environment_id=response_environment_id,\n status=status,\n last_check=timestamp,\n task_id=task_id,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[HealthService][get_health_summary] Aggregated {len(items)} dashboard health records.\"\n )\n\n return HealthSummaryResponse(\n items=items,\n pass_count=pass_count,\n warn_count=warn_count,\n fail_count=fail_count,\n unknown_count=unknown_count,\n )\n\n # [/DEF:get_health_summary:Function]\n\n # [DEF:delete_validation_report:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.\n # @PRE: record_id is a validation record identifier.\n # @POST: Returns True only when a matching record was deleted.\n # @SIDE_EFFECT: Deletes DB rows, optional screenshot file, and optional task/log persistence.\n # @DATA_CONTRACT: Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[TaskCleanupService]\n def delete_validation_report(\n self, record_id: str, task_manager: Optional[TaskManager] = None\n ) -> bool:\n record = (\n self.db.query(ValidationRecord)\n .filter(ValidationRecord.id == record_id)\n .first()\n )\n if not record:\n return False\n\n peer_query = self.db.query(ValidationRecord).filter(\n ValidationRecord.dashboard_id == record.dashboard_id\n )\n if record.environment_id is None:\n peer_query = peer_query.filter(ValidationRecord.environment_id.is_(None))\n else:\n peer_query = peer_query.filter(\n ValidationRecord.environment_id == record.environment_id\n )\n\n records_to_delete = peer_query.all()\n screenshot_paths = [\n str(item.screenshot_path or \"\").strip()\n for item in records_to_delete\n if str(item.screenshot_path or \"\").strip()\n ]\n task_ids = {\n str(item.task_id or \"\").strip()\n for item in records_to_delete\n if str(item.task_id or \"\").strip()\n }\n\n logger.info(\n \"[HealthService][delete_validation_report] Removing %s validation record(s) for dashboard=%s environment=%s triggered_by_record=%s\",\n len(records_to_delete),\n record.dashboard_id,\n record.environment_id,\n record_id,\n )\n\n for item in records_to_delete:\n self.db.delete(item)\n self.db.commit()\n\n for screenshot_path in screenshot_paths:\n try:\n if os.path.exists(screenshot_path):\n os.remove(screenshot_path)\n except OSError as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to remove screenshot %s: %s\",\n screenshot_path,\n exc,\n )\n\n if task_ids and task_manager and self.config_manager:\n try:\n cleanup_service = TaskCleanupService(\n task_manager.persistence_service,\n task_manager.log_persistence_service,\n self.config_manager,\n )\n for task_id in task_ids:\n task_manager.tasks.pop(task_id, None)\n cleanup_service.delete_task_with_logs(task_id)\n except Exception as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to cleanup linked task/logs for dashboard=%s environment=%s: %s\",\n record.dashboard_id,\n record.environment_id,\n exc,\n )\n\n return True\n\n # [/DEF:delete_validation_report:Function]\n\n\n# [/DEF:HealthService:Class]\n" - }, - { - "contract_id": "HealthService_init", - "contract_type": "Function", - "file_path": "backend/src/services/health_service.py", - "start_line": 51, - "end_line": 64, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]", - "POST": "Service is ready to aggregate summaries and delete health reports.", - "PRE": "db is a valid SQLAlchemy session.", - "PURPOSE": "Initialize health service with DB session and optional config access for dashboard metadata resolution.", - "SIDE_EFFECT": "Initializes per-instance dashboard metadata cache." - }, - "relations": [ - { - "source_id": "HealthService_init", - "relation_type": "[BINDS_TO]", - "target_id": "HealthService", - "target_ref": "[HealthService]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [BINDS_TO] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[BINDS_TO]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:HealthService_init:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.\n # @PRE: db is a valid SQLAlchemy session.\n # @POST: Service is ready to aggregate summaries and delete health reports.\n # @SIDE_EFFECT: Initializes per-instance dashboard metadata cache.\n # @DATA_CONTRACT: Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]\n # @RELATION: [BINDS_TO] ->[HealthService]\n def __init__(self, db: Session, config_manager=None):\n self.db = db\n self.config_manager = config_manager\n self._dashboard_meta_cache: Dict[Tuple[str, str], Dict[str, Optional[str]]] = {}\n\n # [/DEF:HealthService_init:Function]\n" - }, - { - "contract_id": "_prime_dashboard_meta_cache", - "contract_type": "Function", - "file_path": "backend/src/services/health_service.py", - "start_line": 66, - "end_line": 156, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[records: List[ValidationRecord]] -> Output[None]", - "POST": "Numeric dashboard ids for known environments are cached when discoverable.", - "PRE": "records may contain mixed numeric and slug dashboard identifiers.", - "PURPOSE": "Warm dashboard slug/title cache with one Superset list fetch per environment.", - "SIDE_EFFECT": "May call Superset dashboard list API once per referenced environment." - }, - "relations": [ - { - "source_id": "_prime_dashboard_meta_cache", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationRecord", - "target_ref": "[ValidationRecord]" - }, - { - "source_id": "_prime_dashboard_meta_cache", - "relation_type": "[DEPENDS_ON]", - "target_id": "ConfigManager", - "target_ref": "[ConfigManager]" - }, - { - "source_id": "_prime_dashboard_meta_cache", - "relation_type": "[DEPENDS_ON]", - "target_id": "SupersetClient", - "target_ref": "[SupersetClient]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_prime_dashboard_meta_cache:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.\n # @PRE: records may contain mixed numeric and slug dashboard identifiers.\n # @POST: Numeric dashboard ids for known environments are cached when discoverable.\n # @SIDE_EFFECT: May call Superset dashboard list API once per referenced environment.\n # @DATA_CONTRACT: Input[records: List[ValidationRecord]] -> Output[None]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[ConfigManager]\n # @RELATION: [DEPENDS_ON] ->[SupersetClient]\n def _prime_dashboard_meta_cache(self, records: List[ValidationRecord]) -> None:\n if not self.config_manager or not records:\n return\n\n numeric_ids_by_env: Dict[str, set[str]] = {}\n for record in records:\n environment_id = str(record.environment_id or \"\").strip()\n dashboard_id = str(record.dashboard_id or \"\").strip()\n if not environment_id or not dashboard_id or not dashboard_id.isdigit():\n continue\n cache_key = (environment_id, dashboard_id)\n if cache_key in self._dashboard_meta_cache:\n continue\n numeric_ids_by_env.setdefault(environment_id, set()).add(dashboard_id)\n\n if not numeric_ids_by_env:\n return\n\n environments = {\n str(getattr(env, \"id\", \"\")).strip(): env\n for env in self.config_manager.get_environments()\n if str(getattr(env, \"id\", \"\")).strip()\n }\n\n for environment_id, dashboard_ids in numeric_ids_by_env.items():\n env = environments.get(environment_id)\n if not env:\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n continue\n\n try:\n cached_meta = self.__class__._dashboard_summary_cache.get(\n environment_id\n )\n dashboard_meta_map: Dict[str, Dict[str, Optional[str]]]\n if (\n cached_meta is not None\n and (time.monotonic() - cached_meta[0])\n < self.__class__._dashboard_summary_cache_ttl_seconds\n ):\n cached_meta_data = cast(\n Tuple[float, Dict[str, Dict[str, Optional[str]]]],\n cached_meta,\n )\n dashboard_meta_map = cached_meta_data[1]\n else:\n dashboards = SupersetClient(env).get_dashboards_summary()\n dashboard_meta_map = {\n str(item.get(\"id\")): {\n \"slug\": item.get(\"slug\"),\n \"title\": item.get(\"title\"),\n }\n for item in dashboards\n if str(item.get(\"id\") or \"\").strip()\n }\n self.__class__._dashboard_summary_cache[environment_id] = (\n time.monotonic(),\n dashboard_meta_map,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n dashboard_meta_map.get(\n dashboard_id,\n _empty_dashboard_meta(),\n )\n )\n except Exception as exc:\n logger.warning(\n \"[HealthService][_prime_dashboard_meta_cache] Failed to preload dashboard metadata for env=%s: %s\",\n environment_id,\n exc,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n\n # [/DEF:_prime_dashboard_meta_cache:Function]\n" - }, - { - "contract_id": "_resolve_dashboard_meta", - "contract_type": "Function", - "file_path": "backend/src/services/health_service.py", - "start_line": 158, - "end_line": 187, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "COMPLEXITY": "1", - "POST": "Returns dict with `slug` and `title` keys, using cache when possible.", - "PRE": "dashboard_id may be numeric or slug-like; environment_id may be empty.", - "PURPOSE": "Resolve slug/title for a dashboard referenced by persisted validation record.", - "SIDE_EFFECT": "Writes default cache entries for unresolved numeric dashboard ids." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_resolve_dashboard_meta:Function]\n # @COMPLEXITY: 1\n # @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record.\n # @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty.\n # @POST: Returns dict with `slug` and `title` keys, using cache when possible.\n # @SIDE_EFFECT: Writes default cache entries for unresolved numeric dashboard ids.\n def _resolve_dashboard_meta(\n self, dashboard_id: str, environment_id: Optional[str]\n ) -> Dict[str, Optional[str]]:\n normalized_dashboard_id = str(dashboard_id or \"\").strip()\n normalized_environment_id = str(environment_id or \"\").strip()\n if not normalized_dashboard_id:\n return _empty_dashboard_meta()\n\n if not normalized_dashboard_id.isdigit():\n return {\"slug\": normalized_dashboard_id, \"title\": None}\n\n if not self.config_manager or not normalized_environment_id:\n return _empty_dashboard_meta()\n\n cache_key = (normalized_environment_id, normalized_dashboard_id)\n cached = self._dashboard_meta_cache.get(cache_key)\n if cached is not None:\n return cached\n\n meta = _empty_dashboard_meta()\n self._dashboard_meta_cache[cache_key] = meta\n return meta\n\n # [/DEF:_resolve_dashboard_meta:Function]\n" - }, - { - "contract_id": "delete_validation_report", - "contract_type": "Function", - "file_path": "backend/src/services/health_service.py", - "start_line": 293, - "end_line": 379, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "DATA_CONTRACT": "Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]", - "POST": "Returns True only when a matching record was deleted.", - "PRE": "record_id is a validation record identifier.", - "PURPOSE": "Delete one persisted health report and optionally clean linked task/log artifacts.", - "SIDE_EFFECT": "Deletes DB rows, optional screenshot file, and optional task/log persistence." - }, - "relations": [ - { - "source_id": "delete_validation_report", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationRecord", - "target_ref": "[ValidationRecord]" - }, - { - "source_id": "delete_validation_report", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskManager", - "target_ref": "[TaskManager]" - }, - { - "source_id": "delete_validation_report", - "relation_type": "[DEPENDS_ON]", - "target_id": "TaskCleanupService", - "target_ref": "[TaskCleanupService]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "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": "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:delete_validation_report:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.\n # @PRE: record_id is a validation record identifier.\n # @POST: Returns True only when a matching record was deleted.\n # @SIDE_EFFECT: Deletes DB rows, optional screenshot file, and optional task/log persistence.\n # @DATA_CONTRACT: Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[TaskCleanupService]\n def delete_validation_report(\n self, record_id: str, task_manager: Optional[TaskManager] = None\n ) -> bool:\n record = (\n self.db.query(ValidationRecord)\n .filter(ValidationRecord.id == record_id)\n .first()\n )\n if not record:\n return False\n\n peer_query = self.db.query(ValidationRecord).filter(\n ValidationRecord.dashboard_id == record.dashboard_id\n )\n if record.environment_id is None:\n peer_query = peer_query.filter(ValidationRecord.environment_id.is_(None))\n else:\n peer_query = peer_query.filter(\n ValidationRecord.environment_id == record.environment_id\n )\n\n records_to_delete = peer_query.all()\n screenshot_paths = [\n str(item.screenshot_path or \"\").strip()\n for item in records_to_delete\n if str(item.screenshot_path or \"\").strip()\n ]\n task_ids = {\n str(item.task_id or \"\").strip()\n for item in records_to_delete\n if str(item.task_id or \"\").strip()\n }\n\n logger.info(\n \"[HealthService][delete_validation_report] Removing %s validation record(s) for dashboard=%s environment=%s triggered_by_record=%s\",\n len(records_to_delete),\n record.dashboard_id,\n record.environment_id,\n record_id,\n )\n\n for item in records_to_delete:\n self.db.delete(item)\n self.db.commit()\n\n for screenshot_path in screenshot_paths:\n try:\n if os.path.exists(screenshot_path):\n os.remove(screenshot_path)\n except OSError as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to remove screenshot %s: %s\",\n screenshot_path,\n exc,\n )\n\n if task_ids and task_manager and self.config_manager:\n try:\n cleanup_service = TaskCleanupService(\n task_manager.persistence_service,\n task_manager.log_persistence_service,\n self.config_manager,\n )\n for task_id in task_ids:\n task_manager.tasks.pop(task_id, None)\n cleanup_service.delete_task_with_logs(task_id)\n except Exception as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to cleanup linked task/logs for dashboard=%s environment=%s: %s\",\n record.dashboard_id,\n record.environment_id,\n exc,\n )\n\n return True\n\n # [/DEF:delete_validation_report:Function]\n" + "anchor_syntax": "region", + "body": "# #region HealthService [C:4] [TYPE Class]\n# @BRIEF Aggregate latest dashboard validation state and manage persisted health report lifecycle.\n# @PRE: Service is constructed with a live SQLAlchemy session and optional config manager.\n# @POST: Exposes health summary aggregation and validation report deletion operations.\n# @SIDE_EFFECT: Maintains in-memory dashboard metadata caches and may coordinate cleanup through collaborators.\n# @DATA_CONTRACT: Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool]\n# @RELATION DEPENDS_ON -> [ValidationRecord]\n# @RELATION DEPENDS_ON -> [DashboardHealthItem]\n# @RELATION DEPENDS_ON -> [HealthSummaryResponse]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [TaskCleanupService]\n# @RELATION DEPENDS_ON -> [TaskManager]\nclass HealthService:\n _dashboard_summary_cache: dict[\n str, tuple[float, dict[str, dict[str, str | None]]]\n ] = {}\n _dashboard_summary_cache_ttl_seconds = 60.0\n\n \"\"\"\n @PURPOSE: Service for managing and querying dashboard health data.\n \"\"\"\n\n # region HealthService_init [TYPE Function]\n # @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.\n # @PRE: db is a valid SQLAlchemy session.\n # @POST: Service is ready to aggregate summaries and delete health reports.\n # @SIDE_EFFECT: Initializes per-instance dashboard metadata cache.\n # @DATA_CONTRACT: Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]\n # @RELATION: [BINDS_TO] ->[HealthService]\n def __init__(self, db: Session, config_manager=None):\n self.db = db\n self.config_manager = config_manager\n self._dashboard_meta_cache: dict[tuple[str, str], dict[str, str | None]] = {}\n\n # endregion HealthService_init\n\n # region _prime_dashboard_meta_cache [TYPE Function]\n # @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.\n # @PRE: records may contain mixed numeric and slug dashboard identifiers.\n # @POST: Numeric dashboard ids for known environments are cached when discoverable.\n # @SIDE_EFFECT: May call Superset dashboard list API once per referenced environment.\n # @DATA_CONTRACT: Input[records: List[ValidationRecord]] -> Output[None]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[ConfigManager]\n # @RELATION: [DEPENDS_ON] ->[SupersetClient]\n def _prime_dashboard_meta_cache(self, records: list[ValidationRecord]) -> None:\n if not self.config_manager or not records:\n return\n\n numeric_ids_by_env: dict[str, set[str]] = {}\n for record in records:\n environment_id = str(record.environment_id or \"\").strip()\n dashboard_id = str(record.dashboard_id or \"\").strip()\n if not environment_id or not dashboard_id or not dashboard_id.isdigit():\n continue\n cache_key = (environment_id, dashboard_id)\n if cache_key in self._dashboard_meta_cache:\n continue\n numeric_ids_by_env.setdefault(environment_id, set()).add(dashboard_id)\n\n if not numeric_ids_by_env:\n return\n\n environments = {\n str(getattr(env, \"id\", \"\")).strip(): env\n for env in self.config_manager.get_environments()\n if str(getattr(env, \"id\", \"\")).strip()\n }\n\n for environment_id, dashboard_ids in numeric_ids_by_env.items():\n env = environments.get(environment_id)\n if not env:\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n continue\n\n try:\n cached_meta = self.__class__._dashboard_summary_cache.get(\n environment_id\n )\n dashboard_meta_map: dict[str, dict[str, str | None]]\n if (\n cached_meta is not None\n and (time.monotonic() - cached_meta[0])\n < self.__class__._dashboard_summary_cache_ttl_seconds\n ):\n cached_meta_data = cast(\n tuple[float, dict[str, dict[str, str | None]]],\n cached_meta,\n )\n dashboard_meta_map = cached_meta_data[1]\n else:\n dashboards = SupersetClient(env).get_dashboards_summary()\n dashboard_meta_map = {\n str(item.get(\"id\")): {\n \"slug\": item.get(\"slug\"),\n \"title\": item.get(\"title\"),\n }\n for item in dashboards\n if str(item.get(\"id\") or \"\").strip()\n }\n self.__class__._dashboard_summary_cache[environment_id] = (\n time.monotonic(),\n dashboard_meta_map,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n dashboard_meta_map.get(\n dashboard_id,\n _empty_dashboard_meta(),\n )\n )\n except Exception as exc:\n logger.warning(\n \"[HealthService][_prime_dashboard_meta_cache] Failed to preload dashboard metadata for env=%s: %s\",\n environment_id,\n exc,\n )\n for dashboard_id in dashboard_ids:\n self._dashboard_meta_cache[(environment_id, dashboard_id)] = (\n _empty_dashboard_meta()\n )\n\n # endregion _prime_dashboard_meta_cache\n\n # region _resolve_dashboard_meta [TYPE Function]\n # @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record.\n # @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty.\n # @POST: Returns dict with `slug` and `title` keys, using cache when possible.\n # @SIDE_EFFECT: Writes default cache entries for unresolved numeric dashboard ids.\n def _resolve_dashboard_meta(\n self, dashboard_id: str, environment_id: str | None\n ) -> dict[str, str | None]:\n normalized_dashboard_id = str(dashboard_id or \"\").strip()\n normalized_environment_id = str(environment_id or \"\").strip()\n if not normalized_dashboard_id:\n return _empty_dashboard_meta()\n\n if not normalized_dashboard_id.isdigit():\n return {\"slug\": normalized_dashboard_id, \"title\": None}\n\n if not self.config_manager or not normalized_environment_id:\n return _empty_dashboard_meta()\n\n cache_key = (normalized_environment_id, normalized_dashboard_id)\n cached = self._dashboard_meta_cache.get(cache_key)\n if cached is not None:\n return cached\n\n meta = _empty_dashboard_meta()\n self._dashboard_meta_cache[cache_key] = meta\n return meta\n\n # endregion _resolve_dashboard_meta\n\n # region get_health_summary [TYPE Function]\n # @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title.\n # @PRE: environment_id may be omitted to aggregate across all environments.\n # @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard.\n # @SIDE_EFFECT: May call Superset API to resolve dashboard metadata.\n # @DATA_CONTRACT: Input[environment_id: Optional[str]] -> Output[HealthSummaryResponse]\n # @RELATION: [CALLS] ->[_prime_dashboard_meta_cache]\n # @RELATION: [CALLS] ->[_resolve_dashboard_meta]\n async def get_health_summary(\n self, environment_id: str = \"\"\n ) -> HealthSummaryResponse:\n \"\"\"\n @PURPOSE: Aggregates the latest validation status for all dashboards.\n @PRE: environment_id (optional) to filter by environment.\n @POST: Returns a HealthSummaryResponse with aggregated status counts and items.\n \"\"\"\n # [REASON] We need the latest ValidationRecord for each unique dashboard_id.\n # We use a subquery to find the max timestamp per dashboard_id.\n\n subquery = self.db.query(\n ValidationRecord.dashboard_id,\n func.max(ValidationRecord.timestamp).label(\"max_ts\"),\n )\n if environment_id:\n subquery = subquery.filter(\n ValidationRecord.environment_id == environment_id\n )\n subquery = subquery.group_by(ValidationRecord.dashboard_id).subquery()\n\n query = self.db.query(ValidationRecord).join(\n subquery,\n (ValidationRecord.dashboard_id == subquery.c.dashboard_id)\n & (ValidationRecord.timestamp == subquery.c.max_ts),\n )\n\n records = query.all()\n\n self._prime_dashboard_meta_cache(records)\n\n items = []\n pass_count = 0\n warn_count = 0\n fail_count = 0\n unknown_count = 0\n\n for rec in records:\n record = cast(Any, rec)\n status = str(record.status or \"\").upper()\n if status == \"PASS\":\n pass_count += 1\n elif status == \"WARN\":\n warn_count += 1\n elif status == \"FAIL\":\n fail_count += 1\n else:\n unknown_count += 1\n status = \"UNKNOWN\"\n\n record_id = str(record.id or \"\")\n dashboard_id = str(record.dashboard_id or \"\")\n resolved_environment_id = (\n str(record.environment_id)\n if record.environment_id is not None\n else None\n )\n response_environment_id = (\n resolved_environment_id\n if resolved_environment_id is not None\n else \"unknown\"\n )\n task_id = str(record.task_id) if record.task_id is not None else None\n summary = str(record.summary) if record.summary is not None else None\n timestamp = cast(Any, record.timestamp)\n\n meta = self._resolve_dashboard_meta(dashboard_id, resolved_environment_id)\n items.append(\n DashboardHealthItem(\n record_id=record_id,\n dashboard_id=dashboard_id,\n dashboard_slug=meta.get(\"slug\"),\n dashboard_title=meta.get(\"title\"),\n environment_id=response_environment_id,\n status=status,\n last_check=timestamp,\n task_id=task_id,\n summary=summary,\n )\n )\n\n logger.info(\n f\"[HealthService][get_health_summary] Aggregated {len(items)} dashboard health records.\"\n )\n\n return HealthSummaryResponse(\n items=items,\n pass_count=pass_count,\n warn_count=warn_count,\n fail_count=fail_count,\n unknown_count=unknown_count,\n )\n\n # endregion get_health_summary\n\n # region delete_validation_report [TYPE Function]\n # @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.\n # @PRE: record_id is a validation record identifier.\n # @POST: Returns True only when a matching record was deleted.\n # @SIDE_EFFECT: Deletes DB rows, optional screenshot file, and optional task/log persistence.\n # @DATA_CONTRACT: Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]\n # @RELATION: [DEPENDS_ON] ->[ValidationRecord]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[TaskCleanupService]\n def delete_validation_report(\n self, record_id: str, task_manager: TaskManager | None = None\n ) -> bool:\n record = (\n self.db.query(ValidationRecord)\n .filter(ValidationRecord.id == record_id)\n .first()\n )\n if not record:\n return False\n\n peer_query = self.db.query(ValidationRecord).filter(\n ValidationRecord.dashboard_id == record.dashboard_id\n )\n if record.environment_id is None:\n peer_query = peer_query.filter(ValidationRecord.environment_id.is_(None))\n else:\n peer_query = peer_query.filter(\n ValidationRecord.environment_id == record.environment_id\n )\n\n records_to_delete = peer_query.all()\n screenshot_paths = [\n str(item.screenshot_path or \"\").strip()\n for item in records_to_delete\n if str(item.screenshot_path or \"\").strip()\n ]\n task_ids = {\n str(item.task_id or \"\").strip()\n for item in records_to_delete\n if str(item.task_id or \"\").strip()\n }\n\n logger.info(\n \"[HealthService][delete_validation_report] Removing %s validation record(s) for dashboard=%s environment=%s triggered_by_record=%s\",\n len(records_to_delete),\n record.dashboard_id,\n record.environment_id,\n record_id,\n )\n\n for item in records_to_delete:\n self.db.delete(item)\n self.db.commit()\n\n for screenshot_path in screenshot_paths:\n try:\n if os.path.exists(screenshot_path):\n os.remove(screenshot_path)\n except OSError as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to remove screenshot %s: %s\",\n screenshot_path,\n exc,\n )\n\n if task_ids and task_manager and self.config_manager:\n try:\n cleanup_service = TaskCleanupService(\n task_manager.persistence_service,\n task_manager.log_persistence_service,\n self.config_manager,\n )\n for task_id in task_ids:\n task_manager.tasks.pop(task_id, None)\n cleanup_service.delete_task_with_logs(task_id)\n except Exception as exc:\n logger.warning(\n \"[HealthService][delete_validation_report] Failed to cleanup linked task/logs for dashboard=%s environment=%s: %s\",\n record.dashboard_id,\n record.environment_id,\n exc,\n )\n\n return True\n\n # endregion delete_validation_report\n\n\n# #endregion HealthService\n" }, { "contract_id": "llm_prompt_templates", "contract_type": "Module", "file_path": "backend/src/services/llm_prompt_templates.py", "start_line": 1, - "end_line": 204, + "end_line": 194, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "INVARIANT": "All required prompt template keys are always present after normalization.", "LAYER": "Domain", - "PURPOSE": "Provide default LLM prompt templates and normalization helpers for runtime usage.", - "SEMANTICS": [ - "llm", - "prompts", - "templates", - "settings" - ] + "PURPOSE": "Provide default LLM prompt templates and normalization helpers for runtime usage." }, "relations": [ { @@ -90165,19 +63205,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:llm_prompt_templates:Module]\n# @COMPLEXITY: 2\n# @SEMANTICS: llm, prompts, templates, settings\n# @PURPOSE: Provide default LLM prompt templates and normalization helpers for runtime usage.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON ->[backend.src.core.config_manager:Function]\n# @INVARIANT: All required prompt template keys are always present after normalization.\n\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nfrom typing import Dict, Any, Optional\n\n\n# [DEF:DEFAULT_LLM_PROMPTS:Constant]\n# @COMPLEXITY: 2\n# @PURPOSE: Default prompt templates used by documentation, dashboard validation, and git commit generation.\nDEFAULT_LLM_PROMPTS: Dict[str, str] = {\n \"dashboard_validation_prompt\": (\n \"Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\\n\\n\"\n \"Logs:\\n\"\n \"{logs}\\n\\n\"\n \"Provide the analysis in JSON format with the following structure:\\n\"\n \"{\\n\"\n ' \"status\": \"PASS\" | \"WARN\" | \"FAIL\",\\n'\n ' \"summary\": \"Short summary of findings\",\\n'\n ' \"issues\": [\\n'\n \" {\\n\"\n ' \"severity\": \"WARN\" | \"FAIL\",\\n'\n ' \"message\": \"Description of the issue\",\\n'\n ' \"location\": \"Optional location info (e.g. chart name)\"\\n'\n \" }\\n\"\n \" ]\\n\"\n \"}\"\n ),\n \"documentation_prompt\": (\n \"Generate professional documentation for the following dataset and its columns.\\n\"\n \"Dataset: {dataset_name}\\n\"\n \"Columns: {columns_json}\\n\\n\"\n \"Provide the documentation in JSON format:\\n\"\n \"{\\n\"\n ' \"dataset_description\": \"General description of the dataset\",\\n'\n ' \"column_descriptions\": [\\n'\n \" {\\n\"\n ' \"name\": \"column_name\",\\n'\n ' \"description\": \"Generated description\"\\n'\n \" }\\n\"\n \" ]\\n\"\n \"}\"\n ),\n \"git_commit_prompt\": (\n \"Generate a concise and professional git commit message based on the following diff and recent history.\\n\"\n \"Use Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\n\"\n \"Recent History:\\n\"\n \"{history}\\n\\n\"\n \"Diff:\\n\"\n \"{diff}\\n\\n\"\n \"Commit Message:\"\n ),\n}\n# [/DEF:DEFAULT_LLM_PROMPTS:Constant]\n\n\n# [DEF:DEFAULT_LLM_PROVIDER_BINDINGS:Constant]\n# @COMPLEXITY: 2\n# @PURPOSE: Default provider binding per task domain.\nDEFAULT_LLM_PROVIDER_BINDINGS: Dict[str, str] = {\n \"dashboard_validation\": \"\",\n \"documentation\": \"\",\n \"git_commit\": \"\",\n}\n# [/DEF:DEFAULT_LLM_PROVIDER_BINDINGS:Constant]\n\n\n# [DEF:DEFAULT_LLM_ASSISTANT_SETTINGS:Constant]\n# @COMPLEXITY: 2\n# @PURPOSE: Default planner settings for assistant chat intent model/provider resolution.\nDEFAULT_LLM_ASSISTANT_SETTINGS: Dict[str, str] = {\n \"assistant_planner_provider\": \"\",\n \"assistant_planner_model\": \"\",\n}\n# [/DEF:DEFAULT_LLM_ASSISTANT_SETTINGS:Constant]\n\n\n# [DEF:normalize_llm_settings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Ensure llm settings contain stable schema with prompts section and default templates.\n# @PRE: llm_settings is dictionary-like value or None.\n# @POST: Returned dict contains prompts with all required template keys.\n# @RELATION: DEPENDS_ON -> LLMProviderService\ndef normalize_llm_settings(llm_settings: Any) -> Dict[str, Any]:\n normalized: Dict[str, Any] = {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": {},\n \"provider_bindings\": {},\n **DEFAULT_LLM_ASSISTANT_SETTINGS,\n }\n if isinstance(llm_settings, dict):\n normalized.update(\n {\n k: v\n for k, v in llm_settings.items()\n if k\n in (\n \"providers\",\n \"default_provider\",\n \"prompts\",\n \"provider_bindings\",\n \"assistant_planner_provider\",\n \"assistant_planner_model\",\n )\n }\n )\n prompts = normalized.get(\"prompts\") if isinstance(normalized.get(\"prompts\"), dict) else {}\n merged_prompts = deepcopy(DEFAULT_LLM_PROMPTS)\n merged_prompts.update({k: v for k, v in prompts.items() if isinstance(v, str) and v.strip()})\n normalized[\"prompts\"] = merged_prompts\n bindings = normalized.get(\"provider_bindings\") if isinstance(normalized.get(\"provider_bindings\"), dict) else {}\n merged_bindings = deepcopy(DEFAULT_LLM_PROVIDER_BINDINGS)\n merged_bindings.update({k: v for k, v in bindings.items() if isinstance(v, str)})\n normalized[\"provider_bindings\"] = merged_bindings\n for key, default_value in DEFAULT_LLM_ASSISTANT_SETTINGS.items():\n value = normalized.get(key, default_value)\n normalized[key] = value.strip() if isinstance(value, str) else default_value\n return normalized\n# [/DEF:normalize_llm_settings:Function]\n\n\n# [DEF:is_multimodal_model:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Heuristically determine whether model supports image input required for dashboard validation.\n# @PRE: model_name may be empty or mixed-case.\n# @POST: Returns True when model likely supports multimodal input.\n# @RELATION: DEPENDS_ON -> LLMProviderService\ndef is_multimodal_model(model_name: str, provider_type: Optional[str] = None) -> bool:\n token = (model_name or \"\").strip().lower()\n if not token:\n return False\n provider = (provider_type or \"\").strip().lower()\n text_only_markers = (\n \"text-only\",\n \"embedding\",\n \"rerank\",\n \"whisper\",\n \"tts\",\n \"transcribe\",\n )\n if any(marker in token for marker in text_only_markers):\n return False\n multimodal_markers = (\n \"gpt-4o\",\n \"gpt-4.1\",\n \"vision\",\n \"vl\",\n \"gemini\",\n \"claude-3\",\n \"claude-sonnet-4\",\n \"omni\",\n \"multimodal\",\n \"pixtral\",\n \"llava\",\n \"internvl\",\n \"qwen-vl\",\n \"qwen2-vl\",\n )\n if any(marker in token for marker in multimodal_markers):\n return True\n return False\n# [/DEF:is_multimodal_model:Function]\n\n\n# [DEF:resolve_bound_provider_id:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resolve provider id configured for a task binding with fallback to default provider.\n# @PRE: llm_settings is normalized or raw dict from config.\n# @POST: Returns configured provider id or fallback id/empty string when not defined.\n# @RELATION: DEPENDS_ON -> LLMProviderService\ndef resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str:\n normalized = normalize_llm_settings(llm_settings)\n bindings = normalized.get(\"provider_bindings\", {})\n bound = bindings.get(task_key)\n if isinstance(bound, str) and bound.strip():\n return bound.strip()\n default_provider = normalized.get(\"default_provider\", \"\")\n return default_provider.strip() if isinstance(default_provider, str) else \"\"\n# [/DEF:resolve_bound_provider_id:Function]\n\n\n# [DEF:render_prompt:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Render prompt template using deterministic placeholder replacement with graceful fallback.\n# @PRE: template is a string and variables values are already stringifiable.\n# @POST: Returns rendered prompt text with known placeholders substituted.\n# @RELATION: DEPENDS_ON -> LLMProviderService\ndef render_prompt(template: str, variables: Dict[str, Any]) -> str:\n rendered = template\n for key, value in variables.items():\n rendered = rendered.replace(\"{\" + key + \"}\", str(value))\n return rendered\n# [/DEF:render_prompt:Function]\n\n\n# [/DEF:llm_prompt_templates:Module]\n" + "anchor_syntax": "region", + "body": "# #region llm_prompt_templates [C:2] [TYPE Module] [SEMANTICS llm, prompt, template, normalization]\n# @BRIEF Provide default LLM prompt templates and normalization helpers for runtime usage.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [backend.src.core.config_manager:Function]\n# @INVARIANT: All required prompt template keys are always present after normalization.\n\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nfrom typing import Any\n\n# #region DEFAULT_LLM_PROMPTS [C:2] [TYPE Constant]\n# @BRIEF Default prompt templates used by documentation, dashboard validation, and git commit generation.\nDEFAULT_LLM_PROMPTS: dict[str, str] = {\n \"dashboard_validation_prompt\": (\n \"Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\\n\\n\"\n \"Logs:\\n\"\n \"{logs}\\n\\n\"\n \"Provide the analysis in JSON format with the following structure:\\n\"\n \"{\\n\"\n ' \"status\": \"PASS\" | \"WARN\" | \"FAIL\",\\n'\n ' \"summary\": \"Short summary of findings\",\\n'\n ' \"issues\": [\\n'\n \" {\\n\"\n ' \"severity\": \"WARN\" | \"FAIL\",\\n'\n ' \"message\": \"Description of the issue\",\\n'\n ' \"location\": \"Optional location info (e.g. chart name)\"\\n'\n \" }\\n\"\n \" ]\\n\"\n \"}\"\n ),\n \"documentation_prompt\": (\n \"Generate professional documentation for the following dataset and its columns.\\n\"\n \"Dataset: {dataset_name}\\n\"\n \"Columns: {columns_json}\\n\\n\"\n \"Provide the documentation in JSON format:\\n\"\n \"{\\n\"\n ' \"dataset_description\": \"General description of the dataset\",\\n'\n ' \"column_descriptions\": [\\n'\n \" {\\n\"\n ' \"name\": \"column_name\",\\n'\n ' \"description\": \"Generated description\"\\n'\n \" }\\n\"\n \" ]\\n\"\n \"}\"\n ),\n \"git_commit_prompt\": (\n \"Generate a concise and professional git commit message based on the following diff and recent history.\\n\"\n \"Use Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\n\"\n \"Recent History:\\n\"\n \"{history}\\n\\n\"\n \"Diff:\\n\"\n \"{diff}\\n\\n\"\n \"Commit Message:\"\n ),\n}\n# #endregion DEFAULT_LLM_PROMPTS\n\n\n# #region DEFAULT_LLM_PROVIDER_BINDINGS [C:2] [TYPE Constant]\n# @BRIEF Default provider binding per task domain.\nDEFAULT_LLM_PROVIDER_BINDINGS: dict[str, str] = {\n \"dashboard_validation\": \"\",\n \"documentation\": \"\",\n \"git_commit\": \"\",\n}\n# #endregion DEFAULT_LLM_PROVIDER_BINDINGS\n\n\n# #region DEFAULT_LLM_ASSISTANT_SETTINGS [C:2] [TYPE Constant]\n# @BRIEF Default planner settings for assistant chat intent model/provider resolution.\nDEFAULT_LLM_ASSISTANT_SETTINGS: dict[str, str] = {\n \"assistant_planner_provider\": \"\",\n \"assistant_planner_model\": \"\",\n}\n# #endregion DEFAULT_LLM_ASSISTANT_SETTINGS\n\n\n# #region normalize_llm_settings [C:3] [TYPE Function]\n# @BRIEF Ensure llm settings contain stable schema with prompts section and default templates.\n# @PRE: llm_settings is dictionary-like value or None.\n# @POST: Returned dict contains prompts with all required template keys.\n# @RELATION DEPENDS_ON -> LLMProviderService\ndef normalize_llm_settings(llm_settings: Any) -> dict[str, Any]:\n normalized: dict[str, Any] = {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": {},\n \"provider_bindings\": {},\n **DEFAULT_LLM_ASSISTANT_SETTINGS,\n }\n if isinstance(llm_settings, dict):\n normalized.update(\n {\n k: v\n for k, v in llm_settings.items()\n if k\n in (\n \"providers\",\n \"default_provider\",\n \"prompts\",\n \"provider_bindings\",\n \"assistant_planner_provider\",\n \"assistant_planner_model\",\n )\n }\n )\n prompts = normalized.get(\"prompts\") if isinstance(normalized.get(\"prompts\"), dict) else {}\n merged_prompts = deepcopy(DEFAULT_LLM_PROMPTS)\n merged_prompts.update({k: v for k, v in prompts.items() if isinstance(v, str) and v.strip()})\n normalized[\"prompts\"] = merged_prompts\n bindings = normalized.get(\"provider_bindings\") if isinstance(normalized.get(\"provider_bindings\"), dict) else {}\n merged_bindings = deepcopy(DEFAULT_LLM_PROVIDER_BINDINGS)\n merged_bindings.update({k: v for k, v in bindings.items() if isinstance(v, str)})\n normalized[\"provider_bindings\"] = merged_bindings\n for key, default_value in DEFAULT_LLM_ASSISTANT_SETTINGS.items():\n value = normalized.get(key, default_value)\n normalized[key] = value.strip() if isinstance(value, str) else default_value\n return normalized\n# #endregion normalize_llm_settings\n\n\n# #region is_multimodal_model [C:3] [TYPE Function]\n# @BRIEF Heuristically determine whether model supports image input required for dashboard validation.\n# @PRE: model_name may be empty or mixed-case.\n# @POST: Returns True when model likely supports multimodal input.\n# @RELATION DEPENDS_ON -> LLMProviderService\ndef is_multimodal_model(model_name: str, provider_type: str | None = None) -> bool:\n token = (model_name or \"\").strip().lower()\n if not token:\n return False\n provider = (provider_type or \"\").strip().lower()\n text_only_markers = (\n \"text-only\",\n \"embedding\",\n \"rerank\",\n \"whisper\",\n \"tts\",\n \"transcribe\",\n )\n if any(marker in token for marker in text_only_markers):\n return False\n multimodal_markers = (\n \"gpt-4o\",\n \"gpt-4.1\",\n \"vision\",\n \"vl\",\n \"gemini\",\n \"claude-3\",\n \"claude-sonnet-4\",\n \"omni\",\n \"multimodal\",\n \"pixtral\",\n \"llava\",\n \"internvl\",\n \"qwen-vl\",\n \"qwen2-vl\",\n )\n if any(marker in token for marker in multimodal_markers):\n return True\n return False\n# #endregion is_multimodal_model\n\n\n# #region resolve_bound_provider_id [C:3] [TYPE Function]\n# @BRIEF Resolve provider id configured for a task binding with fallback to default provider.\n# @PRE: llm_settings is normalized or raw dict from config.\n# @POST: Returns configured provider id or fallback id/empty string when not defined.\n# @RELATION DEPENDS_ON -> LLMProviderService\ndef resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str:\n normalized = normalize_llm_settings(llm_settings)\n bindings = normalized.get(\"provider_bindings\", {})\n bound = bindings.get(task_key)\n if isinstance(bound, str) and bound.strip():\n return bound.strip()\n default_provider = normalized.get(\"default_provider\", \"\")\n return default_provider.strip() if isinstance(default_provider, str) else \"\"\n# #endregion resolve_bound_provider_id\n\n\n# #region render_prompt [C:3] [TYPE Function]\n# @BRIEF Render prompt template using deterministic placeholder replacement with graceful fallback.\n# @PRE: template is a string and variables values are already stringifiable.\n# @POST: Returns rendered prompt text with known placeholders substituted.\n# @RELATION DEPENDS_ON -> LLMProviderService\ndef render_prompt(template: str, variables: dict[str, Any]) -> str:\n rendered = template\n for key, value in variables.items():\n rendered = rendered.replace(\"{\" + key + \"}\", str(value))\n return rendered\n# #endregion render_prompt\n\n\n# #endregion llm_prompt_templates\n" }, { "contract_id": "DEFAULT_LLM_PROMPTS", "contract_type": "Constant", "file_path": "backend/src/services/llm_prompt_templates.py", - "start_line": 15, - "end_line": 61, + "start_line": 12, + "end_line": 57, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Default prompt templates used by documentation, dashboard validation, and git commit generation." }, "relations": [], @@ -90236,19 +63276,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DEFAULT_LLM_PROMPTS:Constant]\n# @COMPLEXITY: 2\n# @PURPOSE: Default prompt templates used by documentation, dashboard validation, and git commit generation.\nDEFAULT_LLM_PROMPTS: Dict[str, str] = {\n \"dashboard_validation_prompt\": (\n \"Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\\n\\n\"\n \"Logs:\\n\"\n \"{logs}\\n\\n\"\n \"Provide the analysis in JSON format with the following structure:\\n\"\n \"{\\n\"\n ' \"status\": \"PASS\" | \"WARN\" | \"FAIL\",\\n'\n ' \"summary\": \"Short summary of findings\",\\n'\n ' \"issues\": [\\n'\n \" {\\n\"\n ' \"severity\": \"WARN\" | \"FAIL\",\\n'\n ' \"message\": \"Description of the issue\",\\n'\n ' \"location\": \"Optional location info (e.g. chart name)\"\\n'\n \" }\\n\"\n \" ]\\n\"\n \"}\"\n ),\n \"documentation_prompt\": (\n \"Generate professional documentation for the following dataset and its columns.\\n\"\n \"Dataset: {dataset_name}\\n\"\n \"Columns: {columns_json}\\n\\n\"\n \"Provide the documentation in JSON format:\\n\"\n \"{\\n\"\n ' \"dataset_description\": \"General description of the dataset\",\\n'\n ' \"column_descriptions\": [\\n'\n \" {\\n\"\n ' \"name\": \"column_name\",\\n'\n ' \"description\": \"Generated description\"\\n'\n \" }\\n\"\n \" ]\\n\"\n \"}\"\n ),\n \"git_commit_prompt\": (\n \"Generate a concise and professional git commit message based on the following diff and recent history.\\n\"\n \"Use Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\n\"\n \"Recent History:\\n\"\n \"{history}\\n\\n\"\n \"Diff:\\n\"\n \"{diff}\\n\\n\"\n \"Commit Message:\"\n ),\n}\n# [/DEF:DEFAULT_LLM_PROMPTS:Constant]\n" + "anchor_syntax": "region", + "body": "# #region DEFAULT_LLM_PROMPTS [C:2] [TYPE Constant]\n# @BRIEF Default prompt templates used by documentation, dashboard validation, and git commit generation.\nDEFAULT_LLM_PROMPTS: dict[str, str] = {\n \"dashboard_validation_prompt\": (\n \"Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\\n\\n\"\n \"Logs:\\n\"\n \"{logs}\\n\\n\"\n \"Provide the analysis in JSON format with the following structure:\\n\"\n \"{\\n\"\n ' \"status\": \"PASS\" | \"WARN\" | \"FAIL\",\\n'\n ' \"summary\": \"Short summary of findings\",\\n'\n ' \"issues\": [\\n'\n \" {\\n\"\n ' \"severity\": \"WARN\" | \"FAIL\",\\n'\n ' \"message\": \"Description of the issue\",\\n'\n ' \"location\": \"Optional location info (e.g. chart name)\"\\n'\n \" }\\n\"\n \" ]\\n\"\n \"}\"\n ),\n \"documentation_prompt\": (\n \"Generate professional documentation for the following dataset and its columns.\\n\"\n \"Dataset: {dataset_name}\\n\"\n \"Columns: {columns_json}\\n\\n\"\n \"Provide the documentation in JSON format:\\n\"\n \"{\\n\"\n ' \"dataset_description\": \"General description of the dataset\",\\n'\n ' \"column_descriptions\": [\\n'\n \" {\\n\"\n ' \"name\": \"column_name\",\\n'\n ' \"description\": \"Generated description\"\\n'\n \" }\\n\"\n \" ]\\n\"\n \"}\"\n ),\n \"git_commit_prompt\": (\n \"Generate a concise and professional git commit message based on the following diff and recent history.\\n\"\n \"Use Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\n\"\n \"Recent History:\\n\"\n \"{history}\\n\\n\"\n \"Diff:\\n\"\n \"{diff}\\n\\n\"\n \"Commit Message:\"\n ),\n}\n# #endregion DEFAULT_LLM_PROMPTS\n" }, { "contract_id": "DEFAULT_LLM_PROVIDER_BINDINGS", "contract_type": "Constant", "file_path": "backend/src/services/llm_prompt_templates.py", - "start_line": 64, - "end_line": 72, + "start_line": 60, + "end_line": 67, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Default provider binding per task domain." }, "relations": [], @@ -90307,19 +63347,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DEFAULT_LLM_PROVIDER_BINDINGS:Constant]\n# @COMPLEXITY: 2\n# @PURPOSE: Default provider binding per task domain.\nDEFAULT_LLM_PROVIDER_BINDINGS: Dict[str, str] = {\n \"dashboard_validation\": \"\",\n \"documentation\": \"\",\n \"git_commit\": \"\",\n}\n# [/DEF:DEFAULT_LLM_PROVIDER_BINDINGS:Constant]\n" + "anchor_syntax": "region", + "body": "# #region DEFAULT_LLM_PROVIDER_BINDINGS [C:2] [TYPE Constant]\n# @BRIEF Default provider binding per task domain.\nDEFAULT_LLM_PROVIDER_BINDINGS: dict[str, str] = {\n \"dashboard_validation\": \"\",\n \"documentation\": \"\",\n \"git_commit\": \"\",\n}\n# #endregion DEFAULT_LLM_PROVIDER_BINDINGS\n" }, { "contract_id": "DEFAULT_LLM_ASSISTANT_SETTINGS", "contract_type": "Constant", "file_path": "backend/src/services/llm_prompt_templates.py", - "start_line": 75, - "end_line": 82, + "start_line": 70, + "end_line": 76, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Default planner settings for assistant chat intent model/provider resolution." }, "relations": [], @@ -90378,19 +63418,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:DEFAULT_LLM_ASSISTANT_SETTINGS:Constant]\n# @COMPLEXITY: 2\n# @PURPOSE: Default planner settings for assistant chat intent model/provider resolution.\nDEFAULT_LLM_ASSISTANT_SETTINGS: Dict[str, str] = {\n \"assistant_planner_provider\": \"\",\n \"assistant_planner_model\": \"\",\n}\n# [/DEF:DEFAULT_LLM_ASSISTANT_SETTINGS:Constant]\n" + "anchor_syntax": "region", + "body": "# #region DEFAULT_LLM_ASSISTANT_SETTINGS [C:2] [TYPE Constant]\n# @BRIEF Default planner settings for assistant chat intent model/provider resolution.\nDEFAULT_LLM_ASSISTANT_SETTINGS: dict[str, str] = {\n \"assistant_planner_provider\": \"\",\n \"assistant_planner_model\": \"\",\n}\n# #endregion DEFAULT_LLM_ASSISTANT_SETTINGS\n" }, { "contract_id": "normalize_llm_settings", "contract_type": "Function", "file_path": "backend/src/services/llm_prompt_templates.py", - "start_line": 85, - "end_line": 127, + "start_line": 79, + "end_line": 120, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Returned dict contains prompts with all required template keys.", "PRE": "llm_settings is dictionary-like value or None.", "PURPOSE": "Ensure llm settings contain stable schema with prompts section and default templates." @@ -90423,19 +63463,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:normalize_llm_settings:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Ensure llm settings contain stable schema with prompts section and default templates.\n# @PRE: llm_settings is dictionary-like value or None.\n# @POST: Returned dict contains prompts with all required template keys.\n# @RELATION: DEPENDS_ON -> LLMProviderService\ndef normalize_llm_settings(llm_settings: Any) -> Dict[str, Any]:\n normalized: Dict[str, Any] = {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": {},\n \"provider_bindings\": {},\n **DEFAULT_LLM_ASSISTANT_SETTINGS,\n }\n if isinstance(llm_settings, dict):\n normalized.update(\n {\n k: v\n for k, v in llm_settings.items()\n if k\n in (\n \"providers\",\n \"default_provider\",\n \"prompts\",\n \"provider_bindings\",\n \"assistant_planner_provider\",\n \"assistant_planner_model\",\n )\n }\n )\n prompts = normalized.get(\"prompts\") if isinstance(normalized.get(\"prompts\"), dict) else {}\n merged_prompts = deepcopy(DEFAULT_LLM_PROMPTS)\n merged_prompts.update({k: v for k, v in prompts.items() if isinstance(v, str) and v.strip()})\n normalized[\"prompts\"] = merged_prompts\n bindings = normalized.get(\"provider_bindings\") if isinstance(normalized.get(\"provider_bindings\"), dict) else {}\n merged_bindings = deepcopy(DEFAULT_LLM_PROVIDER_BINDINGS)\n merged_bindings.update({k: v for k, v in bindings.items() if isinstance(v, str)})\n normalized[\"provider_bindings\"] = merged_bindings\n for key, default_value in DEFAULT_LLM_ASSISTANT_SETTINGS.items():\n value = normalized.get(key, default_value)\n normalized[key] = value.strip() if isinstance(value, str) else default_value\n return normalized\n# [/DEF:normalize_llm_settings:Function]\n" + "anchor_syntax": "region", + "body": "# #region normalize_llm_settings [C:3] [TYPE Function]\n# @BRIEF Ensure llm settings contain stable schema with prompts section and default templates.\n# @PRE: llm_settings is dictionary-like value or None.\n# @POST: Returned dict contains prompts with all required template keys.\n# @RELATION DEPENDS_ON -> LLMProviderService\ndef normalize_llm_settings(llm_settings: Any) -> dict[str, Any]:\n normalized: dict[str, Any] = {\n \"providers\": [],\n \"default_provider\": \"\",\n \"prompts\": {},\n \"provider_bindings\": {},\n **DEFAULT_LLM_ASSISTANT_SETTINGS,\n }\n if isinstance(llm_settings, dict):\n normalized.update(\n {\n k: v\n for k, v in llm_settings.items()\n if k\n in (\n \"providers\",\n \"default_provider\",\n \"prompts\",\n \"provider_bindings\",\n \"assistant_planner_provider\",\n \"assistant_planner_model\",\n )\n }\n )\n prompts = normalized.get(\"prompts\") if isinstance(normalized.get(\"prompts\"), dict) else {}\n merged_prompts = deepcopy(DEFAULT_LLM_PROMPTS)\n merged_prompts.update({k: v for k, v in prompts.items() if isinstance(v, str) and v.strip()})\n normalized[\"prompts\"] = merged_prompts\n bindings = normalized.get(\"provider_bindings\") if isinstance(normalized.get(\"provider_bindings\"), dict) else {}\n merged_bindings = deepcopy(DEFAULT_LLM_PROVIDER_BINDINGS)\n merged_bindings.update({k: v for k, v in bindings.items() if isinstance(v, str)})\n normalized[\"provider_bindings\"] = merged_bindings\n for key, default_value in DEFAULT_LLM_ASSISTANT_SETTINGS.items():\n value = normalized.get(key, default_value)\n normalized[key] = value.strip() if isinstance(value, str) else default_value\n return normalized\n# #endregion normalize_llm_settings\n" }, { "contract_id": "is_multimodal_model", "contract_type": "Function", "file_path": "backend/src/services/llm_prompt_templates.py", - "start_line": 130, - "end_line": 170, + "start_line": 123, + "end_line": 162, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Returns True when model likely supports multimodal input.", "PRE": "model_name may be empty or mixed-case.", "PURPOSE": "Heuristically determine whether model supports image input required for dashboard validation." @@ -90468,19 +63508,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:is_multimodal_model:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Heuristically determine whether model supports image input required for dashboard validation.\n# @PRE: model_name may be empty or mixed-case.\n# @POST: Returns True when model likely supports multimodal input.\n# @RELATION: DEPENDS_ON -> LLMProviderService\ndef is_multimodal_model(model_name: str, provider_type: Optional[str] = None) -> bool:\n token = (model_name or \"\").strip().lower()\n if not token:\n return False\n provider = (provider_type or \"\").strip().lower()\n text_only_markers = (\n \"text-only\",\n \"embedding\",\n \"rerank\",\n \"whisper\",\n \"tts\",\n \"transcribe\",\n )\n if any(marker in token for marker in text_only_markers):\n return False\n multimodal_markers = (\n \"gpt-4o\",\n \"gpt-4.1\",\n \"vision\",\n \"vl\",\n \"gemini\",\n \"claude-3\",\n \"claude-sonnet-4\",\n \"omni\",\n \"multimodal\",\n \"pixtral\",\n \"llava\",\n \"internvl\",\n \"qwen-vl\",\n \"qwen2-vl\",\n )\n if any(marker in token for marker in multimodal_markers):\n return True\n return False\n# [/DEF:is_multimodal_model:Function]\n" + "anchor_syntax": "region", + "body": "# #region is_multimodal_model [C:3] [TYPE Function]\n# @BRIEF Heuristically determine whether model supports image input required for dashboard validation.\n# @PRE: model_name may be empty or mixed-case.\n# @POST: Returns True when model likely supports multimodal input.\n# @RELATION DEPENDS_ON -> LLMProviderService\ndef is_multimodal_model(model_name: str, provider_type: str | None = None) -> bool:\n token = (model_name or \"\").strip().lower()\n if not token:\n return False\n provider = (provider_type or \"\").strip().lower()\n text_only_markers = (\n \"text-only\",\n \"embedding\",\n \"rerank\",\n \"whisper\",\n \"tts\",\n \"transcribe\",\n )\n if any(marker in token for marker in text_only_markers):\n return False\n multimodal_markers = (\n \"gpt-4o\",\n \"gpt-4.1\",\n \"vision\",\n \"vl\",\n \"gemini\",\n \"claude-3\",\n \"claude-sonnet-4\",\n \"omni\",\n \"multimodal\",\n \"pixtral\",\n \"llava\",\n \"internvl\",\n \"qwen-vl\",\n \"qwen2-vl\",\n )\n if any(marker in token for marker in multimodal_markers):\n return True\n return False\n# #endregion is_multimodal_model\n" }, { "contract_id": "resolve_bound_provider_id", "contract_type": "Function", "file_path": "backend/src/services/llm_prompt_templates.py", - "start_line": 173, - "end_line": 187, + "start_line": 165, + "end_line": 178, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Returns configured provider id or fallback id/empty string when not defined.", "PRE": "llm_settings is normalized or raw dict from config.", "PURPOSE": "Resolve provider id configured for a task binding with fallback to default provider." @@ -90513,19 +63553,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:resolve_bound_provider_id:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Resolve provider id configured for a task binding with fallback to default provider.\n# @PRE: llm_settings is normalized or raw dict from config.\n# @POST: Returns configured provider id or fallback id/empty string when not defined.\n# @RELATION: DEPENDS_ON -> LLMProviderService\ndef resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str:\n normalized = normalize_llm_settings(llm_settings)\n bindings = normalized.get(\"provider_bindings\", {})\n bound = bindings.get(task_key)\n if isinstance(bound, str) and bound.strip():\n return bound.strip()\n default_provider = normalized.get(\"default_provider\", \"\")\n return default_provider.strip() if isinstance(default_provider, str) else \"\"\n# [/DEF:resolve_bound_provider_id:Function]\n" + "anchor_syntax": "region", + "body": "# #region resolve_bound_provider_id [C:3] [TYPE Function]\n# @BRIEF Resolve provider id configured for a task binding with fallback to default provider.\n# @PRE: llm_settings is normalized or raw dict from config.\n# @POST: Returns configured provider id or fallback id/empty string when not defined.\n# @RELATION DEPENDS_ON -> LLMProviderService\ndef resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str:\n normalized = normalize_llm_settings(llm_settings)\n bindings = normalized.get(\"provider_bindings\", {})\n bound = bindings.get(task_key)\n if isinstance(bound, str) and bound.strip():\n return bound.strip()\n default_provider = normalized.get(\"default_provider\", \"\")\n return default_provider.strip() if isinstance(default_provider, str) else \"\"\n# #endregion resolve_bound_provider_id\n" }, { "contract_id": "render_prompt", "contract_type": "Function", "file_path": "backend/src/services/llm_prompt_templates.py", - "start_line": 190, - "end_line": 201, + "start_line": 181, + "end_line": 191, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "POST": "Returns rendered prompt text with known placeholders substituted.", "PRE": "template is a string and variables values are already stringifiable.", "PURPOSE": "Render prompt template using deterministic placeholder replacement with graceful fallback." @@ -90558,117 +63598,56 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:render_prompt:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Render prompt template using deterministic placeholder replacement with graceful fallback.\n# @PRE: template is a string and variables values are already stringifiable.\n# @POST: Returns rendered prompt text with known placeholders substituted.\n# @RELATION: DEPENDS_ON -> LLMProviderService\ndef render_prompt(template: str, variables: Dict[str, Any]) -> str:\n rendered = template\n for key, value in variables.items():\n rendered = rendered.replace(\"{\" + key + \"}\", str(value))\n return rendered\n# [/DEF:render_prompt:Function]\n" + "anchor_syntax": "region", + "body": "# #region render_prompt [C:3] [TYPE Function]\n# @BRIEF Render prompt template using deterministic placeholder replacement with graceful fallback.\n# @PRE: template is a string and variables values are already stringifiable.\n# @POST: Returns rendered prompt text with known placeholders substituted.\n# @RELATION DEPENDS_ON -> LLMProviderService\ndef render_prompt(template: str, variables: dict[str, Any]) -> str:\n rendered = template\n for key, value in variables.items():\n rendered = rendered.replace(\"{\" + key + \"}\", str(value))\n return rendered\n# #endregion render_prompt\n" }, { "contract_id": "llm_provider", "contract_type": "Module", "file_path": "backend/src/services/llm_provider.py", "start_line": 1, - "end_line": 270, + "end_line": 261, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "Domain", - "PURPOSE": "Service for managing LLM provider configurations with encrypted API keys.", - "SEMANTICS": [ - "service", - "llm", - "provider", - "encryption" - ] + "PURPOSE": "Service for managing LLM provider configurations with encrypted API keys." }, "relations": [ { "source_id": "llm_provider", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LLMProvider", "target_ref": "[LLMProvider]" }, { "source_id": "llm_provider", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "EncryptionManager", "target_ref": "[EncryptionManager]" }, { "source_id": "llm_provider", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LLMProviderConfig", "target_ref": "[LLMProviderConfig]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:llm_provider:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: service, llm, provider, encryption\n# @PURPOSE: Service for managing LLM provider configurations with encrypted API keys.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[LLMProvider]\n# @RELATION: [DEPENDS_ON] ->[EncryptionManager]\n# @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\nfrom typing import List, Optional, TYPE_CHECKING\nfrom sqlalchemy.orm import Session\nfrom ..models.llm import LLMProvider\nfrom ..core.logger import belief_scope, logger\nfrom cryptography.fernet import Fernet\nimport os\n\nif TYPE_CHECKING:\n from ..plugins.llm_analysis.models import LLMProviderConfig\n\nMASKED_API_KEY_PLACEHOLDER = \"********\"\n\n\n# [DEF:_require_fernet_key:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Load and validate the Fernet key used for secret encryption.\n# @PRE: ENCRYPTION_KEY environment variable must be set to a valid Fernet key.\n# @POST: Returns validated key bytes ready for Fernet initialization.\n# @RELATION: DEPENDS_ON ->[backend.src.core.logger:Function]\n# @SIDE_EFFECT: Emits belief-state logs for missing or invalid encryption configuration.\n# @DATA_CONTRACT: Input[ENCRYPTION_KEY:str] -> Output[bytes]\n# @INVARIANT: Encryption initialization never falls back to a hardcoded secret.\ndef _require_fernet_key() -> bytes:\n with belief_scope(\"_require_fernet_key\"):\n raw_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if not raw_key:\n logger.explore(\n \"Missing ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be set\")\n\n key = raw_key.encode()\n try:\n Fernet(key)\n except Exception as exc:\n logger.explore(\n \"Invalid ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be a valid Fernet key\") from exc\n\n logger.reflect(\"Validated ENCRYPTION_KEY for EncryptionManager initialization\")\n return key\n\n\n# [/DEF:_require_fernet_key:Function]\n\n\n# [DEF:EncryptionManager:Class]\n# @COMPLEXITY: 5\n# @PURPOSE: Handles encryption and decryption of sensitive data like API keys.\n# @RELATION: [CALLS] ->[_require_fernet_key]\n# @PRE: ENCRYPTION_KEY is configured with a valid Fernet key before instantiation.\n# @POST: Manager exposes reversible encrypt/decrypt operations for persisted secrets.\n# @SIDE_EFFECT: Initializes Fernet cryptography state from process environment.\n# @DATA_CONTRACT: Input[str] -> Output[str]\n# @INVARIANT: Uses only a validated secret key from environment.\n#\n# @TEST_CONTRACT: EncryptionManagerModel ->\n# {\n# required_fields: {},\n# invariants: [\n# \"encrypted data can be decrypted back to the original string\"\n# ]\n# }\n# @TEST_FIXTURE: basic_encryption_cycle -> {\"data\": \"my_secret_key\"}\n# @TEST_EDGE: decrypt_invalid_data -> raises Exception\n# @TEST_EDGE: empty_string_encryption -> {\"data\": \"\"}\n# @TEST_INVARIANT: symmetric_encryption -> verifies: [basic_encryption_cycle, empty_string_encryption]\nclass EncryptionManager:\n # [DEF:EncryptionManager_init:Function]\n # @PURPOSE: Initialize the encryption manager with a Fernet key.\n # @PRE: ENCRYPTION_KEY env var must be set to a valid Fernet key.\n # @POST: Fernet instance ready for encryption/decryption.\n def __init__(self):\n self.key = _require_fernet_key()\n self.fernet = Fernet(self.key)\n\n # [/DEF:EncryptionManager_init:Function]\n\n # [DEF:encrypt:Function]\n # @PURPOSE: Encrypt a plaintext string.\n # @PRE: data must be a non-empty string.\n # @POST: Returns encrypted string.\n def encrypt(self, data: str) -> str:\n with belief_scope(\"encrypt\"):\n return self.fernet.encrypt(data.encode()).decode()\n\n # [/DEF:encrypt:Function]\n\n # [DEF:decrypt:Function]\n # @PURPOSE: Decrypt an encrypted string.\n # @PRE: encrypted_data must be a valid Fernet-encrypted string.\n # @POST: Returns original plaintext string.\n def decrypt(self, encrypted_data: str) -> str:\n with belief_scope(\"decrypt\"):\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n # [/DEF:decrypt:Function]\n\n\n# [/DEF:EncryptionManager:Class]\n\n\n# [DEF:LLMProviderService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Service to manage LLM provider lifecycle.\n# @RELATION: [DEPENDS_ON] ->[LLMProvider]\n# @RELATION: [DEPENDS_ON] ->[EncryptionManager]\n# @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\nclass LLMProviderService:\n # [DEF:LLMProviderService_init:Function]\n # @PURPOSE: Initialize the service with database session.\n # @PRE: db must be a valid SQLAlchemy Session.\n # @POST: Service ready for provider operations.\n # @RELATION: [DEPENDS_ON] ->[EncryptionManager]\n def __init__(self, db: Session):\n self.db = db\n self.encryption = EncryptionManager()\n\n # [/DEF:LLMProviderService_init:Function]\n\n # [DEF:get_all_providers:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns all configured LLM providers.\n # @PRE: Database connection must be active.\n # @POST: Returns list of all LLMProvider records.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_all_providers(self) -> List[LLMProvider]:\n with belief_scope(\"get_all_providers\"):\n return self.db.query(LLMProvider).all()\n\n # [/DEF:get_all_providers:Function]\n\n # [DEF:get_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns a single LLM provider by ID.\n # @PRE: provider_id must be a valid string.\n # @POST: Returns LLMProvider or None if not found.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_provider(self, provider_id: str) -> Optional[LLMProvider]:\n with belief_scope(\"get_provider\"):\n return (\n self.db.query(LLMProvider).filter(LLMProvider.id == provider_id).first()\n )\n\n # [/DEF:get_provider:Function]\n\n # [DEF:create_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Creates a new LLM provider with encrypted API key.\n # @PRE: config must contain valid provider configuration.\n # @POST: New provider created and persisted to database.\n # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[encrypt]\n def create_provider(self, config: \"LLMProviderConfig\") -> LLMProvider:\n with belief_scope(\"create_provider\"):\n encrypted_key = self.encryption.encrypt(config.api_key)\n db_provider = LLMProvider(\n provider_type=config.provider_type.value,\n name=config.name,\n base_url=config.base_url,\n api_key=encrypted_key,\n default_model=config.default_model,\n is_active=config.is_active,\n )\n self.db.add(db_provider)\n self.db.commit()\n self.db.refresh(db_provider)\n return db_provider\n\n # [/DEF:create_provider:Function]\n\n # [DEF:update_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Updates an existing LLM provider.\n # @PRE: provider_id must exist, config must be valid.\n # @POST: Provider updated and persisted to database.\n # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[encrypt]\n def update_provider(\n self, provider_id: str, config: \"LLMProviderConfig\"\n ) -> Optional[LLMProvider]:\n with belief_scope(\"update_provider\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n return None\n\n db_provider.provider_type = config.provider_type.value\n db_provider.name = config.name\n db_provider.base_url = config.base_url\n # Ignore masked placeholder values; they are display-only and must not overwrite secrets.\n if (\n config.api_key is not None\n and config.api_key != \"\"\n and config.api_key != MASKED_API_KEY_PLACEHOLDER\n ):\n db_provider.api_key = self.encryption.encrypt(config.api_key)\n db_provider.default_model = config.default_model\n db_provider.is_active = config.is_active\n\n self.db.commit()\n self.db.refresh(db_provider)\n return db_provider\n\n # [/DEF:update_provider:Function]\n\n # [DEF:delete_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Deletes an LLM provider.\n # @PRE: provider_id must exist.\n # @POST: Provider removed from database.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def delete_provider(self, provider_id: str) -> bool:\n with belief_scope(\"delete_provider\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n return False\n self.db.delete(db_provider)\n self.db.commit()\n return True\n\n # [/DEF:delete_provider:Function]\n\n # [DEF:get_decrypted_api_key:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns the decrypted API key for a provider.\n # @PRE: provider_id must exist with valid encrypted key.\n # @POST: Returns decrypted API key or None on failure.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[decrypt]\n def get_decrypted_api_key(self, provider_id: str) -> Optional[str]:\n with belief_scope(\"get_decrypted_api_key\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n logger.warning(\n f\"[get_decrypted_api_key] Provider {provider_id} not found in database\"\n )\n return None\n\n logger.info(f\"[get_decrypted_api_key] Provider found: {db_provider.id}\")\n logger.info(\n f\"[get_decrypted_api_key] Encrypted API key length: {len(db_provider.api_key) if db_provider.api_key else 0}\"\n )\n\n try:\n decrypted_key = self.encryption.decrypt(db_provider.api_key)\n logger.info(\n f\"[get_decrypted_api_key] Decryption successful, key length: {len(decrypted_key) if decrypted_key else 0}\"\n )\n return decrypted_key\n except Exception as e:\n logger.error(f\"[get_decrypted_api_key] Decryption failed: {str(e)}\")\n return None\n\n # [/DEF:get_decrypted_api_key:Function]\n\n\n# [/DEF:LLMProviderService:Class]\n\n# [/DEF:llm_provider:Module]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region llm_provider [C:3] [TYPE Module] [SEMANTICS sqlalchemy, llm, provider, encryption, config]\n# @BRIEF Service for managing LLM provider configurations with encrypted API keys.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [LLMProvider]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\nimport os\nfrom typing import TYPE_CHECKING\n\nfrom cryptography.fernet import Fernet\nfrom sqlalchemy.orm import Session\n\nfrom ..core.logger import belief_scope, logger\nfrom ..models.llm import LLMProvider\n\nif TYPE_CHECKING:\n from ..plugins.llm_analysis.models import LLMProviderConfig\n\nMASKED_API_KEY_PLACEHOLDER = \"********\"\n\n\n# #region _require_fernet_key [C:5] [TYPE Function]\n# @BRIEF Load and validate the Fernet key used for secret encryption.\n# @PRE: ENCRYPTION_KEY environment variable must be set to a valid Fernet key.\n# @POST: Returns validated key bytes ready for Fernet initialization.\n# @RELATION DEPENDS_ON -> [backend.src.core.logger:Function]\n# @SIDE_EFFECT: Emits belief-state logs for missing or invalid encryption configuration.\n# @DATA_CONTRACT: Input[ENCRYPTION_KEY:str] -> Output[bytes]\n# @INVARIANT: Encryption initialization never falls back to a hardcoded secret.\ndef _require_fernet_key() -> bytes:\n with belief_scope(\"_require_fernet_key\"):\n raw_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if not raw_key:\n logger.explore(\n \"Missing ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be set\")\n\n key = raw_key.encode()\n try:\n Fernet(key)\n except Exception as exc:\n logger.explore(\n \"Invalid ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be a valid Fernet key\") from exc\n\n logger.reflect(\"Validated ENCRYPTION_KEY for EncryptionManager initialization\")\n return key\n\n\n# #endregion _require_fernet_key\n\n\n# #region EncryptionManager [C:5] [TYPE Class]\n# @BRIEF Handles encryption and decryption of sensitive data like API keys.\n# @RELATION CALLS -> [_require_fernet_key]\n# @PRE: ENCRYPTION_KEY is configured with a valid Fernet key before instantiation.\n# @POST: Manager exposes reversible encrypt/decrypt operations for persisted secrets.\n# @SIDE_EFFECT: Initializes Fernet cryptography state from process environment.\n# @DATA_CONTRACT: Input[str] -> Output[str]\n# @INVARIANT: Uses only a validated secret key from environment.\n#\n# @TEST_CONTRACT: EncryptionManagerModel ->\n# {\n# required_fields: {},\n# invariants: [\n# \"encrypted data can be decrypted back to the original string\"\n# ]\n# }\n# @TEST_FIXTURE: basic_encryption_cycle -> {\"data\": \"my_secret_key\"}\n# @TEST_EDGE: decrypt_invalid_data -> raises Exception\n# @TEST_EDGE: empty_string_encryption -> {\"data\": \"\"}\n# @TEST_INVARIANT: symmetric_encryption -> verifies: [basic_encryption_cycle, empty_string_encryption]\nclass EncryptionManager:\n # region EncryptionManager_init [TYPE Function]\n # @PURPOSE: Initialize the encryption manager with a Fernet key.\n # @PRE: ENCRYPTION_KEY env var must be set to a valid Fernet key.\n # @POST: Fernet instance ready for encryption/decryption.\n def __init__(self):\n self.key = _require_fernet_key()\n self.fernet = Fernet(self.key)\n\n # endregion EncryptionManager_init\n\n # region encrypt [TYPE Function]\n # @PURPOSE: Encrypt a plaintext string.\n # @PRE: data must be a non-empty string.\n # @POST: Returns encrypted string.\n def encrypt(self, data: str) -> str:\n with belief_scope(\"encrypt\"):\n return self.fernet.encrypt(data.encode()).decode()\n\n # endregion encrypt\n\n # region decrypt [TYPE Function]\n # @PURPOSE: Decrypt an encrypted string.\n # @PRE: encrypted_data must be a valid Fernet-encrypted string.\n # @POST: Returns original plaintext string.\n def decrypt(self, encrypted_data: str) -> str:\n with belief_scope(\"decrypt\"):\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n # endregion decrypt\n\n\n# #endregion EncryptionManager\n\n\n# #region LLMProviderService [C:3] [TYPE Class]\n# @BRIEF Service to manage LLM provider lifecycle.\n# @RELATION DEPENDS_ON -> [LLMProvider]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\nclass LLMProviderService:\n # region LLMProviderService_init [TYPE Function]\n # @PURPOSE: Initialize the service with database session.\n # @PRE: db must be a valid SQLAlchemy Session.\n # @POST: Service ready for provider operations.\n # @RELATION: [DEPENDS_ON] ->[EncryptionManager]\n def __init__(self, db: Session):\n self.db = db\n self.encryption = EncryptionManager()\n\n # endregion LLMProviderService_init\n\n # region get_all_providers [TYPE Function]\n # @PURPOSE: Returns all configured LLM providers.\n # @PRE: Database connection must be active.\n # @POST: Returns list of all LLMProvider records.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_all_providers(self) -> list[LLMProvider]:\n with belief_scope(\"get_all_providers\"):\n return self.db.query(LLMProvider).all()\n\n # endregion get_all_providers\n\n # region get_provider [TYPE Function]\n # @PURPOSE: Returns a single LLM provider by ID.\n # @PRE: provider_id must be a valid string.\n # @POST: Returns LLMProvider or None if not found.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_provider(self, provider_id: str) -> LLMProvider | None:\n with belief_scope(\"get_provider\"):\n return (\n self.db.query(LLMProvider).filter(LLMProvider.id == provider_id).first()\n )\n\n # endregion get_provider\n\n # region create_provider [TYPE Function]\n # @PURPOSE: Creates a new LLM provider with encrypted API key.\n # @PRE: config must contain valid provider configuration.\n # @POST: New provider created and persisted to database.\n # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[encrypt]\n def create_provider(self, config: \"LLMProviderConfig\") -> LLMProvider:\n with belief_scope(\"create_provider\"):\n encrypted_key = self.encryption.encrypt(config.api_key)\n db_provider = LLMProvider(\n provider_type=config.provider_type.value,\n name=config.name,\n base_url=config.base_url,\n api_key=encrypted_key,\n default_model=config.default_model,\n is_active=config.is_active,\n )\n self.db.add(db_provider)\n self.db.commit()\n self.db.refresh(db_provider)\n return db_provider\n\n # endregion create_provider\n\n # region update_provider [TYPE Function]\n # @PURPOSE: Updates an existing LLM provider.\n # @PRE: provider_id must exist, config must be valid.\n # @POST: Provider updated and persisted to database.\n # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[encrypt]\n def update_provider(\n self, provider_id: str, config: \"LLMProviderConfig\"\n ) -> LLMProvider | None:\n with belief_scope(\"update_provider\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n return None\n\n db_provider.provider_type = config.provider_type.value\n db_provider.name = config.name\n db_provider.base_url = config.base_url\n # Ignore masked placeholder values; they are display-only and must not overwrite secrets.\n if (\n config.api_key is not None\n and config.api_key != \"\"\n and config.api_key != MASKED_API_KEY_PLACEHOLDER\n ):\n db_provider.api_key = self.encryption.encrypt(config.api_key)\n db_provider.default_model = config.default_model\n db_provider.is_active = config.is_active\n\n self.db.commit()\n self.db.refresh(db_provider)\n return db_provider\n\n # endregion update_provider\n\n # region delete_provider [TYPE Function]\n # @PURPOSE: Deletes an LLM provider.\n # @PRE: provider_id must exist.\n # @POST: Provider removed from database.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def delete_provider(self, provider_id: str) -> bool:\n with belief_scope(\"delete_provider\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n return False\n self.db.delete(db_provider)\n self.db.commit()\n return True\n\n # endregion delete_provider\n\n # region get_decrypted_api_key [TYPE Function]\n # @PURPOSE: Returns the decrypted API key for a provider.\n # @PRE: provider_id must exist with valid encrypted key.\n # @POST: Returns decrypted API key or None on failure.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[decrypt]\n def get_decrypted_api_key(self, provider_id: str) -> str | None:\n with belief_scope(\"get_decrypted_api_key\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n logger.warning(\n f\"[get_decrypted_api_key] Provider {provider_id} not found in database\"\n )\n return None\n\n logger.info(f\"[get_decrypted_api_key] Provider found: {db_provider.id}\")\n logger.info(\n f\"[get_decrypted_api_key] Encrypted API key length: {len(db_provider.api_key) if db_provider.api_key else 0}\"\n )\n\n try:\n decrypted_key = self.encryption.decrypt(db_provider.api_key)\n logger.info(\n f\"[get_decrypted_api_key] Decryption successful, key length: {len(decrypted_key) if decrypted_key else 0}\"\n )\n return decrypted_key\n except Exception as e:\n logger.error(f\"[get_decrypted_api_key] Decryption failed: {e!s}\")\n return None\n\n # endregion get_decrypted_api_key\n\n\n# #endregion LLMProviderService\n\n# #endregion llm_provider\n" }, { "contract_id": "_require_fernet_key", "contract_type": "Function", "file_path": "backend/src/services/llm_provider.py", "start_line": 22, - "end_line": 53, + "end_line": 52, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[ENCRYPTION_KEY:str] -> Output[bytes]", "INVARIANT": "Encryption initialization never falls back to a hardcoded secret.", "POST": "Returns validated key bytes ready for Fernet initialization.", @@ -90704,19 +63683,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_require_fernet_key:Function]\n# @COMPLEXITY: 5\n# @PURPOSE: Load and validate the Fernet key used for secret encryption.\n# @PRE: ENCRYPTION_KEY environment variable must be set to a valid Fernet key.\n# @POST: Returns validated key bytes ready for Fernet initialization.\n# @RELATION: DEPENDS_ON ->[backend.src.core.logger:Function]\n# @SIDE_EFFECT: Emits belief-state logs for missing or invalid encryption configuration.\n# @DATA_CONTRACT: Input[ENCRYPTION_KEY:str] -> Output[bytes]\n# @INVARIANT: Encryption initialization never falls back to a hardcoded secret.\ndef _require_fernet_key() -> bytes:\n with belief_scope(\"_require_fernet_key\"):\n raw_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if not raw_key:\n logger.explore(\n \"Missing ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be set\")\n\n key = raw_key.encode()\n try:\n Fernet(key)\n except Exception as exc:\n logger.explore(\n \"Invalid ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be a valid Fernet key\") from exc\n\n logger.reflect(\"Validated ENCRYPTION_KEY for EncryptionManager initialization\")\n return key\n\n\n# [/DEF:_require_fernet_key:Function]\n" + "anchor_syntax": "region", + "body": "# #region _require_fernet_key [C:5] [TYPE Function]\n# @BRIEF Load and validate the Fernet key used for secret encryption.\n# @PRE: ENCRYPTION_KEY environment variable must be set to a valid Fernet key.\n# @POST: Returns validated key bytes ready for Fernet initialization.\n# @RELATION DEPENDS_ON -> [backend.src.core.logger:Function]\n# @SIDE_EFFECT: Emits belief-state logs for missing or invalid encryption configuration.\n# @DATA_CONTRACT: Input[ENCRYPTION_KEY:str] -> Output[bytes]\n# @INVARIANT: Encryption initialization never falls back to a hardcoded secret.\ndef _require_fernet_key() -> bytes:\n with belief_scope(\"_require_fernet_key\"):\n raw_key = os.getenv(\"ENCRYPTION_KEY\", \"\").strip()\n if not raw_key:\n logger.explore(\n \"Missing ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be set\")\n\n key = raw_key.encode()\n try:\n Fernet(key)\n except Exception as exc:\n logger.explore(\n \"Invalid ENCRYPTION_KEY blocks EncryptionManager initialization\"\n )\n raise RuntimeError(\"ENCRYPTION_KEY must be a valid Fernet key\") from exc\n\n logger.reflect(\"Validated ENCRYPTION_KEY for EncryptionManager initialization\")\n return key\n\n\n# #endregion _require_fernet_key\n" }, { "contract_id": "EncryptionManager", "contract_type": "Class", "file_path": "backend/src/services/llm_provider.py", - "start_line": 56, - "end_line": 109, + "start_line": 55, + "end_line": 107, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[str] -> Output[str]", "INVARIANT": "Uses only a validated secret key from environment.", "POST": "Manager exposes reversible encrypt/decrypt operations for persisted secrets.", @@ -90728,7 +63707,7 @@ "relations": [ { "source_id": "EncryptionManager", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "_require_fernet_key", "target_ref": "[_require_fernet_key]" } @@ -90772,573 +63751,63 @@ "actual_complexity": 5, "contract_type": "Class" } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:EncryptionManager:Class]\n# @COMPLEXITY: 5\n# @PURPOSE: Handles encryption and decryption of sensitive data like API keys.\n# @RELATION: [CALLS] ->[_require_fernet_key]\n# @PRE: ENCRYPTION_KEY is configured with a valid Fernet key before instantiation.\n# @POST: Manager exposes reversible encrypt/decrypt operations for persisted secrets.\n# @SIDE_EFFECT: Initializes Fernet cryptography state from process environment.\n# @DATA_CONTRACT: Input[str] -> Output[str]\n# @INVARIANT: Uses only a validated secret key from environment.\n#\n# @TEST_CONTRACT: EncryptionManagerModel ->\n# {\n# required_fields: {},\n# invariants: [\n# \"encrypted data can be decrypted back to the original string\"\n# ]\n# }\n# @TEST_FIXTURE: basic_encryption_cycle -> {\"data\": \"my_secret_key\"}\n# @TEST_EDGE: decrypt_invalid_data -> raises Exception\n# @TEST_EDGE: empty_string_encryption -> {\"data\": \"\"}\n# @TEST_INVARIANT: symmetric_encryption -> verifies: [basic_encryption_cycle, empty_string_encryption]\nclass EncryptionManager:\n # [DEF:EncryptionManager_init:Function]\n # @PURPOSE: Initialize the encryption manager with a Fernet key.\n # @PRE: ENCRYPTION_KEY env var must be set to a valid Fernet key.\n # @POST: Fernet instance ready for encryption/decryption.\n def __init__(self):\n self.key = _require_fernet_key()\n self.fernet = Fernet(self.key)\n\n # [/DEF:EncryptionManager_init:Function]\n\n # [DEF:encrypt:Function]\n # @PURPOSE: Encrypt a plaintext string.\n # @PRE: data must be a non-empty string.\n # @POST: Returns encrypted string.\n def encrypt(self, data: str) -> str:\n with belief_scope(\"encrypt\"):\n return self.fernet.encrypt(data.encode()).decode()\n\n # [/DEF:encrypt:Function]\n\n # [DEF:decrypt:Function]\n # @PURPOSE: Decrypt an encrypted string.\n # @PRE: encrypted_data must be a valid Fernet-encrypted string.\n # @POST: Returns original plaintext string.\n def decrypt(self, encrypted_data: str) -> str:\n with belief_scope(\"decrypt\"):\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n # [/DEF:decrypt:Function]\n\n\n# [/DEF:EncryptionManager:Class]\n" - }, - { - "contract_id": "EncryptionManager_init", - "contract_type": "Function", - "file_path": "backend/src/services/llm_provider.py", - "start_line": 78, - "end_line": 86, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Fernet instance ready for encryption/decryption.", - "PRE": "ENCRYPTION_KEY env var must be set to a valid Fernet key.", - "PURPOSE": "Initialize the encryption manager with a Fernet key." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:EncryptionManager_init:Function]\n # @PURPOSE: Initialize the encryption manager with a Fernet key.\n # @PRE: ENCRYPTION_KEY env var must be set to a valid Fernet key.\n # @POST: Fernet instance ready for encryption/decryption.\n def __init__(self):\n self.key = _require_fernet_key()\n self.fernet = Fernet(self.key)\n\n # [/DEF:EncryptionManager_init:Function]\n" - }, - { - "contract_id": "encrypt", - "contract_type": "Function", - "file_path": "backend/src/services/llm_provider.py", - "start_line": 88, - "end_line": 96, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns encrypted string.", - "PRE": "data must be a non-empty string.", - "PURPOSE": "Encrypt a plaintext 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" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:encrypt:Function]\n # @PURPOSE: Encrypt a plaintext string.\n # @PRE: data must be a non-empty string.\n # @POST: Returns encrypted string.\n def encrypt(self, data: str) -> str:\n with belief_scope(\"encrypt\"):\n return self.fernet.encrypt(data.encode()).decode()\n\n # [/DEF:encrypt:Function]\n" - }, - { - "contract_id": "decrypt", - "contract_type": "Function", - "file_path": "backend/src/services/llm_provider.py", - "start_line": 98, - "end_line": 106, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns original plaintext string.", - "PRE": "encrypted_data must be a valid Fernet-encrypted string.", - "PURPOSE": "Decrypt an encrypted 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" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:decrypt:Function]\n # @PURPOSE: Decrypt an encrypted string.\n # @PRE: encrypted_data must be a valid Fernet-encrypted string.\n # @POST: Returns original plaintext string.\n def decrypt(self, encrypted_data: str) -> str:\n with belief_scope(\"decrypt\"):\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n # [/DEF:decrypt:Function]\n" + "anchor_syntax": "region", + "body": "# #region EncryptionManager [C:5] [TYPE Class]\n# @BRIEF Handles encryption and decryption of sensitive data like API keys.\n# @RELATION CALLS -> [_require_fernet_key]\n# @PRE: ENCRYPTION_KEY is configured with a valid Fernet key before instantiation.\n# @POST: Manager exposes reversible encrypt/decrypt operations for persisted secrets.\n# @SIDE_EFFECT: Initializes Fernet cryptography state from process environment.\n# @DATA_CONTRACT: Input[str] -> Output[str]\n# @INVARIANT: Uses only a validated secret key from environment.\n#\n# @TEST_CONTRACT: EncryptionManagerModel ->\n# {\n# required_fields: {},\n# invariants: [\n# \"encrypted data can be decrypted back to the original string\"\n# ]\n# }\n# @TEST_FIXTURE: basic_encryption_cycle -> {\"data\": \"my_secret_key\"}\n# @TEST_EDGE: decrypt_invalid_data -> raises Exception\n# @TEST_EDGE: empty_string_encryption -> {\"data\": \"\"}\n# @TEST_INVARIANT: symmetric_encryption -> verifies: [basic_encryption_cycle, empty_string_encryption]\nclass EncryptionManager:\n # region EncryptionManager_init [TYPE Function]\n # @PURPOSE: Initialize the encryption manager with a Fernet key.\n # @PRE: ENCRYPTION_KEY env var must be set to a valid Fernet key.\n # @POST: Fernet instance ready for encryption/decryption.\n def __init__(self):\n self.key = _require_fernet_key()\n self.fernet = Fernet(self.key)\n\n # endregion EncryptionManager_init\n\n # region encrypt [TYPE Function]\n # @PURPOSE: Encrypt a plaintext string.\n # @PRE: data must be a non-empty string.\n # @POST: Returns encrypted string.\n def encrypt(self, data: str) -> str:\n with belief_scope(\"encrypt\"):\n return self.fernet.encrypt(data.encode()).decode()\n\n # endregion encrypt\n\n # region decrypt [TYPE Function]\n # @PURPOSE: Decrypt an encrypted string.\n # @PRE: encrypted_data must be a valid Fernet-encrypted string.\n # @POST: Returns original plaintext string.\n def decrypt(self, encrypted_data: str) -> str:\n with belief_scope(\"decrypt\"):\n return self.fernet.decrypt(encrypted_data.encode()).decode()\n\n # endregion decrypt\n\n\n# #endregion EncryptionManager\n" }, { "contract_id": "LLMProviderService", "contract_type": "Class", "file_path": "backend/src/services/llm_provider.py", - "start_line": 112, - "end_line": 268, + "start_line": 110, + "end_line": 259, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Service to manage LLM provider lifecycle." }, "relations": [ { "source_id": "LLMProviderService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LLMProvider", "target_ref": "[LLMProvider]" }, { "source_id": "LLMProviderService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "EncryptionManager", "target_ref": "[EncryptionManager]" }, { "source_id": "LLMProviderService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "LLMProviderConfig", "target_ref": "[LLMProviderConfig]" } ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:LLMProviderService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Service to manage LLM provider lifecycle.\n# @RELATION: [DEPENDS_ON] ->[LLMProvider]\n# @RELATION: [DEPENDS_ON] ->[EncryptionManager]\n# @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\nclass LLMProviderService:\n # [DEF:LLMProviderService_init:Function]\n # @PURPOSE: Initialize the service with database session.\n # @PRE: db must be a valid SQLAlchemy Session.\n # @POST: Service ready for provider operations.\n # @RELATION: [DEPENDS_ON] ->[EncryptionManager]\n def __init__(self, db: Session):\n self.db = db\n self.encryption = EncryptionManager()\n\n # [/DEF:LLMProviderService_init:Function]\n\n # [DEF:get_all_providers:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns all configured LLM providers.\n # @PRE: Database connection must be active.\n # @POST: Returns list of all LLMProvider records.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_all_providers(self) -> List[LLMProvider]:\n with belief_scope(\"get_all_providers\"):\n return self.db.query(LLMProvider).all()\n\n # [/DEF:get_all_providers:Function]\n\n # [DEF:get_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns a single LLM provider by ID.\n # @PRE: provider_id must be a valid string.\n # @POST: Returns LLMProvider or None if not found.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_provider(self, provider_id: str) -> Optional[LLMProvider]:\n with belief_scope(\"get_provider\"):\n return (\n self.db.query(LLMProvider).filter(LLMProvider.id == provider_id).first()\n )\n\n # [/DEF:get_provider:Function]\n\n # [DEF:create_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Creates a new LLM provider with encrypted API key.\n # @PRE: config must contain valid provider configuration.\n # @POST: New provider created and persisted to database.\n # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[encrypt]\n def create_provider(self, config: \"LLMProviderConfig\") -> LLMProvider:\n with belief_scope(\"create_provider\"):\n encrypted_key = self.encryption.encrypt(config.api_key)\n db_provider = LLMProvider(\n provider_type=config.provider_type.value,\n name=config.name,\n base_url=config.base_url,\n api_key=encrypted_key,\n default_model=config.default_model,\n is_active=config.is_active,\n )\n self.db.add(db_provider)\n self.db.commit()\n self.db.refresh(db_provider)\n return db_provider\n\n # [/DEF:create_provider:Function]\n\n # [DEF:update_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Updates an existing LLM provider.\n # @PRE: provider_id must exist, config must be valid.\n # @POST: Provider updated and persisted to database.\n # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[encrypt]\n def update_provider(\n self, provider_id: str, config: \"LLMProviderConfig\"\n ) -> Optional[LLMProvider]:\n with belief_scope(\"update_provider\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n return None\n\n db_provider.provider_type = config.provider_type.value\n db_provider.name = config.name\n db_provider.base_url = config.base_url\n # Ignore masked placeholder values; they are display-only and must not overwrite secrets.\n if (\n config.api_key is not None\n and config.api_key != \"\"\n and config.api_key != MASKED_API_KEY_PLACEHOLDER\n ):\n db_provider.api_key = self.encryption.encrypt(config.api_key)\n db_provider.default_model = config.default_model\n db_provider.is_active = config.is_active\n\n self.db.commit()\n self.db.refresh(db_provider)\n return db_provider\n\n # [/DEF:update_provider:Function]\n\n # [DEF:delete_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Deletes an LLM provider.\n # @PRE: provider_id must exist.\n # @POST: Provider removed from database.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def delete_provider(self, provider_id: str) -> bool:\n with belief_scope(\"delete_provider\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n return False\n self.db.delete(db_provider)\n self.db.commit()\n return True\n\n # [/DEF:delete_provider:Function]\n\n # [DEF:get_decrypted_api_key:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns the decrypted API key for a provider.\n # @PRE: provider_id must exist with valid encrypted key.\n # @POST: Returns decrypted API key or None on failure.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[decrypt]\n def get_decrypted_api_key(self, provider_id: str) -> Optional[str]:\n with belief_scope(\"get_decrypted_api_key\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n logger.warning(\n f\"[get_decrypted_api_key] Provider {provider_id} not found in database\"\n )\n return None\n\n logger.info(f\"[get_decrypted_api_key] Provider found: {db_provider.id}\")\n logger.info(\n f\"[get_decrypted_api_key] Encrypted API key length: {len(db_provider.api_key) if db_provider.api_key else 0}\"\n )\n\n try:\n decrypted_key = self.encryption.decrypt(db_provider.api_key)\n logger.info(\n f\"[get_decrypted_api_key] Decryption successful, key length: {len(decrypted_key) if decrypted_key else 0}\"\n )\n return decrypted_key\n except Exception as e:\n logger.error(f\"[get_decrypted_api_key] Decryption failed: {str(e)}\")\n return None\n\n # [/DEF:get_decrypted_api_key:Function]\n\n\n# [/DEF:LLMProviderService:Class]\n" - }, - { - "contract_id": "LLMProviderService_init", - "contract_type": "Function", - "file_path": "backend/src/services/llm_provider.py", - "start_line": 119, - "end_line": 128, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Service ready for provider operations.", - "PRE": "db must be a valid SQLAlchemy Session.", - "PURPOSE": "Initialize the service with database session." - }, - "relations": [ - { - "source_id": "LLMProviderService_init", - "relation_type": "[DEPENDS_ON]", - "target_id": "EncryptionManager", - "target_ref": "[EncryptionManager]" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:LLMProviderService_init:Function]\n # @PURPOSE: Initialize the service with database session.\n # @PRE: db must be a valid SQLAlchemy Session.\n # @POST: Service ready for provider operations.\n # @RELATION: [DEPENDS_ON] ->[EncryptionManager]\n def __init__(self, db: Session):\n self.db = db\n self.encryption = EncryptionManager()\n\n # [/DEF:LLMProviderService_init:Function]\n" - }, - { - "contract_id": "get_all_providers", - "contract_type": "Function", - "file_path": "backend/src/services/llm_provider.py", - "start_line": 130, - "end_line": 140, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns list of all LLMProvider records.", - "PRE": "Database connection must be active.", - "PURPOSE": "Returns all configured LLM providers." - }, - "relations": [ - { - "source_id": "get_all_providers", - "relation_type": "[DEPENDS_ON]", - "target_id": "LLMProvider", - "target_ref": "[LLMProvider]" - } - ], - "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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_all_providers:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns all configured LLM providers.\n # @PRE: Database connection must be active.\n # @POST: Returns list of all LLMProvider records.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_all_providers(self) -> List[LLMProvider]:\n with belief_scope(\"get_all_providers\"):\n return self.db.query(LLMProvider).all()\n\n # [/DEF:get_all_providers:Function]\n" - }, - { - "contract_id": "get_provider", - "contract_type": "Function", - "file_path": "backend/src/services/llm_provider.py", - "start_line": 142, - "end_line": 154, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns LLMProvider or None if not found.", - "PRE": "provider_id must be a valid string.", - "PURPOSE": "Returns a single LLM provider by ID." - }, - "relations": [ - { - "source_id": "get_provider", - "relation_type": "[DEPENDS_ON]", - "target_id": "LLMProvider", - "target_ref": "[LLMProvider]" - } - ], - "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": "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_provider:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns a single LLM provider by ID.\n # @PRE: provider_id must be a valid string.\n # @POST: Returns LLMProvider or None if not found.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_provider(self, provider_id: str) -> Optional[LLMProvider]:\n with belief_scope(\"get_provider\"):\n return (\n self.db.query(LLMProvider).filter(LLMProvider.id == provider_id).first()\n )\n\n # [/DEF:get_provider:Function]\n" - }, - { - "contract_id": "get_decrypted_api_key", - "contract_type": "Function", - "file_path": "backend/src/services/llm_provider.py", - "start_line": 234, - "end_line": 265, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Returns decrypted API key or None on failure.", - "PRE": "provider_id must exist with valid encrypted key.", - "PURPOSE": "Returns the decrypted API key for a provider." - }, - "relations": [ - { - "source_id": "get_decrypted_api_key", - "relation_type": "[DEPENDS_ON]", - "target_id": "LLMProvider", - "target_ref": "[LLMProvider]" - }, - { - "source_id": "get_decrypted_api_key", - "relation_type": "[CALLS]", - "target_id": "decrypt", - "target_ref": "[decrypt]" - } - ], - "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": "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]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_decrypted_api_key:Function]\n # @COMPLEXITY: 3\n # @PURPOSE: Returns the decrypted API key for a provider.\n # @PRE: provider_id must exist with valid encrypted key.\n # @POST: Returns decrypted API key or None on failure.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[decrypt]\n def get_decrypted_api_key(self, provider_id: str) -> Optional[str]:\n with belief_scope(\"get_decrypted_api_key\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n logger.warning(\n f\"[get_decrypted_api_key] Provider {provider_id} not found in database\"\n )\n return None\n\n logger.info(f\"[get_decrypted_api_key] Provider found: {db_provider.id}\")\n logger.info(\n f\"[get_decrypted_api_key] Encrypted API key length: {len(db_provider.api_key) if db_provider.api_key else 0}\"\n )\n\n try:\n decrypted_key = self.encryption.decrypt(db_provider.api_key)\n logger.info(\n f\"[get_decrypted_api_key] Decryption successful, key length: {len(decrypted_key) if decrypted_key else 0}\"\n )\n return decrypted_key\n except Exception as e:\n logger.error(f\"[get_decrypted_api_key] Decryption failed: {str(e)}\")\n return None\n\n # [/DEF:get_decrypted_api_key:Function]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region LLMProviderService [C:3] [TYPE Class]\n# @BRIEF Service to manage LLM provider lifecycle.\n# @RELATION DEPENDS_ON -> [LLMProvider]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION DEPENDS_ON -> [LLMProviderConfig]\nclass LLMProviderService:\n # region LLMProviderService_init [TYPE Function]\n # @PURPOSE: Initialize the service with database session.\n # @PRE: db must be a valid SQLAlchemy Session.\n # @POST: Service ready for provider operations.\n # @RELATION: [DEPENDS_ON] ->[EncryptionManager]\n def __init__(self, db: Session):\n self.db = db\n self.encryption = EncryptionManager()\n\n # endregion LLMProviderService_init\n\n # region get_all_providers [TYPE Function]\n # @PURPOSE: Returns all configured LLM providers.\n # @PRE: Database connection must be active.\n # @POST: Returns list of all LLMProvider records.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_all_providers(self) -> list[LLMProvider]:\n with belief_scope(\"get_all_providers\"):\n return self.db.query(LLMProvider).all()\n\n # endregion get_all_providers\n\n # region get_provider [TYPE Function]\n # @PURPOSE: Returns a single LLM provider by ID.\n # @PRE: provider_id must be a valid string.\n # @POST: Returns LLMProvider or None if not found.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def get_provider(self, provider_id: str) -> LLMProvider | None:\n with belief_scope(\"get_provider\"):\n return (\n self.db.query(LLMProvider).filter(LLMProvider.id == provider_id).first()\n )\n\n # endregion get_provider\n\n # region create_provider [TYPE Function]\n # @PURPOSE: Creates a new LLM provider with encrypted API key.\n # @PRE: config must contain valid provider configuration.\n # @POST: New provider created and persisted to database.\n # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[encrypt]\n def create_provider(self, config: \"LLMProviderConfig\") -> LLMProvider:\n with belief_scope(\"create_provider\"):\n encrypted_key = self.encryption.encrypt(config.api_key)\n db_provider = LLMProvider(\n provider_type=config.provider_type.value,\n name=config.name,\n base_url=config.base_url,\n api_key=encrypted_key,\n default_model=config.default_model,\n is_active=config.is_active,\n )\n self.db.add(db_provider)\n self.db.commit()\n self.db.refresh(db_provider)\n return db_provider\n\n # endregion create_provider\n\n # region update_provider [TYPE Function]\n # @PURPOSE: Updates an existing LLM provider.\n # @PRE: provider_id must exist, config must be valid.\n # @POST: Provider updated and persisted to database.\n # @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[encrypt]\n def update_provider(\n self, provider_id: str, config: \"LLMProviderConfig\"\n ) -> LLMProvider | None:\n with belief_scope(\"update_provider\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n return None\n\n db_provider.provider_type = config.provider_type.value\n db_provider.name = config.name\n db_provider.base_url = config.base_url\n # Ignore masked placeholder values; they are display-only and must not overwrite secrets.\n if (\n config.api_key is not None\n and config.api_key != \"\"\n and config.api_key != MASKED_API_KEY_PLACEHOLDER\n ):\n db_provider.api_key = self.encryption.encrypt(config.api_key)\n db_provider.default_model = config.default_model\n db_provider.is_active = config.is_active\n\n self.db.commit()\n self.db.refresh(db_provider)\n return db_provider\n\n # endregion update_provider\n\n # region delete_provider [TYPE Function]\n # @PURPOSE: Deletes an LLM provider.\n # @PRE: provider_id must exist.\n # @POST: Provider removed from database.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n def delete_provider(self, provider_id: str) -> bool:\n with belief_scope(\"delete_provider\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n return False\n self.db.delete(db_provider)\n self.db.commit()\n return True\n\n # endregion delete_provider\n\n # region get_decrypted_api_key [TYPE Function]\n # @PURPOSE: Returns the decrypted API key for a provider.\n # @PRE: provider_id must exist with valid encrypted key.\n # @POST: Returns decrypted API key or None on failure.\n # @RELATION: [DEPENDS_ON] ->[LLMProvider]\n # @RELATION: [CALLS] ->[decrypt]\n def get_decrypted_api_key(self, provider_id: str) -> str | None:\n with belief_scope(\"get_decrypted_api_key\"):\n db_provider = self.get_provider(provider_id)\n if not db_provider:\n logger.warning(\n f\"[get_decrypted_api_key] Provider {provider_id} not found in database\"\n )\n return None\n\n logger.info(f\"[get_decrypted_api_key] Provider found: {db_provider.id}\")\n logger.info(\n f\"[get_decrypted_api_key] Encrypted API key length: {len(db_provider.api_key) if db_provider.api_key else 0}\"\n )\n\n try:\n decrypted_key = self.encryption.decrypt(db_provider.api_key)\n logger.info(\n f\"[get_decrypted_api_key] Decryption successful, key length: {len(decrypted_key) if decrypted_key else 0}\"\n )\n return decrypted_key\n except Exception as e:\n logger.error(f\"[get_decrypted_api_key] Decryption failed: {e!s}\")\n return None\n\n # endregion get_decrypted_api_key\n\n\n# #endregion LLMProviderService\n" }, { "contract_id": "mapping_service", "contract_type": "Module", "file_path": "backend/src/services/mapping_service.py", "start_line": 1, - "end_line": 99, + "end_line": 91, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]]", "INVARIANT": "Suggestions are based on database names.", "LAYER": "Service", "POST": "Exposes stateless mapping suggestion orchestration over configured environments.", "PRE": "source/target environment identifiers are provided by caller.", "PURPOSE": "Orchestrates database fetching and fuzzy matching suggestions.", - "SEMANTICS": [ - "service", - "mapping", - "fuzzy-matching", - "superset" - ], "SIDE_EFFECT": "Performs remote metadata reads through Superset API clients." }, "relations": [ @@ -91402,19 +63871,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:mapping_service:Module]\n#\n# @SEMANTICS: service, mapping, fuzzy-matching, superset\n# @PURPOSE: Orchestrates database fetching and fuzzy matching suggestions.\n# @COMPLEXITY: 3\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# [SECTION: IMPORTS]\nfrom typing import List, Dict\nfrom ..core.logger import belief_scope\nfrom ..core.superset_client import SupersetClient\nfrom ..core.utils.matching import suggest_mappings\n# [/SECTION]\n\n\n# [DEF:MappingService:Class]\n# @PURPOSE: Service for handling database mapping logic.\n# @COMPLEXITY: 3\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 # [DEF:init:Function]\n # @PURPOSE: Initializes the mapping service with a config manager.\n # @COMPLEXITY: 3\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 # [/DEF:init:Function]\n\n # [DEF:_get_client:Function]\n # @PURPOSE: Helper to get an initialized SupersetClient for an environment.\n # @COMPLEXITY: 3\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 # [/DEF:_get_client:Function]\n\n # [DEF:get_suggestions:Function]\n # @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.\n # @COMPLEXITY: 3\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 # [/DEF:get_suggestions:Function]\n\n\n# [/DEF:MappingService:Class]\n\n# [/DEF:mapping_service:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "MappingService", "contract_type": "Class", "file_path": "backend/src/services/mapping_service.py", - "start_line": 24, - "end_line": 97, + "start_line": 20, + "end_line": 89, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input[config_manager] -> Output[List[Dict]]", "POST": "Provides client resolution and mapping suggestion methods.", "PRE": "config_manager exposes get_environments() with environment objects containing id.", @@ -91473,132 +63942,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:MappingService:Class]\n# @PURPOSE: Service for handling database mapping logic.\n# @COMPLEXITY: 3\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 # [DEF:init:Function]\n # @PURPOSE: Initializes the mapping service with a config manager.\n # @COMPLEXITY: 3\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 # [/DEF:init:Function]\n\n # [DEF:_get_client:Function]\n # @PURPOSE: Helper to get an initialized SupersetClient for an environment.\n # @COMPLEXITY: 3\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 # [/DEF:_get_client:Function]\n\n # [DEF:get_suggestions:Function]\n # @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.\n # @COMPLEXITY: 3\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 # [/DEF:get_suggestions:Function]\n\n\n# [/DEF:MappingService:Class]\n" - }, - { - "contract_id": "_get_client", - "contract_type": "Function", - "file_path": "backend/src/services/mapping_service.py", - "start_line": 47, - "end_line": 64, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "env_id (str) - The ID of the environment.", - "POST": "Returns an initialized SupersetClient.", - "PRE": "environment must exist in config.", - "PURPOSE": "Helper to get an initialized SupersetClient for an environment.", - "RETURN": "SupersetClient - Initialized client." - }, - "relations": [ - { - "source_id": "_get_client", - "relation_type": "CALLS", - "target_id": "SupersetClient", - "target_ref": "SupersetClient" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_client:Function]\n # @PURPOSE: Helper to get an initialized SupersetClient for an environment.\n # @COMPLEXITY: 3\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 # [/DEF:_get_client:Function]\n" - }, - { - "contract_id": "get_suggestions", - "contract_type": "Function", - "file_path": "backend/src/services/mapping_service.py", - "start_line": 66, - "end_line": 94, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "target_env_id (str) - Target environment ID.", - "POST": "Returns fuzzy-matched database suggestions.", - "PRE": "Both environments must be accessible.", - "PURPOSE": "Fetches databases from both environments and returns fuzzy matching suggestions.", - "RETURN": "List[Dict] - Suggested mappings." - }, - "relations": [ - { - "source_id": "get_suggestions", - "relation_type": "CALLS", - "target_id": "_get_client", - "target_ref": "_get_client" - }, - { - "source_id": "get_suggestions", - "relation_type": "CALLS", - "target_id": "suggest_mappings", - "target_ref": "suggest_mappings" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_suggestions:Function]\n # @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.\n # @COMPLEXITY: 3\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 # [/DEF:get_suggestions:Function]\n" + "anchor_syntax": "region", + "body": "# #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" }, { "contract_id": "notifications", @@ -91675,115 +64020,55 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:notifications:Package]\n# @PURPOSE: Notification service package root.\n# @RELATION: EXPORTS ->[NotificationService:Class]\n# [/DEF:notifications:Package]\n" - }, - { - "contract_id": "test_notification_service", - "contract_type": "Module", - "file_path": "backend/src/services/notifications/__tests__/test_notification_service.py", - "start_line": 1, - "end_line": 121, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Unit tests for NotificationService routing and dispatch logic." - }, - "relations": [ - { - "source_id": "test_notification_service", - "relation_type": "TESTS", - "target_id": "NotificationService:Class", - "target_ref": "[NotificationService:Class]" - } - ], - "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" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate TESTS is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "TESTS" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_notification_service:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Unit tests for NotificationService routing and dispatch logic.\n# @RELATION: TESTS ->[NotificationService:Class]\n\nimport pytest\nfrom unittest.mock import MagicMock, AsyncMock, patch\nfrom datetime import time\n\nfrom src.models.llm import ValidationRecord, ValidationPolicy\nfrom src.models.profile import UserDashboardPreference\nfrom src.models.auth import User\nfrom src.services.notifications.service import NotificationService\n\n\n@pytest.fixture\ndef mock_db():\n return MagicMock()\n\n\n@pytest.fixture\ndef mock_config_manager():\n cm = MagicMock()\n cm.get_payload.return_value = {\n \"notifications\": {\n \"smtp\": {\"host\": \"localhost\", \"port\": 25, \"from_email\": \"test@example.com\"},\n \"telegram\": {\"bot_token\": \"test_token\"}\n }\n }\n return cm\n\n\n@pytest.fixture\ndef service(mock_db, mock_config_manager):\n return NotificationService(mock_db, mock_config_manager)\n\n\n@pytest.mark.asyncio\nasync def test_should_notify_fail_only(service):\n record = ValidationRecord(status=\"FAIL\")\n policy = ValidationPolicy(alert_condition=\"FAIL_ONLY\")\n assert service._should_notify(record, policy) is True\n\n record.status = \"WARN\"\n assert service._should_notify(record, policy) is False\n\n\n@pytest.mark.asyncio\nasync def test_should_notify_warn_and_fail(service):\n policy = ValidationPolicy(alert_condition=\"WARN_AND_FAIL\")\n \n record = ValidationRecord(status=\"FAIL\")\n assert service._should_notify(record, policy) is True\n\n record.status = \"WARN\"\n assert service._should_notify(record, policy) is True\n\n record.status = \"PASS\"\n assert service._should_notify(record, policy) is False\n\n\n@pytest.mark.asyncio\nasync def test_resolve_targets_owner_routing(service, mock_db):\n record = ValidationRecord(dashboard_id=\"dash-1\", environment_id=\"env-1\")\n \n user = User(email=\"user@example.com\")\n pref = UserDashboardPreference(\n user=user,\n telegram_id=\"12345\",\n notify_on_fail=True,\n superset_username=\"user1\"\n )\n \n mock_db.query.return_value.filter.return_value.all.return_value = [pref]\n \n targets = service._resolve_targets(record, None)\n \n assert (\"TELEGRAM\", \"12345\") in targets\n assert (\"SMTP\", \"user@example.com\") in targets\n\n\n@pytest.mark.asyncio\nasync def test_resolve_targets_custom_channels(service):\n record = ValidationRecord(status=\"FAIL\")\n policy = ValidationPolicy(\n notify_owners=False,\n custom_channels=[{\"type\": \"SLACK\", \"target\": \"#alerts\"}]\n )\n \n targets = service._resolve_targets(record, policy)\n assert targets == [(\"SLACK\", \"#alerts\")]\n\n\n@pytest.mark.asyncio\nasync def test_dispatch_report_skips_if_no_notify(service):\n record = ValidationRecord(status=\"PASS\")\n policy = ValidationPolicy(alert_condition=\"FAIL_ONLY\")\n \n with patch.object(service, \"_resolve_targets\") as mock_resolve:\n await service.dispatch_report(record, policy)\n mock_resolve.assert_not_called()\n\n\n@pytest.mark.asyncio\nasync def test_dispatch_report_calls_providers(service, mock_db):\n record = ValidationRecord(id=\"rec-1\", status=\"FAIL\", summary=\"Bad\", issues=[])\n \n # Mock providers\n service._initialize_providers()\n service._providers[\"TELEGRAM\"] = AsyncMock()\n service._providers[\"SMTP\"] = AsyncMock()\n \n # Mock targets\n with patch.object(service, \"_resolve_targets\") as mock_resolve:\n mock_resolve.return_value = [(\"TELEGRAM\", \"123\"), (\"SMTP\", \"a@b.com\")]\n await service.dispatch_report(record, None)\n \n service._providers[\"TELEGRAM\"].send.assert_called_once()\n service._providers[\"SMTP\"].send.assert_called_once()\n\n# [/DEF:test_notification_service:Module]\n" + "anchor_syntax": "region", + "body": "# #region notifications [TYPE Package] [SEMANTICS notification, package, service]\n# @BRIEF Notification service package root.\n# @RELATION EXPORTS -> [NotificationService:Class]\n# #endregion notifications\n" }, { "contract_id": "providers", "contract_type": "Module", "file_path": "backend/src/services/notifications/providers.py", "start_line": 1, - "end_line": 177, + "end_line": 172, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[target, subject, body, context?] -> Output[bool]", "INVARIANT": "Sensitive credentials must be handled via encrypted config.", "LAYER": "Infra", "POST": "Each provider exposes async send contract returning boolean delivery outcome.", "PRE": "Provider configuration dictionaries are supplied by trusted configuration sources.", "PURPOSE": "Defines abstract base and concrete implementations for external notification delivery.", - "SEMANTICS": [ - "notifications", - "providers", - "smtp", - "slack", - "telegram", - "abstraction" - ], "SIDE_EFFECT": "Performs outbound network I/O to SMTP or HTTP endpoints." }, "relations": [ { "source_id": "providers", - "relation_type": "[DEPENDED_ON_BY]", + "relation_type": "DEPENDED_ON_BY", "target_id": "NotificationService", "target_ref": "[NotificationService]" }, { "source_id": "providers", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "NotificationProvider", "target_ref": "[NotificationProvider]" }, { "source_id": "providers", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SMTPProvider", "target_ref": "[SMTPProvider]" }, { "source_id": "providers", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TelegramProvider", "target_ref": "[TelegramProvider]" }, { "source_id": "providers", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SlackProvider", "target_ref": "[SlackProvider]" } @@ -91810,7 +64095,7 @@ { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate [DEPENDED_ON_BY] is not in allowed_predicates", + "message": "Predicate DEPENDED_ON_BY is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -91822,113 +64107,41 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "[DEPENDED_ON_BY]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" + "predicate": "DEPENDED_ON_BY" } } ], - "anchor_syntax": "def", - "body": "# [DEF:providers:Module]\n#\n# @COMPLEXITY: 5\n# @SEMANTICS: notifications, providers, smtp, slack, telegram, abstraction\n# @PURPOSE: Defines abstract base and concrete implementations for external notification delivery.\n# @RELATION: [DEPENDED_ON_BY] ->[NotificationService]\n# @RELATION: [DEPENDS_ON] ->[NotificationProvider]\n# @RELATION: [DEPENDS_ON] ->[SMTPProvider]\n# @RELATION: [DEPENDS_ON] ->[TelegramProvider]\n# @RELATION: [DEPENDS_ON] ->[SlackProvider]\n# @LAYER: Infra\n# @PRE: Provider configuration dictionaries are supplied by trusted configuration sources.\n# @POST: Each provider exposes async send contract returning boolean delivery outcome.\n# @SIDE_EFFECT: Performs outbound network I/O to SMTP or HTTP endpoints.\n# @DATA_CONTRACT: Input[target, subject, body, context?] -> Output[bool]\n# @INVARIANT: Concrete providers preserve boolean send contract and swallow transport exceptions into False.\n#\n# @INVARIANT: Providers must be stateless and resilient to network failures.\n# @INVARIANT: Sensitive credentials must be handled via encrypted config.\n\nfrom abc import ABC, abstractmethod\nfrom typing import Any, Dict, Optional\nimport requests\nimport smtplib\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\n\nfrom ...core.logger import logger\n\n\n# [DEF:NotificationProvider:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Abstract base class for all notification providers.\n# @RELATION: [DEPENDED_ON_BY] ->[SMTPProvider]\n# @RELATION: [DEPENDED_ON_BY] ->[TelegramProvider]\n# @RELATION: [DEPENDED_ON_BY] ->[SlackProvider]\nclass NotificationProvider(ABC):\n @abstractmethod\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: Optional[Dict[str, Any]] = None,\n ) -> bool:\n \"\"\"\n Send a notification to a specific target.\n :param target: Recipient identifier (email, channel ID, user ID).\n :param subject: Notification subject or title.\n :param body: Main content of the notification.\n :param context: Additional metadata for the provider.\n :return: True if successfully dispatched.\n \"\"\"\n pass\n\n\n# [/DEF:NotificationProvider:Class]\n\n\n# [DEF:SMTPProvider:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Delivers notifications via SMTP.\n# @RELATION: [INHERITS] ->[NotificationProvider]\nclass SMTPProvider(NotificationProvider):\n def __init__(self, config: Dict[str, Any]):\n self.host = config.get(\"host\")\n self.port = int(config.get(\"port\", 587))\n self.username = config.get(\"username\")\n self.password = config.get(\"password\")\n self.from_email = config.get(\"from_email\")\n self.use_tls = config.get(\"use_tls\", True)\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: Optional[Dict[str, Any]] = None,\n ) -> bool:\n try:\n msg = MIMEMultipart()\n msg[\"From\"] = self.from_email\n msg[\"To\"] = target\n msg[\"Subject\"] = subject\n msg.attach(MIMEText(body, \"plain\"))\n\n server = smtplib.SMTP(self.host, self.port)\n if self.use_tls:\n server.starttls()\n if self.username and self.password:\n server.login(self.username, self.password)\n server.send_message(msg)\n server.quit()\n return True\n except Exception as e:\n logger.error(\n f\"[SMTPProvider][FAILED] Failed to send email to {target}: {e}\"\n )\n return False\n\n\n# [/DEF:SMTPProvider:Class]\n\n\n# [DEF:TelegramProvider:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Delivers notifications via Telegram Bot API.\n# @RELATION: [INHERITS] ->[NotificationProvider]\nclass TelegramProvider(NotificationProvider):\n def __init__(self, config: Dict[str, Any]):\n self.bot_token = config.get(\"bot_token\")\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: Optional[Dict[str, Any]] = None,\n ) -> bool:\n if not self.bot_token:\n logger.error(\"[TelegramProvider][FAILED] Bot token not configured\")\n return False\n\n url = f\"https://api.telegram.org/bot{self.bot_token}/sendMessage\"\n payload = {\n \"chat_id\": target,\n \"text\": f\"*{subject}*\\n\\n{body}\",\n \"parse_mode\": \"Markdown\",\n }\n\n try:\n response = requests.post(url, json=payload, timeout=10)\n response.raise_for_status()\n return True\n except Exception as e:\n logger.error(\n f\"[TelegramProvider][FAILED] Failed to send Telegram message to {target}: {e}\"\n )\n return False\n\n\n# [/DEF:TelegramProvider:Class]\n\n\n# [DEF:SlackProvider:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Delivers notifications via Slack Webhooks or API.\n# @RELATION: [INHERITS] ->[NotificationProvider]\nclass SlackProvider(NotificationProvider):\n def __init__(self, config: Dict[str, Any]):\n self.webhook_url = config.get(\"webhook_url\")\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: Optional[Dict[str, Any]] = None,\n ) -> bool:\n if not self.webhook_url:\n logger.error(\"[SlackProvider][FAILED] Webhook URL not configured\")\n return False\n\n payload = {\"text\": f\"*{subject}*\\n{body}\"}\n\n try:\n response = requests.post(self.webhook_url, json=payload, timeout=10)\n response.raise_for_status()\n return True\n except Exception as e:\n logger.error(f\"[SlackProvider][FAILED] Failed to send Slack message: {e}\")\n return False\n\n\n# [/DEF:SlackProvider:Class]\n\n# [/DEF:providers:Module]\n" + "anchor_syntax": "region", + "body": "# #region providers [C:5] [TYPE Module] [SEMANTICS notification, provider, smtp, telegram, slack]\n#\n# @BRIEF Defines abstract base and concrete implementations for external notification delivery.\n# @RELATION DEPENDED_ON_BY -> [NotificationService]\n# @RELATION DEPENDS_ON -> [NotificationProvider]\n# @RELATION DEPENDS_ON -> [SMTPProvider]\n# @RELATION DEPENDS_ON -> [TelegramProvider]\n# @RELATION DEPENDS_ON -> [SlackProvider]\n# @LAYER: Infra\n# @PRE: Provider configuration dictionaries are supplied by trusted configuration sources.\n# @POST: Each provider exposes async send contract returning boolean delivery outcome.\n# @SIDE_EFFECT: Performs outbound network I/O to SMTP or HTTP endpoints.\n# @DATA_CONTRACT: Input[target, subject, body, context?] -> Output[bool]\n# @INVARIANT: Concrete providers preserve boolean send contract and swallow transport exceptions into False.\n#\n# @INVARIANT: Providers must be stateless and resilient to network failures.\n# @INVARIANT: Sensitive credentials must be handled via encrypted config.\n\nimport smtplib\nfrom abc import ABC, abstractmethod\nfrom email.mime.multipart import MIMEMultipart\nfrom email.mime.text import MIMEText\nfrom typing import Any\n\nimport requests\n\nfrom ...core.logger import logger\n\n\n# #region NotificationProvider [C:2] [TYPE Class]\n# @BRIEF Abstract base class for all notification providers.\n# @RELATION DEPENDED_ON_BY -> [SMTPProvider]\n# @RELATION DEPENDED_ON_BY -> [TelegramProvider]\n# @RELATION DEPENDED_ON_BY -> [SlackProvider]\nclass NotificationProvider(ABC):\n @abstractmethod\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: dict[str, Any] | None = None,\n ) -> bool:\n \"\"\"\n Send a notification to a specific target.\n :param target: Recipient identifier (email, channel ID, user ID).\n :param subject: Notification subject or title.\n :param body: Main content of the notification.\n :param context: Additional metadata for the provider.\n :return: True if successfully dispatched.\n \"\"\"\n pass\n\n\n# #endregion NotificationProvider\n\n\n# #region SMTPProvider [C:3] [TYPE Class]\n# @BRIEF Delivers notifications via SMTP.\n# @RELATION INHERITS -> [NotificationProvider]\nclass SMTPProvider(NotificationProvider):\n def __init__(self, config: dict[str, Any]):\n self.host = config.get(\"host\")\n self.port = int(config.get(\"port\", 587))\n self.username = config.get(\"username\")\n self.password = config.get(\"password\")\n self.from_email = config.get(\"from_email\")\n self.use_tls = config.get(\"use_tls\", True)\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: dict[str, Any] | None = None,\n ) -> bool:\n try:\n msg = MIMEMultipart()\n msg[\"From\"] = self.from_email\n msg[\"To\"] = target\n msg[\"Subject\"] = subject\n msg.attach(MIMEText(body, \"plain\"))\n\n server = smtplib.SMTP(self.host, self.port)\n if self.use_tls:\n server.starttls()\n if self.username and self.password:\n server.login(self.username, self.password)\n server.send_message(msg)\n server.quit()\n return True\n except Exception as e:\n logger.error(\n f\"[SMTPProvider][FAILED] Failed to send email to {target}: {e}\"\n )\n return False\n\n\n# #endregion SMTPProvider\n\n\n# #region TelegramProvider [C:3] [TYPE Class]\n# @BRIEF Delivers notifications via Telegram Bot API.\n# @RELATION INHERITS -> [NotificationProvider]\nclass TelegramProvider(NotificationProvider):\n def __init__(self, config: dict[str, Any]):\n self.bot_token = config.get(\"bot_token\")\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: dict[str, Any] | None = None,\n ) -> bool:\n if not self.bot_token:\n logger.error(\"[TelegramProvider][FAILED] Bot token not configured\")\n return False\n\n url = f\"https://api.telegram.org/bot{self.bot_token}/sendMessage\"\n payload = {\n \"chat_id\": target,\n \"text\": f\"*{subject}*\\n\\n{body}\",\n \"parse_mode\": \"Markdown\",\n }\n\n try:\n response = requests.post(url, json=payload, timeout=10)\n response.raise_for_status()\n return True\n except Exception as e:\n logger.error(\n f\"[TelegramProvider][FAILED] Failed to send Telegram message to {target}: {e}\"\n )\n return False\n\n\n# #endregion TelegramProvider\n\n\n# #region SlackProvider [C:3] [TYPE Class]\n# @BRIEF Delivers notifications via Slack Webhooks or API.\n# @RELATION INHERITS -> [NotificationProvider]\nclass SlackProvider(NotificationProvider):\n def __init__(self, config: dict[str, Any]):\n self.webhook_url = config.get(\"webhook_url\")\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: dict[str, Any] | None = None,\n ) -> bool:\n if not self.webhook_url:\n logger.error(\"[SlackProvider][FAILED] Webhook URL not configured\")\n return False\n\n payload = {\"text\": f\"*{subject}*\\n{body}\"}\n\n try:\n response = requests.post(self.webhook_url, json=payload, timeout=10)\n response.raise_for_status()\n return True\n except Exception as e:\n logger.error(f\"[SlackProvider][FAILED] Failed to send Slack message: {e}\")\n return False\n\n\n# #endregion SlackProvider\n\n# #endregion providers\n" }, { "contract_id": "NotificationProvider", "contract_type": "Class", "file_path": "backend/src/services/notifications/providers.py", - "start_line": 31, - "end_line": 57, + "start_line": 30, + "end_line": 55, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Abstract base class for all notification providers." }, "relations": [ { "source_id": "NotificationProvider", - "relation_type": "[DEPENDED_ON_BY]", + "relation_type": "DEPENDED_ON_BY", "target_id": "SMTPProvider", "target_ref": "[SMTPProvider]" }, { "source_id": "NotificationProvider", - "relation_type": "[DEPENDED_ON_BY]", + "relation_type": "DEPENDED_ON_BY", "target_id": "TelegramProvider", "target_ref": "[TelegramProvider]" }, { "source_id": "NotificationProvider", - "relation_type": "[DEPENDED_ON_BY]", + "relation_type": "DEPENDED_ON_BY", "target_id": "SlackProvider", "target_ref": "[SlackProvider]" } @@ -91946,7 +64159,7 @@ { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate [DEPENDED_ON_BY] is not in allowed_predicates", + "message": "Predicate DEPENDED_ON_BY is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -91958,13 +64171,13 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "[DEPENDED_ON_BY]" + "predicate": "DEPENDED_ON_BY" } }, { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate [DEPENDED_ON_BY] is not in allowed_predicates", + "message": "Predicate DEPENDED_ON_BY is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -91976,13 +64189,13 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "[DEPENDED_ON_BY]" + "predicate": "DEPENDED_ON_BY" } }, { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate [DEPENDED_ON_BY] is not in allowed_predicates", + "message": "Predicate DEPENDED_ON_BY is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -91994,152 +64207,180 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "[DEPENDED_ON_BY]" + "predicate": "DEPENDED_ON_BY" } } ], - "anchor_syntax": "def", - "body": "# [DEF:NotificationProvider:Class]\n# @COMPLEXITY: 2\n# @PURPOSE: Abstract base class for all notification providers.\n# @RELATION: [DEPENDED_ON_BY] ->[SMTPProvider]\n# @RELATION: [DEPENDED_ON_BY] ->[TelegramProvider]\n# @RELATION: [DEPENDED_ON_BY] ->[SlackProvider]\nclass NotificationProvider(ABC):\n @abstractmethod\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: Optional[Dict[str, Any]] = None,\n ) -> bool:\n \"\"\"\n Send a notification to a specific target.\n :param target: Recipient identifier (email, channel ID, user ID).\n :param subject: Notification subject or title.\n :param body: Main content of the notification.\n :param context: Additional metadata for the provider.\n :return: True if successfully dispatched.\n \"\"\"\n pass\n\n\n# [/DEF:NotificationProvider:Class]\n" + "anchor_syntax": "region", + "body": "# #region NotificationProvider [C:2] [TYPE Class]\n# @BRIEF Abstract base class for all notification providers.\n# @RELATION DEPENDED_ON_BY -> [SMTPProvider]\n# @RELATION DEPENDED_ON_BY -> [TelegramProvider]\n# @RELATION DEPENDED_ON_BY -> [SlackProvider]\nclass NotificationProvider(ABC):\n @abstractmethod\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: dict[str, Any] | None = None,\n ) -> bool:\n \"\"\"\n Send a notification to a specific target.\n :param target: Recipient identifier (email, channel ID, user ID).\n :param subject: Notification subject or title.\n :param body: Main content of the notification.\n :param context: Additional metadata for the provider.\n :return: True if successfully dispatched.\n \"\"\"\n pass\n\n\n# #endregion NotificationProvider\n" }, { "contract_id": "SMTPProvider", "contract_type": "Class", "file_path": "backend/src/services/notifications/providers.py", - "start_line": 60, - "end_line": 102, + "start_line": 58, + "end_line": 99, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Delivers notifications via SMTP." }, "relations": [ { "source_id": "SMTPProvider", - "relation_type": "[INHERITS]", + "relation_type": "INHERITS", "target_id": "NotificationProvider", "target_ref": "[NotificationProvider]" } ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [INHERITS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[INHERITS]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:SMTPProvider:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Delivers notifications via SMTP.\n# @RELATION: [INHERITS] ->[NotificationProvider]\nclass SMTPProvider(NotificationProvider):\n def __init__(self, config: Dict[str, Any]):\n self.host = config.get(\"host\")\n self.port = int(config.get(\"port\", 587))\n self.username = config.get(\"username\")\n self.password = config.get(\"password\")\n self.from_email = config.get(\"from_email\")\n self.use_tls = config.get(\"use_tls\", True)\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: Optional[Dict[str, Any]] = None,\n ) -> bool:\n try:\n msg = MIMEMultipart()\n msg[\"From\"] = self.from_email\n msg[\"To\"] = target\n msg[\"Subject\"] = subject\n msg.attach(MIMEText(body, \"plain\"))\n\n server = smtplib.SMTP(self.host, self.port)\n if self.use_tls:\n server.starttls()\n if self.username and self.password:\n server.login(self.username, self.password)\n server.send_message(msg)\n server.quit()\n return True\n except Exception as e:\n logger.error(\n f\"[SMTPProvider][FAILED] Failed to send email to {target}: {e}\"\n )\n return False\n\n\n# [/DEF:SMTPProvider:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region SMTPProvider [C:3] [TYPE Class]\n# @BRIEF Delivers notifications via SMTP.\n# @RELATION INHERITS -> [NotificationProvider]\nclass SMTPProvider(NotificationProvider):\n def __init__(self, config: dict[str, Any]):\n self.host = config.get(\"host\")\n self.port = int(config.get(\"port\", 587))\n self.username = config.get(\"username\")\n self.password = config.get(\"password\")\n self.from_email = config.get(\"from_email\")\n self.use_tls = config.get(\"use_tls\", True)\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: dict[str, Any] | None = None,\n ) -> bool:\n try:\n msg = MIMEMultipart()\n msg[\"From\"] = self.from_email\n msg[\"To\"] = target\n msg[\"Subject\"] = subject\n msg.attach(MIMEText(body, \"plain\"))\n\n server = smtplib.SMTP(self.host, self.port)\n if self.use_tls:\n server.starttls()\n if self.username and self.password:\n server.login(self.username, self.password)\n server.send_message(msg)\n server.quit()\n return True\n except Exception as e:\n logger.error(\n f\"[SMTPProvider][FAILED] Failed to send email to {target}: {e}\"\n )\n return False\n\n\n# #endregion SMTPProvider\n" }, { "contract_id": "TelegramProvider", "contract_type": "Class", "file_path": "backend/src/services/notifications/providers.py", - "start_line": 105, - "end_line": 142, + "start_line": 102, + "end_line": 138, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Delivers notifications via Telegram Bot API." }, "relations": [ { "source_id": "TelegramProvider", - "relation_type": "[INHERITS]", + "relation_type": "INHERITS", "target_id": "NotificationProvider", "target_ref": "[NotificationProvider]" } ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [INHERITS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[INHERITS]" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:TelegramProvider:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Delivers notifications via Telegram Bot API.\n# @RELATION: [INHERITS] ->[NotificationProvider]\nclass TelegramProvider(NotificationProvider):\n def __init__(self, config: Dict[str, Any]):\n self.bot_token = config.get(\"bot_token\")\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: Optional[Dict[str, Any]] = None,\n ) -> bool:\n if not self.bot_token:\n logger.error(\"[TelegramProvider][FAILED] Bot token not configured\")\n return False\n\n url = f\"https://api.telegram.org/bot{self.bot_token}/sendMessage\"\n payload = {\n \"chat_id\": target,\n \"text\": f\"*{subject}*\\n\\n{body}\",\n \"parse_mode\": \"Markdown\",\n }\n\n try:\n response = requests.post(url, json=payload, timeout=10)\n response.raise_for_status()\n return True\n except Exception as e:\n logger.error(\n f\"[TelegramProvider][FAILED] Failed to send Telegram message to {target}: {e}\"\n )\n return False\n\n\n# [/DEF:TelegramProvider:Class]\n" + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region TelegramProvider [C:3] [TYPE Class]\n# @BRIEF Delivers notifications via Telegram Bot API.\n# @RELATION INHERITS -> [NotificationProvider]\nclass TelegramProvider(NotificationProvider):\n def __init__(self, config: dict[str, Any]):\n self.bot_token = config.get(\"bot_token\")\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: dict[str, Any] | None = None,\n ) -> bool:\n if not self.bot_token:\n logger.error(\"[TelegramProvider][FAILED] Bot token not configured\")\n return False\n\n url = f\"https://api.telegram.org/bot{self.bot_token}/sendMessage\"\n payload = {\n \"chat_id\": target,\n \"text\": f\"*{subject}*\\n\\n{body}\",\n \"parse_mode\": \"Markdown\",\n }\n\n try:\n response = requests.post(url, json=payload, timeout=10)\n response.raise_for_status()\n return True\n except Exception as e:\n logger.error(\n f\"[TelegramProvider][FAILED] Failed to send Telegram message to {target}: {e}\"\n )\n return False\n\n\n# #endregion TelegramProvider\n" }, { "contract_id": "SlackProvider", "contract_type": "Class", "file_path": "backend/src/services/notifications/providers.py", - "start_line": 145, - "end_line": 175, + "start_line": 141, + "end_line": 170, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Delivers notifications via Slack Webhooks or API." }, "relations": [ { "source_id": "SlackProvider", - "relation_type": "[INHERITS]", + "relation_type": "INHERITS", "target_id": "NotificationProvider", "target_ref": "[NotificationProvider]" } ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region SlackProvider [C:3] [TYPE Class]\n# @BRIEF Delivers notifications via Slack Webhooks or API.\n# @RELATION INHERITS -> [NotificationProvider]\nclass SlackProvider(NotificationProvider):\n def __init__(self, config: dict[str, Any]):\n self.webhook_url = config.get(\"webhook_url\")\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: dict[str, Any] | None = None,\n ) -> bool:\n if not self.webhook_url:\n logger.error(\"[SlackProvider][FAILED] Webhook URL not configured\")\n return False\n\n payload = {\"text\": f\"*{subject}*\\n{body}\"}\n\n try:\n response = requests.post(self.webhook_url, json=payload, timeout=10)\n response.raise_for_status()\n return True\n except Exception as e:\n logger.error(f\"[SlackProvider][FAILED] Failed to send Slack message: {e}\")\n return False\n\n\n# #endregion SlackProvider\n" + }, + { + "contract_id": "service", + "contract_type": "Module", + "file_path": "backend/src/services/notifications/service.py", + "start_line": 1, + "end_line": 232, + "tier": "TIER_3", + "complexity": 5, + "metadata": { + "COMPLEXITY": 5, + "DATA_CONTRACT": "NotificationChannelConfig -> NotificationRecipient", + "INVARIANT": "NotificationService maintains singleton pattern for per-channel notifications", + "LAYER": "Domain", + "POST": "Notification dispatched via configured providers", + "PRE": "channel_config is loaded", + "PURPOSE": "Orchestrates notification routing based on user preferences and policy context.", + "SIDE_EFFECT": "Sends notifications via configured providers" + }, + "relations": [ + { + "source_id": "service", + "relation_type": "DEPENDS_ON", + "target_id": "NotificationProvider", + "target_ref": "[NotificationProvider]" + }, + { + "source_id": "service", + "relation_type": "DEPENDS_ON", + "target_id": "SMTPProvider", + "target_ref": "[SMTPProvider]" + }, + { + "source_id": "service", + "relation_type": "DEPENDS_ON", + "target_id": "TelegramProvider", + "target_ref": "[TelegramProvider]" + }, + { + "source_id": "service", + "relation_type": "DEPENDS_ON", + "target_id": "SlackProvider", + "target_ref": "[SlackProvider]" + }, + { + "source_id": "service", + "relation_type": "DEPENDS_ON", + "target_id": "ValidationRecord", + "target_ref": "[ValidationRecord]" + }, + { + "source_id": "service", + "relation_type": "DEPENDS_ON", + "target_id": "ValidationPolicy", + "target_ref": "[ValidationPolicy]" + }, + { + "source_id": "service", + "relation_type": "DEPENDS_ON", + "target_id": "UserDashboardPreference", + "target_ref": "[UserDashboardPreference]" + } + ], "schema_warnings": [ { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [INHERITS] is not in allowed_predicates", + "code": "missing_required_tag", + "tag": "RATIONALE", + "message": "@RATIONALE is required for contract type 'Module' at C5", "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[INHERITS]" + "actual_complexity": 5, + "contract_type": "Module" + } + }, + { + "code": "missing_required_tag", + "tag": "REJECTED", + "message": "@REJECTED is required for contract type 'Module' at C5", + "detail": { + "actual_complexity": 5, + "contract_type": "Module" } } ], - "anchor_syntax": "def", - "body": "# [DEF:SlackProvider:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Delivers notifications via Slack Webhooks or API.\n# @RELATION: [INHERITS] ->[NotificationProvider]\nclass SlackProvider(NotificationProvider):\n def __init__(self, config: Dict[str, Any]):\n self.webhook_url = config.get(\"webhook_url\")\n\n async def send(\n self,\n target: str,\n subject: str,\n body: str,\n context: Optional[Dict[str, Any]] = None,\n ) -> bool:\n if not self.webhook_url:\n logger.error(\"[SlackProvider][FAILED] Webhook URL not configured\")\n return False\n\n payload = {\"text\": f\"*{subject}*\\n{body}\"}\n\n try:\n response = requests.post(self.webhook_url, json=payload, timeout=10)\n response.raise_for_status()\n return True\n except Exception as e:\n logger.error(f\"[SlackProvider][FAILED] Failed to send Slack message: {e}\")\n return False\n\n\n# [/DEF:SlackProvider:Class]\n" + "anchor_syntax": "region", + "body": "# #region service [C:5] [TYPE Module] [SEMANTICS fastapi, notification, dispatch, policy, routing]\n#\n# @BRIEF Orchestrates notification routing based on user preferences and policy context.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [NotificationProvider]\n# @RELATION DEPENDS_ON -> [SMTPProvider]\n# @RELATION DEPENDS_ON -> [TelegramProvider]\n# @RELATION DEPENDS_ON -> [SlackProvider]\n# @RELATION DEPENDS_ON -> [ValidationRecord]\n# @RELATION DEPENDS_ON -> [ValidationPolicy]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n#\n# @INVARIANT: NotificationService maintains singleton pattern for per-channel notifications\n# @DATA_CONTRACT: NotificationChannelConfig -> NotificationRecipient\n# @PRE: channel_config is loaded\n# @POST: Notification dispatched via configured providers\n# @SIDE_EFFECT: Sends notifications via configured providers\n\nfrom fastapi import BackgroundTasks\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.llm import ValidationPolicy, ValidationRecord\nfrom ...models.profile import UserDashboardPreference\nfrom .providers import (\n NotificationProvider,\n SlackProvider,\n SMTPProvider,\n TelegramProvider,\n)\n\n\n# #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\n# #endregion service\n" }, { "contract_id": "NotificationService", "contract_type": "Class", "file_path": "backend/src/services/notifications/service.py", - "start_line": 37, - "end_line": 239, + "start_line": 34, + "end_line": 230, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, "DATA_CONTRACT": "Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]", "POST": "Service can resolve targets and dispatch provider sends without mutating validation records.", "PRE": "Service receives a live DB session and configuration manager with notification payload settings.", @@ -92149,25 +64390,25 @@ "relations": [ { "source_id": "NotificationService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "NotificationProvider", "target_ref": "[NotificationProvider]" }, { "source_id": "NotificationService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ValidationRecord", "target_ref": "[ValidationRecord]" }, { "source_id": "NotificationService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ValidationPolicy", "target_ref": "[ValidationPolicy]" }, { "source_id": "NotificationService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "UserDashboardPreference", "target_ref": "[UserDashboardPreference]" } @@ -92181,671 +64422,27 @@ "actual_complexity": 4, "contract_type": "Class" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:NotificationService:Class]\n# @PURPOSE: Routes validation reports to appropriate users and channels.\n# @COMPLEXITY: 4\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 # [DEF:NotificationService_init:Function]\n # @COMPLEXITY: 3\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 # [/DEF:NotificationService_init:Function]\n\n # [DEF:_initialize_providers:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_initialize_providers:Function]\n\n # [DEF:dispatch_report:Function]\n # @COMPLEXITY: 4\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: Optional[ValidationPolicy] = None,\n background_tasks: Optional[BackgroundTasks] = 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\"[REASON] Notification skipped for record {record.id} (status={record.status})\"\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.warning(\n f\"[NotificationService][EXPLORE] Unsupported or unconfigured channel: {channel_type}\"\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 # [/DEF:dispatch_report:Function]\n\n # [DEF:_should_notify:Function]\n # @COMPLEXITY: 3\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: Optional[ValidationPolicy]\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 # [/DEF:_should_notify:Function]\n\n # [DEF:_resolve_targets:Function]\n # @COMPLEXITY: 3\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: Optional[ValidationPolicy]\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 # [/DEF:_resolve_targets:Function]\n\n # [DEF:_find_dashboard_owners:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_find_dashboard_owners:Function]\n\n # [DEF:_build_body:Function]\n # @COMPLEXITY: 2\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 # [/DEF:_build_body:Function]\n\n\n# [/DEF:NotificationService:Class]\n" - }, - { - "contract_id": "NotificationService_init", - "contract_type": "Function", - "file_path": "backend/src/services/notifications/service.py", - "start_line": 49, - "end_line": 60, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Bind DB and configuration collaborators used for provider initialization and routing." - }, - "relations": [ - { - "source_id": "NotificationService_init", - "relation_type": "[BINDS_TO]", - "target_id": "NotificationService", - "target_ref": "[NotificationService]" - }, - { - "source_id": "NotificationService_init", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationPolicy", - "target_ref": "[ValidationPolicy]" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [BINDS_TO] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[BINDS_TO]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:NotificationService_init:Function]\n # @COMPLEXITY: 3\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 # [/DEF:NotificationService_init:Function]\n" - }, - { - "contract_id": "_initialize_providers", - "contract_type": "Function", - "file_path": "backend/src/services/notifications/service.py", - "start_line": 62, - "end_line": 86, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Materialize configured notification channel adapters once per service lifetime." - }, - "relations": [ - { - "source_id": "_initialize_providers", - "relation_type": "[DEPENDS_ON]", - "target_id": "SMTPProvider", - "target_ref": "[SMTPProvider]" - }, - { - "source_id": "_initialize_providers", - "relation_type": "[DEPENDS_ON]", - "target_id": "TelegramProvider", - "target_ref": "[TelegramProvider]" - }, - { - "source_id": "_initialize_providers", - "relation_type": "[DEPENDS_ON]", - "target_id": "SlackProvider", - "target_ref": "[SlackProvider]" - } - ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_initialize_providers:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_initialize_providers:Function]\n" - }, - { - "contract_id": "dispatch_report", - "contract_type": "Function", - "file_path": "backend/src/services/notifications/service.py", - "start_line": 88, - "end_line": 144, - "tier": "TIER_2", - "complexity": 4, - "metadata": { - "COMPLEXITY": "4", - "DATA_CONTRACT": "Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]", - "POST": "Eligible notification sends are scheduled in background or awaited inline.", - "PRE": "record is persisted and providers can be initialized from configuration payload.", - "PURPOSE": "Route one validation record to resolved owners and configured custom channels.", - "SIDE_EFFECT": "Schedules or performs outbound provider sends and emits notification logs." - }, - "relations": [ - { - "source_id": "dispatch_report", - "relation_type": "[CALLS]", - "target_id": "_initialize_providers", - "target_ref": "[_initialize_providers]" - }, - { - "source_id": "dispatch_report", - "relation_type": "[CALLS]", - "target_id": "_should_notify", - "target_ref": "[_should_notify]" - }, - { - "source_id": "dispatch_report", - "relation_type": "[CALLS]", - "target_id": "_resolve_targets", - "target_ref": "[_resolve_targets]" - }, - { - "source_id": "dispatch_report", - "relation_type": "[CALLS]", - "target_id": "_build_body", - "target_ref": "[_build_body]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Function' at C4", - "detail": { - "actual_complexity": 4, - "contract_type": "Function" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:dispatch_report:Function]\n # @COMPLEXITY: 4\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: Optional[ValidationPolicy] = None,\n background_tasks: Optional[BackgroundTasks] = 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\"[REASON] Notification skipped for record {record.id} (status={record.status})\"\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.warning(\n f\"[NotificationService][EXPLORE] Unsupported or unconfigured channel: {channel_type}\"\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 # [/DEF:dispatch_report:Function]\n" - }, - { - "contract_id": "_should_notify", - "contract_type": "Function", - "file_path": "backend/src/services/notifications/service.py", - "start_line": 146, - "end_line": 162, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Evaluate record status against effective alert policy." - }, - "relations": [ - { - "source_id": "_should_notify", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationRecord", - "target_ref": "[ValidationRecord]" - }, - { - "source_id": "_should_notify", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationPolicy", - "target_ref": "[ValidationPolicy]" - } - ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_should_notify:Function]\n # @COMPLEXITY: 3\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: Optional[ValidationPolicy]\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 # [/DEF:_should_notify:Function]\n" - }, - { - "contract_id": "_resolve_targets", - "contract_type": "Function", - "file_path": "backend/src/services/notifications/service.py", - "start_line": 164, - "end_line": 199, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Resolve owner and policy-defined delivery targets for one validation record." - }, - "relations": [ - { - "source_id": "_resolve_targets", - "relation_type": "[CALLS]", - "target_id": "_find_dashboard_owners", - "target_ref": "[_find_dashboard_owners]" - }, - { - "source_id": "_resolve_targets", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationRecord", - "target_ref": "[ValidationRecord]" - }, - { - "source_id": "_resolve_targets", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationPolicy", - "target_ref": "[ValidationPolicy]" - } - ], - "schema_warnings": [ - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_resolve_targets:Function]\n # @COMPLEXITY: 3\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: Optional[ValidationPolicy]\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 # [/DEF:_resolve_targets:Function]\n" - }, - { - "contract_id": "_find_dashboard_owners", - "contract_type": "Function", - "file_path": "backend/src/services/notifications/service.py", - "start_line": 201, - "end_line": 221, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PURPOSE": "Load candidate dashboard owners from persisted profile preferences." - }, - "relations": [ - { - "source_id": "_find_dashboard_owners", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationRecord", - "target_ref": "[ValidationRecord]" - }, - { - "source_id": "_find_dashboard_owners", - "relation_type": "[DEPENDS_ON]", - "target_id": "UserDashboardPreference", - "target_ref": "[UserDashboardPreference]" - } - ], - "schema_warnings": [ - { - "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]" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_find_dashboard_owners:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_find_dashboard_owners:Function]\n" - }, - { - "contract_id": "_build_body", - "contract_type": "Function", - "file_path": "backend/src/services/notifications/service.py", - "start_line": 223, - "end_line": 236, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "PURPOSE": "Format one validation record into provider-ready body text." - }, - "relations": [ - { - "source_id": "_build_body", - "relation_type": "[DEPENDS_ON]", - "target_id": "ValidationRecord", - "target_ref": "[ValidationRecord]" - } - ], - "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" - } - }, - { - "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]" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_build_body:Function]\n # @COMPLEXITY: 2\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 # [/DEF:_build_body:Function]\n" + "anchor_syntax": "region", + "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_type": "Module", "file_path": "backend/src/services/profile_service.py", "start_line": 1, - "end_line": 865, + "end_line": 857, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Profile_id -> ProfileInfo; session_id -> valid UUID", "INVARIANT": "Profile ID needs to unique per-user session", "LAYER": "Domain", "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.", - "SEMANTICS": [ - "profile", - "service", - "validation", - "ownership", - "filtering", - "superset", - "preferences" - ], "SIDE_EFFECT": "Database read/write operations", "TEST_CONTRACT": "ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse", "TEST_EDGE": "lookup_env_not_found -> unknown environment_id returns not found", @@ -92972,19 +64569,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:profile_service:Module]\n#\n# @COMPLEXITY: 5\n# @SEMANTICS: profile, service, validation, ownership, filtering, superset, preferences\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom typing import Any, Iterable, List, Optional, Sequence, Set, Tuple\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.repository import AuthRepository\nfrom ..core.logger import logger, belief_scope\nfrom ..core.superset_client import SupersetClient\nfrom ..core.superset_profile_lookup import SupersetAccountLookupAdapter\nfrom ..models.auth import User\nfrom ..models.profile import UserDashboardPreference\nfrom .llm_provider import EncryptionManager\nfrom .rbac_permission_catalog import discover_declared_permissions\nfrom ..schemas.profile import (\n ProfilePermissionState,\n ProfilePreference,\n ProfilePreferenceResponse,\n ProfilePreferenceUpdateRequest,\n ProfileSecuritySummary,\n SupersetAccountLookupRequest,\n SupersetAccountLookupResponse,\n SupersetAccountCandidate,\n)\n# [/SECTION]\n\nSUPPORTED_START_PAGES = {\"dashboards\", \"datasets\", \"reports\"}\nSUPPORTED_DENSITIES = {\"compact\", \"comfortable\"}\n\n\n# [DEF:ProfileValidationError:Class]\n# @RELATION: INHERITS -> Exception\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:ProfileValidationError:Class]\n\n\n# [DEF:EnvironmentNotFoundError:Class]\n# @RELATION: INHERITS -> Exception\n# @COMPLEXITY: 2\n# @PURPOSE: Raised when environment_id from lookup request is unknown in app configuration.\nclass EnvironmentNotFoundError(Exception):\n pass\n\n\n# [/DEF:EnvironmentNotFoundError:Class]\n\n\n# [DEF:ProfileAuthorizationError:Class]\n# @RELATION: INHERITS -> Exception\n# @COMPLEXITY: 2\n# @PURPOSE: Raised when caller attempts cross-user preference mutation.\nclass ProfileAuthorizationError(Exception):\n pass\n\n\n# [/DEF:ProfileAuthorizationError:Class]\n\n\n# [DEF:ProfileService: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# @COMPLEXITY: 5\n# @PURPOSE: 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 # [DEF:init: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 # [/DEF:init:Function]\n\n # [DEF:get_my_preference: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(\"[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 # [/DEF:get_my_preference:Function]\n\n # [DEF:get_dashboard_filter_binding: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 # [/DEF:get_dashboard_filter_binding:Function]\n\n # [DEF:update_my_preference: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: Optional[str] = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfileService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\n \"[REASON] Evaluating self-scope guard before preference mutation\"\n )\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"[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(\"[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(\"[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 # [/DEF:update_my_preference:Function]\n\n # [DEF:lookup_superset_accounts: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(\"[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 \"[REFLECT] 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(\"[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\"[EXPLORE] 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 # [/DEF:lookup_superset_accounts:Function]\n\n # [DEF:matches_dashboard_actor: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: Optional[str],\n owners: Optional[Iterable[Any]],\n modified_by: Optional[str],\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 # [/DEF:matches_dashboard_actor:Function]\n\n # [DEF:_build_security_summary: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.warning(\n \"[ProfileService][EXPLORE] Failed to build declared permission catalog: %s\",\n discovery_error,\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 # [/DEF:_build_security_summary:Function]\n\n # [DEF:_collect_user_permission_pairs: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 # [/DEF:_collect_user_permission_pairs:Function]\n\n # [DEF:_format_permission_key: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 # [/DEF:_format_permission_key:Function]\n\n # [DEF:_to_preference_payload: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 # [/DEF:_to_preference_payload:Function]\n\n # [DEF:_mask_secret_value: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: Optional[str]) -> Optional[str]:\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 # [/DEF:_mask_secret_value:Function]\n\n # [DEF:_sanitize_text: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: Optional[str]) -> Optional[str]:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:_sanitize_text:Function]\n\n # [DEF:_sanitize_secret: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: Optional[str]) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:_sanitize_secret:Function]\n\n # [DEF:_normalize_start_page: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: Optional[str]) -> 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 # [/DEF:_normalize_start_page:Function]\n\n # [DEF:_normalize_density: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: Optional[str]) -> 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 # [/DEF:_normalize_density:Function]\n\n # [DEF:_resolve_environment: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 # [/DEF:_resolve_environment:Function]\n\n # [DEF:_get_preference_row: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) -> Optional[UserDashboardPreference]:\n return self.auth_repository.get_user_dashboard_preference(str(user_id))\n\n # [/DEF:_get_preference_row:Function]\n\n # [DEF:_get_or_create_preference_row: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 # [/DEF:_get_or_create_preference_row:Function]\n\n # [DEF:_build_default_preference: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 # [/DEF:_build_default_preference:Function]\n\n # [DEF:_validate_update_payload: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: Optional[str],\n show_only_my_dashboards: bool,\n git_email: Optional[str],\n start_page: str,\n dashboards_table_density: str,\n email_address: Optional[str] = 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 # [/DEF:_validate_update_payload:Function]\n\n # [DEF:_sanitize_username: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: Optional[str]) -> Optional[str]:\n return self._sanitize_text(value)\n\n # [/DEF:_sanitize_username:Function]\n\n # [DEF:_normalize_username: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: Optional[str]) -> Optional[str]:\n sanitized = self._sanitize_username(value)\n if sanitized is None:\n return None\n return sanitized.lower()\n\n # [/DEF:_normalize_username:Function]\n\n # [DEF:_normalize_owner_tokens: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: Optional[Iterable[Any]]) -> 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 # [/DEF:_normalize_owner_tokens:Function]\n\n\n# [/DEF:ProfileService:Class]\n\n# [/DEF:profile_service:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ProfileValidationError", "contract_type": "Class", "file_path": "backend/src/services/profile_service.py", - "start_line": 56, - "end_line": 66, + "start_line": 54, + "end_line": 63, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Domain validation error for profile preference update requests." }, "relations": [ @@ -93006,19 +64603,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ProfileValidationError:Class]\n# @RELATION: INHERITS -> Exception\n# @COMPLEXITY: 2\n# @PURPOSE: 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# [/DEF:ProfileValidationError:Class]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "EnvironmentNotFoundError", "contract_type": "Class", "file_path": "backend/src/services/profile_service.py", - "start_line": 69, - "end_line": 77, + "start_line": 66, + "end_line": 73, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Raised when environment_id from lookup request is unknown in app configuration." }, "relations": [ @@ -93040,19 +64637,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:EnvironmentNotFoundError:Class]\n# @RELATION: INHERITS -> Exception\n# @COMPLEXITY: 2\n# @PURPOSE: Raised when environment_id from lookup request is unknown in app configuration.\nclass EnvironmentNotFoundError(Exception):\n pass\n\n\n# [/DEF:EnvironmentNotFoundError:Class]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ProfileAuthorizationError", "contract_type": "Class", "file_path": "backend/src/services/profile_service.py", - "start_line": 80, - "end_line": 88, + "start_line": 76, + "end_line": 83, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Raised when caller attempts cross-user preference mutation." }, "relations": [ @@ -93074,19 +64671,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ProfileAuthorizationError:Class]\n# @RELATION: INHERITS -> Exception\n# @COMPLEXITY: 2\n# @PURPOSE: Raised when caller attempts cross-user preference mutation.\nclass ProfileAuthorizationError(Exception):\n pass\n\n\n# [/DEF:ProfileAuthorizationError:Class]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ProfileService", "contract_type": "Class", "file_path": "backend/src/services/profile_service.py", - "start_line": 91, - "end_line": 863, + "start_line": 86, + "end_line": 855, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool]", "INVARIANT": "Profile data integrity maintained, cache consistency with database state", "POST": "Preference operations remain user-scoped and return normalized profile/lookup responses.", @@ -93097,37 +64694,37 @@ "relations": [ { "source_id": "ProfileService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "sqlalchemy.orm.Session", "target_ref": "[sqlalchemy.orm.Session]" }, { "source_id": "ProfileService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "AuthRepository", "target_ref": "[AuthRepository]" }, { "source_id": "ProfileService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetClient", "target_ref": "[SupersetClient]" }, { "source_id": "ProfileService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "SupersetAccountLookupAdapter", "target_ref": "[SupersetAccountLookupAdapter]" }, { "source_id": "ProfileService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "UserDashboardPreference", "target_ref": "[UserDashboardPreference]" }, { "source_id": "ProfileService", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "discover_declared_permissions", "target_ref": "[discover_declared_permissions]" } @@ -93150,1506 +64747,34 @@ "actual_complexity": 5, "contract_type": "Class" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ProfileService: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# @COMPLEXITY: 5\n# @PURPOSE: 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 # [DEF:init: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 # [/DEF:init:Function]\n\n # [DEF:get_my_preference: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(\"[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 # [/DEF:get_my_preference:Function]\n\n # [DEF:get_dashboard_filter_binding: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 # [/DEF:get_dashboard_filter_binding:Function]\n\n # [DEF:update_my_preference: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: Optional[str] = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfileService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\n \"[REASON] Evaluating self-scope guard before preference mutation\"\n )\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"[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(\"[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(\"[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 # [/DEF:update_my_preference:Function]\n\n # [DEF:lookup_superset_accounts: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(\"[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 \"[REFLECT] 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(\"[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\"[EXPLORE] 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 # [/DEF:lookup_superset_accounts:Function]\n\n # [DEF:matches_dashboard_actor: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: Optional[str],\n owners: Optional[Iterable[Any]],\n modified_by: Optional[str],\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 # [/DEF:matches_dashboard_actor:Function]\n\n # [DEF:_build_security_summary: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.warning(\n \"[ProfileService][EXPLORE] Failed to build declared permission catalog: %s\",\n discovery_error,\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 # [/DEF:_build_security_summary:Function]\n\n # [DEF:_collect_user_permission_pairs: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 # [/DEF:_collect_user_permission_pairs:Function]\n\n # [DEF:_format_permission_key: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 # [/DEF:_format_permission_key:Function]\n\n # [DEF:_to_preference_payload: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 # [/DEF:_to_preference_payload:Function]\n\n # [DEF:_mask_secret_value: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: Optional[str]) -> Optional[str]:\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 # [/DEF:_mask_secret_value:Function]\n\n # [DEF:_sanitize_text: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: Optional[str]) -> Optional[str]:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:_sanitize_text:Function]\n\n # [DEF:_sanitize_secret: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: Optional[str]) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:_sanitize_secret:Function]\n\n # [DEF:_normalize_start_page: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: Optional[str]) -> 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 # [/DEF:_normalize_start_page:Function]\n\n # [DEF:_normalize_density: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: Optional[str]) -> 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 # [/DEF:_normalize_density:Function]\n\n # [DEF:_resolve_environment: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 # [/DEF:_resolve_environment:Function]\n\n # [DEF:_get_preference_row: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) -> Optional[UserDashboardPreference]:\n return self.auth_repository.get_user_dashboard_preference(str(user_id))\n\n # [/DEF:_get_preference_row:Function]\n\n # [DEF:_get_or_create_preference_row: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 # [/DEF:_get_or_create_preference_row:Function]\n\n # [DEF:_build_default_preference: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 # [/DEF:_build_default_preference:Function]\n\n # [DEF:_validate_update_payload: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: Optional[str],\n show_only_my_dashboards: bool,\n git_email: Optional[str],\n start_page: str,\n dashboards_table_density: str,\n email_address: Optional[str] = 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 # [/DEF:_validate_update_payload:Function]\n\n # [DEF:_sanitize_username: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: Optional[str]) -> Optional[str]:\n return self._sanitize_text(value)\n\n # [/DEF:_sanitize_username:Function]\n\n # [DEF:_normalize_username: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: Optional[str]) -> Optional[str]:\n sanitized = self._sanitize_username(value)\n if sanitized is None:\n return None\n return sanitized.lower()\n\n # [/DEF:_normalize_username:Function]\n\n # [DEF:_normalize_owner_tokens: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: Optional[Iterable[Any]]) -> 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 # [/DEF:_normalize_owner_tokens:Function]\n\n\n# [/DEF:ProfileService:Class]\n" - }, - { - "contract_id": "init", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 106, - "end_line": 118, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Service is ready for preference persistence and lookup operations.", - "PRE": "db session is active and config_manager supports get_environments().", - "PURPOSE": "Initialize service with DB session and config manager." - }, - "relations": [ - { - "source_id": "init", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:init: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 # [/DEF:init:Function]\n" - }, - { - "contract_id": "get_my_preference", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 120, - "end_line": 149, - "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": [ - { - "source_id": "get_my_preference", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_my_preference: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(\"[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 # [/DEF:get_my_preference:Function]\n" - }, - { - "contract_id": "get_dashboard_filter_binding", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 151, - "end_line": 184, - "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": [ - { - "source_id": "get_dashboard_filter_binding", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_dashboard_filter_binding: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 # [/DEF:get_dashboard_filter_binding:Function]\n" - }, - { - "contract_id": "update_my_preference", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 186, - "end_line": 333, - "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": [ - { - "source_id": "update_my_preference", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:update_my_preference: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: Optional[str] = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfileService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\n \"[REASON] Evaluating self-scope guard before preference mutation\"\n )\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"[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(\"[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(\"[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 # [/DEF:update_my_preference:Function]\n" - }, - { - "contract_id": "matches_dashboard_actor", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 417, - "end_line": 441, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns True when normalized username matches owners or modified_by.", - "PRE": "bound_username can be empty; owners may contain mixed payload.", - "PURPOSE": "Apply trim+case-insensitive actor match across owners OR modified_by." - }, - "relations": [ - { - "source_id": "matches_dashboard_actor", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:matches_dashboard_actor: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: Optional[str],\n owners: Optional[Iterable[Any]],\n modified_by: Optional[str],\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 # [/DEF:matches_dashboard_actor:Function]\n" - }, - { - "contract_id": "_build_security_summary", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 443, - "end_line": 505, - "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": [ - { - "source_id": "_build_security_summary", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_build_security_summary: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.warning(\n \"[ProfileService][EXPLORE] Failed to build declared permission catalog: %s\",\n discovery_error,\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 # [/DEF:_build_security_summary:Function]\n" - }, - { - "contract_id": "_collect_user_permission_pairs", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 507, - "end_line": 526, - "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": [ - { - "source_id": "_collect_user_permission_pairs", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_collect_user_permission_pairs: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 # [/DEF:_collect_user_permission_pairs:Function]\n" - }, - { - "contract_id": "_format_permission_key", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 528, - "end_line": 540, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns user-facing badge key.", - "PRE": "resource and action are normalized.", - "PURPOSE": "Convert normalized permission pair to compact UI key." - }, - "relations": [ - { - "source_id": "_format_permission_key", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_format_permission_key: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 # [/DEF:_format_permission_key:Function]\n" - }, - { - "contract_id": "_to_preference_payload", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 542, - "end_line": 600, - "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": [ - { - "source_id": "_to_preference_payload", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_to_preference_payload: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 # [/DEF:_to_preference_payload:Function]\n" - }, - { - "contract_id": "_mask_secret_value", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 602, - "end_line": 615, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns masked representation or None.", - "PRE": "secret may be None or plaintext.", - "PURPOSE": "Build a safe display value for sensitive secrets." - }, - "relations": [ - { - "source_id": "_mask_secret_value", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_mask_secret_value: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: Optional[str]) -> Optional[str]:\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 # [/DEF:_mask_secret_value:Function]\n" - }, - { - "contract_id": "_sanitize_text", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 617, - "end_line": 628, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns trimmed value or None.", - "PRE": "value may be empty or None.", - "PURPOSE": "Normalize optional text into trimmed form or None." - }, - "relations": [ - { - "source_id": "_sanitize_text", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_sanitize_text: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: Optional[str]) -> Optional[str]:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:_sanitize_text:Function]\n" - }, - { - "contract_id": "_sanitize_secret", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 630, - "end_line": 643, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns trimmed secret or None.", - "PRE": "value may be None or blank.", - "PURPOSE": "Normalize secret input into trimmed form or None." - }, - "relations": [ - { - "source_id": "_sanitize_secret", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_sanitize_secret: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: Optional[str]) -> Optional[str]:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n\n # [/DEF:_sanitize_secret:Function]\n" - }, - { - "contract_id": "_normalize_start_page", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 645, - "end_line": 658, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns one of SUPPORTED_START_PAGES.", - "PRE": "value may be None or alias.", - "PURPOSE": "Normalize supported start page aliases to canonical values." - }, - "relations": [ - { - "source_id": "_normalize_start_page", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_start_page: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: Optional[str]) -> 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 # [/DEF:_normalize_start_page:Function]\n" - }, - { - "contract_id": "_normalize_density", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 660, - "end_line": 673, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns one of SUPPORTED_DENSITIES.", - "PRE": "value may be None or alias.", - "PURPOSE": "Normalize supported density aliases to canonical values." - }, - "relations": [ - { - "source_id": "_normalize_density", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_density: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: Optional[str]) -> 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 # [/DEF:_normalize_density:Function]\n" - }, - { - "contract_id": "_resolve_environment", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 675, - "end_line": 687, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns environment object when found else None.", - "PRE": "environment_id is provided.", - "PURPOSE": "Resolve environment model from configured environments by id." - }, - "relations": [ - { - "source_id": "_resolve_environment", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_resolve_environment: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 # [/DEF:_resolve_environment:Function]\n" - }, - { - "contract_id": "_get_preference_row", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 689, - "end_line": 697, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns matching row or None.", - "PRE": "user_id is provided.", - "PURPOSE": "Return persisted preference row for user or None." - }, - "relations": [ - { - "source_id": "_get_preference_row", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_preference_row: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) -> Optional[UserDashboardPreference]:\n return self.auth_repository.get_user_dashboard_preference(str(user_id))\n\n # [/DEF:_get_preference_row:Function]\n" - }, - { - "contract_id": "_get_or_create_preference_row", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 699, - "end_line": 710, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returned row always contains user_id.", - "PRE": "user_id is provided.", - "PURPOSE": "Return existing preference row or create new unsaved row." - }, - "relations": [ - { - "source_id": "_get_or_create_preference_row", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_or_create_preference_row: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 # [/DEF:_get_or_create_preference_row:Function]\n" - }, - { - "contract_id": "_build_default_preference", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 712, - "end_line": 739, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns ProfilePreference with disabled toggle and empty username.", - "PRE": "user_id is provided.", - "PURPOSE": "Build non-persisted default preference DTO for unconfigured users." - }, - "relations": [ - { - "source_id": "_build_default_preference", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_build_default_preference: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 # [/DEF:_build_default_preference:Function]\n" - }, - { - "contract_id": "_validate_update_payload", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 741, - "end_line": 795, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns validation errors list; empty list means valid.", - "PRE": "payload is provided.", - "PURPOSE": "Validate username/toggle constraints for preference mutation." - }, - "relations": [ - { - "source_id": "_validate_update_payload", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_validate_update_payload: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: Optional[str],\n show_only_my_dashboards: bool,\n git_email: Optional[str],\n start_page: str,\n dashboards_table_density: str,\n email_address: Optional[str] = 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 # [/DEF:_validate_update_payload:Function]\n" - }, - { - "contract_id": "_sanitize_username", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 797, - "end_line": 805, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns trimmed username or None.", - "PRE": "value can be empty or None.", - "PURPOSE": "Normalize raw username into trimmed form or None for empty input." - }, - "relations": [ - { - "source_id": "_sanitize_username", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_sanitize_username: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: Optional[str]) -> Optional[str]:\n return self._sanitize_text(value)\n\n # [/DEF:_sanitize_username:Function]\n" - }, - { - "contract_id": "_normalize_username", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 807, - "end_line": 818, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns lowercase normalized token or None.", - "PRE": "value can be empty or None.", - "PURPOSE": "Apply deterministic trim+lower normalization for actor matching." - }, - "relations": [ - { - "source_id": "_normalize_username", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_username: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: Optional[str]) -> Optional[str]:\n sanitized = self._sanitize_username(value)\n if sanitized is None:\n return None\n return sanitized.lower()\n\n # [/DEF:_normalize_username:Function]\n" - }, - { - "contract_id": "_normalize_owner_tokens", - "contract_type": "Function", - "file_path": "backend/src/services/profile_service.py", - "start_line": 820, - "end_line": 860, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Returns list of unique normalized owner tokens.", - "PRE": "owners can be iterable of scalars or dict-like values.", - "PURPOSE": "Normalize owners payload into deduplicated lower-cased tokens." - }, - "relations": [ - { - "source_id": "_normalize_owner_tokens", - "relation_type": "BINDS_TO", - "target_id": "ProfileService", - "target_ref": "ProfileService" - } - ], - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_owner_tokens: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: Optional[Iterable[Any]]) -> 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 # [/DEF:_normalize_owner_tokens:Function]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "rbac_permission_catalog", "contract_type": "Module", "file_path": "backend/src/services/rbac_permission_catalog.py", "start_line": 1, - "end_line": 185, + "end_line": 178, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:rbac_permission_catalog:Module]\n#\n# @COMPLEXITY: 2\n# @PURPOSE: Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database.\n\n# [SECTION: IMPORTS]\nimport re\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typing import Iterable, Set, Tuple\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.logger import belief_scope, logger\nfrom ..models.auth import Permission\n# [/SECTION: IMPORTS]\n\n# [DEF:HAS_PERMISSION_PATTERN:Constant]\n# @PURPOSE: Regex pattern for extracting has_permission(\"resource\", \"ACTION\") declarations.\nHAS_PERMISSION_PATTERN = re.compile(\n r\"\"\"has_permission\\(\\s*['\"]([^'\"]+)['\"]\\s*,\\s*['\"]([A-Z]+)['\"]\\s*\\)\"\"\"\n)\n# [/DEF:HAS_PERMISSION_PATTERN:Constant]\n\n# [DEF:ROUTES_DIR:Constant]\n# @PURPOSE: Absolute directory path where API route RBAC declarations are defined.\nROUTES_DIR = Path(__file__).resolve().parent.parent / \"api\" / \"routes\"\n# [/DEF:ROUTES_DIR:Constant]\n\n\n# [DEF:_iter_route_files:Function]\n# @PURPOSE: Iterates API route files that may contain RBAC declarations.\n# @PRE: ROUTES_DIR points to backend/src/api/routes.\n# @POST: Yields Python files excluding test and cache directories.\n# @RETURN: Iterable[Path] - Route file paths for permission extraction.\ndef _iter_route_files() -> Iterable[Path]:\n with belief_scope(\"rbac_permission_catalog._iter_route_files\"):\n if not ROUTES_DIR.exists():\n return []\n\n files = []\n for file_path in ROUTES_DIR.rglob(\"*.py\"):\n path_parts = set(file_path.parts)\n if \"__tests__\" in path_parts or \"__pycache__\" in path_parts:\n continue\n files.append(file_path)\n return files\n# [/DEF:_iter_route_files:Function]\n\n\n# [DEF:_discover_route_permissions:Function]\n# @PURPOSE: Extracts explicit has_permission declarations from API route source code.\n# @PRE: Route files are readable UTF-8 text files.\n# @POST: Returns unique set of (resource, action) pairs declared in route guards.\n# @RETURN: Set[Tuple[str, str]] - Permission pairs from route-level RBAC declarations.\ndef _discover_route_permissions() -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions\"):\n discovered: Set[Tuple[str, str]] = set()\n for route_file in _iter_route_files():\n try:\n source = route_file.read_text(encoding=\"utf-8\")\n except OSError as read_error:\n logger.warning(\n \"[rbac_permission_catalog][EXPLORE] Failed to read route file %s: %s\",\n route_file,\n read_error,\n )\n continue\n\n for resource, action in HAS_PERMISSION_PATTERN.findall(source):\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n discovered.add((normalized_resource, normalized_action))\n return discovered\n# [/DEF:_discover_route_permissions:Function]\n\n\n# [DEF:_discover_route_permissions_cached:Function]\n# @PURPOSE: Cache route permission discovery because route source files are static during normal runtime.\n# @PRE: None.\n# @POST: Returns stable discovered route permission pairs without repeated filesystem scans.\n@lru_cache(maxsize=1)\ndef _discover_route_permissions_cached() -> Tuple[Tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions_cached\"):\n return tuple(sorted(_discover_route_permissions()))\n# [/DEF:_discover_route_permissions_cached:Function]\n\n\n# [DEF:_discover_plugin_execute_permissions:Function]\n# @PURPOSE: Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.\n# @PRE: plugin_loader is optional and may expose get_all_plugin_configs.\n# @POST: Returns unique plugin EXECUTE permissions if loader is available.\n# @RETURN: Set[Tuple[str, str]] - Permission pairs derived from loaded plugin IDs.\ndef _discover_plugin_execute_permissions(plugin_loader=None) -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions\"):\n discovered: Set[Tuple[str, str]] = set()\n if plugin_loader is None:\n return discovered\n\n try:\n plugin_configs = plugin_loader.get_all_plugin_configs()\n except Exception as plugin_error:\n logger.warning(\n \"[rbac_permission_catalog][EXPLORE] Failed to read plugin configs for RBAC discovery: %s\",\n plugin_error,\n )\n return discovered\n\n for plugin_config in plugin_configs:\n plugin_id = str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n if plugin_id:\n discovered.add((f\"plugin:{plugin_id}\", \"EXECUTE\"))\n return discovered\n# [/DEF:_discover_plugin_execute_permissions:Function]\n\n\n# [DEF:_discover_plugin_execute_permissions_cached:Function]\n# @PURPOSE: Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple.\n# @PRE: plugin_ids is a deterministic tuple of plugin ids.\n# @POST: Returns stable permission tuple without repeated plugin catalog expansion.\n@lru_cache(maxsize=8)\ndef _discover_plugin_execute_permissions_cached(\n plugin_ids: Tuple[str, ...],\n) -> Tuple[Tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions_cached\"):\n return tuple((f\"plugin:{plugin_id}\", \"EXECUTE\") for plugin_id in plugin_ids)\n# [/DEF:_discover_plugin_execute_permissions_cached:Function]\n\n\n# [DEF:discover_declared_permissions:Function]\n# @PURPOSE: Builds canonical RBAC permission catalog from routes and plugin registry.\n# @PRE: plugin_loader may be provided for dynamic task plugin permission discovery.\n# @POST: Returns union of route-declared and dynamic plugin EXECUTE permissions.\n# @RETURN: Set[Tuple[str, str]] - Complete discovered permission set.\ndef discover_declared_permissions(plugin_loader=None) -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog.discover_declared_permissions\"):\n permissions = set(_discover_route_permissions_cached())\n plugin_ids = tuple(\n sorted(\n {\n str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n for plugin_config in (plugin_loader.get_all_plugin_configs() if plugin_loader else [])\n if str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n }\n )\n )\n permissions.update(_discover_plugin_execute_permissions_cached(plugin_ids))\n return permissions\n# [/DEF:discover_declared_permissions:Function]\n\n\n# [DEF:sync_permission_catalog:Function]\n# @PURPOSE: Persists missing RBAC permission pairs into auth database.\n# @PRE: db is a valid SQLAlchemy session bound to auth database.\n# @PRE: declared_permissions is an iterable of (resource, action) tuples.\n# @POST: Missing permissions are inserted; existing permissions remain untouched.\n# @SIDE_EFFECT: Commits auth database transaction when new permissions are added.\n# @RETURN: int - Number of inserted permission records.\ndef sync_permission_catalog(\n db: Session,\n declared_permissions: Iterable[Tuple[str, str]],\n) -> int:\n with belief_scope(\"rbac_permission_catalog.sync_permission_catalog\"):\n normalized_declared: Set[Tuple[str, str]] = set()\n for resource, action in declared_permissions:\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n normalized_declared.add((normalized_resource, normalized_action))\n\n existing_permissions = db.query(Permission).all()\n existing_pairs = {(perm.resource, perm.action.upper()) for perm in existing_permissions}\n\n missing_pairs = sorted(normalized_declared - existing_pairs)\n for resource, action in missing_pairs:\n db.add(Permission(resource=resource, action=action))\n\n if missing_pairs:\n db.commit()\n\n return len(missing_pairs)\n# [/DEF:sync_permission_catalog:Function]\n\n# [/DEF:rbac_permission_catalog:Module]\n" + "anchor_syntax": "region", + "body": "# #region rbac_permission_catalog [C:2] [TYPE Module] [SEMANTICS rbac, permission, discovery, route, sync]\n#\n# @BRIEF Discovers declared RBAC permissions from API routes/plugins and synchronizes them into auth database.\n\nimport re\nfrom collections.abc import Iterable\nfrom functools import lru_cache\nfrom pathlib import Path\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.logger import belief_scope, logger\nfrom ..models.auth import Permission\n\n# [/SECTION: IMPORTS]\n\n# #region HAS_PERMISSION_PATTERN [TYPE Constant]\n# @BRIEF Regex pattern for extracting has_permission(\"resource\", \"ACTION\") declarations.\nHAS_PERMISSION_PATTERN = re.compile(\n r\"\"\"has_permission\\(\\s*['\"]([^'\"]+)['\"]\\s*,\\s*['\"]([A-Z]+)['\"]\\s*\\)\"\"\"\n)\n# #endregion HAS_PERMISSION_PATTERN\n\n# #region ROUTES_DIR [TYPE Constant]\n# @BRIEF Absolute directory path where API route RBAC declarations are defined.\nROUTES_DIR = Path(__file__).resolve().parent.parent / \"api\" / \"routes\"\n# #endregion ROUTES_DIR\n\n\n# #region _iter_route_files [TYPE Function]\n# @BRIEF Iterates API route files that may contain RBAC declarations.\n# @PRE: ROUTES_DIR points to backend/src/api/routes.\n# @POST: Yields Python files excluding test and cache directories.\ndef _iter_route_files() -> Iterable[Path]:\n with belief_scope(\"rbac_permission_catalog._iter_route_files\"):\n if not ROUTES_DIR.exists():\n return []\n\n files = []\n for file_path in ROUTES_DIR.rglob(\"*.py\"):\n path_parts = set(file_path.parts)\n if \"__tests__\" in path_parts or \"__pycache__\" in path_parts:\n continue\n files.append(file_path)\n return files\n# #endregion _iter_route_files\n\n\n# #region _discover_route_permissions [TYPE Function]\n# @BRIEF Extracts explicit has_permission declarations from API route source code.\n# @PRE: Route files are readable UTF-8 text files.\n# @POST: Returns unique set of (resource, action) pairs declared in route guards.\ndef _discover_route_permissions() -> set[tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions\"):\n discovered: set[tuple[str, str]] = set()\n for route_file in _iter_route_files():\n try:\n source = route_file.read_text(encoding=\"utf-8\")\n except OSError as read_error:\n logger.explore(\n \"Failed to read route file\",\n extra={\"src\": \"rbac_permission_catalog\", \"payload\": {\"file\": str(route_file)}, \"error\": str(read_error)},\n )\n continue\n\n for resource, action in HAS_PERMISSION_PATTERN.findall(source):\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n discovered.add((normalized_resource, normalized_action))\n return discovered\n# #endregion _discover_route_permissions\n\n\n# #region _discover_route_permissions_cached [TYPE Function]\n# @BRIEF Cache route permission discovery because route source files are static during normal runtime.\n# @PRE: None.\n# @POST: Returns stable discovered route permission pairs without repeated filesystem scans.\n@lru_cache(maxsize=1)\ndef _discover_route_permissions_cached() -> tuple[tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions_cached\"):\n return tuple(sorted(_discover_route_permissions()))\n# #endregion _discover_route_permissions_cached\n\n\n# #region _discover_plugin_execute_permissions [TYPE Function]\n# @BRIEF Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.\n# @PRE: plugin_loader is optional and may expose get_all_plugin_configs.\n# @POST: Returns unique plugin EXECUTE permissions if loader is available.\ndef _discover_plugin_execute_permissions(plugin_loader=None) -> set[tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions\"):\n discovered: set[tuple[str, str]] = set()\n if plugin_loader is None:\n return discovered\n\n try:\n plugin_configs = plugin_loader.get_all_plugin_configs()\n except Exception as plugin_error:\n logger.explore(\n \"Failed to read plugin configs for RBAC discovery\",\n extra={\"src\": \"rbac_permission_catalog\", \"error\": str(plugin_error)},\n )\n return discovered\n\n for plugin_config in plugin_configs:\n plugin_id = str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n if plugin_id:\n discovered.add((f\"plugin:{plugin_id}\", \"EXECUTE\"))\n return discovered\n# #endregion _discover_plugin_execute_permissions\n\n\n# #region _discover_plugin_execute_permissions_cached [TYPE Function]\n# @BRIEF Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple.\n# @PRE: plugin_ids is a deterministic tuple of plugin ids.\n# @POST: Returns stable permission tuple without repeated plugin catalog expansion.\n@lru_cache(maxsize=8)\ndef _discover_plugin_execute_permissions_cached(\n plugin_ids: tuple[str, ...],\n) -> tuple[tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions_cached\"):\n return tuple((f\"plugin:{plugin_id}\", \"EXECUTE\") for plugin_id in plugin_ids)\n# #endregion _discover_plugin_execute_permissions_cached\n\n\n# #region discover_declared_permissions [TYPE Function]\n# @BRIEF Builds canonical RBAC permission catalog from routes and plugin registry.\n# @PRE: plugin_loader may be provided for dynamic task plugin permission discovery.\n# @POST: Returns union of route-declared and dynamic plugin EXECUTE permissions.\ndef discover_declared_permissions(plugin_loader=None) -> set[tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog.discover_declared_permissions\"):\n permissions = set(_discover_route_permissions_cached())\n plugin_ids = tuple(\n sorted(\n {\n str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n for plugin_config in (plugin_loader.get_all_plugin_configs() if plugin_loader else [])\n if str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n }\n )\n )\n permissions.update(_discover_plugin_execute_permissions_cached(plugin_ids))\n return permissions\n# #endregion discover_declared_permissions\n\n\n# #region sync_permission_catalog [TYPE Function]\n# @BRIEF Persists missing RBAC permission pairs into auth database.\n# @PRE: db is a valid SQLAlchemy session bound to auth database.\n# @PRE: declared_permissions is an iterable of (resource, action) tuples.\n# @POST: Missing permissions are inserted; existing permissions remain untouched.\n# @SIDE_EFFECT: Commits auth database transaction when new permissions are added.\ndef sync_permission_catalog(\n db: Session,\n declared_permissions: Iterable[tuple[str, str]],\n) -> int:\n with belief_scope(\"rbac_permission_catalog.sync_permission_catalog\"):\n normalized_declared: set[tuple[str, str]] = set()\n for resource, action in declared_permissions:\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n normalized_declared.add((normalized_resource, normalized_action))\n\n existing_permissions = db.query(Permission).all()\n existing_pairs = {(perm.resource, perm.action.upper()) for perm in existing_permissions}\n\n missing_pairs = sorted(normalized_declared - existing_pairs)\n for resource, action in missing_pairs:\n db.add(Permission(resource=resource, action=action))\n\n if missing_pairs:\n db.commit()\n\n return len(missing_pairs)\n# #endregion sync_permission_catalog\n\n# #endregion rbac_permission_catalog\n" }, { "contract_id": "HAS_PERMISSION_PATTERN", "contract_type": "Constant", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 18, - "end_line": 23, + "start_line": 17, + "end_line": 22, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -94685,15 +64810,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:HAS_PERMISSION_PATTERN:Constant]\n# @PURPOSE: Regex pattern for extracting has_permission(\"resource\", \"ACTION\") declarations.\nHAS_PERMISSION_PATTERN = re.compile(\n r\"\"\"has_permission\\(\\s*['\"]([^'\"]+)['\"]\\s*,\\s*['\"]([A-Z]+)['\"]\\s*\\)\"\"\"\n)\n# [/DEF:HAS_PERMISSION_PATTERN:Constant]\n" + "anchor_syntax": "region", + "body": "# #region HAS_PERMISSION_PATTERN [TYPE Constant]\n# @BRIEF Regex pattern for extracting has_permission(\"resource\", \"ACTION\") declarations.\nHAS_PERMISSION_PATTERN = re.compile(\n r\"\"\"has_permission\\(\\s*['\"]([^'\"]+)['\"]\\s*,\\s*['\"]([A-Z]+)['\"]\\s*\\)\"\"\"\n)\n# #endregion HAS_PERMISSION_PATTERN\n" }, { "contract_id": "ROUTES_DIR", "contract_type": "Constant", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 25, - "end_line": 28, + "start_line": 24, + "end_line": 27, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -94729,22 +64854,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ROUTES_DIR:Constant]\n# @PURPOSE: Absolute directory path where API route RBAC declarations are defined.\nROUTES_DIR = Path(__file__).resolve().parent.parent / \"api\" / \"routes\"\n# [/DEF:ROUTES_DIR:Constant]\n" + "anchor_syntax": "region", + "body": "# #region ROUTES_DIR [TYPE Constant]\n# @BRIEF Absolute directory path where API route RBAC declarations are defined.\nROUTES_DIR = Path(__file__).resolve().parent.parent / \"api\" / \"routes\"\n# #endregion ROUTES_DIR\n" }, { "contract_id": "_iter_route_files", "contract_type": "Function", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 31, - "end_line": 48, + "start_line": 30, + "end_line": 46, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Yields Python files excluding test and cache directories.", "PRE": "ROUTES_DIR points to backend/src/api/routes.", - "PURPOSE": "Iterates API route files that may contain RBAC declarations.", - "RETURN": "Iterable[Path] - Route file paths for permission extraction." + "PURPOSE": "Iterates API route files that may contain RBAC declarations." }, "relations": [], "schema_warnings": [ @@ -94774,30 +64898,23 @@ "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": "def", - "body": "# [DEF:_iter_route_files:Function]\n# @PURPOSE: Iterates API route files that may contain RBAC declarations.\n# @PRE: ROUTES_DIR points to backend/src/api/routes.\n# @POST: Yields Python files excluding test and cache directories.\n# @RETURN: Iterable[Path] - Route file paths for permission extraction.\ndef _iter_route_files() -> Iterable[Path]:\n with belief_scope(\"rbac_permission_catalog._iter_route_files\"):\n if not ROUTES_DIR.exists():\n return []\n\n files = []\n for file_path in ROUTES_DIR.rglob(\"*.py\"):\n path_parts = set(file_path.parts)\n if \"__tests__\" in path_parts or \"__pycache__\" in path_parts:\n continue\n files.append(file_path)\n return files\n# [/DEF:_iter_route_files:Function]\n" + "anchor_syntax": "region", + "body": "# #region _iter_route_files [TYPE Function]\n# @BRIEF Iterates API route files that may contain RBAC declarations.\n# @PRE: ROUTES_DIR points to backend/src/api/routes.\n# @POST: Yields Python files excluding test and cache directories.\ndef _iter_route_files() -> Iterable[Path]:\n with belief_scope(\"rbac_permission_catalog._iter_route_files\"):\n if not ROUTES_DIR.exists():\n return []\n\n files = []\n for file_path in ROUTES_DIR.rglob(\"*.py\"):\n path_parts = set(file_path.parts)\n if \"__tests__\" in path_parts or \"__pycache__\" in path_parts:\n continue\n files.append(file_path)\n return files\n# #endregion _iter_route_files\n" }, { "contract_id": "_discover_route_permissions", "contract_type": "Function", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 51, - "end_line": 76, + "start_line": 49, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Returns unique set of (resource, action) pairs declared in route guards.", "PRE": "Route files are readable UTF-8 text files.", - "PURPOSE": "Extracts explicit has_permission declarations from API route source code.", - "RETURN": "Set[Tuple[str, str]] - Permission pairs from route-level RBAC declarations." + "PURPOSE": "Extracts explicit has_permission declarations from API route source code." }, "relations": [], "schema_warnings": [ @@ -94827,23 +64944,17 @@ "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": "def", - "body": "# [DEF:_discover_route_permissions:Function]\n# @PURPOSE: Extracts explicit has_permission declarations from API route source code.\n# @PRE: Route files are readable UTF-8 text files.\n# @POST: Returns unique set of (resource, action) pairs declared in route guards.\n# @RETURN: Set[Tuple[str, str]] - Permission pairs from route-level RBAC declarations.\ndef _discover_route_permissions() -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions\"):\n discovered: Set[Tuple[str, str]] = set()\n for route_file in _iter_route_files():\n try:\n source = route_file.read_text(encoding=\"utf-8\")\n except OSError as read_error:\n logger.warning(\n \"[rbac_permission_catalog][EXPLORE] Failed to read route file %s: %s\",\n route_file,\n read_error,\n )\n continue\n\n for resource, action in HAS_PERMISSION_PATTERN.findall(source):\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n discovered.add((normalized_resource, normalized_action))\n return discovered\n# [/DEF:_discover_route_permissions:Function]\n" + "anchor_syntax": "region", + "body": "# #region _discover_route_permissions [TYPE Function]\n# @BRIEF Extracts explicit has_permission declarations from API route source code.\n# @PRE: Route files are readable UTF-8 text files.\n# @POST: Returns unique set of (resource, action) pairs declared in route guards.\ndef _discover_route_permissions() -> set[tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions\"):\n discovered: set[tuple[str, str]] = set()\n for route_file in _iter_route_files():\n try:\n source = route_file.read_text(encoding=\"utf-8\")\n except OSError as read_error:\n logger.explore(\n \"Failed to read route file\",\n extra={\"src\": \"rbac_permission_catalog\", \"payload\": {\"file\": str(route_file)}, \"error\": str(read_error)},\n )\n continue\n\n for resource, action in HAS_PERMISSION_PATTERN.findall(source):\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n discovered.add((normalized_resource, normalized_action))\n return discovered\n# #endregion _discover_route_permissions\n" }, { "contract_id": "_discover_route_permissions_cached", "contract_type": "Function", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 79, - "end_line": 87, + "start_line": 75, + "end_line": 83, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -94881,22 +64992,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_discover_route_permissions_cached:Function]\n# @PURPOSE: Cache route permission discovery because route source files are static during normal runtime.\n# @PRE: None.\n# @POST: Returns stable discovered route permission pairs without repeated filesystem scans.\n@lru_cache(maxsize=1)\ndef _discover_route_permissions_cached() -> Tuple[Tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions_cached\"):\n return tuple(sorted(_discover_route_permissions()))\n# [/DEF:_discover_route_permissions_cached:Function]\n" + "anchor_syntax": "region", + "body": "# #region _discover_route_permissions_cached [TYPE Function]\n# @BRIEF Cache route permission discovery because route source files are static during normal runtime.\n# @PRE: None.\n# @POST: Returns stable discovered route permission pairs without repeated filesystem scans.\n@lru_cache(maxsize=1)\ndef _discover_route_permissions_cached() -> tuple[tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_route_permissions_cached\"):\n return tuple(sorted(_discover_route_permissions()))\n# #endregion _discover_route_permissions_cached\n" }, { "contract_id": "_discover_plugin_execute_permissions", "contract_type": "Function", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 90, - "end_line": 115, + "start_line": 86, + "end_line": 110, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Returns unique plugin EXECUTE permissions if loader is available.", "PRE": "plugin_loader is optional and may expose get_all_plugin_configs.", - "PURPOSE": "Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.", - "RETURN": "Set[Tuple[str, str]] - Permission pairs derived from loaded plugin IDs." + "PURPOSE": "Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry." }, "relations": [], "schema_warnings": [ @@ -94926,23 +65036,17 @@ "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": "def", - "body": "# [DEF:_discover_plugin_execute_permissions:Function]\n# @PURPOSE: Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.\n# @PRE: plugin_loader is optional and may expose get_all_plugin_configs.\n# @POST: Returns unique plugin EXECUTE permissions if loader is available.\n# @RETURN: Set[Tuple[str, str]] - Permission pairs derived from loaded plugin IDs.\ndef _discover_plugin_execute_permissions(plugin_loader=None) -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions\"):\n discovered: Set[Tuple[str, str]] = set()\n if plugin_loader is None:\n return discovered\n\n try:\n plugin_configs = plugin_loader.get_all_plugin_configs()\n except Exception as plugin_error:\n logger.warning(\n \"[rbac_permission_catalog][EXPLORE] Failed to read plugin configs for RBAC discovery: %s\",\n plugin_error,\n )\n return discovered\n\n for plugin_config in plugin_configs:\n plugin_id = str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n if plugin_id:\n discovered.add((f\"plugin:{plugin_id}\", \"EXECUTE\"))\n return discovered\n# [/DEF:_discover_plugin_execute_permissions:Function]\n" + "anchor_syntax": "region", + "body": "# #region _discover_plugin_execute_permissions [TYPE Function]\n# @BRIEF Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.\n# @PRE: plugin_loader is optional and may expose get_all_plugin_configs.\n# @POST: Returns unique plugin EXECUTE permissions if loader is available.\ndef _discover_plugin_execute_permissions(plugin_loader=None) -> set[tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions\"):\n discovered: set[tuple[str, str]] = set()\n if plugin_loader is None:\n return discovered\n\n try:\n plugin_configs = plugin_loader.get_all_plugin_configs()\n except Exception as plugin_error:\n logger.explore(\n \"Failed to read plugin configs for RBAC discovery\",\n extra={\"src\": \"rbac_permission_catalog\", \"error\": str(plugin_error)},\n )\n return discovered\n\n for plugin_config in plugin_configs:\n plugin_id = str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n if plugin_id:\n discovered.add((f\"plugin:{plugin_id}\", \"EXECUTE\"))\n return discovered\n# #endregion _discover_plugin_execute_permissions\n" }, { "contract_id": "_discover_plugin_execute_permissions_cached", "contract_type": "Function", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 118, - "end_line": 128, + "start_line": 113, + "end_line": 123, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -94980,22 +65084,21 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:_discover_plugin_execute_permissions_cached:Function]\n# @PURPOSE: Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple.\n# @PRE: plugin_ids is a deterministic tuple of plugin ids.\n# @POST: Returns stable permission tuple without repeated plugin catalog expansion.\n@lru_cache(maxsize=8)\ndef _discover_plugin_execute_permissions_cached(\n plugin_ids: Tuple[str, ...],\n) -> Tuple[Tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions_cached\"):\n return tuple((f\"plugin:{plugin_id}\", \"EXECUTE\") for plugin_id in plugin_ids)\n# [/DEF:_discover_plugin_execute_permissions_cached:Function]\n" + "anchor_syntax": "region", + "body": "# #region _discover_plugin_execute_permissions_cached [TYPE Function]\n# @BRIEF Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple.\n# @PRE: plugin_ids is a deterministic tuple of plugin ids.\n# @POST: Returns stable permission tuple without repeated plugin catalog expansion.\n@lru_cache(maxsize=8)\ndef _discover_plugin_execute_permissions_cached(\n plugin_ids: tuple[str, ...],\n) -> tuple[tuple[str, str], ...]:\n with belief_scope(\"rbac_permission_catalog._discover_plugin_execute_permissions_cached\"):\n return tuple((f\"plugin:{plugin_id}\", \"EXECUTE\") for plugin_id in plugin_ids)\n# #endregion _discover_plugin_execute_permissions_cached\n" }, { "contract_id": "discover_declared_permissions", "contract_type": "Function", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 131, - "end_line": 150, + "start_line": 126, + "end_line": 144, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Returns union of route-declared and dynamic plugin EXECUTE permissions.", "PRE": "plugin_loader may be provided for dynamic task plugin permission discovery.", - "PURPOSE": "Builds canonical RBAC permission catalog from routes and plugin registry.", - "RETURN": "Set[Tuple[str, str]] - Complete discovered permission set." + "PURPOSE": "Builds canonical RBAC permission catalog from routes and plugin registry." }, "relations": [], "schema_warnings": [ @@ -95025,30 +65128,23 @@ "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": "def", - "body": "# [DEF:discover_declared_permissions:Function]\n# @PURPOSE: Builds canonical RBAC permission catalog from routes and plugin registry.\n# @PRE: plugin_loader may be provided for dynamic task plugin permission discovery.\n# @POST: Returns union of route-declared and dynamic plugin EXECUTE permissions.\n# @RETURN: Set[Tuple[str, str]] - Complete discovered permission set.\ndef discover_declared_permissions(plugin_loader=None) -> Set[Tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog.discover_declared_permissions\"):\n permissions = set(_discover_route_permissions_cached())\n plugin_ids = tuple(\n sorted(\n {\n str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n for plugin_config in (plugin_loader.get_all_plugin_configs() if plugin_loader else [])\n if str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n }\n )\n )\n permissions.update(_discover_plugin_execute_permissions_cached(plugin_ids))\n return permissions\n# [/DEF:discover_declared_permissions:Function]\n" + "anchor_syntax": "region", + "body": "# #region discover_declared_permissions [TYPE Function]\n# @BRIEF Builds canonical RBAC permission catalog from routes and plugin registry.\n# @PRE: plugin_loader may be provided for dynamic task plugin permission discovery.\n# @POST: Returns union of route-declared and dynamic plugin EXECUTE permissions.\ndef discover_declared_permissions(plugin_loader=None) -> set[tuple[str, str]]:\n with belief_scope(\"rbac_permission_catalog.discover_declared_permissions\"):\n permissions = set(_discover_route_permissions_cached())\n plugin_ids = tuple(\n sorted(\n {\n str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n for plugin_config in (plugin_loader.get_all_plugin_configs() if plugin_loader else [])\n if str(getattr(plugin_config, \"id\", \"\") or \"\").strip()\n }\n )\n )\n permissions.update(_discover_plugin_execute_permissions_cached(plugin_ids))\n return permissions\n# #endregion discover_declared_permissions\n" }, { "contract_id": "sync_permission_catalog", "contract_type": "Function", "file_path": "backend/src/services/rbac_permission_catalog.py", - "start_line": 153, - "end_line": 183, + "start_line": 147, + "end_line": 176, "tier": "TIER_1", "complexity": 1, "metadata": { "POST": "Missing permissions are inserted; existing permissions remain untouched.", "PRE": "declared_permissions is an iterable of (resource, action) tuples.", "PURPOSE": "Persists missing RBAC permission pairs into auth database.", - "RETURN": "int - Number of inserted permission records.", "SIDE_EFFECT": "Commits auth database transaction when new permissions are added." }, "relations": [], @@ -95080,12 +65176,6 @@ "contract_type": "Function" } }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_forbidden_by_complexity", "tag": "SIDE_EFFECT", @@ -95096,8 +65186,8 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:sync_permission_catalog:Function]\n# @PURPOSE: Persists missing RBAC permission pairs into auth database.\n# @PRE: db is a valid SQLAlchemy session bound to auth database.\n# @PRE: declared_permissions is an iterable of (resource, action) tuples.\n# @POST: Missing permissions are inserted; existing permissions remain untouched.\n# @SIDE_EFFECT: Commits auth database transaction when new permissions are added.\n# @RETURN: int - Number of inserted permission records.\ndef sync_permission_catalog(\n db: Session,\n declared_permissions: Iterable[Tuple[str, str]],\n) -> int:\n with belief_scope(\"rbac_permission_catalog.sync_permission_catalog\"):\n normalized_declared: Set[Tuple[str, str]] = set()\n for resource, action in declared_permissions:\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n normalized_declared.add((normalized_resource, normalized_action))\n\n existing_permissions = db.query(Permission).all()\n existing_pairs = {(perm.resource, perm.action.upper()) for perm in existing_permissions}\n\n missing_pairs = sorted(normalized_declared - existing_pairs)\n for resource, action in missing_pairs:\n db.add(Permission(resource=resource, action=action))\n\n if missing_pairs:\n db.commit()\n\n return len(missing_pairs)\n# [/DEF:sync_permission_catalog:Function]\n" + "anchor_syntax": "region", + "body": "# #region sync_permission_catalog [TYPE Function]\n# @BRIEF Persists missing RBAC permission pairs into auth database.\n# @PRE: db is a valid SQLAlchemy session bound to auth database.\n# @PRE: declared_permissions is an iterable of (resource, action) tuples.\n# @POST: Missing permissions are inserted; existing permissions remain untouched.\n# @SIDE_EFFECT: Commits auth database transaction when new permissions are added.\ndef sync_permission_catalog(\n db: Session,\n declared_permissions: Iterable[tuple[str, str]],\n) -> int:\n with belief_scope(\"rbac_permission_catalog.sync_permission_catalog\"):\n normalized_declared: set[tuple[str, str]] = set()\n for resource, action in declared_permissions:\n normalized_resource = str(resource or \"\").strip()\n normalized_action = str(action or \"\").strip().upper()\n if normalized_resource and normalized_action:\n normalized_declared.add((normalized_resource, normalized_action))\n\n existing_permissions = db.query(Permission).all()\n existing_pairs = {(perm.resource, perm.action.upper()) for perm in existing_permissions}\n\n missing_pairs = sorted(normalized_declared - existing_pairs)\n for resource, action in missing_pairs:\n db.add(Permission(resource=resource, action=action))\n\n if missing_pairs:\n db.commit()\n\n return len(missing_pairs)\n# #endregion sync_permission_catalog\n" }, { "contract_id": "reports", @@ -95140,470 +65230,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:reports:Package]\n# @PURPOSE: Report service package root.\n# [/DEF:reports:Package]\n" - }, - { - "contract_id": "test_report_normalizer", - "contract_type": "Module", - "file_path": "backend/src/services/reports/__tests__/test_report_normalizer.py", - "start_line": 1, - "end_line": 84, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "INVARIANT": "Unknown plugin types are mapped to canonical unknown task type.", - "LAYER": "Domain (Tests)", - "PURPOSE": "Validate unknown task type fallback and partial payload normalization behavior.", - "SEMANTICS": [ - "tests", - "reports", - "normalizer", - "fallback" - ] - }, - "relations": [ - { - "source_id": "test_report_normalizer", - "relation_type": "TESTS", - "target_id": "normalize_report:Function", - "target_ref": "[normalize_report:Function]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT 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" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate TESTS is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "TESTS" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_report_normalizer:Module]\n# @COMPLEXITY: 2\n# @SEMANTICS: tests, reports, normalizer, fallback\n# @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior.\n# @RELATION: TESTS ->[normalize_report:Function]\n# @LAYER: Domain (Tests)\n# @INVARIANT: Unknown plugin types are mapped to canonical unknown task type.\n\nfrom datetime import datetime\n\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.services.reports.normalizer import normalize_task_report\n\n\n# [DEF:test_unknown_type_maps_to_unknown_profile:Function]\n# @RELATION: BINDS_TO -> test_report_normalizer\n# @PURPOSE: Ensure unknown plugin IDs map to unknown profile with populated summary and error context.\ndef test_unknown_type_maps_to_unknown_profile():\n task = Task(\n id=\"unknown-1\",\n plugin_id=\"custom-unmapped-plugin\",\n status=TaskStatus.FAILED,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n params={},\n result={\"error_message\": \"Unexpected plugin payload\"},\n )\n\n report = normalize_task_report(task)\n\n assert report.task_type.value == \"unknown\"\n assert report.summary\n assert report.error_context is not None\n\n\n# [/DEF:test_unknown_type_maps_to_unknown_profile:Function]\n\n\n# [DEF:test_partial_payload_keeps_report_visible_with_placeholders:Function]\n# @RELATION: BINDS_TO -> test_report_normalizer\n# @PURPOSE: Ensure missing result payload still yields visible report details with result placeholder.\ndef test_partial_payload_keeps_report_visible_with_placeholders():\n task = Task(\n id=\"partial-1\",\n plugin_id=\"superset-backup\",\n status=TaskStatus.SUCCESS,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n params={},\n result=None,\n )\n\n report = normalize_task_report(task)\n\n assert report.task_type.value == \"backup\"\n assert report.details is not None\n assert \"result\" in report.details\n\n\n# [/DEF:test_partial_payload_keeps_report_visible_with_placeholders:Function]\n\n\n# [DEF:test_clean_release_plugin_maps_to_clean_release_task_type:Function]\n# @RELATION: BINDS_TO -> test_report_normalizer\n# @PURPOSE: Ensure clean-release plugin ID maps to clean_release task profile and summary passthrough.\ndef test_clean_release_plugin_maps_to_clean_release_task_type():\n task = Task(\n id=\"clean-release-1\",\n plugin_id=\"clean-release-compliance\",\n status=TaskStatus.SUCCESS,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n params={\"run_id\": \"run-1\"},\n result={\"summary\": \"Clean release compliance passed\", \"run_id\": \"run-1\"},\n )\n\n report = normalize_task_report(task)\n\n assert report.task_type.value == \"clean_release\"\n assert report.summary == \"Clean release compliance passed\"\n\n\n# [/DEF:test_clean_release_plugin_maps_to_clean_release_task_type:Function]\n# [/DEF:test_report_normalizer:Module]\n" - }, - { - "contract_id": "test_unknown_type_maps_to_unknown_profile", - "contract_type": "Function", - "file_path": "backend/src/services/reports/__tests__/test_report_normalizer.py", - "start_line": 15, - "end_line": 36, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Ensure unknown plugin IDs map to unknown profile with populated summary and error context." - }, - "relations": [ - { - "source_id": "test_unknown_type_maps_to_unknown_profile", - "relation_type": "BINDS_TO", - "target_id": "test_report_normalizer", - "target_ref": "test_report_normalizer" - } - ], - "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": "def", - "body": "# [DEF:test_unknown_type_maps_to_unknown_profile:Function]\n# @RELATION: BINDS_TO -> test_report_normalizer\n# @PURPOSE: Ensure unknown plugin IDs map to unknown profile with populated summary and error context.\ndef test_unknown_type_maps_to_unknown_profile():\n task = Task(\n id=\"unknown-1\",\n plugin_id=\"custom-unmapped-plugin\",\n status=TaskStatus.FAILED,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n params={},\n result={\"error_message\": \"Unexpected plugin payload\"},\n )\n\n report = normalize_task_report(task)\n\n assert report.task_type.value == \"unknown\"\n assert report.summary\n assert report.error_context is not None\n\n\n# [/DEF:test_unknown_type_maps_to_unknown_profile:Function]\n" - }, - { - "contract_id": "test_partial_payload_keeps_report_visible_with_placeholders", - "contract_type": "Function", - "file_path": "backend/src/services/reports/__tests__/test_report_normalizer.py", - "start_line": 39, - "end_line": 60, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Ensure missing result payload still yields visible report details with result placeholder." - }, - "relations": [ - { - "source_id": "test_partial_payload_keeps_report_visible_with_placeholders", - "relation_type": "BINDS_TO", - "target_id": "test_report_normalizer", - "target_ref": "test_report_normalizer" - } - ], - "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": "def", - "body": "# [DEF:test_partial_payload_keeps_report_visible_with_placeholders:Function]\n# @RELATION: BINDS_TO -> test_report_normalizer\n# @PURPOSE: Ensure missing result payload still yields visible report details with result placeholder.\ndef test_partial_payload_keeps_report_visible_with_placeholders():\n task = Task(\n id=\"partial-1\",\n plugin_id=\"superset-backup\",\n status=TaskStatus.SUCCESS,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n params={},\n result=None,\n )\n\n report = normalize_task_report(task)\n\n assert report.task_type.value == \"backup\"\n assert report.details is not None\n assert \"result\" in report.details\n\n\n# [/DEF:test_partial_payload_keeps_report_visible_with_placeholders:Function]\n" - }, - { - "contract_id": "test_clean_release_plugin_maps_to_clean_release_task_type", - "contract_type": "Function", - "file_path": "backend/src/services/reports/__tests__/test_report_normalizer.py", - "start_line": 63, - "end_line": 83, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Ensure clean-release plugin ID maps to clean_release task profile and summary passthrough." - }, - "relations": [ - { - "source_id": "test_clean_release_plugin_maps_to_clean_release_task_type", - "relation_type": "BINDS_TO", - "target_id": "test_report_normalizer", - "target_ref": "test_report_normalizer" - } - ], - "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": "def", - "body": "# [DEF:test_clean_release_plugin_maps_to_clean_release_task_type:Function]\n# @RELATION: BINDS_TO -> test_report_normalizer\n# @PURPOSE: Ensure clean-release plugin ID maps to clean_release task profile and summary passthrough.\ndef test_clean_release_plugin_maps_to_clean_release_task_type():\n task = Task(\n id=\"clean-release-1\",\n plugin_id=\"clean-release-compliance\",\n status=TaskStatus.SUCCESS,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n params={\"run_id\": \"run-1\"},\n result={\"summary\": \"Clean release compliance passed\", \"run_id\": \"run-1\"},\n )\n\n report = normalize_task_report(task)\n\n assert report.task_type.value == \"clean_release\"\n assert report.summary == \"Clean release compliance passed\"\n\n\n# [/DEF:test_clean_release_plugin_maps_to_clean_release_task_type:Function]\n" - }, - { - "contract_id": "test_report_service", - "contract_type": "Module", - "file_path": "backend/src/services/reports/__tests__/test_report_service.py", - "start_line": 1, - "end_line": 187, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "COMPLEXITY": "2", - "LAYER": "Domain", - "PURPOSE": "Unit tests for ReportsService list/detail operations" - }, - "relations": [ - { - "source_id": "test_report_service", - "relation_type": "TESTS", - "target_id": "ReportsService:Class", - "target_ref": "[ReportsService:Class]" - } - ], - "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" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate TESTS is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "TESTS" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_report_service:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Unit tests for ReportsService list/detail operations\n# @RELATION: TESTS ->[ReportsService:Class]\n# @LAYER: Domain\n\nimport sys\nfrom pathlib import Path\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom unittest.mock import MagicMock, patch\nfrom datetime import datetime, timezone, timedelta\n\n\n# [DEF:_make_task:Function]\n# @RELATION: BINDS_TO -> test_report_service\ndef _make_task(task_id=\"task-1\", plugin_id=\"superset-backup\", status_value=\"SUCCESS\",\n started_at=None, finished_at=None, result=None, params=None, logs=None):\n \"\"\"Create a mock Task object matching the Task model interface.\"\"\"\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=plugin_id, params=params or {})\n task.id = task_id\n task.status = TaskStatus(status_value)\n task.started_at = started_at or datetime(2024, 1, 15, 10, 0, 0)\n task.finished_at = finished_at or datetime(2024, 1, 15, 10, 5, 0)\n task.result = result\n if logs is not None:\n task.logs = logs\n return task\n\n\n# [/DEF:_make_task:Function]\n\nclass TestReportsServiceList:\n \"\"\"Tests for ReportsService.list_reports.\"\"\"\n\n def _make_service(self, tasks):\n from src.services.reports.report_service import ReportsService\n mock_tm = MagicMock()\n mock_tm.get_all_tasks.return_value = tasks\n return ReportsService(task_manager=mock_tm)\n\n def test_empty_tasks_returns_empty_collection(self):\n from src.models.report import ReportQuery\n svc = self._make_service([])\n result = svc.list_reports(ReportQuery())\n assert result.total == 0\n assert result.items == []\n assert result.has_next is False\n\n def test_single_task_normalized(self):\n from src.models.report import ReportQuery\n task = _make_task(result={\"summary\": \"Backup completed\"})\n svc = self._make_service([task])\n result = svc.list_reports(ReportQuery())\n assert result.total == 1\n assert result.items[0].task_id == \"task-1\"\n assert result.items[0].summary == \"Backup completed\"\n\n def test_pagination_first_page(self):\n from src.models.report import ReportQuery\n tasks = [\n _make_task(task_id=f\"task-{i}\",\n finished_at=datetime(2024, 1, 15, 10, i, 0))\n for i in range(5)\n ]\n svc = self._make_service(tasks)\n result = svc.list_reports(ReportQuery(page=1, page_size=2))\n assert len(result.items) == 2\n assert result.total == 5\n assert result.has_next is True\n\n def test_pagination_last_page(self):\n from src.models.report import ReportQuery\n tasks = [\n _make_task(task_id=f\"task-{i}\",\n finished_at=datetime(2024, 1, 15, 10, i, 0))\n for i in range(5)\n ]\n svc = self._make_service(tasks)\n result = svc.list_reports(ReportQuery(page=3, page_size=2))\n assert len(result.items) == 1\n assert result.has_next is False\n\n def test_filter_by_status(self):\n from src.models.report import ReportQuery, ReportStatus\n tasks = [\n _make_task(task_id=\"ok\", status_value=\"SUCCESS\"),\n _make_task(task_id=\"fail\", status_value=\"FAILED\"),\n ]\n svc = self._make_service(tasks)\n result = svc.list_reports(ReportQuery(statuses=[ReportStatus.SUCCESS]))\n assert result.total == 1\n assert result.items[0].task_id == \"ok\"\n\n def test_filter_by_task_type(self):\n from src.models.report import ReportQuery, TaskType\n tasks = [\n _make_task(task_id=\"backup\", plugin_id=\"superset-backup\"),\n _make_task(task_id=\"migrate\", plugin_id=\"superset-migration\"),\n ]\n svc = self._make_service(tasks)\n result = svc.list_reports(ReportQuery(task_types=[TaskType.BACKUP]))\n assert result.total == 1\n assert result.items[0].task_id == \"backup\"\n\n def test_search_filter(self):\n from src.models.report import ReportQuery\n tasks = [\n _make_task(task_id=\"t1\", plugin_id=\"superset-migration\",\n result={\"summary\": \"Migration complete\"}),\n _make_task(task_id=\"t2\", plugin_id=\"documentation\",\n result={\"summary\": \"Docs generated\"}),\n ]\n svc = self._make_service(tasks)\n result = svc.list_reports(ReportQuery(search=\"migration\"))\n assert result.total == 1\n assert result.items[0].task_id == \"t1\"\n\n def test_sort_by_status(self):\n from src.models.report import ReportQuery\n tasks = [\n _make_task(task_id=\"t1\", status_value=\"SUCCESS\"),\n _make_task(task_id=\"t2\", status_value=\"FAILED\"),\n ]\n svc = self._make_service(tasks)\n # Order: FAILED (desc) -> SUCCESS (asc) if it's alphanumeric\n # status values are 'SUCCESS', 'FAILED'. 'FAILED' < 'SUCCESS'\n result = svc.list_reports(ReportQuery(sort_by=\"status\", sort_order=\"asc\"))\n assert result.items[0].status.value == \"failed\"\n assert result.items[1].status.value == \"success\"\n\n def test_applied_filters_echoed(self):\n from src.models.report import ReportQuery\n query = ReportQuery(page=2, page_size=5)\n svc = self._make_service([])\n result = svc.list_reports(query)\n assert result.applied_filters.page == 2\n assert result.applied_filters.page_size == 5\n\n\nclass TestReportsServiceDetail:\n \"\"\"Tests for ReportsService.get_report_detail.\"\"\"\n\n def _make_service(self, tasks):\n from src.services.reports.report_service import ReportsService\n mock_tm = MagicMock()\n mock_tm.get_all_tasks.return_value = tasks\n return ReportsService(task_manager=mock_tm)\n\n def test_detail_found(self):\n task = _make_task(task_id=\"detail-task\", result={\"summary\": \"Done\"})\n svc = self._make_service([task])\n detail = svc.get_report_detail(\"detail-task\")\n assert detail is not None\n assert detail.report.task_id == \"detail-task\"\n\n def test_detail_not_found(self):\n svc = self._make_service([])\n detail = svc.get_report_detail(\"nonexistent\")\n assert detail is None\n\n def test_detail_includes_timeline(self):\n task = _make_task(task_id=\"tl-task\",\n started_at=datetime(2024, 1, 15, 10, 0, 0),\n finished_at=datetime(2024, 1, 15, 10, 5, 0))\n svc = self._make_service([task])\n detail = svc.get_report_detail(\"tl-task\")\n events = [e[\"event\"] for e in detail.timeline]\n assert \"started\" in events\n assert \"updated\" in events\n\n def test_detail_failed_task_has_next_actions(self):\n task = _make_task(task_id=\"fail-task\", status_value=\"FAILED\")\n svc = self._make_service([task])\n detail = svc.get_report_detail(\"fail-task\")\n assert len(detail.next_actions) > 0\n\n def test_detail_success_task_no_error_next_actions(self):\n task = _make_task(task_id=\"ok-task\", status_value=\"SUCCESS\",\n result={\"summary\": \"All good\"})\n svc = self._make_service([task])\n detail = svc.get_report_detail(\"ok-task\")\n assert detail.next_actions == []\n\n# [/DEF:test_report_service:Module]\n" - }, - { - "contract_id": "__tests__/test_report_type_profiles", - "contract_type": "Module", - "file_path": "backend/src/services/reports/__tests__/test_type_profiles.py", - "start_line": 1, - "end_line": 4, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "PURPOSE": "Contract testing for task type profiles and resolution logic." - }, - "relations": [ - { - "source_id": "__tests__/test_report_type_profiles", - "relation_type": "VERIFIES", - "target_id": "../type_profiles.py", - "target_ref": "../type_profiles.py" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Module' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Module" - } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:__tests__/test_report_type_profiles:Module]\n# @RELATION: VERIFIES -> ../type_profiles.py\n# @PURPOSE: Contract testing for task type profiles and resolution logic.\n# [/DEF:__tests__/test_report_type_profiles:Module]\n" - }, - { - "contract_id": "test_resolve_task_type_fallbacks", - "contract_type": "Function", - "file_path": "backend/src/services/reports/__tests__/test_type_profiles.py", - "start_line": 12, - "end_line": 23, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify resolve_task_type_fallbacks returns correct fallback type when primary is missing.", - "TEST_FIXTURE": "valid_plugin" - }, - "relations": [ - { - "source_id": "test_resolve_task_type_fallbacks", - "relation_type": "BINDS_TO", - "target_id": "__tests__/test_report_type_profiles", - "target_ref": "__tests__/test_report_type_profiles" - } - ], - "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 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": "def", - "body": "# [DEF:test_resolve_task_type_fallbacks:Function]\n# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles\n# @PURPOSE: Verify resolve_task_type_fallbacks returns correct fallback type when primary is missing.\ndef test_resolve_task_type_fallbacks():\n \"\"\"Verify missing/unmapped plugin_id returns TaskType.UNKNOWN.\"\"\"\n assert resolve_task_type(None) == TaskType.UNKNOWN\n assert resolve_task_type(\"\") == TaskType.UNKNOWN\n assert resolve_task_type(\" \") == TaskType.UNKNOWN\n assert resolve_task_type(\"invalid_plugin\") == TaskType.UNKNOWN\n\n# @TEST_FIXTURE: valid_plugin\n# [/DEF:test_resolve_task_type_fallbacks:Function]\n" - }, - { - "contract_id": "test_resolve_task_type_valid", - "contract_type": "Function", - "file_path": "backend/src/services/reports/__tests__/test_type_profiles.py", - "start_line": 25, - "end_line": 36, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify resolve_task_type_valid returns the correct type when valid input is provided.", - "TEST_FIXTURE": "valid_profile" - }, - "relations": [ - { - "source_id": "test_resolve_task_type_valid", - "relation_type": "BINDS_TO", - "target_id": "__tests__/test_report_type_profiles", - "target_ref": "__tests__/test_report_type_profiles" - } - ], - "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 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": "def", - "body": "# [DEF:test_resolve_task_type_valid:Function]\n# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles\n# @PURPOSE: Verify resolve_task_type_valid returns the correct type when valid input is provided.\ndef test_resolve_task_type_valid():\n \"\"\"Verify known plugin IDs map correctly.\"\"\"\n assert resolve_task_type(\"superset-migration\") == TaskType.MIGRATION\n assert resolve_task_type(\"llm_dashboard_validation\") == TaskType.LLM_VERIFICATION\n assert resolve_task_type(\"superset-backup\") == TaskType.BACKUP\n assert resolve_task_type(\"documentation\") == TaskType.DOCUMENTATION\n\n# @TEST_FIXTURE: valid_profile\n# [/DEF:test_resolve_task_type_valid:Function]\n" - }, - { - "contract_id": "test_get_type_profile_valid", - "contract_type": "Function", - "file_path": "backend/src/services/reports/__tests__/test_type_profiles.py", - "start_line": 38, - "end_line": 50, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify get_type_profile_valid returns the correct profile for a valid task type.", - "TEST_EDGE": "missing_profile", - "TEST_INVARIANT": "always_returns_dict" - }, - "relations": [ - { - "source_id": "test_get_type_profile_valid", - "relation_type": "BINDS_TO", - "target_id": "__tests__/test_report_type_profiles", - "target_ref": "__tests__/test_report_type_profiles" - } - ], - "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": "def", - "body": "# [DEF:test_get_type_profile_valid:Function]\n# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles\n# @PURPOSE: Verify get_type_profile_valid returns the correct profile for a valid task type.\ndef test_get_type_profile_valid():\n \"\"\"Verify known task types return correct profile metadata.\"\"\"\n profile = get_type_profile(TaskType.MIGRATION)\n assert profile[\"display_label\"] == \"Migration\"\n assert profile[\"visual_variant\"] == \"migration\"\n assert profile[\"fallback\"] is False\n\n# @TEST_INVARIANT: always_returns_dict\n# @TEST_EDGE: missing_profile\n# [/DEF:test_get_type_profile_valid:Function]\n" - }, - { - "contract_id": "test_get_type_profile_fallback", - "contract_type": "Function", - "file_path": "backend/src/services/reports/__tests__/test_type_profiles.py", - "start_line": 52, - "end_line": 66, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "PURPOSE": "Verify get_type_profile_fallback returns default profile when type is unknown." - }, - "relations": [ - { - "source_id": "test_get_type_profile_fallback", - "relation_type": "BINDS_TO", - "target_id": "__tests__/test_report_type_profiles", - "target_ref": "__tests__/test_report_type_profiles" - } - ], - "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": "def", - "body": "# [DEF:test_get_type_profile_fallback:Function]\n# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles\n# @PURPOSE: Verify get_type_profile_fallback returns default profile when type is unknown.\ndef test_get_type_profile_fallback():\n \"\"\"Verify unknown task type returns fallback profile.\"\"\"\n # Assuming TaskType.UNKNOWN or any non-mapped value\n profile = get_type_profile(TaskType.UNKNOWN)\n assert profile[\"display_label\"] == \"Other / Unknown\"\n assert profile[\"fallback\"] is True\n \n # Passing a value that might not be in the dict explicitly\n profile_fallback = get_type_profile(\"non-enum-value\")\n assert profile_fallback[\"display_label\"] == \"Other / Unknown\"\n assert profile_fallback[\"fallback\"] is True\n# [/DEF:test_get_type_profile_fallback:Function]\n" + "anchor_syntax": "region", + "body": "# #region reports [TYPE Package] [SEMANTICS report, package, service, root]\n# @BRIEF Report service package root.\n# #endregion reports\n" }, { "contract_id": "normalizer", "contract_type": "Module", "file_path": "backend/src/services/reports/normalizer.py", "start_line": 1, - "end_line": 175, + "end_line": 161, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "ReportRow -> NormalizerInput; session_id -> valid UUID", "INVARIANT": "Normalizer instance maintains consistent field order", "LAYER": "Domain", "POST": "Returns Normalizer output with normalized fields", "PRE": "session is active and valid", "PURPOSE": "Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior.", - "SEMANTICS": [ - "reports", - "normalization", - "tasks", - "fallback" - ], "SIDE_EFFECT": "Read-only database operations" }, "relations": [ @@ -95646,32 +65291,24 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:normalizer:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: reports, normalization, tasks, fallback\n# @PURPOSE: Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON ->[backend.src.core.task_manager.models.Task:Function]\n# @RELATION: DEPENDS_ON ->[backend.src.models.report:Function]\n# @RELATION: DEPENDS_ON ->[backend.src.services.reports.type_profiles:Function]\n# @INVARIANT: Normalizer instance maintains consistent field order\n# @DATA_CONTRACT: ReportRow -> NormalizerInput; session_id -> valid UUID\n# @PRE: session is active and valid\n# @POST: Returns Normalizer output with normalized fields\n# @SIDE_EFFECT: Read-only database operations\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom typing import Any, Dict, Optional\n\nfrom ...core.logger import belief_scope\nfrom ...core.task_manager.models import Task, TaskStatus\nfrom ...models.report import ErrorContext, ReportStatus, TaskReport\nfrom .type_profiles import get_type_profile, resolve_task_type\n# [/SECTION]\n\n\n# [DEF:status_to_report_status:Function]\n# @PURPOSE: Normalize internal task status to canonical report status.\n# @PRE: status may be known or unknown string/enum value.\n# @POST: Always returns one of canonical ReportStatus values.\n# @PARAM: status (Any) - Internal task status value.\n# @RETURN: ReportStatus - Canonical report status.\ndef status_to_report_status(status: Any) -> ReportStatus:\n with belief_scope(\"status_to_report_status\"):\n raw = str(status.value if isinstance(status, TaskStatus) else status).upper()\n if raw == TaskStatus.SUCCESS.value:\n return ReportStatus.SUCCESS\n if raw == TaskStatus.FAILED.value:\n return ReportStatus.FAILED\n if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:\n return ReportStatus.IN_PROGRESS\n return ReportStatus.PARTIAL\n# [/DEF:status_to_report_status:Function]\n\n\n# [DEF:build_summary:Function]\n# @PURPOSE: Build deterministic user-facing summary from task payload and status.\n# @PRE: report_status is canonical; plugin_id may be unknown.\n# @POST: Returns non-empty summary text.\n# @PARAM: task (Task) - Source task object.\n# @PARAM: report_status (ReportStatus) - Canonical status.\n# @RETURN: str - Normalized summary.\ndef build_summary(task: Task, report_status: ReportStatus) -> str:\n with belief_scope(\"build_summary\"):\n result = task.result\n if isinstance(result, dict):\n for key in (\"summary\", \"message\", \"status_message\", \"description\"):\n value = result.get(key)\n if isinstance(value, str) and value.strip():\n return value.strip()\n if report_status == ReportStatus.SUCCESS:\n return \"Task completed successfully\"\n if report_status == ReportStatus.FAILED:\n return \"Task failed\"\n if report_status == ReportStatus.IN_PROGRESS:\n return \"Task is in progress\"\n return \"Task completed with partial data\"\n# [/DEF:build_summary:Function]\n\n\n# [DEF:extract_error_context:Function]\n# @PURPOSE: Extract normalized error context and next actions for failed/partial reports.\n# @PRE: task is a valid Task object.\n# @POST: Returns ErrorContext for failed/partial when context exists; otherwise None.\n# @PARAM: task (Task) - Source task.\n# @PARAM: report_status (ReportStatus) - Canonical status.\n# @RETURN: Optional[ErrorContext] - Error context block.\ndef extract_error_context(task: Task, report_status: ReportStatus) -> Optional[ErrorContext]:\n with belief_scope(\"extract_error_context\"):\n if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n return None\n\n result = task.result if isinstance(task.result, dict) else {}\n message = None\n code = None\n next_actions = []\n\n if isinstance(result.get(\"error\"), dict):\n error_obj = result.get(\"error\", {})\n message = error_obj.get(\"message\") or message\n code = error_obj.get(\"code\") or code\n actions = error_obj.get(\"next_actions\")\n if isinstance(actions, list):\n next_actions = [str(action) for action in actions if str(action).strip()]\n\n if not message:\n message = result.get(\"error_message\") if isinstance(result.get(\"error_message\"), str) else None\n\n if not message:\n for log in reversed(task.logs):\n if str(log.level).upper() == \"ERROR\" and log.message:\n message = log.message\n break\n\n if not message:\n message = \"Not provided\"\n\n if not next_actions:\n next_actions = [\"Review task diagnostics\", \"Retry the operation\"]\n\n return ErrorContext(code=code, message=message, next_actions=next_actions)\n# [/DEF:extract_error_context:Function]\n\n\n# [DEF:normalize_task_report:Function]\n# @PURPOSE: Convert one Task to canonical TaskReport envelope.\n# @PRE: task has valid id and plugin_id fields.\n# @POST: Returns TaskReport with required fields and deterministic fallback behavior.\n# @PARAM: task (Task) - Source task.\n# @RETURN: TaskReport - Canonical normalized report.\n#\n# @TEST_CONTRACT: NormalizeTaskReport ->\n# {\n# required_fields: {task: Task},\n# invariants: [\n# \"Returns a valid TaskReport object\",\n# \"Maps TaskStatus to ReportStatus deterministically\",\n# \"Extracts ErrorContext for FAILED/PARTIAL tasks\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task -> {\"task\": \"MockTask(id='1', plugin_id='superset-migration', status=TaskStatus.SUCCESS)\"}\n# @TEST_EDGE: task_with_error -> {\"task\": \"MockTask(status=TaskStatus.FAILED, logs=[LogEntry(level='ERROR', message='Failed')])\"}\n# @TEST_EDGE: unknown_plugin_type -> {\"task\": \"MockTask(plugin_id='unknown-plugin', status=TaskStatus.PENDING)\"}\n# @TEST_INVARIANT: deterministic_normalization -> verifies: [valid_task, task_with_error, unknown_plugin_type]\ndef normalize_task_report(task: Task) -> TaskReport:\n with belief_scope(\"normalize_task_report\"):\n task_type = resolve_task_type(task.plugin_id)\n report_status = status_to_report_status(task.status)\n profile = get_type_profile(task_type)\n\n started_at = task.started_at if isinstance(task.started_at, datetime) else None\n updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None\n if not updated_at:\n updated_at = started_at or datetime.utcnow()\n\n details: Dict[str, Any] = {\n \"profile\": {\n \"display_label\": profile.get(\"display_label\"),\n \"visual_variant\": profile.get(\"visual_variant\"),\n \"icon_token\": profile.get(\"icon_token\"),\n \"emphasis_rules\": profile.get(\"emphasis_rules\", []),\n },\n \"result\": task.result if task.result is not None else {\"note\": \"Not provided\"},\n }\n\n source_ref: Dict[str, Any] = {}\n if isinstance(task.params, dict):\n for key in (\"environment_id\", \"source_env_id\", \"target_env_id\", \"dashboard_id\", \"dataset_id\", \"resource_id\"):\n if key in task.params:\n source_ref[key] = task.params.get(key)\n\n return TaskReport(\n report_id=task.id,\n task_id=task.id,\n task_type=task_type,\n status=report_status,\n started_at=started_at,\n updated_at=updated_at,\n summary=build_summary(task, report_status),\n details=details,\n error_context=extract_error_context(task, report_status),\n source_ref=source_ref or None,\n )\n# [/DEF:normalize_task_report:Function]\n\n# [/DEF:normalizer:Module]\n" + "anchor_syntax": "region", + "body": "# #region normalizer [C:5] [TYPE Module] [SEMANTICS pydantic, report, task, normalize, status]\n# @BRIEF Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [backend.src.core.task_manager.models.Task:Function]\n# @RELATION DEPENDS_ON -> [backend.src.models.report:Function]\n# @RELATION DEPENDS_ON -> [backend.src.services.reports.type_profiles:Function]\n# @INVARIANT: Normalizer instance maintains consistent field order\n# @DATA_CONTRACT: ReportRow -> NormalizerInput; session_id -> valid UUID\n# @PRE: session is active and valid\n# @POST: Returns Normalizer output with normalized fields\n# @SIDE_EFFECT: Read-only database operations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom ...core.logger import belief_scope\nfrom ...core.task_manager.models import Task, TaskStatus\nfrom ...models.report import ErrorContext, ReportStatus, TaskReport\nfrom .type_profiles import get_type_profile, resolve_task_type\n\n\n# #region status_to_report_status [TYPE Function]\n# @BRIEF Normalize internal task status to canonical report status.\n# @PRE: status may be known or unknown string/enum value.\n# @POST: Always returns one of canonical ReportStatus values.\ndef status_to_report_status(status: Any) -> ReportStatus:\n with belief_scope(\"status_to_report_status\"):\n raw = str(status.value if isinstance(status, TaskStatus) else status).upper()\n if raw == TaskStatus.SUCCESS.value:\n return ReportStatus.SUCCESS\n if raw == TaskStatus.FAILED.value:\n return ReportStatus.FAILED\n if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:\n return ReportStatus.IN_PROGRESS\n return ReportStatus.PARTIAL\n# #endregion status_to_report_status\n\n\n# #region build_summary [TYPE Function]\n# @BRIEF Build deterministic user-facing summary from task payload and status.\n# @PRE: report_status is canonical; plugin_id may be unknown.\n# @POST: Returns non-empty summary text.\ndef build_summary(task: Task, report_status: ReportStatus) -> str:\n with belief_scope(\"build_summary\"):\n result = task.result\n if isinstance(result, dict):\n for key in (\"summary\", \"message\", \"status_message\", \"description\"):\n value = result.get(key)\n if isinstance(value, str) and value.strip():\n return value.strip()\n if report_status == ReportStatus.SUCCESS:\n return \"Task completed successfully\"\n if report_status == ReportStatus.FAILED:\n return \"Task failed\"\n if report_status == ReportStatus.IN_PROGRESS:\n return \"Task is in progress\"\n return \"Task completed with partial data\"\n# #endregion build_summary\n\n\n# #region extract_error_context [TYPE Function]\n# @BRIEF Extract normalized error context and next actions for failed/partial reports.\n# @PRE: task is a valid Task object.\n# @POST: Returns ErrorContext for failed/partial when context exists; otherwise None.\ndef extract_error_context(task: Task, report_status: ReportStatus) -> ErrorContext | None:\n with belief_scope(\"extract_error_context\"):\n if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n return None\n\n result = task.result if isinstance(task.result, dict) else {}\n message = None\n code = None\n next_actions = []\n\n if isinstance(result.get(\"error\"), dict):\n error_obj = result.get(\"error\", {})\n message = error_obj.get(\"message\") or message\n code = error_obj.get(\"code\") or code\n actions = error_obj.get(\"next_actions\")\n if isinstance(actions, list):\n next_actions = [str(action) for action in actions if str(action).strip()]\n\n if not message:\n message = result.get(\"error_message\") if isinstance(result.get(\"error_message\"), str) else None\n\n if not message:\n for log in reversed(task.logs):\n if str(log.level).upper() == \"ERROR\" and log.message:\n message = log.message\n break\n\n if not message:\n message = \"Not provided\"\n\n if not next_actions:\n next_actions = [\"Review task diagnostics\", \"Retry the operation\"]\n\n return ErrorContext(code=code, message=message, next_actions=next_actions)\n# #endregion extract_error_context\n\n\n# #region normalize_task_report [TYPE Function]\n# @BRIEF Convert one Task to canonical TaskReport envelope.\n# @PRE: task has valid id and plugin_id fields.\n# @POST: Returns TaskReport with required fields and deterministic fallback behavior.\n#\n# @TEST_CONTRACT: NormalizeTaskReport ->\n# {\n# required_fields: {task: Task},\n# invariants: [\n# \"Returns a valid TaskReport object\",\n# \"Maps TaskStatus to ReportStatus deterministically\",\n# \"Extracts ErrorContext for FAILED/PARTIAL tasks\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task -> {\"task\": \"MockTask(id='1', plugin_id='superset-migration', status=TaskStatus.SUCCESS)\"}\n# @TEST_EDGE: task_with_error -> {\"task\": \"MockTask(status=TaskStatus.FAILED, logs=[LogEntry(level='ERROR', message='Failed')])\"}\n# @TEST_EDGE: unknown_plugin_type -> {\"task\": \"MockTask(plugin_id='unknown-plugin', status=TaskStatus.PENDING)\"}\n# @TEST_INVARIANT: deterministic_normalization -> verifies: [valid_task, task_with_error, unknown_plugin_type]\ndef normalize_task_report(task: Task) -> TaskReport:\n with belief_scope(\"normalize_task_report\"):\n task_type = resolve_task_type(task.plugin_id)\n report_status = status_to_report_status(task.status)\n profile = get_type_profile(task_type)\n\n started_at = task.started_at if isinstance(task.started_at, datetime) else None\n updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None\n if not updated_at:\n updated_at = started_at or datetime.utcnow()\n\n details: dict[str, Any] = {\n \"profile\": {\n \"display_label\": profile.get(\"display_label\"),\n \"visual_variant\": profile.get(\"visual_variant\"),\n \"icon_token\": profile.get(\"icon_token\"),\n \"emphasis_rules\": profile.get(\"emphasis_rules\", []),\n },\n \"result\": task.result if task.result is not None else {\"note\": \"Not provided\"},\n }\n\n source_ref: dict[str, Any] = {}\n if isinstance(task.params, dict):\n for key in (\"environment_id\", \"source_env_id\", \"target_env_id\", \"dashboard_id\", \"dataset_id\", \"resource_id\"):\n if key in task.params:\n source_ref[key] = task.params.get(key)\n\n return TaskReport(\n report_id=task.id,\n task_id=task.id,\n task_type=task_type,\n status=report_status,\n started_at=started_at,\n updated_at=updated_at,\n summary=build_summary(task, report_status),\n details=details,\n error_context=extract_error_context(task, report_status),\n source_ref=source_ref or None,\n )\n# #endregion normalize_task_report\n\n# #endregion normalizer\n" }, { "contract_id": "status_to_report_status", "contract_type": "Function", "file_path": "backend/src/services/reports/normalizer.py", - "start_line": 26, - "end_line": 42, + "start_line": 22, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "status (Any) - Internal task status value.", "POST": "Always returns one of canonical ReportStatus values.", "PRE": "status may be known or unknown string/enum value.", - "PURPOSE": "Normalize internal task status to canonical report status.", - "RETURN": "ReportStatus - Canonical report status." + "PURPOSE": "Normalize internal task status to canonical report status." }, "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", @@ -95698,40 +65335,26 @@ "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": "def", - "body": "# [DEF:status_to_report_status:Function]\n# @PURPOSE: Normalize internal task status to canonical report status.\n# @PRE: status may be known or unknown string/enum value.\n# @POST: Always returns one of canonical ReportStatus values.\n# @PARAM: status (Any) - Internal task status value.\n# @RETURN: ReportStatus - Canonical report status.\ndef status_to_report_status(status: Any) -> ReportStatus:\n with belief_scope(\"status_to_report_status\"):\n raw = str(status.value if isinstance(status, TaskStatus) else status).upper()\n if raw == TaskStatus.SUCCESS.value:\n return ReportStatus.SUCCESS\n if raw == TaskStatus.FAILED.value:\n return ReportStatus.FAILED\n if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:\n return ReportStatus.IN_PROGRESS\n return ReportStatus.PARTIAL\n# [/DEF:status_to_report_status:Function]\n" + "anchor_syntax": "region", + "body": "# #region status_to_report_status [TYPE Function]\n# @BRIEF Normalize internal task status to canonical report status.\n# @PRE: status may be known or unknown string/enum value.\n# @POST: Always returns one of canonical ReportStatus values.\ndef status_to_report_status(status: Any) -> ReportStatus:\n with belief_scope(\"status_to_report_status\"):\n raw = str(status.value if isinstance(status, TaskStatus) else status).upper()\n if raw == TaskStatus.SUCCESS.value:\n return ReportStatus.SUCCESS\n if raw == TaskStatus.FAILED.value:\n return ReportStatus.FAILED\n if raw in {TaskStatus.PENDING.value, TaskStatus.RUNNING.value, TaskStatus.AWAITING_INPUT.value, TaskStatus.AWAITING_MAPPING.value}:\n return ReportStatus.IN_PROGRESS\n return ReportStatus.PARTIAL\n# #endregion status_to_report_status\n" }, { "contract_id": "build_summary", "contract_type": "Function", "file_path": "backend/src/services/reports/normalizer.py", - "start_line": 45, - "end_line": 67, + "start_line": 39, + "end_line": 58, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "report_status (ReportStatus) - Canonical status.", "POST": "Returns non-empty summary text.", "PRE": "report_status is canonical; plugin_id may be unknown.", - "PURPOSE": "Build deterministic user-facing summary from task payload and status.", - "RETURN": "str - Normalized summary." + "PURPOSE": "Build deterministic user-facing summary from task payload and status." }, "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", @@ -95758,40 +65381,26 @@ "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": "def", - "body": "# [DEF:build_summary:Function]\n# @PURPOSE: Build deterministic user-facing summary from task payload and status.\n# @PRE: report_status is canonical; plugin_id may be unknown.\n# @POST: Returns non-empty summary text.\n# @PARAM: task (Task) - Source task object.\n# @PARAM: report_status (ReportStatus) - Canonical status.\n# @RETURN: str - Normalized summary.\ndef build_summary(task: Task, report_status: ReportStatus) -> str:\n with belief_scope(\"build_summary\"):\n result = task.result\n if isinstance(result, dict):\n for key in (\"summary\", \"message\", \"status_message\", \"description\"):\n value = result.get(key)\n if isinstance(value, str) and value.strip():\n return value.strip()\n if report_status == ReportStatus.SUCCESS:\n return \"Task completed successfully\"\n if report_status == ReportStatus.FAILED:\n return \"Task failed\"\n if report_status == ReportStatus.IN_PROGRESS:\n return \"Task is in progress\"\n return \"Task completed with partial data\"\n# [/DEF:build_summary:Function]\n" + "anchor_syntax": "region", + "body": "# #region build_summary [TYPE Function]\n# @BRIEF Build deterministic user-facing summary from task payload and status.\n# @PRE: report_status is canonical; plugin_id may be unknown.\n# @POST: Returns non-empty summary text.\ndef build_summary(task: Task, report_status: ReportStatus) -> str:\n with belief_scope(\"build_summary\"):\n result = task.result\n if isinstance(result, dict):\n for key in (\"summary\", \"message\", \"status_message\", \"description\"):\n value = result.get(key)\n if isinstance(value, str) and value.strip():\n return value.strip()\n if report_status == ReportStatus.SUCCESS:\n return \"Task completed successfully\"\n if report_status == ReportStatus.FAILED:\n return \"Task failed\"\n if report_status == ReportStatus.IN_PROGRESS:\n return \"Task is in progress\"\n return \"Task completed with partial data\"\n# #endregion build_summary\n" }, { "contract_id": "extract_error_context", "contract_type": "Function", "file_path": "backend/src/services/reports/normalizer.py", - "start_line": 70, - "end_line": 111, + "start_line": 61, + "end_line": 99, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "report_status (ReportStatus) - Canonical status.", "POST": "Returns ErrorContext for failed/partial when context exists; otherwise None.", "PRE": "task is a valid Task object.", - "PURPOSE": "Extract normalized error context and next actions for failed/partial reports.", - "RETURN": "Optional[ErrorContext] - Error context block." + "PURPOSE": "Extract normalized error context and next actions for failed/partial reports." }, "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", @@ -95818,41 +65427,27 @@ "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": "def", - "body": "# [DEF:extract_error_context:Function]\n# @PURPOSE: Extract normalized error context and next actions for failed/partial reports.\n# @PRE: task is a valid Task object.\n# @POST: Returns ErrorContext for failed/partial when context exists; otherwise None.\n# @PARAM: task (Task) - Source task.\n# @PARAM: report_status (ReportStatus) - Canonical status.\n# @RETURN: Optional[ErrorContext] - Error context block.\ndef extract_error_context(task: Task, report_status: ReportStatus) -> Optional[ErrorContext]:\n with belief_scope(\"extract_error_context\"):\n if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n return None\n\n result = task.result if isinstance(task.result, dict) else {}\n message = None\n code = None\n next_actions = []\n\n if isinstance(result.get(\"error\"), dict):\n error_obj = result.get(\"error\", {})\n message = error_obj.get(\"message\") or message\n code = error_obj.get(\"code\") or code\n actions = error_obj.get(\"next_actions\")\n if isinstance(actions, list):\n next_actions = [str(action) for action in actions if str(action).strip()]\n\n if not message:\n message = result.get(\"error_message\") if isinstance(result.get(\"error_message\"), str) else None\n\n if not message:\n for log in reversed(task.logs):\n if str(log.level).upper() == \"ERROR\" and log.message:\n message = log.message\n break\n\n if not message:\n message = \"Not provided\"\n\n if not next_actions:\n next_actions = [\"Review task diagnostics\", \"Retry the operation\"]\n\n return ErrorContext(code=code, message=message, next_actions=next_actions)\n# [/DEF:extract_error_context:Function]\n" + "anchor_syntax": "region", + "body": "# #region extract_error_context [TYPE Function]\n# @BRIEF Extract normalized error context and next actions for failed/partial reports.\n# @PRE: task is a valid Task object.\n# @POST: Returns ErrorContext for failed/partial when context exists; otherwise None.\ndef extract_error_context(task: Task, report_status: ReportStatus) -> ErrorContext | None:\n with belief_scope(\"extract_error_context\"):\n if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n return None\n\n result = task.result if isinstance(task.result, dict) else {}\n message = None\n code = None\n next_actions = []\n\n if isinstance(result.get(\"error\"), dict):\n error_obj = result.get(\"error\", {})\n message = error_obj.get(\"message\") or message\n code = error_obj.get(\"code\") or code\n actions = error_obj.get(\"next_actions\")\n if isinstance(actions, list):\n next_actions = [str(action) for action in actions if str(action).strip()]\n\n if not message:\n message = result.get(\"error_message\") if isinstance(result.get(\"error_message\"), str) else None\n\n if not message:\n for log in reversed(task.logs):\n if str(log.level).upper() == \"ERROR\" and log.message:\n message = log.message\n break\n\n if not message:\n message = \"Not provided\"\n\n if not next_actions:\n next_actions = [\"Review task diagnostics\", \"Retry the operation\"]\n\n return ErrorContext(code=code, message=message, next_actions=next_actions)\n# #endregion extract_error_context\n" }, { "contract_id": "normalize_task_report", "contract_type": "Function", "file_path": "backend/src/services/reports/normalizer.py", - "start_line": 114, - "end_line": 173, + "start_line": 102, + "end_line": 159, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "task (Task) - Source task.", "POST": "Returns TaskReport with required fields and deterministic fallback behavior.", "PRE": "task has valid id and plugin_id fields.", "PURPOSE": "Convert one Task to canonical TaskReport envelope.", - "RETURN": "TaskReport - Canonical normalized report.", "TEST_CONTRACT": "NormalizeTaskReport ->" }, "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", @@ -95879,83 +65474,69 @@ "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": "def", - "body": "# [DEF:normalize_task_report:Function]\n# @PURPOSE: Convert one Task to canonical TaskReport envelope.\n# @PRE: task has valid id and plugin_id fields.\n# @POST: Returns TaskReport with required fields and deterministic fallback behavior.\n# @PARAM: task (Task) - Source task.\n# @RETURN: TaskReport - Canonical normalized report.\n#\n# @TEST_CONTRACT: NormalizeTaskReport ->\n# {\n# required_fields: {task: Task},\n# invariants: [\n# \"Returns a valid TaskReport object\",\n# \"Maps TaskStatus to ReportStatus deterministically\",\n# \"Extracts ErrorContext for FAILED/PARTIAL tasks\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task -> {\"task\": \"MockTask(id='1', plugin_id='superset-migration', status=TaskStatus.SUCCESS)\"}\n# @TEST_EDGE: task_with_error -> {\"task\": \"MockTask(status=TaskStatus.FAILED, logs=[LogEntry(level='ERROR', message='Failed')])\"}\n# @TEST_EDGE: unknown_plugin_type -> {\"task\": \"MockTask(plugin_id='unknown-plugin', status=TaskStatus.PENDING)\"}\n# @TEST_INVARIANT: deterministic_normalization -> verifies: [valid_task, task_with_error, unknown_plugin_type]\ndef normalize_task_report(task: Task) -> TaskReport:\n with belief_scope(\"normalize_task_report\"):\n task_type = resolve_task_type(task.plugin_id)\n report_status = status_to_report_status(task.status)\n profile = get_type_profile(task_type)\n\n started_at = task.started_at if isinstance(task.started_at, datetime) else None\n updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None\n if not updated_at:\n updated_at = started_at or datetime.utcnow()\n\n details: Dict[str, Any] = {\n \"profile\": {\n \"display_label\": profile.get(\"display_label\"),\n \"visual_variant\": profile.get(\"visual_variant\"),\n \"icon_token\": profile.get(\"icon_token\"),\n \"emphasis_rules\": profile.get(\"emphasis_rules\", []),\n },\n \"result\": task.result if task.result is not None else {\"note\": \"Not provided\"},\n }\n\n source_ref: Dict[str, Any] = {}\n if isinstance(task.params, dict):\n for key in (\"environment_id\", \"source_env_id\", \"target_env_id\", \"dashboard_id\", \"dataset_id\", \"resource_id\"):\n if key in task.params:\n source_ref[key] = task.params.get(key)\n\n return TaskReport(\n report_id=task.id,\n task_id=task.id,\n task_type=task_type,\n status=report_status,\n started_at=started_at,\n updated_at=updated_at,\n summary=build_summary(task, report_status),\n details=details,\n error_context=extract_error_context(task, report_status),\n source_ref=source_ref or None,\n )\n# [/DEF:normalize_task_report:Function]\n" + "anchor_syntax": "region", + "body": "# #region normalize_task_report [TYPE Function]\n# @BRIEF Convert one Task to canonical TaskReport envelope.\n# @PRE: task has valid id and plugin_id fields.\n# @POST: Returns TaskReport with required fields and deterministic fallback behavior.\n#\n# @TEST_CONTRACT: NormalizeTaskReport ->\n# {\n# required_fields: {task: Task},\n# invariants: [\n# \"Returns a valid TaskReport object\",\n# \"Maps TaskStatus to ReportStatus deterministically\",\n# \"Extracts ErrorContext for FAILED/PARTIAL tasks\"\n# ]\n# }\n# @TEST_FIXTURE: valid_task -> {\"task\": \"MockTask(id='1', plugin_id='superset-migration', status=TaskStatus.SUCCESS)\"}\n# @TEST_EDGE: task_with_error -> {\"task\": \"MockTask(status=TaskStatus.FAILED, logs=[LogEntry(level='ERROR', message='Failed')])\"}\n# @TEST_EDGE: unknown_plugin_type -> {\"task\": \"MockTask(plugin_id='unknown-plugin', status=TaskStatus.PENDING)\"}\n# @TEST_INVARIANT: deterministic_normalization -> verifies: [valid_task, task_with_error, unknown_plugin_type]\ndef normalize_task_report(task: Task) -> TaskReport:\n with belief_scope(\"normalize_task_report\"):\n task_type = resolve_task_type(task.plugin_id)\n report_status = status_to_report_status(task.status)\n profile = get_type_profile(task_type)\n\n started_at = task.started_at if isinstance(task.started_at, datetime) else None\n updated_at = task.finished_at if isinstance(task.finished_at, datetime) else None\n if not updated_at:\n updated_at = started_at or datetime.utcnow()\n\n details: dict[str, Any] = {\n \"profile\": {\n \"display_label\": profile.get(\"display_label\"),\n \"visual_variant\": profile.get(\"visual_variant\"),\n \"icon_token\": profile.get(\"icon_token\"),\n \"emphasis_rules\": profile.get(\"emphasis_rules\", []),\n },\n \"result\": task.result if task.result is not None else {\"note\": \"Not provided\"},\n }\n\n source_ref: dict[str, Any] = {}\n if isinstance(task.params, dict):\n for key in (\"environment_id\", \"source_env_id\", \"target_env_id\", \"dashboard_id\", \"dataset_id\", \"resource_id\"):\n if key in task.params:\n source_ref[key] = task.params.get(key)\n\n return TaskReport(\n report_id=task.id,\n task_id=task.id,\n task_type=task_type,\n status=report_status,\n started_at=started_at,\n updated_at=updated_at,\n summary=build_summary(task, report_status),\n details=details,\n error_context=extract_error_context(task, report_status),\n source_ref=source_ref or None,\n )\n# #endregion normalize_task_report\n" }, { "contract_id": "report_service", "contract_type": "Module", "file_path": "backend/src/services/reports/report_service.py", "start_line": 1, - "end_line": 310, + "end_line": 302, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "ReportQuery -> ReportRow; session_id -> valid UUID", "INVARIANT": "ReportService maintains consistent report structure", "LAYER": "Domain", "POST": "Returns Report with generated summary", "PRE": "session is active and valid", "PURPOSE": "Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases.", - "SEMANTICS": [ - "reports", - "service", - "aggregation", - "filtering", - "pagination", - "detail" - ], "SIDE_EFFECT": "Read-only database operations; logs report generation" }, "relations": [ { "source_id": "report_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" }, { "source_id": "report_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskReport", "target_ref": "[TaskReport]" }, { "source_id": "report_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ReportQuery", "target_ref": "[ReportQuery]" }, { "source_id": "report_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ReportCollection", "target_ref": "[ReportCollection]" }, { "source_id": "report_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "ReportDetailView", "target_ref": "[ReportDetailView]" }, { "source_id": "report_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "normalize_task_report", "target_ref": "[normalize_task_report]" }, { "source_id": "report_service", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseRepository", "target_ref": "[CleanReleaseRepository]" } @@ -95978,147 +65559,21 @@ "actual_complexity": 5, "contract_type": "Module" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "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]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:report_service:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: reports, service, aggregation, filtering, pagination, detail\n# @PURPOSE: Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases.\n# @LAYER: Domain\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n# @RELATION: [DEPENDS_ON] ->[TaskReport]\n# @RELATION: [DEPENDS_ON] ->[ReportQuery]\n# @RELATION: [DEPENDS_ON] ->[ReportCollection]\n# @RELATION: [DEPENDS_ON] ->[ReportDetailView]\n# @RELATION: [DEPENDS_ON] ->[normalize_task_report]\n# @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository]\n# @INVARIANT: ReportService maintains consistent report structure\n# @DATA_CONTRACT: ReportQuery -> ReportRow; session_id -> valid UUID\n# @PRE: session is active and valid\n# @POST: Returns Report with generated summary\n# @SIDE_EFFECT: Read-only database operations; logs report generation\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime, timezone\nfrom typing import List, Optional\n\nfrom ...core.logger import belief_scope\n\nfrom ...core.task_manager import TaskManager\nfrom ...models.report import (\n ReportCollection,\n ReportDetailView,\n ReportQuery,\n ReportStatus,\n TaskReport,\n TaskType,\n)\nfrom ..clean_release.repository import CleanReleaseRepository\nfrom .normalizer import normalize_task_report\n# [/SECTION]\n\n\n# [DEF:ReportsService:Class]\n# @PURPOSE: Service layer for list/detail report retrieval and normalization.\n# @COMPLEXITY: 5\n# @PRE: TaskManager dependency is initialized.\n# @POST: Provides deterministic list/detail report responses.\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n# @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository]\n# @RELATION: [CALLS] ->[normalize_task_report]\n# @SIDE_EFFECT: Reads task history and optional clean-release repository state without mutating source records.\n# @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]\n# @INVARIANT: Service methods are read-only over task history source.\n#\n# @TEST_CONTRACT: ReportsServiceModel ->\n# {\n# required_fields: {task_manager: TaskManager},\n# invariants: [\n# \"list_reports returns a matching ReportCollection\",\n# \"get_report_detail returns a valid ReportDetailView or None\"\n# ]\n# }\n# @TEST_FIXTURE: valid_service -> {\"task_manager\": \"MockTaskManager()\"}\n# @TEST_EDGE: empty_task_list -> returns empty ReportCollection\n# @TEST_EDGE: report_not_found -> get_report_detail returns None\n# @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service]\nclass ReportsService:\n # [DEF:init:Function]\n # @COMPLEXITY: 5\n # @PURPOSE: Initialize service with TaskManager dependency.\n # @PRE: task_manager is a live TaskManager instance.\n # @POST: self.task_manager is assigned and ready for read operations.\n # @INVARIANT: Constructor performs no task mutations.\n # @RELATION: [BINDS_TO] ->[ReportsService]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository]\n # @SIDE_EFFECT: Stores collaborator references for later read-only report projections.\n # @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService]\n # @PARAM: task_manager (TaskManager) - Task manager providing source task history.\n def __init__(\n self,\n task_manager: TaskManager,\n clean_release_repository: Optional[CleanReleaseRepository] = None,\n ):\n with belief_scope(\"__init__\"):\n self.task_manager = task_manager\n self.clean_release_repository = clean_release_repository\n\n # [/DEF:init:Function]\n\n # [DEF:_load_normalized_reports:Function]\n # @PURPOSE: Build normalized reports from all available tasks.\n # @PRE: Task manager returns iterable task history records.\n # @POST: Returns normalized report list preserving source cardinality.\n # @INVARIANT: Every returned item is a TaskReport.\n # @RETURN: List[TaskReport] - Reports sorted later by list logic.\n def _load_normalized_reports(self) -> List[TaskReport]:\n with belief_scope(\"_load_normalized_reports\"):\n tasks = self.task_manager.get_all_tasks()\n reports = [normalize_task_report(task) for task in tasks]\n return reports\n\n # [/DEF:_load_normalized_reports:Function]\n\n # [DEF:_to_utc_datetime:Function]\n # @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.\n # @PRE: value is either datetime or None.\n # @POST: Returns UTC-aware datetime or None.\n # @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.\n # @PARAM: value (Optional[datetime]) - Source datetime value.\n # @RETURN: Optional[datetime] - UTC-aware datetime or None.\n def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]:\n with belief_scope(\"_to_utc_datetime\"):\n if value is None:\n return None\n if value.tzinfo is None:\n return value.replace(tzinfo=timezone.utc)\n return value.astimezone(timezone.utc)\n\n # [/DEF:_to_utc_datetime:Function]\n\n # [DEF:_datetime_sort_key:Function]\n # @PURPOSE: Produce stable numeric sort key for report timestamps.\n # @PRE: report contains updated_at datetime.\n # @POST: Returns float timestamp suitable for deterministic sorting.\n # @INVARIANT: Mixed naive/aware datetimes never raise TypeError.\n # @PARAM: report (TaskReport) - Report item.\n # @RETURN: float - UTC timestamp key.\n def _datetime_sort_key(self, report: TaskReport) -> float:\n with belief_scope(\"_datetime_sort_key\"):\n updated = self._to_utc_datetime(report.updated_at)\n if updated is None:\n return 0.0\n return updated.timestamp()\n\n # [/DEF:_datetime_sort_key:Function]\n\n # [DEF:_matches_query:Function]\n # @PURPOSE: Apply query filtering to a report.\n # @PRE: report and query are normalized schema instances.\n # @POST: Returns True iff report satisfies all active query filters.\n # @INVARIANT: Filter evaluation is side-effect free.\n # @PARAM: report (TaskReport) - Candidate report.\n # @PARAM: query (ReportQuery) - Applied query.\n # @RETURN: bool - True if report matches all filters.\n def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:\n with belief_scope(\"_matches_query\"):\n if query.task_types and report.task_type not in query.task_types:\n return False\n if query.statuses and report.status not in query.statuses:\n return False\n report_updated_at = self._to_utc_datetime(report.updated_at)\n query_time_from = self._to_utc_datetime(query.time_from)\n query_time_to = self._to_utc_datetime(query.time_to)\n\n if (\n query_time_from\n and report_updated_at\n and report_updated_at < query_time_from\n ):\n return False\n if (\n query_time_to\n and report_updated_at\n and report_updated_at > query_time_to\n ):\n return False\n if query.search:\n needle = query.search.lower()\n haystack = f\"{report.summary} {report.task_type.value} {report.status.value}\".lower()\n if needle not in haystack:\n return False\n return True\n\n # [/DEF:_matches_query:Function]\n\n # [DEF:_sort_reports:Function]\n # @PURPOSE: Sort reports deterministically according to query settings.\n # @PRE: reports contains only TaskReport items.\n # @POST: Returns reports ordered by selected sort field and order.\n # @INVARIANT: Sorting criteria are deterministic for equal input.\n # @PARAM: reports (List[TaskReport]) - Filtered reports.\n # @PARAM: query (ReportQuery) - Sort config.\n # @RETURN: List[TaskReport] - Sorted reports.\n def _sort_reports(\n self, reports: List[TaskReport], query: ReportQuery\n ) -> List[TaskReport]:\n with belief_scope(\"_sort_reports\"):\n reverse = query.sort_order == \"desc\"\n\n if query.sort_by == \"status\":\n reports.sort(key=lambda item: item.status.value, reverse=reverse)\n elif query.sort_by == \"task_type\":\n reports.sort(key=lambda item: item.task_type.value, reverse=reverse)\n else:\n reports.sort(key=self._datetime_sort_key, reverse=reverse)\n\n return reports\n\n # [/DEF:_sort_reports:Function]\n\n # [DEF:list_reports:Function]\n # @PURPOSE: Return filtered, sorted, paginated report collection.\n # @PRE: query has passed schema validation.\n # @POST: Returns {items,total,page,page_size,has_next,applied_filters}.\n # @PARAM: query (ReportQuery) - List filters and pagination.\n # @RETURN: ReportCollection - Paginated unified reports payload.\n def list_reports(self, query: ReportQuery) -> ReportCollection:\n with belief_scope(\"list_reports\"):\n reports = self._load_normalized_reports()\n filtered = [\n report for report in reports if self._matches_query(report, query)\n ]\n sorted_reports = self._sort_reports(filtered, query)\n\n total = len(sorted_reports)\n start = (query.page - 1) * query.page_size\n end = start + query.page_size\n items = sorted_reports[start:end]\n has_next = end < total\n\n return ReportCollection(\n items=items,\n total=total,\n page=query.page,\n page_size=query.page_size,\n has_next=has_next,\n applied_filters=query,\n )\n\n # [/DEF:list_reports:Function]\n\n # [DEF:get_report_detail:Function]\n # @PURPOSE: Return one normalized report with timeline/diagnostics/next actions.\n # @PRE: report_id exists in normalized report set.\n # @POST: Returns normalized detail envelope with diagnostics and next actions where applicable.\n # @PARAM: report_id (str) - Stable report identifier.\n # @RETURN: Optional[ReportDetailView] - Detailed report or None if not found.\n def get_report_detail(self, report_id: str) -> Optional[ReportDetailView]:\n with belief_scope(\"get_report_detail\"):\n reports = self._load_normalized_reports()\n target = next(\n (report for report in reports if report.report_id == report_id), None\n )\n if not target:\n return None\n\n timeline = []\n if target.started_at:\n timeline.append(\n {\"event\": \"started\", \"at\": target.started_at.isoformat()}\n )\n timeline.append({\"event\": \"updated\", \"at\": target.updated_at.isoformat()})\n\n diagnostics = target.details or {}\n if not diagnostics:\n diagnostics = {\"note\": \"Not provided\"}\n if target.error_context:\n diagnostics[\"error_context\"] = target.error_context.model_dump()\n\n if (\n target.task_type == TaskType.CLEAN_RELEASE\n and self.clean_release_repository is not None\n ):\n run_id = None\n if isinstance(diagnostics, dict):\n result_payload = diagnostics.get(\"result\")\n if isinstance(result_payload, dict):\n run_id = result_payload.get(\"run_id\") or result_payload.get(\n \"check_run_id\"\n )\n if run_id:\n run = self.clean_release_repository.get_check_run(str(run_id))\n if run is not None:\n diagnostics[\"clean_release_run\"] = {\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"status\": run.status,\n \"final_status\": run.final_status,\n \"requested_by\": run.requested_by,\n }\n linked_report = next(\n (\n item\n for item in self.clean_release_repository.reports.values()\n if item.run_id == run.id\n ),\n None,\n )\n if linked_report is not None:\n diagnostics[\"clean_release_report\"] = {\n \"report_id\": linked_report.id,\n \"final_status\": linked_report.final_status,\n }\n\n next_actions = []\n if target.error_context and target.error_context.next_actions:\n next_actions = target.error_context.next_actions\n elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n next_actions = [\"Review diagnostics\", \"Retry task if applicable\"]\n\n return ReportDetailView(\n report=target,\n timeline=timeline,\n diagnostics=diagnostics,\n next_actions=next_actions,\n )\n\n # [/DEF:get_report_detail:Function]\n\n\n# [/DEF:ReportsService:Class]\n\n# [/DEF:report_service:Module]\n" + "anchor_syntax": "region", + "body": "# #region report_service [C:5] [TYPE Module] [SEMANTICS report, task, filter, paginate, aggregate]\n# @BRIEF Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases.\n# @LAYER: Domain\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskReport]\n# @RELATION DEPENDS_ON -> [ReportQuery]\n# @RELATION DEPENDS_ON -> [ReportCollection]\n# @RELATION DEPENDS_ON -> [ReportDetailView]\n# @RELATION DEPENDS_ON -> [normalize_task_report]\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @INVARIANT: ReportService maintains consistent report structure\n# @DATA_CONTRACT: ReportQuery -> ReportRow; session_id -> valid UUID\n# @PRE: session is active and valid\n# @POST: Returns Report with generated summary\n# @SIDE_EFFECT: Read-only database operations; logs report generation\n\nfrom datetime import UTC, datetime\n\nfrom ...core.logger import belief_scope\nfrom ...core.task_manager import TaskManager\nfrom ...models.report import (\n ReportCollection,\n ReportDetailView,\n ReportQuery,\n ReportStatus,\n TaskReport,\n TaskType,\n)\nfrom ..clean_release.repository import CleanReleaseRepository\nfrom .normalizer import normalize_task_report\n\n\n# #region ReportsService [C:5] [TYPE Class]\n# @BRIEF Service layer for list/detail report retrieval and normalization.\n# @PRE: TaskManager dependency is initialized.\n# @POST: Provides deterministic list/detail report responses.\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION CALLS -> [normalize_task_report]\n# @SIDE_EFFECT: Reads task history and optional clean-release repository state without mutating source records.\n# @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]\n# @INVARIANT: Service methods are read-only over task history source.\n#\n# @TEST_CONTRACT: ReportsServiceModel ->\n# {\n# required_fields: {task_manager: TaskManager},\n# invariants: [\n# \"list_reports returns a matching ReportCollection\",\n# \"get_report_detail returns a valid ReportDetailView or None\"\n# ]\n# }\n# @TEST_FIXTURE: valid_service -> {\"task_manager\": \"MockTaskManager()\"}\n# @TEST_EDGE: empty_task_list -> returns empty ReportCollection\n# @TEST_EDGE: report_not_found -> get_report_detail returns None\n# @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service]\nclass ReportsService:\n # region init [TYPE Function]\n # @PURPOSE: Initialize service with TaskManager dependency.\n # @PRE: task_manager is a live TaskManager instance.\n # @POST: self.task_manager is assigned and ready for read operations.\n # @INVARIANT: Constructor performs no task mutations.\n # @RELATION: [BINDS_TO] ->[ReportsService]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository]\n # @SIDE_EFFECT: Stores collaborator references for later read-only report projections.\n # @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService]\n # @PARAM: task_manager (TaskManager) - Task manager providing source task history.\n def __init__(\n self,\n task_manager: TaskManager,\n clean_release_repository: CleanReleaseRepository | None = None,\n ):\n with belief_scope(\"__init__\"):\n self.task_manager = task_manager\n self.clean_release_repository = clean_release_repository\n\n # endregion init\n\n # region _load_normalized_reports [TYPE Function]\n # @PURPOSE: Build normalized reports from all available tasks.\n # @PRE: Task manager returns iterable task history records.\n # @POST: Returns normalized report list preserving source cardinality.\n # @INVARIANT: Every returned item is a TaskReport.\n # @RETURN: List[TaskReport] - Reports sorted later by list logic.\n def _load_normalized_reports(self) -> list[TaskReport]:\n with belief_scope(\"_load_normalized_reports\"):\n tasks = self.task_manager.get_all_tasks()\n reports = [normalize_task_report(task) for task in tasks]\n return reports\n\n # endregion _load_normalized_reports\n\n # region _to_utc_datetime [TYPE Function]\n # @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.\n # @PRE: value is either datetime or None.\n # @POST: Returns UTC-aware datetime or None.\n # @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.\n # @PARAM: value (Optional[datetime]) - Source datetime value.\n # @RETURN: Optional[datetime] - UTC-aware datetime or None.\n def _to_utc_datetime(self, value: datetime | None) -> datetime | None:\n with belief_scope(\"_to_utc_datetime\"):\n if value is None:\n return None\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n\n # endregion _to_utc_datetime\n\n # region _datetime_sort_key [TYPE Function]\n # @PURPOSE: Produce stable numeric sort key for report timestamps.\n # @PRE: report contains updated_at datetime.\n # @POST: Returns float timestamp suitable for deterministic sorting.\n # @INVARIANT: Mixed naive/aware datetimes never raise TypeError.\n # @PARAM: report (TaskReport) - Report item.\n # @RETURN: float - UTC timestamp key.\n def _datetime_sort_key(self, report: TaskReport) -> float:\n with belief_scope(\"_datetime_sort_key\"):\n updated = self._to_utc_datetime(report.updated_at)\n if updated is None:\n return 0.0\n return updated.timestamp()\n\n # endregion _datetime_sort_key\n\n # region _matches_query [TYPE Function]\n # @PURPOSE: Apply query filtering to a report.\n # @PRE: report and query are normalized schema instances.\n # @POST: Returns True iff report satisfies all active query filters.\n # @INVARIANT: Filter evaluation is side-effect free.\n # @PARAM: report (TaskReport) - Candidate report.\n # @PARAM: query (ReportQuery) - Applied query.\n # @RETURN: bool - True if report matches all filters.\n def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:\n with belief_scope(\"_matches_query\"):\n if query.task_types and report.task_type not in query.task_types:\n return False\n if query.statuses and report.status not in query.statuses:\n return False\n report_updated_at = self._to_utc_datetime(report.updated_at)\n query_time_from = self._to_utc_datetime(query.time_from)\n query_time_to = self._to_utc_datetime(query.time_to)\n\n if (\n query_time_from\n and report_updated_at\n and report_updated_at < query_time_from\n ):\n return False\n if (\n query_time_to\n and report_updated_at\n and report_updated_at > query_time_to\n ):\n return False\n if query.search:\n needle = query.search.lower()\n haystack = f\"{report.summary} {report.task_type.value} {report.status.value}\".lower()\n if needle not in haystack:\n return False\n return True\n\n # endregion _matches_query\n\n # region _sort_reports [TYPE Function]\n # @PURPOSE: Sort reports deterministically according to query settings.\n # @PRE: reports contains only TaskReport items.\n # @POST: Returns reports ordered by selected sort field and order.\n # @INVARIANT: Sorting criteria are deterministic for equal input.\n # @PARAM: reports (List[TaskReport]) - Filtered reports.\n # @PARAM: query (ReportQuery) - Sort config.\n # @RETURN: List[TaskReport] - Sorted reports.\n def _sort_reports(\n self, reports: list[TaskReport], query: ReportQuery\n ) -> list[TaskReport]:\n with belief_scope(\"_sort_reports\"):\n reverse = query.sort_order == \"desc\"\n\n if query.sort_by == \"status\":\n reports.sort(key=lambda item: item.status.value, reverse=reverse)\n elif query.sort_by == \"task_type\":\n reports.sort(key=lambda item: item.task_type.value, reverse=reverse)\n else:\n reports.sort(key=self._datetime_sort_key, reverse=reverse)\n\n return reports\n\n # endregion _sort_reports\n\n # region list_reports [TYPE Function]\n # @PURPOSE: Return filtered, sorted, paginated report collection.\n # @PRE: query has passed schema validation.\n # @POST: Returns {items,total,page,page_size,has_next,applied_filters}.\n # @PARAM: query (ReportQuery) - List filters and pagination.\n # @RETURN: ReportCollection - Paginated unified reports payload.\n def list_reports(self, query: ReportQuery) -> ReportCollection:\n with belief_scope(\"list_reports\"):\n reports = self._load_normalized_reports()\n filtered = [\n report for report in reports if self._matches_query(report, query)\n ]\n sorted_reports = self._sort_reports(filtered, query)\n\n total = len(sorted_reports)\n start = (query.page - 1) * query.page_size\n end = start + query.page_size\n items = sorted_reports[start:end]\n has_next = end < total\n\n return ReportCollection(\n items=items,\n total=total,\n page=query.page,\n page_size=query.page_size,\n has_next=has_next,\n applied_filters=query,\n )\n\n # endregion list_reports\n\n # region get_report_detail [TYPE Function]\n # @PURPOSE: Return one normalized report with timeline/diagnostics/next actions.\n # @PRE: report_id exists in normalized report set.\n # @POST: Returns normalized detail envelope with diagnostics and next actions where applicable.\n # @PARAM: report_id (str) - Stable report identifier.\n # @RETURN: Optional[ReportDetailView] - Detailed report or None if not found.\n def get_report_detail(self, report_id: str) -> ReportDetailView | None:\n with belief_scope(\"get_report_detail\"):\n reports = self._load_normalized_reports()\n target = next(\n (report for report in reports if report.report_id == report_id), None\n )\n if not target:\n return None\n\n timeline = []\n if target.started_at:\n timeline.append(\n {\"event\": \"started\", \"at\": target.started_at.isoformat()}\n )\n timeline.append({\"event\": \"updated\", \"at\": target.updated_at.isoformat()})\n\n diagnostics = target.details or {}\n if not diagnostics:\n diagnostics = {\"note\": \"Not provided\"}\n if target.error_context:\n diagnostics[\"error_context\"] = target.error_context.model_dump()\n\n if (\n target.task_type == TaskType.CLEAN_RELEASE\n and self.clean_release_repository is not None\n ):\n run_id = None\n if isinstance(diagnostics, dict):\n result_payload = diagnostics.get(\"result\")\n if isinstance(result_payload, dict):\n run_id = result_payload.get(\"run_id\") or result_payload.get(\n \"check_run_id\"\n )\n if run_id:\n run = self.clean_release_repository.get_check_run(str(run_id))\n if run is not None:\n diagnostics[\"clean_release_run\"] = {\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"status\": run.status,\n \"final_status\": run.final_status,\n \"requested_by\": run.requested_by,\n }\n linked_report = next(\n (\n item\n for item in self.clean_release_repository.reports.values()\n if item.run_id == run.id\n ),\n None,\n )\n if linked_report is not None:\n diagnostics[\"clean_release_report\"] = {\n \"report_id\": linked_report.id,\n \"final_status\": linked_report.final_status,\n }\n\n next_actions = []\n if target.error_context and target.error_context.next_actions:\n next_actions = target.error_context.next_actions\n elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n next_actions = [\"Review diagnostics\", \"Retry task if applicable\"]\n\n return ReportDetailView(\n report=target,\n timeline=timeline,\n diagnostics=diagnostics,\n next_actions=next_actions,\n )\n\n # endregion get_report_detail\n\n\n# #endregion ReportsService\n\n# #endregion report_service\n" }, { "contract_id": "ReportsService", "contract_type": "Class", "file_path": "backend/src/services/reports/report_service.py", - "start_line": 39, - "end_line": 308, + "start_line": 33, + "end_line": 300, "tier": "TIER_3", "complexity": 5, "metadata": { - "COMPLEXITY": "5", + "COMPLEXITY": 5, "DATA_CONTRACT": "Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]", "INVARIANT": "Service methods are read-only over task history source.", "POST": "Provides deterministic list/detail report responses.", @@ -96130,19 +65585,19 @@ "relations": [ { "source_id": "ReportsService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "TaskManager", "target_ref": "[TaskManager]" }, { "source_id": "ReportsService", - "relation_type": "[DEPENDS_ON]", + "relation_type": "DEPENDS_ON", "target_id": "CleanReleaseRepository", "target_ref": "[CleanReleaseRepository]" }, { "source_id": "ReportsService", - "relation_type": "[CALLS]", + "relation_type": "CALLS", "target_id": "normalize_task_report", "target_ref": "[normalize_task_report]" } @@ -96186,431 +65641,34 @@ "actual_complexity": 5, "contract_type": "Class" } - }, - { - "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]" - } - }, - { - "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]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } } ], - "anchor_syntax": "def", - "body": "# [DEF:ReportsService:Class]\n# @PURPOSE: Service layer for list/detail report retrieval and normalization.\n# @COMPLEXITY: 5\n# @PRE: TaskManager dependency is initialized.\n# @POST: Provides deterministic list/detail report responses.\n# @RELATION: [DEPENDS_ON] ->[TaskManager]\n# @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository]\n# @RELATION: [CALLS] ->[normalize_task_report]\n# @SIDE_EFFECT: Reads task history and optional clean-release repository state without mutating source records.\n# @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]\n# @INVARIANT: Service methods are read-only over task history source.\n#\n# @TEST_CONTRACT: ReportsServiceModel ->\n# {\n# required_fields: {task_manager: TaskManager},\n# invariants: [\n# \"list_reports returns a matching ReportCollection\",\n# \"get_report_detail returns a valid ReportDetailView or None\"\n# ]\n# }\n# @TEST_FIXTURE: valid_service -> {\"task_manager\": \"MockTaskManager()\"}\n# @TEST_EDGE: empty_task_list -> returns empty ReportCollection\n# @TEST_EDGE: report_not_found -> get_report_detail returns None\n# @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service]\nclass ReportsService:\n # [DEF:init:Function]\n # @COMPLEXITY: 5\n # @PURPOSE: Initialize service with TaskManager dependency.\n # @PRE: task_manager is a live TaskManager instance.\n # @POST: self.task_manager is assigned and ready for read operations.\n # @INVARIANT: Constructor performs no task mutations.\n # @RELATION: [BINDS_TO] ->[ReportsService]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository]\n # @SIDE_EFFECT: Stores collaborator references for later read-only report projections.\n # @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService]\n # @PARAM: task_manager (TaskManager) - Task manager providing source task history.\n def __init__(\n self,\n task_manager: TaskManager,\n clean_release_repository: Optional[CleanReleaseRepository] = None,\n ):\n with belief_scope(\"__init__\"):\n self.task_manager = task_manager\n self.clean_release_repository = clean_release_repository\n\n # [/DEF:init:Function]\n\n # [DEF:_load_normalized_reports:Function]\n # @PURPOSE: Build normalized reports from all available tasks.\n # @PRE: Task manager returns iterable task history records.\n # @POST: Returns normalized report list preserving source cardinality.\n # @INVARIANT: Every returned item is a TaskReport.\n # @RETURN: List[TaskReport] - Reports sorted later by list logic.\n def _load_normalized_reports(self) -> List[TaskReport]:\n with belief_scope(\"_load_normalized_reports\"):\n tasks = self.task_manager.get_all_tasks()\n reports = [normalize_task_report(task) for task in tasks]\n return reports\n\n # [/DEF:_load_normalized_reports:Function]\n\n # [DEF:_to_utc_datetime:Function]\n # @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.\n # @PRE: value is either datetime or None.\n # @POST: Returns UTC-aware datetime or None.\n # @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.\n # @PARAM: value (Optional[datetime]) - Source datetime value.\n # @RETURN: Optional[datetime] - UTC-aware datetime or None.\n def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]:\n with belief_scope(\"_to_utc_datetime\"):\n if value is None:\n return None\n if value.tzinfo is None:\n return value.replace(tzinfo=timezone.utc)\n return value.astimezone(timezone.utc)\n\n # [/DEF:_to_utc_datetime:Function]\n\n # [DEF:_datetime_sort_key:Function]\n # @PURPOSE: Produce stable numeric sort key for report timestamps.\n # @PRE: report contains updated_at datetime.\n # @POST: Returns float timestamp suitable for deterministic sorting.\n # @INVARIANT: Mixed naive/aware datetimes never raise TypeError.\n # @PARAM: report (TaskReport) - Report item.\n # @RETURN: float - UTC timestamp key.\n def _datetime_sort_key(self, report: TaskReport) -> float:\n with belief_scope(\"_datetime_sort_key\"):\n updated = self._to_utc_datetime(report.updated_at)\n if updated is None:\n return 0.0\n return updated.timestamp()\n\n # [/DEF:_datetime_sort_key:Function]\n\n # [DEF:_matches_query:Function]\n # @PURPOSE: Apply query filtering to a report.\n # @PRE: report and query are normalized schema instances.\n # @POST: Returns True iff report satisfies all active query filters.\n # @INVARIANT: Filter evaluation is side-effect free.\n # @PARAM: report (TaskReport) - Candidate report.\n # @PARAM: query (ReportQuery) - Applied query.\n # @RETURN: bool - True if report matches all filters.\n def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:\n with belief_scope(\"_matches_query\"):\n if query.task_types and report.task_type not in query.task_types:\n return False\n if query.statuses and report.status not in query.statuses:\n return False\n report_updated_at = self._to_utc_datetime(report.updated_at)\n query_time_from = self._to_utc_datetime(query.time_from)\n query_time_to = self._to_utc_datetime(query.time_to)\n\n if (\n query_time_from\n and report_updated_at\n and report_updated_at < query_time_from\n ):\n return False\n if (\n query_time_to\n and report_updated_at\n and report_updated_at > query_time_to\n ):\n return False\n if query.search:\n needle = query.search.lower()\n haystack = f\"{report.summary} {report.task_type.value} {report.status.value}\".lower()\n if needle not in haystack:\n return False\n return True\n\n # [/DEF:_matches_query:Function]\n\n # [DEF:_sort_reports:Function]\n # @PURPOSE: Sort reports deterministically according to query settings.\n # @PRE: reports contains only TaskReport items.\n # @POST: Returns reports ordered by selected sort field and order.\n # @INVARIANT: Sorting criteria are deterministic for equal input.\n # @PARAM: reports (List[TaskReport]) - Filtered reports.\n # @PARAM: query (ReportQuery) - Sort config.\n # @RETURN: List[TaskReport] - Sorted reports.\n def _sort_reports(\n self, reports: List[TaskReport], query: ReportQuery\n ) -> List[TaskReport]:\n with belief_scope(\"_sort_reports\"):\n reverse = query.sort_order == \"desc\"\n\n if query.sort_by == \"status\":\n reports.sort(key=lambda item: item.status.value, reverse=reverse)\n elif query.sort_by == \"task_type\":\n reports.sort(key=lambda item: item.task_type.value, reverse=reverse)\n else:\n reports.sort(key=self._datetime_sort_key, reverse=reverse)\n\n return reports\n\n # [/DEF:_sort_reports:Function]\n\n # [DEF:list_reports:Function]\n # @PURPOSE: Return filtered, sorted, paginated report collection.\n # @PRE: query has passed schema validation.\n # @POST: Returns {items,total,page,page_size,has_next,applied_filters}.\n # @PARAM: query (ReportQuery) - List filters and pagination.\n # @RETURN: ReportCollection - Paginated unified reports payload.\n def list_reports(self, query: ReportQuery) -> ReportCollection:\n with belief_scope(\"list_reports\"):\n reports = self._load_normalized_reports()\n filtered = [\n report for report in reports if self._matches_query(report, query)\n ]\n sorted_reports = self._sort_reports(filtered, query)\n\n total = len(sorted_reports)\n start = (query.page - 1) * query.page_size\n end = start + query.page_size\n items = sorted_reports[start:end]\n has_next = end < total\n\n return ReportCollection(\n items=items,\n total=total,\n page=query.page,\n page_size=query.page_size,\n has_next=has_next,\n applied_filters=query,\n )\n\n # [/DEF:list_reports:Function]\n\n # [DEF:get_report_detail:Function]\n # @PURPOSE: Return one normalized report with timeline/diagnostics/next actions.\n # @PRE: report_id exists in normalized report set.\n # @POST: Returns normalized detail envelope with diagnostics and next actions where applicable.\n # @PARAM: report_id (str) - Stable report identifier.\n # @RETURN: Optional[ReportDetailView] - Detailed report or None if not found.\n def get_report_detail(self, report_id: str) -> Optional[ReportDetailView]:\n with belief_scope(\"get_report_detail\"):\n reports = self._load_normalized_reports()\n target = next(\n (report for report in reports if report.report_id == report_id), None\n )\n if not target:\n return None\n\n timeline = []\n if target.started_at:\n timeline.append(\n {\"event\": \"started\", \"at\": target.started_at.isoformat()}\n )\n timeline.append({\"event\": \"updated\", \"at\": target.updated_at.isoformat()})\n\n diagnostics = target.details or {}\n if not diagnostics:\n diagnostics = {\"note\": \"Not provided\"}\n if target.error_context:\n diagnostics[\"error_context\"] = target.error_context.model_dump()\n\n if (\n target.task_type == TaskType.CLEAN_RELEASE\n and self.clean_release_repository is not None\n ):\n run_id = None\n if isinstance(diagnostics, dict):\n result_payload = diagnostics.get(\"result\")\n if isinstance(result_payload, dict):\n run_id = result_payload.get(\"run_id\") or result_payload.get(\n \"check_run_id\"\n )\n if run_id:\n run = self.clean_release_repository.get_check_run(str(run_id))\n if run is not None:\n diagnostics[\"clean_release_run\"] = {\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"status\": run.status,\n \"final_status\": run.final_status,\n \"requested_by\": run.requested_by,\n }\n linked_report = next(\n (\n item\n for item in self.clean_release_repository.reports.values()\n if item.run_id == run.id\n ),\n None,\n )\n if linked_report is not None:\n diagnostics[\"clean_release_report\"] = {\n \"report_id\": linked_report.id,\n \"final_status\": linked_report.final_status,\n }\n\n next_actions = []\n if target.error_context and target.error_context.next_actions:\n next_actions = target.error_context.next_actions\n elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n next_actions = [\"Review diagnostics\", \"Retry task if applicable\"]\n\n return ReportDetailView(\n report=target,\n timeline=timeline,\n diagnostics=diagnostics,\n next_actions=next_actions,\n )\n\n # [/DEF:get_report_detail:Function]\n\n\n# [/DEF:ReportsService:Class]\n" - }, - { - "contract_id": "_load_normalized_reports", - "contract_type": "Function", - "file_path": "backend/src/services/reports/report_service.py", - "start_line": 87, - "end_line": 99, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Every returned item is a TaskReport.", - "POST": "Returns normalized report list preserving source cardinality.", - "PRE": "Task manager returns iterable task history records.", - "PURPOSE": "Build normalized reports from all available tasks.", - "RETURN": "List[TaskReport] - Reports sorted later by list logic." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_load_normalized_reports:Function]\n # @PURPOSE: Build normalized reports from all available tasks.\n # @PRE: Task manager returns iterable task history records.\n # @POST: Returns normalized report list preserving source cardinality.\n # @INVARIANT: Every returned item is a TaskReport.\n # @RETURN: List[TaskReport] - Reports sorted later by list logic.\n def _load_normalized_reports(self) -> List[TaskReport]:\n with belief_scope(\"_load_normalized_reports\"):\n tasks = self.task_manager.get_all_tasks()\n reports = [normalize_task_report(task) for task in tasks]\n return reports\n\n # [/DEF:_load_normalized_reports:Function]\n" - }, - { - "contract_id": "_to_utc_datetime", - "contract_type": "Function", - "file_path": "backend/src/services/reports/report_service.py", - "start_line": 101, - "end_line": 116, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.", - "PARAM": "value (Optional[datetime]) - Source datetime value.", - "POST": "Returns UTC-aware datetime or None.", - "PRE": "value is either datetime or None.", - "PURPOSE": "Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.", - "RETURN": "Optional[datetime] - UTC-aware datetime or None." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_to_utc_datetime:Function]\n # @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.\n # @PRE: value is either datetime or None.\n # @POST: Returns UTC-aware datetime or None.\n # @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.\n # @PARAM: value (Optional[datetime]) - Source datetime value.\n # @RETURN: Optional[datetime] - UTC-aware datetime or None.\n def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]:\n with belief_scope(\"_to_utc_datetime\"):\n if value is None:\n return None\n if value.tzinfo is None:\n return value.replace(tzinfo=timezone.utc)\n return value.astimezone(timezone.utc)\n\n # [/DEF:_to_utc_datetime:Function]\n" - }, - { - "contract_id": "_datetime_sort_key", - "contract_type": "Function", - "file_path": "backend/src/services/reports/report_service.py", - "start_line": 118, - "end_line": 132, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Mixed naive/aware datetimes never raise TypeError.", - "PARAM": "report (TaskReport) - Report item.", - "POST": "Returns float timestamp suitable for deterministic sorting.", - "PRE": "report contains updated_at datetime.", - "PURPOSE": "Produce stable numeric sort key for report timestamps.", - "RETURN": "float - UTC timestamp key." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_datetime_sort_key:Function]\n # @PURPOSE: Produce stable numeric sort key for report timestamps.\n # @PRE: report contains updated_at datetime.\n # @POST: Returns float timestamp suitable for deterministic sorting.\n # @INVARIANT: Mixed naive/aware datetimes never raise TypeError.\n # @PARAM: report (TaskReport) - Report item.\n # @RETURN: float - UTC timestamp key.\n def _datetime_sort_key(self, report: TaskReport) -> float:\n with belief_scope(\"_datetime_sort_key\"):\n updated = self._to_utc_datetime(report.updated_at)\n if updated is None:\n return 0.0\n return updated.timestamp()\n\n # [/DEF:_datetime_sort_key:Function]\n" - }, - { - "contract_id": "_matches_query", - "contract_type": "Function", - "file_path": "backend/src/services/reports/report_service.py", - "start_line": 134, - "end_line": 171, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Filter evaluation is side-effect free.", - "PARAM": "query (ReportQuery) - Applied query.", - "POST": "Returns True iff report satisfies all active query filters.", - "PRE": "report and query are normalized schema instances.", - "PURPOSE": "Apply query filtering to a report.", - "RETURN": "bool - True if report matches all filters." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_matches_query:Function]\n # @PURPOSE: Apply query filtering to a report.\n # @PRE: report and query are normalized schema instances.\n # @POST: Returns True iff report satisfies all active query filters.\n # @INVARIANT: Filter evaluation is side-effect free.\n # @PARAM: report (TaskReport) - Candidate report.\n # @PARAM: query (ReportQuery) - Applied query.\n # @RETURN: bool - True if report matches all filters.\n def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:\n with belief_scope(\"_matches_query\"):\n if query.task_types and report.task_type not in query.task_types:\n return False\n if query.statuses and report.status not in query.statuses:\n return False\n report_updated_at = self._to_utc_datetime(report.updated_at)\n query_time_from = self._to_utc_datetime(query.time_from)\n query_time_to = self._to_utc_datetime(query.time_to)\n\n if (\n query_time_from\n and report_updated_at\n and report_updated_at < query_time_from\n ):\n return False\n if (\n query_time_to\n and report_updated_at\n and report_updated_at > query_time_to\n ):\n return False\n if query.search:\n needle = query.search.lower()\n haystack = f\"{report.summary} {report.task_type.value} {report.status.value}\".lower()\n if needle not in haystack:\n return False\n return True\n\n # [/DEF:_matches_query:Function]\n" - }, - { - "contract_id": "_sort_reports", - "contract_type": "Function", - "file_path": "backend/src/services/reports/report_service.py", - "start_line": 173, - "end_line": 196, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Sorting criteria are deterministic for equal input.", - "PARAM": "query (ReportQuery) - Sort config.", - "POST": "Returns reports ordered by selected sort field and order.", - "PRE": "reports contains only TaskReport items.", - "PURPOSE": "Sort reports deterministically according to query settings.", - "RETURN": "List[TaskReport] - Sorted reports." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_sort_reports:Function]\n # @PURPOSE: Sort reports deterministically according to query settings.\n # @PRE: reports contains only TaskReport items.\n # @POST: Returns reports ordered by selected sort field and order.\n # @INVARIANT: Sorting criteria are deterministic for equal input.\n # @PARAM: reports (List[TaskReport]) - Filtered reports.\n # @PARAM: query (ReportQuery) - Sort config.\n # @RETURN: List[TaskReport] - Sorted reports.\n def _sort_reports(\n self, reports: List[TaskReport], query: ReportQuery\n ) -> List[TaskReport]:\n with belief_scope(\"_sort_reports\"):\n reverse = query.sort_order == \"desc\"\n\n if query.sort_by == \"status\":\n reports.sort(key=lambda item: item.status.value, reverse=reverse)\n elif query.sort_by == \"task_type\":\n reports.sort(key=lambda item: item.task_type.value, reverse=reverse)\n else:\n reports.sort(key=self._datetime_sort_key, reverse=reverse)\n\n return reports\n\n # [/DEF:_sort_reports:Function]\n" + "anchor_syntax": "region", + "body": "# #region ReportsService [C:5] [TYPE Class]\n# @BRIEF Service layer for list/detail report retrieval and normalization.\n# @PRE: TaskManager dependency is initialized.\n# @POST: Provides deterministic list/detail report responses.\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [CleanReleaseRepository]\n# @RELATION CALLS -> [normalize_task_report]\n# @SIDE_EFFECT: Reads task history and optional clean-release repository state without mutating source records.\n# @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]\n# @INVARIANT: Service methods are read-only over task history source.\n#\n# @TEST_CONTRACT: ReportsServiceModel ->\n# {\n# required_fields: {task_manager: TaskManager},\n# invariants: [\n# \"list_reports returns a matching ReportCollection\",\n# \"get_report_detail returns a valid ReportDetailView or None\"\n# ]\n# }\n# @TEST_FIXTURE: valid_service -> {\"task_manager\": \"MockTaskManager()\"}\n# @TEST_EDGE: empty_task_list -> returns empty ReportCollection\n# @TEST_EDGE: report_not_found -> get_report_detail returns None\n# @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service]\nclass ReportsService:\n # region init [TYPE Function]\n # @PURPOSE: Initialize service with TaskManager dependency.\n # @PRE: task_manager is a live TaskManager instance.\n # @POST: self.task_manager is assigned and ready for read operations.\n # @INVARIANT: Constructor performs no task mutations.\n # @RELATION: [BINDS_TO] ->[ReportsService]\n # @RELATION: [DEPENDS_ON] ->[TaskManager]\n # @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository]\n # @SIDE_EFFECT: Stores collaborator references for later read-only report projections.\n # @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService]\n # @PARAM: task_manager (TaskManager) - Task manager providing source task history.\n def __init__(\n self,\n task_manager: TaskManager,\n clean_release_repository: CleanReleaseRepository | None = None,\n ):\n with belief_scope(\"__init__\"):\n self.task_manager = task_manager\n self.clean_release_repository = clean_release_repository\n\n # endregion init\n\n # region _load_normalized_reports [TYPE Function]\n # @PURPOSE: Build normalized reports from all available tasks.\n # @PRE: Task manager returns iterable task history records.\n # @POST: Returns normalized report list preserving source cardinality.\n # @INVARIANT: Every returned item is a TaskReport.\n # @RETURN: List[TaskReport] - Reports sorted later by list logic.\n def _load_normalized_reports(self) -> list[TaskReport]:\n with belief_scope(\"_load_normalized_reports\"):\n tasks = self.task_manager.get_all_tasks()\n reports = [normalize_task_report(task) for task in tasks]\n return reports\n\n # endregion _load_normalized_reports\n\n # region _to_utc_datetime [TYPE Function]\n # @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.\n # @PRE: value is either datetime or None.\n # @POST: Returns UTC-aware datetime or None.\n # @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.\n # @PARAM: value (Optional[datetime]) - Source datetime value.\n # @RETURN: Optional[datetime] - UTC-aware datetime or None.\n def _to_utc_datetime(self, value: datetime | None) -> datetime | None:\n with belief_scope(\"_to_utc_datetime\"):\n if value is None:\n return None\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n\n # endregion _to_utc_datetime\n\n # region _datetime_sort_key [TYPE Function]\n # @PURPOSE: Produce stable numeric sort key for report timestamps.\n # @PRE: report contains updated_at datetime.\n # @POST: Returns float timestamp suitable for deterministic sorting.\n # @INVARIANT: Mixed naive/aware datetimes never raise TypeError.\n # @PARAM: report (TaskReport) - Report item.\n # @RETURN: float - UTC timestamp key.\n def _datetime_sort_key(self, report: TaskReport) -> float:\n with belief_scope(\"_datetime_sort_key\"):\n updated = self._to_utc_datetime(report.updated_at)\n if updated is None:\n return 0.0\n return updated.timestamp()\n\n # endregion _datetime_sort_key\n\n # region _matches_query [TYPE Function]\n # @PURPOSE: Apply query filtering to a report.\n # @PRE: report and query are normalized schema instances.\n # @POST: Returns True iff report satisfies all active query filters.\n # @INVARIANT: Filter evaluation is side-effect free.\n # @PARAM: report (TaskReport) - Candidate report.\n # @PARAM: query (ReportQuery) - Applied query.\n # @RETURN: bool - True if report matches all filters.\n def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:\n with belief_scope(\"_matches_query\"):\n if query.task_types and report.task_type not in query.task_types:\n return False\n if query.statuses and report.status not in query.statuses:\n return False\n report_updated_at = self._to_utc_datetime(report.updated_at)\n query_time_from = self._to_utc_datetime(query.time_from)\n query_time_to = self._to_utc_datetime(query.time_to)\n\n if (\n query_time_from\n and report_updated_at\n and report_updated_at < query_time_from\n ):\n return False\n if (\n query_time_to\n and report_updated_at\n and report_updated_at > query_time_to\n ):\n return False\n if query.search:\n needle = query.search.lower()\n haystack = f\"{report.summary} {report.task_type.value} {report.status.value}\".lower()\n if needle not in haystack:\n return False\n return True\n\n # endregion _matches_query\n\n # region _sort_reports [TYPE Function]\n # @PURPOSE: Sort reports deterministically according to query settings.\n # @PRE: reports contains only TaskReport items.\n # @POST: Returns reports ordered by selected sort field and order.\n # @INVARIANT: Sorting criteria are deterministic for equal input.\n # @PARAM: reports (List[TaskReport]) - Filtered reports.\n # @PARAM: query (ReportQuery) - Sort config.\n # @RETURN: List[TaskReport] - Sorted reports.\n def _sort_reports(\n self, reports: list[TaskReport], query: ReportQuery\n ) -> list[TaskReport]:\n with belief_scope(\"_sort_reports\"):\n reverse = query.sort_order == \"desc\"\n\n if query.sort_by == \"status\":\n reports.sort(key=lambda item: item.status.value, reverse=reverse)\n elif query.sort_by == \"task_type\":\n reports.sort(key=lambda item: item.task_type.value, reverse=reverse)\n else:\n reports.sort(key=self._datetime_sort_key, reverse=reverse)\n\n return reports\n\n # endregion _sort_reports\n\n # region list_reports [TYPE Function]\n # @PURPOSE: Return filtered, sorted, paginated report collection.\n # @PRE: query has passed schema validation.\n # @POST: Returns {items,total,page,page_size,has_next,applied_filters}.\n # @PARAM: query (ReportQuery) - List filters and pagination.\n # @RETURN: ReportCollection - Paginated unified reports payload.\n def list_reports(self, query: ReportQuery) -> ReportCollection:\n with belief_scope(\"list_reports\"):\n reports = self._load_normalized_reports()\n filtered = [\n report for report in reports if self._matches_query(report, query)\n ]\n sorted_reports = self._sort_reports(filtered, query)\n\n total = len(sorted_reports)\n start = (query.page - 1) * query.page_size\n end = start + query.page_size\n items = sorted_reports[start:end]\n has_next = end < total\n\n return ReportCollection(\n items=items,\n total=total,\n page=query.page,\n page_size=query.page_size,\n has_next=has_next,\n applied_filters=query,\n )\n\n # endregion list_reports\n\n # region get_report_detail [TYPE Function]\n # @PURPOSE: Return one normalized report with timeline/diagnostics/next actions.\n # @PRE: report_id exists in normalized report set.\n # @POST: Returns normalized detail envelope with diagnostics and next actions where applicable.\n # @PARAM: report_id (str) - Stable report identifier.\n # @RETURN: Optional[ReportDetailView] - Detailed report or None if not found.\n def get_report_detail(self, report_id: str) -> ReportDetailView | None:\n with belief_scope(\"get_report_detail\"):\n reports = self._load_normalized_reports()\n target = next(\n (report for report in reports if report.report_id == report_id), None\n )\n if not target:\n return None\n\n timeline = []\n if target.started_at:\n timeline.append(\n {\"event\": \"started\", \"at\": target.started_at.isoformat()}\n )\n timeline.append({\"event\": \"updated\", \"at\": target.updated_at.isoformat()})\n\n diagnostics = target.details or {}\n if not diagnostics:\n diagnostics = {\"note\": \"Not provided\"}\n if target.error_context:\n diagnostics[\"error_context\"] = target.error_context.model_dump()\n\n if (\n target.task_type == TaskType.CLEAN_RELEASE\n and self.clean_release_repository is not None\n ):\n run_id = None\n if isinstance(diagnostics, dict):\n result_payload = diagnostics.get(\"result\")\n if isinstance(result_payload, dict):\n run_id = result_payload.get(\"run_id\") or result_payload.get(\n \"check_run_id\"\n )\n if run_id:\n run = self.clean_release_repository.get_check_run(str(run_id))\n if run is not None:\n diagnostics[\"clean_release_run\"] = {\n \"run_id\": run.id,\n \"candidate_id\": run.candidate_id,\n \"status\": run.status,\n \"final_status\": run.final_status,\n \"requested_by\": run.requested_by,\n }\n linked_report = next(\n (\n item\n for item in self.clean_release_repository.reports.values()\n if item.run_id == run.id\n ),\n None,\n )\n if linked_report is not None:\n diagnostics[\"clean_release_report\"] = {\n \"report_id\": linked_report.id,\n \"final_status\": linked_report.final_status,\n }\n\n next_actions = []\n if target.error_context and target.error_context.next_actions:\n next_actions = target.error_context.next_actions\n elif target.status in {ReportStatus.FAILED, ReportStatus.PARTIAL}:\n next_actions = [\"Review diagnostics\", \"Retry task if applicable\"]\n\n return ReportDetailView(\n report=target,\n timeline=timeline,\n diagnostics=diagnostics,\n next_actions=next_actions,\n )\n\n # endregion get_report_detail\n\n\n# #endregion ReportsService\n" }, { "contract_id": "type_profiles", "contract_type": "Module", "file_path": "backend/src/services/reports/type_profiles.py", "start_line": 1, - "end_line": 123, + "end_line": 116, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "PURPOSE": "Deterministic mapping of plugin/task identifiers to canonical report task types and fallback profile metadata." }, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:type_profiles:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Deterministic mapping of plugin/task identifiers to canonical report task types and fallback profile metadata.\n\n# [SECTION: IMPORTS]\nfrom typing import Any, Dict, Optional\n\nfrom ...core.logger import belief_scope\nfrom ...models.report import TaskType\n# [/SECTION]\n\n# [DEF:PLUGIN_TO_TASK_TYPE:Data]\n# @PURPOSE: Maps plugin identifiers to normalized report task types.\nPLUGIN_TO_TASK_TYPE: Dict[str, TaskType] = {\n \"llm_dashboard_validation\": TaskType.LLM_VERIFICATION,\n \"superset-backup\": TaskType.BACKUP,\n \"superset-migration\": TaskType.MIGRATION,\n \"documentation\": TaskType.DOCUMENTATION,\n \"clean-release-compliance\": TaskType.CLEAN_RELEASE,\n \"clean_release_compliance\": TaskType.CLEAN_RELEASE,\n}\n# [/DEF:PLUGIN_TO_TASK_TYPE:Data]\n\n# [DEF:TASK_TYPE_PROFILES:Data]\n# @PURPOSE: Profile metadata registry for each normalized task type.\nTASK_TYPE_PROFILES: Dict[TaskType, Dict[str, Any]] = {\n TaskType.LLM_VERIFICATION: {\n \"display_label\": \"LLM Verification\",\n \"visual_variant\": \"llm\",\n \"icon_token\": \"sparkles\",\n \"emphasis_rules\": [\"summary\", \"status\", \"next_actions\"],\n \"fallback\": False,\n },\n TaskType.BACKUP: {\n \"display_label\": \"Backup\",\n \"visual_variant\": \"backup\",\n \"icon_token\": \"archive\",\n \"emphasis_rules\": [\"summary\", \"status\", \"updated_at\"],\n \"fallback\": False,\n },\n TaskType.MIGRATION: {\n \"display_label\": \"Migration\",\n \"visual_variant\": \"migration\",\n \"icon_token\": \"shuffle\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\"],\n \"fallback\": False,\n },\n TaskType.DOCUMENTATION: {\n \"display_label\": \"Documentation\",\n \"visual_variant\": \"documentation\",\n \"icon_token\": \"file-text\",\n \"emphasis_rules\": [\"summary\", \"status\", \"details\"],\n \"fallback\": False,\n },\n TaskType.CLEAN_RELEASE: {\n \"display_label\": \"Clean Release\",\n \"visual_variant\": \"clean-release\",\n \"icon_token\": \"shield-check\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\", \"details\"],\n \"fallback\": False,\n },\n TaskType.UNKNOWN: {\n \"display_label\": \"Other / Unknown\",\n \"visual_variant\": \"unknown\",\n \"icon_token\": \"help-circle\",\n \"emphasis_rules\": [\"summary\", \"status\"],\n \"fallback\": True,\n },\n}\n# [/DEF:TASK_TYPE_PROFILES:Data]\n\n\n# [DEF:resolve_task_type:Function]\n# @PURPOSE: Resolve canonical task type from plugin/task identifier with guaranteed fallback.\n# @PRE: plugin_id may be None or unknown.\n# @POST: Always returns one of TaskType enum values.\n# @PARAM: plugin_id (Optional[str]) - Source plugin/task identifier from task record.\n# @RETURN: TaskType - Resolved canonical type or UNKNOWN fallback.\n#\n# @TEST_CONTRACT: ResolveTaskType ->\n# {\n# required_fields: {plugin_id: str},\n# invariants: [\"returns TaskType.UNKNOWN for missing/unmapped plugin_id\"]\n# }\n# @TEST_FIXTURE: valid_plugin -> {\"plugin_id\": \"superset-migration\"}\n# @TEST_EDGE: empty_plugin -> {\"plugin_id\": \"\"}\n# @TEST_EDGE: none_plugin -> {\"plugin_id\": None}\n# @TEST_EDGE: unknown_plugin -> {\"plugin_id\": \"invalid-plugin\"}\n# @TEST_INVARIANT: fallback_to_unknown -> verifies: [empty_plugin, none_plugin, unknown_plugin]\ndef resolve_task_type(plugin_id: Optional[str]) -> TaskType:\n with belief_scope(\"resolve_task_type\"):\n normalized = (plugin_id or \"\").strip()\n if not normalized:\n return TaskType.UNKNOWN\n return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)\n\n\n# [/DEF:resolve_task_type:Function]\n\n\n# [DEF:get_type_profile:Function]\n# @PURPOSE: Return deterministic profile metadata for a task type.\n# @PRE: task_type may be known or unknown.\n# @POST: Returns a profile dict and never raises for unknown types.\n# @PARAM: task_type (TaskType) - Canonical task type.\n# @RETURN: Dict[str, Any] - Profile metadata used by normalization and UI contracts.\n#\n# @TEST_CONTRACT: GetTypeProfile ->\n# {\n# required_fields: {task_type: TaskType},\n# invariants: [\"returns a valid metadata dictionary even for UNKNOWN\"]\n# }\n# @TEST_FIXTURE: valid_profile -> {\"task_type\": \"migration\"}\n# @TEST_EDGE: missing_profile -> {\"task_type\": \"some_new_type\"}\n# @TEST_INVARIANT: always_returns_dict -> verifies: [valid_profile, missing_profile]\ndef get_type_profile(task_type: TaskType) -> Dict[str, Any]:\n with belief_scope(\"get_type_profile\"):\n return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])\n\n\n# [/DEF:get_type_profile:Function]\n\n# [/DEF:type_profiles:Module]\n" + "anchor_syntax": "region", + "body": "# #region type_profiles [C:2] [TYPE Module] [SEMANTICS report, task-type, mapping, profile, fallback]\n# @BRIEF Deterministic mapping of plugin/task identifiers to canonical report task types and fallback profile metadata.\n\nfrom typing import Any\n\nfrom ...core.logger import belief_scope\nfrom ...models.report import TaskType\n\n# #region PLUGIN_TO_TASK_TYPE [TYPE Data]\n# @BRIEF Maps plugin identifiers to normalized report task types.\nPLUGIN_TO_TASK_TYPE: dict[str, TaskType] = {\n \"llm_dashboard_validation\": TaskType.LLM_VERIFICATION,\n \"superset-backup\": TaskType.BACKUP,\n \"superset-migration\": TaskType.MIGRATION,\n \"documentation\": TaskType.DOCUMENTATION,\n \"clean-release-compliance\": TaskType.CLEAN_RELEASE,\n \"clean_release_compliance\": TaskType.CLEAN_RELEASE,\n}\n# #endregion PLUGIN_TO_TASK_TYPE\n\n# #region TASK_TYPE_PROFILES [TYPE Data]\n# @BRIEF Profile metadata registry for each normalized task type.\nTASK_TYPE_PROFILES: dict[TaskType, dict[str, Any]] = {\n TaskType.LLM_VERIFICATION: {\n \"display_label\": \"LLM Verification\",\n \"visual_variant\": \"llm\",\n \"icon_token\": \"sparkles\",\n \"emphasis_rules\": [\"summary\", \"status\", \"next_actions\"],\n \"fallback\": False,\n },\n TaskType.BACKUP: {\n \"display_label\": \"Backup\",\n \"visual_variant\": \"backup\",\n \"icon_token\": \"archive\",\n \"emphasis_rules\": [\"summary\", \"status\", \"updated_at\"],\n \"fallback\": False,\n },\n TaskType.MIGRATION: {\n \"display_label\": \"Migration\",\n \"visual_variant\": \"migration\",\n \"icon_token\": \"shuffle\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\"],\n \"fallback\": False,\n },\n TaskType.DOCUMENTATION: {\n \"display_label\": \"Documentation\",\n \"visual_variant\": \"documentation\",\n \"icon_token\": \"file-text\",\n \"emphasis_rules\": [\"summary\", \"status\", \"details\"],\n \"fallback\": False,\n },\n TaskType.CLEAN_RELEASE: {\n \"display_label\": \"Clean Release\",\n \"visual_variant\": \"clean-release\",\n \"icon_token\": \"shield-check\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\", \"details\"],\n \"fallback\": False,\n },\n TaskType.UNKNOWN: {\n \"display_label\": \"Other / Unknown\",\n \"visual_variant\": \"unknown\",\n \"icon_token\": \"help-circle\",\n \"emphasis_rules\": [\"summary\", \"status\"],\n \"fallback\": True,\n },\n}\n# #endregion TASK_TYPE_PROFILES\n\n\n# #region resolve_task_type [TYPE Function]\n# @BRIEF Resolve canonical task type from plugin/task identifier with guaranteed fallback.\n# @PRE: plugin_id may be None or unknown.\n# @POST: Always returns one of TaskType enum values.\n#\n# @TEST_CONTRACT: ResolveTaskType ->\n# {\n# required_fields: {plugin_id: str},\n# invariants: [\"returns TaskType.UNKNOWN for missing/unmapped plugin_id\"]\n# }\n# @TEST_FIXTURE: valid_plugin -> {\"plugin_id\": \"superset-migration\"}\n# @TEST_EDGE: empty_plugin -> {\"plugin_id\": \"\"}\n# @TEST_EDGE: none_plugin -> {\"plugin_id\": None}\n# @TEST_EDGE: unknown_plugin -> {\"plugin_id\": \"invalid-plugin\"}\n# @TEST_INVARIANT: fallback_to_unknown -> verifies: [empty_plugin, none_plugin, unknown_plugin]\ndef resolve_task_type(plugin_id: str | None) -> TaskType:\n with belief_scope(\"resolve_task_type\"):\n normalized = (plugin_id or \"\").strip()\n if not normalized:\n return TaskType.UNKNOWN\n return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)\n\n\n# #endregion resolve_task_type\n\n\n# #region get_type_profile [TYPE Function]\n# @BRIEF Return deterministic profile metadata for a task type.\n# @PRE: task_type may be known or unknown.\n# @POST: Returns a profile dict and never raises for unknown types.\n#\n# @TEST_CONTRACT: GetTypeProfile ->\n# {\n# required_fields: {task_type: TaskType},\n# invariants: [\"returns a valid metadata dictionary even for UNKNOWN\"]\n# }\n# @TEST_FIXTURE: valid_profile -> {\"task_type\": \"migration\"}\n# @TEST_EDGE: missing_profile -> {\"task_type\": \"some_new_type\"}\n# @TEST_INVARIANT: always_returns_dict -> verifies: [valid_profile, missing_profile]\ndef get_type_profile(task_type: TaskType) -> dict[str, Any]:\n with belief_scope(\"get_type_profile\"):\n return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])\n\n\n# #endregion get_type_profile\n\n# #endregion type_profiles\n" }, { "contract_id": "PLUGIN_TO_TASK_TYPE", "contract_type": "Data", "file_path": "backend/src/services/reports/type_profiles.py", - "start_line": 12, - "end_line": 22, + "start_line": 9, + "end_line": 19, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -96646,15 +65704,15 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:PLUGIN_TO_TASK_TYPE:Data]\n# @PURPOSE: Maps plugin identifiers to normalized report task types.\nPLUGIN_TO_TASK_TYPE: Dict[str, TaskType] = {\n \"llm_dashboard_validation\": TaskType.LLM_VERIFICATION,\n \"superset-backup\": TaskType.BACKUP,\n \"superset-migration\": TaskType.MIGRATION,\n \"documentation\": TaskType.DOCUMENTATION,\n \"clean-release-compliance\": TaskType.CLEAN_RELEASE,\n \"clean_release_compliance\": TaskType.CLEAN_RELEASE,\n}\n# [/DEF:PLUGIN_TO_TASK_TYPE:Data]\n" + "anchor_syntax": "region", + "body": "# #region PLUGIN_TO_TASK_TYPE [TYPE Data]\n# @BRIEF Maps plugin identifiers to normalized report task types.\nPLUGIN_TO_TASK_TYPE: dict[str, TaskType] = {\n \"llm_dashboard_validation\": TaskType.LLM_VERIFICATION,\n \"superset-backup\": TaskType.BACKUP,\n \"superset-migration\": TaskType.MIGRATION,\n \"documentation\": TaskType.DOCUMENTATION,\n \"clean-release-compliance\": TaskType.CLEAN_RELEASE,\n \"clean_release_compliance\": TaskType.CLEAN_RELEASE,\n}\n# #endregion PLUGIN_TO_TASK_TYPE\n" }, { "contract_id": "TASK_TYPE_PROFILES", "contract_type": "Data", "file_path": "backend/src/services/reports/type_profiles.py", - "start_line": 24, - "end_line": 70, + "start_line": 21, + "end_line": 67, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -96690,33 +65748,25 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:TASK_TYPE_PROFILES:Data]\n# @PURPOSE: Profile metadata registry for each normalized task type.\nTASK_TYPE_PROFILES: Dict[TaskType, Dict[str, Any]] = {\n TaskType.LLM_VERIFICATION: {\n \"display_label\": \"LLM Verification\",\n \"visual_variant\": \"llm\",\n \"icon_token\": \"sparkles\",\n \"emphasis_rules\": [\"summary\", \"status\", \"next_actions\"],\n \"fallback\": False,\n },\n TaskType.BACKUP: {\n \"display_label\": \"Backup\",\n \"visual_variant\": \"backup\",\n \"icon_token\": \"archive\",\n \"emphasis_rules\": [\"summary\", \"status\", \"updated_at\"],\n \"fallback\": False,\n },\n TaskType.MIGRATION: {\n \"display_label\": \"Migration\",\n \"visual_variant\": \"migration\",\n \"icon_token\": \"shuffle\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\"],\n \"fallback\": False,\n },\n TaskType.DOCUMENTATION: {\n \"display_label\": \"Documentation\",\n \"visual_variant\": \"documentation\",\n \"icon_token\": \"file-text\",\n \"emphasis_rules\": [\"summary\", \"status\", \"details\"],\n \"fallback\": False,\n },\n TaskType.CLEAN_RELEASE: {\n \"display_label\": \"Clean Release\",\n \"visual_variant\": \"clean-release\",\n \"icon_token\": \"shield-check\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\", \"details\"],\n \"fallback\": False,\n },\n TaskType.UNKNOWN: {\n \"display_label\": \"Other / Unknown\",\n \"visual_variant\": \"unknown\",\n \"icon_token\": \"help-circle\",\n \"emphasis_rules\": [\"summary\", \"status\"],\n \"fallback\": True,\n },\n}\n# [/DEF:TASK_TYPE_PROFILES:Data]\n" + "anchor_syntax": "region", + "body": "# #region TASK_TYPE_PROFILES [TYPE Data]\n# @BRIEF Profile metadata registry for each normalized task type.\nTASK_TYPE_PROFILES: dict[TaskType, dict[str, Any]] = {\n TaskType.LLM_VERIFICATION: {\n \"display_label\": \"LLM Verification\",\n \"visual_variant\": \"llm\",\n \"icon_token\": \"sparkles\",\n \"emphasis_rules\": [\"summary\", \"status\", \"next_actions\"],\n \"fallback\": False,\n },\n TaskType.BACKUP: {\n \"display_label\": \"Backup\",\n \"visual_variant\": \"backup\",\n \"icon_token\": \"archive\",\n \"emphasis_rules\": [\"summary\", \"status\", \"updated_at\"],\n \"fallback\": False,\n },\n TaskType.MIGRATION: {\n \"display_label\": \"Migration\",\n \"visual_variant\": \"migration\",\n \"icon_token\": \"shuffle\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\"],\n \"fallback\": False,\n },\n TaskType.DOCUMENTATION: {\n \"display_label\": \"Documentation\",\n \"visual_variant\": \"documentation\",\n \"icon_token\": \"file-text\",\n \"emphasis_rules\": [\"summary\", \"status\", \"details\"],\n \"fallback\": False,\n },\n TaskType.CLEAN_RELEASE: {\n \"display_label\": \"Clean Release\",\n \"visual_variant\": \"clean-release\",\n \"icon_token\": \"shield-check\",\n \"emphasis_rules\": [\"summary\", \"status\", \"error_context\", \"details\"],\n \"fallback\": False,\n },\n TaskType.UNKNOWN: {\n \"display_label\": \"Other / Unknown\",\n \"visual_variant\": \"unknown\",\n \"icon_token\": \"help-circle\",\n \"emphasis_rules\": [\"summary\", \"status\"],\n \"fallback\": True,\n },\n}\n# #endregion TASK_TYPE_PROFILES\n" }, { "contract_id": "resolve_task_type", "contract_type": "Function", "file_path": "backend/src/services/reports/type_profiles.py", - "start_line": 73, - "end_line": 98, + "start_line": 70, + "end_line": 93, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "plugin_id (Optional[str]) - Source plugin/task identifier from task record.", "POST": "Always returns one of TaskType enum values.", "PRE": "plugin_id may be None or unknown.", "PURPOSE": "Resolve canonical task type from plugin/task identifier with guaranteed fallback.", - "RETURN": "TaskType - Resolved canonical type or UNKNOWN fallback.", "TEST_CONTRACT": "ResolveTaskType ->" }, "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", @@ -96743,41 +65793,27 @@ "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": "def", - "body": "# [DEF:resolve_task_type:Function]\n# @PURPOSE: Resolve canonical task type from plugin/task identifier with guaranteed fallback.\n# @PRE: plugin_id may be None or unknown.\n# @POST: Always returns one of TaskType enum values.\n# @PARAM: plugin_id (Optional[str]) - Source plugin/task identifier from task record.\n# @RETURN: TaskType - Resolved canonical type or UNKNOWN fallback.\n#\n# @TEST_CONTRACT: ResolveTaskType ->\n# {\n# required_fields: {plugin_id: str},\n# invariants: [\"returns TaskType.UNKNOWN for missing/unmapped plugin_id\"]\n# }\n# @TEST_FIXTURE: valid_plugin -> {\"plugin_id\": \"superset-migration\"}\n# @TEST_EDGE: empty_plugin -> {\"plugin_id\": \"\"}\n# @TEST_EDGE: none_plugin -> {\"plugin_id\": None}\n# @TEST_EDGE: unknown_plugin -> {\"plugin_id\": \"invalid-plugin\"}\n# @TEST_INVARIANT: fallback_to_unknown -> verifies: [empty_plugin, none_plugin, unknown_plugin]\ndef resolve_task_type(plugin_id: Optional[str]) -> TaskType:\n with belief_scope(\"resolve_task_type\"):\n normalized = (plugin_id or \"\").strip()\n if not normalized:\n return TaskType.UNKNOWN\n return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)\n\n\n# [/DEF:resolve_task_type:Function]\n" + "anchor_syntax": "region", + "body": "# #region resolve_task_type [TYPE Function]\n# @BRIEF Resolve canonical task type from plugin/task identifier with guaranteed fallback.\n# @PRE: plugin_id may be None or unknown.\n# @POST: Always returns one of TaskType enum values.\n#\n# @TEST_CONTRACT: ResolveTaskType ->\n# {\n# required_fields: {plugin_id: str},\n# invariants: [\"returns TaskType.UNKNOWN for missing/unmapped plugin_id\"]\n# }\n# @TEST_FIXTURE: valid_plugin -> {\"plugin_id\": \"superset-migration\"}\n# @TEST_EDGE: empty_plugin -> {\"plugin_id\": \"\"}\n# @TEST_EDGE: none_plugin -> {\"plugin_id\": None}\n# @TEST_EDGE: unknown_plugin -> {\"plugin_id\": \"invalid-plugin\"}\n# @TEST_INVARIANT: fallback_to_unknown -> verifies: [empty_plugin, none_plugin, unknown_plugin]\ndef resolve_task_type(plugin_id: str | None) -> TaskType:\n with belief_scope(\"resolve_task_type\"):\n normalized = (plugin_id or \"\").strip()\n if not normalized:\n return TaskType.UNKNOWN\n return PLUGIN_TO_TASK_TYPE.get(normalized, TaskType.UNKNOWN)\n\n\n# #endregion resolve_task_type\n" }, { "contract_id": "get_type_profile", "contract_type": "Function", "file_path": "backend/src/services/reports/type_profiles.py", - "start_line": 101, - "end_line": 121, + "start_line": 96, + "end_line": 114, "tier": "TIER_1", "complexity": 1, "metadata": { - "PARAM": "task_type (TaskType) - Canonical task type.", "POST": "Returns a profile dict and never raises for unknown types.", "PRE": "task_type may be known or unknown.", "PURPOSE": "Return deterministic profile metadata for a task type.", - "RETURN": "Dict[str, Any] - Profile metadata used by normalization and UI contracts.", "TEST_CONTRACT": "GetTypeProfile ->" }, "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", @@ -96804,38 +65840,24 @@ "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": "def", - "body": "# [DEF:get_type_profile:Function]\n# @PURPOSE: Return deterministic profile metadata for a task type.\n# @PRE: task_type may be known or unknown.\n# @POST: Returns a profile dict and never raises for unknown types.\n# @PARAM: task_type (TaskType) - Canonical task type.\n# @RETURN: Dict[str, Any] - Profile metadata used by normalization and UI contracts.\n#\n# @TEST_CONTRACT: GetTypeProfile ->\n# {\n# required_fields: {task_type: TaskType},\n# invariants: [\"returns a valid metadata dictionary even for UNKNOWN\"]\n# }\n# @TEST_FIXTURE: valid_profile -> {\"task_type\": \"migration\"}\n# @TEST_EDGE: missing_profile -> {\"task_type\": \"some_new_type\"}\n# @TEST_INVARIANT: always_returns_dict -> verifies: [valid_profile, missing_profile]\ndef get_type_profile(task_type: TaskType) -> Dict[str, Any]:\n with belief_scope(\"get_type_profile\"):\n return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])\n\n\n# [/DEF:get_type_profile:Function]\n" + "anchor_syntax": "region", + "body": "# #region get_type_profile [TYPE Function]\n# @BRIEF Return deterministic profile metadata for a task type.\n# @PRE: task_type may be known or unknown.\n# @POST: Returns a profile dict and never raises for unknown types.\n#\n# @TEST_CONTRACT: GetTypeProfile ->\n# {\n# required_fields: {task_type: TaskType},\n# invariants: [\"returns a valid metadata dictionary even for UNKNOWN\"]\n# }\n# @TEST_FIXTURE: valid_profile -> {\"task_type\": \"migration\"}\n# @TEST_EDGE: missing_profile -> {\"task_type\": \"some_new_type\"}\n# @TEST_INVARIANT: always_returns_dict -> verifies: [valid_profile, missing_profile]\ndef get_type_profile(task_type: TaskType) -> dict[str, Any]:\n with belief_scope(\"get_type_profile\"):\n return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])\n\n\n# #endregion get_type_profile\n" }, { "contract_id": "ResourceServiceModule", "contract_type": "Module", "file_path": "backend/src/services/resource_service.py", "start_line": 1, - "end_line": 495, + "end_line": 482, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "All resources include metadata about their current state", "LAYER": "Service", - "PURPOSE": "Shared service for fetching resource data with Git status and task status", - "SEMANTICS": [ - "service", - "resources", - "dashboards", - "datasets", - "tasks", - "git" - ] + "PURPOSE": "Shared service for fetching resource data with Git status and task status" }, "relations": [ { @@ -96874,19 +65896,19 @@ } } ], - "anchor_syntax": "def", - "body": "# [DEF:ResourceServiceModule:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: service, resources, dashboards, datasets, tasks, git\n# @PURPOSE: 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\n# [SECTION: IMPORTS]\nfrom typing import List, Dict, Optional, Any\nfrom datetime import datetime, timezone\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager.models import Task\nfrom ..services.git_service import GitService\nfrom ..core.cot_logger import MarkerLogger\nfrom ..core.logger import logger, belief_scope\n\nlog = MarkerLogger(\"ResourceService\")\n# [/SECTION]\n\n# [DEF:ResourceService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Provides centralized access to resource data with enhanced metadata\n# @RELATION: DEPENDS_ON ->[SupersetClient]\n# @RELATION: DEPENDS_ON ->[GitService]\nclass ResourceService:\n \n # [DEF:ResourceService_init:Function]\n # @COMPLEXITY: 1\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 log.reason(\"Initialized ResourceService\")\n # [/DEF:ResourceService_init:Function]\n \n # [DEF:get_dashboards_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = 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 log.reflect(f\"Fetched {len(result)} dashboards with status\")\n return result\n # [/DEF:get_dashboards_with_status:Function]\n\n # [DEF:get_dashboards_page_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = None,\n page: int = 1,\n page_size: int = 10,\n search: Optional[str] = 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 log.reflect(f\"Fetched dashboards page {page}/{total_pages} ({len(result)} items, total={total})\")\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # [/DEF:get_dashboards_page_with_status:Function]\n\n # [DEF:_get_last_llm_task_for_dashboard:Function]\n # @COMPLEXITY: 3\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: Optional[str],\n tasks: Optional[List[Task]] = None,\n ) -> Optional[Dict[str, Any]]:\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 # [/DEF:_get_last_llm_task_for_dashboard:Function]\n\n # [DEF:_normalize_task_status:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_normalize_task_status:Function]\n\n # [DEF:_normalize_validation_status:Function]\n # @COMPLEXITY: 3\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) -> Optional[str]:\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 # [/DEF:_normalize_validation_status:Function]\n\n # [DEF:_normalize_datetime_for_compare:Function]\n # @COMPLEXITY: 3\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=timezone.utc)\n return value.astimezone(timezone.utc)\n return datetime.min.replace(tzinfo=timezone.utc)\n # [/DEF:_normalize_datetime_for_compare:Function]\n \n # [DEF:get_datasets_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = 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 log.reflect(f\"Fetched {len(result)} datasets with status\")\n return result\n # [/DEF:get_datasets_with_status:Function]\n \n # [DEF:get_activity_summary:Function]\n # @COMPLEXITY: 3\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 # [/DEF:get_activity_summary:Function]\n \n # [DEF:_get_git_status_for_dashboard:Function]\n # @COMPLEXITY: 3\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) -> Optional[Dict[str, Any]]:\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 log.explore(f\"Failed to get git status for dashboard {dashboard_id}\", error=\"Git status check failed\")\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 # [/DEF:_get_git_status_for_dashboard:Function]\n \n # [DEF:_get_last_task_for_resource:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = None\n ) -> Optional[Dict[str, Any]]:\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 # [/DEF:_get_last_task_for_resource:Function]\n \n # [DEF:_extract_resource_name_from_task:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_extract_resource_name_from_task:Function]\n \n # [DEF:_extract_resource_type_from_task:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_extract_resource_type_from_task:Function]\n# [/DEF:ResourceService:Class]\n# [/DEF:ResourceServiceModule:Module]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ResourceService", "contract_type": "Class", "file_path": "backend/src/services/resource_service.py", - "start_line": 24, - "end_line": 494, + "start_line": 19, + "end_line": 481, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "PURPOSE": "Provides centralized access to resource data with enhanced metadata" }, "relations": [ @@ -96904,936 +65926,72 @@ } ], "schema_warnings": [], - "anchor_syntax": "def", - "body": "# [DEF:ResourceService:Class]\n# @COMPLEXITY: 3\n# @PURPOSE: Provides centralized access to resource data with enhanced metadata\n# @RELATION: DEPENDS_ON ->[SupersetClient]\n# @RELATION: DEPENDS_ON ->[GitService]\nclass ResourceService:\n \n # [DEF:ResourceService_init:Function]\n # @COMPLEXITY: 1\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 log.reason(\"Initialized ResourceService\")\n # [/DEF:ResourceService_init:Function]\n \n # [DEF:get_dashboards_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = 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 log.reflect(f\"Fetched {len(result)} dashboards with status\")\n return result\n # [/DEF:get_dashboards_with_status:Function]\n\n # [DEF:get_dashboards_page_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = None,\n page: int = 1,\n page_size: int = 10,\n search: Optional[str] = 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 log.reflect(f\"Fetched dashboards page {page}/{total_pages} ({len(result)} items, total={total})\")\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # [/DEF:get_dashboards_page_with_status:Function]\n\n # [DEF:_get_last_llm_task_for_dashboard:Function]\n # @COMPLEXITY: 3\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: Optional[str],\n tasks: Optional[List[Task]] = None,\n ) -> Optional[Dict[str, Any]]:\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 # [/DEF:_get_last_llm_task_for_dashboard:Function]\n\n # [DEF:_normalize_task_status:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_normalize_task_status:Function]\n\n # [DEF:_normalize_validation_status:Function]\n # @COMPLEXITY: 3\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) -> Optional[str]:\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 # [/DEF:_normalize_validation_status:Function]\n\n # [DEF:_normalize_datetime_for_compare:Function]\n # @COMPLEXITY: 3\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=timezone.utc)\n return value.astimezone(timezone.utc)\n return datetime.min.replace(tzinfo=timezone.utc)\n # [/DEF:_normalize_datetime_for_compare:Function]\n \n # [DEF:get_datasets_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = 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 log.reflect(f\"Fetched {len(result)} datasets with status\")\n return result\n # [/DEF:get_datasets_with_status:Function]\n \n # [DEF:get_activity_summary:Function]\n # @COMPLEXITY: 3\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 # [/DEF:get_activity_summary:Function]\n \n # [DEF:_get_git_status_for_dashboard:Function]\n # @COMPLEXITY: 3\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) -> Optional[Dict[str, Any]]:\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 log.explore(f\"Failed to get git status for dashboard {dashboard_id}\", error=\"Git status check failed\")\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 # [/DEF:_get_git_status_for_dashboard:Function]\n \n # [DEF:_get_last_task_for_resource:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = None\n ) -> Optional[Dict[str, Any]]:\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 # [/DEF:_get_last_task_for_resource:Function]\n \n # [DEF:_extract_resource_name_from_task:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_extract_resource_name_from_task:Function]\n \n # [DEF:_extract_resource_type_from_task:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_extract_resource_type_from_task:Function]\n# [/DEF:ResourceService:Class]\n" + "anchor_syntax": "region", + "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": "ResourceService_init", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 31, - "end_line": 40, + "contract_id": "TestSessionConfig", + "contract_type": "Module", + "file_path": "backend/tests/conftest.py", + "start_line": 1, + "end_line": 25, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { - "COMPLEXITY": "1", - "POST": "ResourceService is ready to fetch resources", - "PRE": "None", - "PURPOSE": "Initialize the resource service with dependencies" - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " # [DEF:ResourceService_init:Function]\n # @COMPLEXITY: 1\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 log.reason(\"Initialized ResourceService\")\n # [/DEF:ResourceService_init:Function]\n" - }, - { - "contract_id": "get_dashboards_with_status", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 42, - "end_line": 90, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "tasks (List[Task]) - List of tasks to check for status", - "POST": "Returns list of dashboards with enhanced metadata", - "PRE": "env is a valid Environment object", - "PURPOSE": "Fetch dashboards from environment with Git status and last task status", - "RETURN": "List[Dict] - Dashboards with git_status and last_task fields" + "COMPLEXITY": 2, + "POST": "Database tables exist before test session executes.", + "PRE": "All test modules use sys.path patching to import from src/.", + "PURPOSE": "Shared pytest fixtures and session configuration for backend tests." }, "relations": [ { - "source_id": "get_dashboards_with_status", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDashboardsSummary", - "target_ref": "[SupersetClientGetDashboardsSummary]" - }, - { - "source_id": "get_dashboards_with_status", - "relation_type": "CALLS", - "target_id": "_get_git_status_for_dashboard", - "target_ref": "[_get_git_status_for_dashboard]" - }, - { - "source_id": "get_dashboards_with_status", - "relation_type": "CALLS", - "target_id": "_get_last_llm_task_for_dashboard", - "target_ref": "[_get_last_llm_task_for_dashboard]" + "source_id": "TestSessionConfig", + "relation_type": "BINDS_TO", + "target_id": "init_db", + "target_ref": "[init_db]" } ], "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 C3", + "message": "@POST is forbidden for contract type 'Module' at C2", "detail": { - "actual_complexity": 3, - "contract_type": "Function" + "actual_complexity": 2, + "contract_type": "Module" } }, { "code": "tag_forbidden_by_complexity", "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", + "message": "@PRE is forbidden for contract type 'Module' at C2", "detail": { - "actual_complexity": 3, - "contract_type": "Function" - } - }, - { - "code": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_dashboards_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = 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 log.reflect(f\"Fetched {len(result)} dashboards with status\")\n return result\n # [/DEF:get_dashboards_with_status:Function]\n" - }, - { - "contract_id": "get_dashboards_page_with_status", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 92, - "end_line": 151, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "page_size (int) - Page size.", - "POST": "Returns page items plus total counters without scanning all pages locally.", - "PRE": "env is valid; page >= 1; page_size > 0.", - "PURPOSE": "Fetch one dashboard page from environment and enrich only that page with status metadata.", - "RETURN": "Dict[str, Any] - {\"dashboards\": List[Dict], \"total\": int, \"total_pages\": int}" - }, - "relations": [ - { - "source_id": "get_dashboards_page_with_status", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDashboardsSummaryPage", - "target_ref": "[SupersetClientGetDashboardsSummaryPage]" - }, - { - "source_id": "get_dashboards_page_with_status", - "relation_type": "CALLS", - "target_id": "_get_git_status_for_dashboard", - "target_ref": "[_get_git_status_for_dashboard]" - }, - { - "source_id": "get_dashboards_page_with_status", - "relation_type": "CALLS", - "target_id": "_get_last_llm_task_for_dashboard", - "target_ref": "[_get_last_llm_task_for_dashboard]" - } - ], - "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 C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Function" + "actual_complexity": 2, + "contract_type": "Module" } }, { "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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_dashboards_page_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = None,\n page: int = 1,\n page_size: int = 10,\n search: Optional[str] = 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 log.reflect(f\"Fetched dashboards page {page}/{total_pages} ({len(result)} items, total={total})\")\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # [/DEF:get_dashboards_page_with_status:Function]\n" - }, - { - "contract_id": "_get_last_llm_task_for_dashboard", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 153, - "end_line": 235, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "tasks (Optional[List[Task]]) - List of tasks to search", - "POST": "Returns the newest llm_dashboard_validation task summary or None", - "PRE": "dashboard_id is a valid integer identifier", - "PURPOSE": "Get most recent LLM validation task for a dashboard in an environment", - "RETURN": "Optional[Dict] - Task summary with task_id and status" - }, - "relations": [ - { - "source_id": "_get_last_llm_task_for_dashboard", - "relation_type": "CALLS", - "target_id": "_normalize_datetime_for_compare", - "target_ref": "[_normalize_datetime_for_compare]" - }, - { - "source_id": "_get_last_llm_task_for_dashboard", - "relation_type": "CALLS", - "target_id": "_normalize_validation_status", - "target_ref": "[_normalize_validation_status]" - }, - { - "source_id": "_get_last_llm_task_for_dashboard", - "relation_type": "CALLS", - "target_id": "_normalize_task_status", - "target_ref": "[_normalize_task_status]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_last_llm_task_for_dashboard:Function]\n # @COMPLEXITY: 3\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: Optional[str],\n tasks: Optional[List[Task]] = None,\n ) -> Optional[Dict[str, Any]]:\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 # [/DEF:_get_last_llm_task_for_dashboard:Function]\n" - }, - { - "contract_id": "_normalize_task_status", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 237, - "end_line": 253, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "raw_status (Any) - Raw task status object/value", - "POST": "Returns uppercase status without enum class prefix", - "PRE": "raw_status can be enum or string", - "PURPOSE": "Normalize task status to stable uppercase values for UI/API projections", - "RETURN": "str - Normalized status token" - }, - "relations": [ - { - "source_id": "_normalize_task_status", - "relation_type": "USED_BY", - "target_id": "_get_last_llm_task_for_dashboard", - "target_ref": "[_get_last_llm_task_for_dashboard]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate USED_BY is not in allowed_predicates", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "USED_BY" + "actual_complexity": 2, + "contract_type": "Module" } } ], - "anchor_syntax": "def", - "body": " # [DEF:_normalize_task_status:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_normalize_task_status:Function]\n" - }, - { - "contract_id": "_normalize_validation_status", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 255, - "end_line": 270, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "raw_status (Any) - Raw validation status from task result", - "POST": "Returns normalized validation status token or None", - "PRE": "raw_status can be any scalar type", - "PURPOSE": "Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN", - "RETURN": "Optional[str] - PASS|FAIL|WARN|UNKNOWN" - }, - "relations": [ - { - "source_id": "_normalize_validation_status", - "relation_type": "USED_BY", - "target_id": "_get_last_llm_task_for_dashboard", - "target_ref": "[_get_last_llm_task_for_dashboard]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": " # [DEF:_normalize_validation_status:Function]\n # @COMPLEXITY: 3\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) -> Optional[str]:\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 # [/DEF:_normalize_validation_status:Function]\n" - }, - { - "contract_id": "_normalize_datetime_for_compare", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 272, - "end_line": 287, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "value (Any) - Candidate datetime-like value.", - "POST": "Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.", - "PRE": "value may be datetime or any scalar.", - "PURPOSE": "Normalize datetime values to UTC-aware values for safe comparisons.", - "RETURN": "datetime - UTC-aware comparable datetime." - }, - "relations": [ - { - "source_id": "_normalize_datetime_for_compare", - "relation_type": "USED_BY", - "target_id": "_get_last_llm_task_for_dashboard", - "target_ref": "[_get_last_llm_task_for_dashboard]" - }, - { - "source_id": "_normalize_datetime_for_compare", - "relation_type": "USED_BY", - "target_id": "_get_last_task_for_resource", - "target_ref": "[_get_last_task_for_resource]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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" - } - }, - { - "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": "def", - "body": " # [DEF:_normalize_datetime_for_compare:Function]\n # @COMPLEXITY: 3\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=timezone.utc)\n return value.astimezone(timezone.utc)\n return datetime.min.replace(tzinfo=timezone.utc)\n # [/DEF:_normalize_datetime_for_compare:Function]\n" - }, - { - "contract_id": "get_datasets_with_status", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 289, - "end_line": 326, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "tasks (List[Task]) - List of tasks to check for status", - "POST": "Returns list of datasets with enhanced metadata", - "PRE": "env is a valid Environment object", - "PURPOSE": "Fetch datasets from environment with mapping progress and last task status", - "RETURN": "List[Dict] - Datasets with mapped_fields and last_task fields" - }, - "relations": [ - { - "source_id": "get_datasets_with_status", - "relation_type": "CALLS", - "target_id": "SupersetClientGetDatasetsSummary", - "target_ref": "[SupersetClientGetDatasetsSummary]" - }, - { - "source_id": "get_datasets_with_status", - "relation_type": "CALLS", - "target_id": "_get_last_task_for_resource", - "target_ref": "[_get_last_task_for_resource]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_datasets_with_status:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = 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 log.reflect(f\"Fetched {len(result)} datasets with status\")\n return result\n # [/DEF:get_datasets_with_status:Function]\n" - }, - { - "contract_id": "get_activity_summary", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 328, - "end_line": 368, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "tasks (List[Task]) - List of tasks to summarize", - "POST": "Returns summary with active_count and recent_tasks", - "PRE": "tasks is a list of Task objects", - "PURPOSE": "Get summary of active and recent tasks for the activity indicator", - "RETURN": "Dict - Activity summary" - }, - "relations": [ - { - "source_id": "get_activity_summary", - "relation_type": "CALLS", - "target_id": "_extract_resource_name_from_task", - "target_ref": "[_extract_resource_name_from_task]" - }, - { - "source_id": "get_activity_summary", - "relation_type": "CALLS", - "target_id": "_extract_resource_type_from_task", - "target_ref": "[_extract_resource_type_from_task]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:get_activity_summary:Function]\n # @COMPLEXITY: 3\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 # [/DEF:get_activity_summary:Function]\n" - }, - { - "contract_id": "_get_git_status_for_dashboard", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 370, - "end_line": 428, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "dashboard_id (int) - The dashboard ID", - "POST": "Returns git status or None if no repo exists", - "PRE": "dashboard_id is a valid integer", - "PURPOSE": "Get Git sync status for a dashboard", - "RETURN": "Optional[Dict] - Git status with branch and sync_status" - }, - "relations": [ - { - "source_id": "_get_git_status_for_dashboard", - "relation_type": "CALLS", - "target_id": "get_repo", - "target_ref": "[get_repo]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_git_status_for_dashboard:Function]\n # @COMPLEXITY: 3\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) -> Optional[Dict[str, Any]]:\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 log.explore(f\"Failed to get git status for dashboard {dashboard_id}\", error=\"Git status check failed\")\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 # [/DEF:_get_git_status_for_dashboard:Function]\n" - }, - { - "contract_id": "_get_last_task_for_resource", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 430, - "end_line": 467, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "tasks (Optional[List[Task]]) - List of tasks to search", - "POST": "Returns task summary or None if no tasks found", - "PRE": "resource_id is a valid string", - "PURPOSE": "Get the most recent task for a specific resource", - "RETURN": "Optional[Dict] - Task summary with task_id and status" - }, - "relations": [ - { - "source_id": "_get_last_task_for_resource", - "relation_type": "CALLS", - "target_id": "_normalize_datetime_for_compare", - "target_ref": "[_normalize_datetime_for_compare]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": " # [DEF:_get_last_task_for_resource:Function]\n # @COMPLEXITY: 3\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: Optional[List[Task]] = None\n ) -> Optional[Dict[str, Any]]:\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 # [/DEF:_get_last_task_for_resource:Function]\n" - }, - { - "contract_id": "_extract_resource_name_from_task", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 469, - "end_line": 480, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task (Task) - The task to extract from", - "POST": "Returns resource name or task ID", - "PRE": "task is a valid Task object", - "PURPOSE": "Extract resource name from task params", - "RETURN": "str - Resource name or fallback" - }, - "relations": [ - { - "source_id": "_extract_resource_name_from_task", - "relation_type": "USED_BY", - "target_id": "get_activity_summary", - "target_ref": "[get_activity_summary]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": " # [DEF:_extract_resource_name_from_task:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_extract_resource_name_from_task:Function]\n" - }, - { - "contract_id": "_extract_resource_type_from_task", - "contract_type": "Function", - "file_path": "backend/src/services/resource_service.py", - "start_line": 482, - "end_line": 493, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "PARAM": "task (Task) - The task to extract from", - "POST": "Returns resource type or 'unknown'", - "PRE": "task is a valid Task object", - "PURPOSE": "Extract resource type from task params", - "RETURN": "str - Resource type" - }, - "relations": [ - { - "source_id": "_extract_resource_type_from_task", - "relation_type": "USED_BY", - "target_id": "get_activity_summary", - "target_ref": "[get_activity_summary]" - } - ], - "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 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": "unknown_tag", - "tag": "RETURN", - "message": "@RETURN is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": " # [DEF:_extract_resource_type_from_task:Function]\n # @COMPLEXITY: 3\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 # [/DEF:_extract_resource_type_from_task:Function]\n" + "anchor_syntax": "region", + "body": "# #region TestSessionConfig [C:2] [TYPE Module] [SEMANTICS test, conftest, db]\n# @BRIEF Shared pytest fixtures and session configuration for backend tests.\n# @RELATION BINDS_TO -> [init_db]\n# @PRE All test modules use sys.path patching to import from src/.\n# @POST Database tables exist before test session executes.\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef ensure_db_tables():\n \"\"\"Ensure all tables exist before test session.\n\n This fixture runs once per test session, creating all registered\n SQLAlchemy tables. Individual test modules may also create their\n own in-memory engines — this guarantees the real engine schema\n is initialized for tests that depend on it.\n \"\"\"\n from src.core.database import init_db\n init_db()\n# #endregion TestSessionConfig\n" }, { "contract_id": "TestArchiveParser", "contract_type": "Module", "file_path": "backend/tests/core/migration/test_archive_parser.py", "start_line": 1, - "end_line": 79, - "tier": "TIER_2", - "complexity": 3, + "end_line": 78, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for MigrationArchiveParser ZIP extraction contract." }, @@ -97845,16 +66003,35 @@ "target_ref": "[MigrationArchiveParserModule]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", - "body": "# [DEF:TestArchiveParser:Module]\n#\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for MigrationArchiveParser ZIP extraction contract.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [MigrationArchiveParserModule]\n#\nimport os\nimport sys\nimport tempfile\nimport zipfile\nfrom pathlib import Path\n\nimport yaml\n\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.migration.archive_parser import MigrationArchiveParser\n\n\n# [DEF:test_extract_objects_from_zip_collects_all_types:Function]\n# @RELATION: BINDS_TO -> TestArchiveParser\n# @PURPOSE: Verify archive parser collects dashboard/chart/dataset YAML objects into typed buckets.\n# @TEST_CONTRACT: zip_archive_fixture -> typed dashboard/chart/dataset extraction buckets\n# @TEST_SCENARIO: archive_with_supported_objects_extracts_all_types -> One YAML file per supported type lands in matching bucket.\n# @TEST_EDGE: missing_field -> Missing typed folder prevents that object class from being extracted.\n# @TEST_EDGE: invalid_type -> Unsupported file layout is ignored by typed extraction buckets.\n# @TEST_EDGE: external_fail -> Corrupt archive would fail extraction before typed buckets are returned.\ndef test_extract_objects_from_zip_collects_all_types():\n parser = MigrationArchiveParser()\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"objects.zip\"\n src_dir = td_path / \"src\"\n (src_dir / \"dashboards\").mkdir(parents=True)\n (src_dir / \"charts\").mkdir(parents=True)\n (src_dir / \"datasets\").mkdir(parents=True)\n\n with open(src_dir / \"dashboards\" / \"dash.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"dash-u1\", \"dashboard_title\": \"D1\", \"json_metadata\": \"{}\"},\n file_obj,\n )\n with open(src_dir / \"charts\" / \"chart.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"chart-u1\", \"slice_name\": \"C1\", \"viz_type\": \"bar\"}, file_obj\n )\n with open(src_dir / \"datasets\" / \"dataset.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"ds-u1\", \"table_name\": \"orders\", \"database_uuid\": \"db-u1\"},\n file_obj,\n )\n\n with zipfile.ZipFile(zip_path, \"w\") as zip_obj:\n for root, _, files in os.walk(src_dir):\n for file_name in files:\n file_path = Path(root) / file_name\n zip_obj.write(file_path, file_path.relative_to(src_dir))\n\n extracted = parser.extract_objects_from_zip(str(zip_path))\n\n if len(extracted[\"dashboards\"]) != 1:\n raise AssertionError(\"dashboards extraction size mismatch\")\n if extracted[\"dashboards\"][0][\"uuid\"] != \"dash-u1\":\n raise AssertionError(\"dashboard uuid mismatch\")\n if len(extracted[\"charts\"]) != 1:\n raise AssertionError(\"charts extraction size mismatch\")\n if extracted[\"charts\"][0][\"uuid\"] != \"chart-u1\":\n raise AssertionError(\"chart uuid mismatch\")\n if len(extracted[\"datasets\"]) != 1:\n raise AssertionError(\"datasets extraction size mismatch\")\n if extracted[\"datasets\"][0][\"uuid\"] != \"ds-u1\":\n raise AssertionError(\"dataset uuid mismatch\")\n\n\n# [/DEF:test_extract_objects_from_zip_collects_all_types:Function]\n# [/DEF:TestArchiveParser:Module]\n" + "body": "# [DEF:TestArchiveParser:Module]\n#\n# @PURPOSE: Unit tests for MigrationArchiveParser ZIP extraction contract.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [MigrationArchiveParserModule]\n#\nimport os\nimport sys\nimport tempfile\nimport zipfile\nfrom pathlib import Path\n\nimport yaml\n\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.migration.archive_parser import MigrationArchiveParser\n\n\n# [DEF:test_extract_objects_from_zip_collects_all_types:Function]\n# @RELATION: BINDS_TO -> TestArchiveParser\n# @PURPOSE: Verify archive parser collects dashboard/chart/dataset YAML objects into typed buckets.\n# @TEST_CONTRACT: zip_archive_fixture -> typed dashboard/chart/dataset extraction buckets\n# @TEST_SCENARIO: archive_with_supported_objects_extracts_all_types -> One YAML file per supported type lands in matching bucket.\n# @TEST_EDGE: missing_field -> Missing typed folder prevents that object class from being extracted.\n# @TEST_EDGE: invalid_type -> Unsupported file layout is ignored by typed extraction buckets.\n# @TEST_EDGE: external_fail -> Corrupt archive would fail extraction before typed buckets are returned.\ndef test_extract_objects_from_zip_collects_all_types():\n parser = MigrationArchiveParser()\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"objects.zip\"\n src_dir = td_path / \"src\"\n (src_dir / \"dashboards\").mkdir(parents=True)\n (src_dir / \"charts\").mkdir(parents=True)\n (src_dir / \"datasets\").mkdir(parents=True)\n\n with open(src_dir / \"dashboards\" / \"dash.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"dash-u1\", \"dashboard_title\": \"D1\", \"json_metadata\": \"{}\"},\n file_obj,\n )\n with open(src_dir / \"charts\" / \"chart.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"chart-u1\", \"slice_name\": \"C1\", \"viz_type\": \"bar\"}, file_obj\n )\n with open(src_dir / \"datasets\" / \"dataset.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"ds-u1\", \"table_name\": \"orders\", \"database_uuid\": \"db-u1\"},\n file_obj,\n )\n\n with zipfile.ZipFile(zip_path, \"w\") as zip_obj:\n for root, _, files in os.walk(src_dir):\n for file_name in files:\n file_path = Path(root) / file_name\n zip_obj.write(file_path, file_path.relative_to(src_dir))\n\n extracted = parser.extract_objects_from_zip(str(zip_path))\n\n if len(extracted[\"dashboards\"]) != 1:\n raise AssertionError(\"dashboards extraction size mismatch\")\n if extracted[\"dashboards\"][0][\"uuid\"] != \"dash-u1\":\n raise AssertionError(\"dashboard uuid mismatch\")\n if len(extracted[\"charts\"]) != 1:\n raise AssertionError(\"charts extraction size mismatch\")\n if extracted[\"charts\"][0][\"uuid\"] != \"chart-u1\":\n raise AssertionError(\"chart uuid mismatch\")\n if len(extracted[\"datasets\"]) != 1:\n raise AssertionError(\"datasets extraction size mismatch\")\n if extracted[\"datasets\"][0][\"uuid\"] != \"ds-u1\":\n raise AssertionError(\"dataset uuid mismatch\")\n\n\n# [/DEF:test_extract_objects_from_zip_collects_all_types:Function]\n# [/DEF:TestArchiveParser:Module]\n" }, { "contract_id": "test_extract_objects_from_zip_collects_all_types", "contract_type": "Function", "file_path": "backend/tests/core/migration/test_archive_parser.py", - "start_line": 23, - "end_line": 78, + "start_line": 22, + "end_line": 77, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -97890,11 +66067,10 @@ "contract_type": "Module", "file_path": "backend/tests/core/migration/test_dry_run_orchestrator.py", "start_line": 1, - "end_line": 135, - "tier": "TIER_2", - "complexity": 3, + "end_line": 134, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for MigrationDryRunService diff and risk computation contracts." }, @@ -97906,16 +66082,35 @@ "target_ref": "[MigrationDryRunOrchestratorModule]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", - "body": "# [DEF:TestDryRunOrchestrator:Module]\n#\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [MigrationDryRunOrchestratorModule]\n#\nimport json\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import StaticPool\n\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.migration.dry_run_orchestrator import MigrationDryRunService\nfrom src.models.dashboard import DashboardSelection\nfrom src.models.mapping import Base\n\n\n# [DEF:_load_fixture:Function]\n# @RELATION: BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Load canonical migration dry-run fixture payload used by deterministic orchestration assertions.\ndef _load_fixture() -> dict:\n fixture_path = (\n Path(__file__).parents[2] / \"fixtures\" / \"migration_dry_run_fixture.json\"\n )\n return json.loads(fixture_path.read_text())\n\n\n# [/DEF:_load_fixture:Function]\n\n\n# [DEF:_make_session:Function]\n# @RELATION: BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Build isolated in-memory SQLAlchemy session for dry-run service tests.\ndef _make_session():\n engine = create_engine(\n \"sqlite:///:memory:\",\n connect_args={\"check_same_thread\": False},\n poolclass=StaticPool,\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n return Session()\n\n\n# [/DEF:_make_session:Function]\n\n\n# [DEF:test_migration_dry_run_service_builds_diff_and_risk:Function]\n# @RELATION: BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Verify dry-run orchestration returns stable diff summary and required risk codes.\n# @TEST_SCENARIO: dry_run_builds_diff_and_risk -> Stable diff summary and required risk codes are returned.\n# @TEST_EDGE: missing_field -> Missing target datasource remains visible in risk items.\n# @TEST_EDGE: invalid_type -> Broken dataset reference remains visible in risk items.\n# @TEST_EDGE: external_fail -> Engine transform stub failure would stop result production.\n# @TEST_INVARIANT: dry_run_result_contract_matches_fixture -> VERIFIED_BY: [dry_run_builds_diff_and_risk]\ndef test_migration_dry_run_service_builds_diff_and_risk():\n # @TEST_CONTRACT: dry_run_result_contract -> {\n # required_fields: {diff: object, summary: object, risk: object},\n # invariants: [\"risk.score >= 0\", \"summary.selected_dashboards == len(selection.selected_ids)\"]\n # }\n # @TEST_FIXTURE: migration_dry_run_fixture -> backend/tests/fixtures/migration_dry_run_fixture.json\n # @TEST_EDGE: missing_target_datasource -> fixture.transformed_zip_objects.datasets[0].database_uuid\n # @TEST_EDGE: breaking_reference -> fixture.transformed_zip_objects.charts[0].dataset_uuid\n fixture = _load_fixture()\n db = _make_session()\n selection = DashboardSelection(\n selected_ids=[42],\n source_env_id=\"src\",\n target_env_id=\"tgt\",\n replace_db_config=False,\n fix_cross_filters=True,\n )\n\n source_client = MagicMock()\n source_client.get_dashboards_summary.return_value = fixture[\n \"source_dashboard_summary\"\n ]\n source_client.export_dashboard.return_value = (b\"PK\\x03\\x04\", \"source.zip\")\n\n target_client = MagicMock()\n target_client.get_dashboards.return_value = (\n len(fixture[\"target\"][\"dashboards\"]),\n fixture[\"target\"][\"dashboards\"],\n )\n target_client.get_datasets.return_value = (\n len(fixture[\"target\"][\"datasets\"]),\n fixture[\"target\"][\"datasets\"],\n )\n target_client.get_charts.return_value = (\n len(fixture[\"target\"][\"charts\"]),\n fixture[\"target\"][\"charts\"],\n )\n target_client.get_databases.return_value = (\n len(fixture[\"target\"][\"databases\"]),\n fixture[\"target\"][\"databases\"],\n )\n\n parser = MagicMock()\n parser.extract_objects_from_zip.return_value = fixture[\"transformed_zip_objects\"]\n service = MigrationDryRunService(parser=parser)\n\n with patch(\"src.core.migration.dry_run_orchestrator.MigrationEngine\") as EngineMock:\n engine = MagicMock()\n engine.transform_zip.return_value = True\n EngineMock.return_value = engine\n result = service.run(selection, source_client, target_client, db)\n\n if \"summary\" not in result:\n raise AssertionError(\"summary is missing in dry-run payload\")\n if result[\"summary\"][\"selected_dashboards\"] != 1:\n raise AssertionError(\"selected_dashboards summary mismatch\")\n if result[\"summary\"][\"dashboards\"][\"update\"] != 1:\n raise AssertionError(\"dashboard update count mismatch\")\n if result[\"summary\"][\"charts\"][\"create\"] != 1:\n raise AssertionError(\"chart create count mismatch\")\n if result[\"summary\"][\"datasets\"][\"create\"] != 1:\n raise AssertionError(\"dataset create count mismatch\")\n\n risk_codes = {item[\"code\"] for item in result[\"risk\"][\"items\"]}\n if \"missing_datasource\" not in risk_codes:\n raise AssertionError(\"missing_datasource risk is not detected\")\n if \"breaking_reference\" not in risk_codes:\n raise AssertionError(\"breaking_reference risk is not detected\")\n\n\n# [/DEF:test_migration_dry_run_service_builds_diff_and_risk:Function]\n# [/DEF:TestDryRunOrchestrator:Module]\n" + "body": "# [DEF:TestDryRunOrchestrator:Module]\n#\n# @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts.\n# @LAYER: Domain\n# @RELATION: DEPENDS_ON -> [MigrationDryRunOrchestratorModule]\n#\nimport json\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import StaticPool\n\nbackend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.migration.dry_run_orchestrator import MigrationDryRunService\nfrom src.models.dashboard import DashboardSelection\nfrom src.models.mapping import Base\n\n\n# [DEF:_load_fixture:Function]\n# @RELATION: BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Load canonical migration dry-run fixture payload used by deterministic orchestration assertions.\ndef _load_fixture() -> dict:\n fixture_path = (\n Path(__file__).parents[2] / \"fixtures\" / \"migration_dry_run_fixture.json\"\n )\n return json.loads(fixture_path.read_text())\n\n\n# [/DEF:_load_fixture:Function]\n\n\n# [DEF:_make_session:Function]\n# @RELATION: BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Build isolated in-memory SQLAlchemy session for dry-run service tests.\ndef _make_session():\n engine = create_engine(\n \"sqlite:///:memory:\",\n connect_args={\"check_same_thread\": False},\n poolclass=StaticPool,\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n return Session()\n\n\n# [/DEF:_make_session:Function]\n\n\n# [DEF:test_migration_dry_run_service_builds_diff_and_risk:Function]\n# @RELATION: BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Verify dry-run orchestration returns stable diff summary and required risk codes.\n# @TEST_SCENARIO: dry_run_builds_diff_and_risk -> Stable diff summary and required risk codes are returned.\n# @TEST_EDGE: missing_field -> Missing target datasource remains visible in risk items.\n# @TEST_EDGE: invalid_type -> Broken dataset reference remains visible in risk items.\n# @TEST_EDGE: external_fail -> Engine transform stub failure would stop result production.\n# @TEST_INVARIANT: dry_run_result_contract_matches_fixture -> VERIFIED_BY: [dry_run_builds_diff_and_risk]\ndef test_migration_dry_run_service_builds_diff_and_risk():\n # @TEST_CONTRACT: dry_run_result_contract -> {\n # required_fields: {diff: object, summary: object, risk: object},\n # invariants: [\"risk.score >= 0\", \"summary.selected_dashboards == len(selection.selected_ids)\"]\n # }\n # @TEST_FIXTURE: migration_dry_run_fixture -> backend/tests/fixtures/migration_dry_run_fixture.json\n # @TEST_EDGE: missing_target_datasource -> fixture.transformed_zip_objects.datasets[0].database_uuid\n # @TEST_EDGE: breaking_reference -> fixture.transformed_zip_objects.charts[0].dataset_uuid\n fixture = _load_fixture()\n db = _make_session()\n selection = DashboardSelection(\n selected_ids=[42],\n source_env_id=\"src\",\n target_env_id=\"tgt\",\n replace_db_config=False,\n fix_cross_filters=True,\n )\n\n source_client = MagicMock()\n source_client.get_dashboards_summary.return_value = fixture[\n \"source_dashboard_summary\"\n ]\n source_client.export_dashboard.return_value = (b\"PK\\x03\\x04\", \"source.zip\")\n\n target_client = MagicMock()\n target_client.get_dashboards.return_value = (\n len(fixture[\"target\"][\"dashboards\"]),\n fixture[\"target\"][\"dashboards\"],\n )\n target_client.get_datasets.return_value = (\n len(fixture[\"target\"][\"datasets\"]),\n fixture[\"target\"][\"datasets\"],\n )\n target_client.get_charts.return_value = (\n len(fixture[\"target\"][\"charts\"]),\n fixture[\"target\"][\"charts\"],\n )\n target_client.get_databases.return_value = (\n len(fixture[\"target\"][\"databases\"]),\n fixture[\"target\"][\"databases\"],\n )\n\n parser = MagicMock()\n parser.extract_objects_from_zip.return_value = fixture[\"transformed_zip_objects\"]\n service = MigrationDryRunService(parser=parser)\n\n with patch(\"src.core.migration.dry_run_orchestrator.MigrationEngine\") as EngineMock:\n engine = MagicMock()\n engine.transform_zip.return_value = True\n EngineMock.return_value = engine\n result = service.run(selection, source_client, target_client, db)\n\n if \"summary\" not in result:\n raise AssertionError(\"summary is missing in dry-run payload\")\n if result[\"summary\"][\"selected_dashboards\"] != 1:\n raise AssertionError(\"selected_dashboards summary mismatch\")\n if result[\"summary\"][\"dashboards\"][\"update\"] != 1:\n raise AssertionError(\"dashboard update count mismatch\")\n if result[\"summary\"][\"charts\"][\"create\"] != 1:\n raise AssertionError(\"chart create count mismatch\")\n if result[\"summary\"][\"datasets\"][\"create\"] != 1:\n raise AssertionError(\"dataset create count mismatch\")\n\n risk_codes = {item[\"code\"] for item in result[\"risk\"][\"items\"]}\n if \"missing_datasource\" not in risk_codes:\n raise AssertionError(\"missing_datasource risk is not detected\")\n if \"breaking_reference\" not in risk_codes:\n raise AssertionError(\"breaking_reference risk is not detected\")\n\n\n# [/DEF:test_migration_dry_run_service_builds_diff_and_risk:Function]\n# [/DEF:TestDryRunOrchestrator:Module]\n" }, { "contract_id": "_load_fixture", "contract_type": "Function", "file_path": "backend/tests/core/migration/test_dry_run_orchestrator.py", - "start_line": 26, - "end_line": 36, + "start_line": 25, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -97956,8 +66151,8 @@ "contract_id": "test_migration_dry_run_service_builds_diff_and_risk", "contract_type": "Function", "file_path": "backend/tests/core/migration/test_dry_run_orchestrator.py", - "start_line": 56, - "end_line": 134, + "start_line": 55, + "end_line": 133, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -97993,8 +66188,8 @@ "contract_id": "test_git_service_get_repo_path_guard", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 14, - "end_line": 23, + "start_line": 16, + "end_line": 25, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98033,8 +66228,8 @@ "contract_id": "test_git_service_get_repo_path_recreates_base_dir", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 25, - "end_line": 37, + "start_line": 27, + "end_line": 39, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98073,8 +66268,8 @@ "contract_id": "test_superset_client_import_dashboard_guard", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 39, - "end_line": 55, + "start_line": 41, + "end_line": 57, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98113,8 +66308,8 @@ "contract_id": "test_git_service_init_repo_reclones_when_path_is_not_a_git_repo", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 57, - "end_line": 78, + "start_line": 59, + "end_line": 80, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98153,8 +66348,8 @@ "contract_id": "test_git_service_ensure_gitflow_branches_creates_and_pushes_missing_defaults", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 80, - "end_line": 136, + "start_line": 82, + "end_line": 138, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98193,8 +66388,8 @@ "contract_id": "test_git_service_configure_identity_updates_repo_local_config", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 138, - "end_line": 155, + "start_line": 140, + "end_line": 157, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98234,11 +66429,10 @@ "contract_type": "Module", "file_path": "backend/tests/core/test_git_service_gitea_pr.py", "start_line": 1, - "end_line": 107, - "tier": "TIER_2", - "complexity": 3, + "end_line": 106, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "INVARIANT": "A 404 from primary Gitea URL retries once against remote-url host when different.", "LAYER": "Domain", "PURPOSE": "Validate Gitea PR creation fallback behavior when configured server URL is stale.", @@ -98262,9 +66456,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 3, + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Module" } }, @@ -98288,14 +66500,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:TestGitServiceGiteaPr:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, git, gitea, pull_request, fallback\n# @PURPOSE: Validate Gitea PR creation fallback behavior when configured server URL is stale.\n# @LAYER: Domain\n# @INVARIANT: A 404 from primary Gitea URL retries once against remote-url host when different.\n\nimport asyncio\nimport sys\nfrom pathlib import Path\n\nfrom fastapi import HTTPException\nimport pytest\n\nsys.path.insert(0, str(Path(__file__).parent.parent.parent))\n\nfrom src.services.git_service import GitService\n\n\n# [DEF:test_derive_server_url_from_remote_strips_credentials:Function]\n# @RELATION: BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Ensure helper returns host base URL and removes embedded credentials.\n# @PRE: remote_url is an https URL with username/token.\n# @POST: Result is scheme+host only.\ndef test_derive_server_url_from_remote_strips_credentials():\n service = GitService(base_path=\"test_repos\")\n derived = service._derive_server_url_from_remote(\n \"https://oauth2:token@giteabusya.bebesh.ru/busya/covid-vaccine-dashboard.git\"\n )\n assert derived == \"https://giteabusya.bebesh.ru\"\n# [/DEF:test_derive_server_url_from_remote_strips_credentials:Function]\n\n\n# [DEF:test_create_gitea_pull_request_retries_with_remote_host_on_404:Function]\n# @RELATION: BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Verify create_gitea_pull_request retries with remote URL host after primary 404.\n# @PRE: primary server_url differs from remote_url host.\n# @POST: Method returns success payload from fallback request.\ndef test_create_gitea_pull_request_retries_with_remote_host_on_404(monkeypatch):\n service = GitService(base_path=\"test_repos\")\n calls = []\n\n async def fake_gitea_request(method, server_url, pat, endpoint, payload=None):\n calls.append((method, server_url, endpoint))\n if len(calls) == 1:\n raise HTTPException(status_code=404, detail=\"Gitea API error: The target couldn't be found.\")\n return {\"number\": 42, \"html_url\": \"https://giteabusya.bebesh.ru/busya/covid-vaccine-dashboard/pulls/42\", \"state\": \"open\"}\n\n monkeypatch.setattr(service, \"_gitea_request\", fake_gitea_request)\n\n result = asyncio.run(\n service.create_gitea_pull_request(\n server_url=\"https://gitea.bebesh.ru\",\n pat=\"secret\",\n remote_url=\"https://oauth2:secret@giteabusya.bebesh.ru/busya/covid-vaccine-dashboard.git\",\n from_branch=\"ss-dev\",\n to_branch=\"main\",\n title=\"Promote ss-dev -> main\",\n description=\"\",\n )\n )\n\n assert result[\"id\"] == 42\n assert len(calls) == 2\n assert calls[0][1] == \"https://gitea.bebesh.ru\"\n assert calls[1][1] == \"https://giteabusya.bebesh.ru\"\n# [/DEF:test_create_gitea_pull_request_retries_with_remote_host_on_404:Function]\n\n\n# [DEF:test_create_gitea_pull_request_returns_branch_error_when_target_missing:Function]\n# @RELATION: BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Ensure Gitea 404 on PR creation is mapped to actionable target-branch validation error.\n# @PRE: PR create call returns 404 and target branch is absent.\n# @POST: Service raises HTTPException 400 with explicit missing target branch message.\ndef test_create_gitea_pull_request_returns_branch_error_when_target_missing(monkeypatch):\n service = GitService(base_path=\"test_repos\")\n\n async def fake_gitea_request(method, server_url, pat, endpoint, payload=None):\n if method == \"POST\" and endpoint.endswith(\"/pulls\"):\n raise HTTPException(status_code=404, detail=\"Gitea API error: The target couldn't be found.\")\n if method == \"GET\" and endpoint.endswith(\"/branches/dev\"):\n return {\"name\": \"dev\"}\n if method == \"GET\" and endpoint.endswith(\"/branches/preprod\"):\n raise HTTPException(status_code=404, detail=\"branch not found\")\n raise AssertionError(f\"Unexpected request: {method} {endpoint}\")\n\n monkeypatch.setattr(service, \"_gitea_request\", fake_gitea_request)\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(\n service.create_gitea_pull_request(\n server_url=\"https://gitea.bebesh.ru\",\n pat=\"secret\",\n remote_url=\"https://gitea.bebesh.ru/busya/covid-vaccine-dashboard.git\",\n from_branch=\"dev\",\n to_branch=\"preprod\",\n title=\"Promote dev -> preprod\",\n description=\"\",\n )\n )\n\n assert exc_info.value.status_code == 400\n assert \"target branch 'preprod'\" in str(exc_info.value.detail)\n# [/DEF:test_create_gitea_pull_request_returns_branch_error_when_target_missing:Function]\n\n# [/DEF:TestGitServiceGiteaPr:Module]\n" + "body": "# [DEF:TestGitServiceGiteaPr:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: tests, git, gitea, pull_request, fallback\n# @PURPOSE: Validate Gitea PR creation fallback behavior when configured server URL is stale.\n# @LAYER: Domain\n# @INVARIANT: A 404 from primary Gitea URL retries once against remote-url host when different.\n\nimport asyncio\nimport sys\nfrom pathlib import Path\n\nimport pytest\nfrom fastapi import HTTPException\n\nsys.path.insert(0, str(Path(__file__).parent.parent.parent))\n\nfrom src.services.git_service import GitService\n\n\n# [DEF:test_derive_server_url_from_remote_strips_credentials:Function]\n# @RELATION: BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Ensure helper returns host base URL and removes embedded credentials.\n# @PRE: remote_url is an https URL with username/token.\n# @POST: Result is scheme+host only.\ndef test_derive_server_url_from_remote_strips_credentials():\n service = GitService(base_path=\"test_repos\")\n derived = service._derive_server_url_from_remote(\n \"https://oauth2:token@giteabusya.bebesh.ru/busya/covid-vaccine-dashboard.git\"\n )\n assert derived == \"https://giteabusya.bebesh.ru\"\n# [/DEF:test_derive_server_url_from_remote_strips_credentials:Function]\n\n\n# [DEF:test_create_gitea_pull_request_retries_with_remote_host_on_404:Function]\n# @RELATION: BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Verify create_gitea_pull_request retries with remote URL host after primary 404.\n# @PRE: primary server_url differs from remote_url host.\n# @POST: Method returns success payload from fallback request.\ndef test_create_gitea_pull_request_retries_with_remote_host_on_404(monkeypatch):\n service = GitService(base_path=\"test_repos\")\n calls = []\n\n async def fake_gitea_request(method, server_url, pat, endpoint, payload=None):\n calls.append((method, server_url, endpoint))\n if len(calls) == 1:\n raise HTTPException(status_code=404, detail=\"Gitea API error: The target couldn't be found.\")\n return {\"number\": 42, \"html_url\": \"https://giteabusya.bebesh.ru/busya/covid-vaccine-dashboard/pulls/42\", \"state\": \"open\"}\n\n monkeypatch.setattr(service, \"_gitea_request\", fake_gitea_request)\n\n result = asyncio.run(\n service.create_gitea_pull_request(\n server_url=\"https://gitea.bebesh.ru\",\n pat=\"secret\",\n remote_url=\"https://oauth2:secret@giteabusya.bebesh.ru/busya/covid-vaccine-dashboard.git\",\n from_branch=\"ss-dev\",\n to_branch=\"main\",\n title=\"Promote ss-dev -> main\",\n description=\"\",\n )\n )\n\n assert result[\"id\"] == 42\n assert len(calls) == 2\n assert calls[0][1] == \"https://gitea.bebesh.ru\"\n assert calls[1][1] == \"https://giteabusya.bebesh.ru\"\n# [/DEF:test_create_gitea_pull_request_retries_with_remote_host_on_404:Function]\n\n\n# [DEF:test_create_gitea_pull_request_returns_branch_error_when_target_missing:Function]\n# @RELATION: BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Ensure Gitea 404 on PR creation is mapped to actionable target-branch validation error.\n# @PRE: PR create call returns 404 and target branch is absent.\n# @POST: Service raises HTTPException 400 with explicit missing target branch message.\ndef test_create_gitea_pull_request_returns_branch_error_when_target_missing(monkeypatch):\n service = GitService(base_path=\"test_repos\")\n\n async def fake_gitea_request(method, server_url, pat, endpoint, payload=None):\n if method == \"POST\" and endpoint.endswith(\"/pulls\"):\n raise HTTPException(status_code=404, detail=\"Gitea API error: The target couldn't be found.\")\n if method == \"GET\" and endpoint.endswith(\"/branches/dev\"):\n return {\"name\": \"dev\"}\n if method == \"GET\" and endpoint.endswith(\"/branches/preprod\"):\n raise HTTPException(status_code=404, detail=\"branch not found\")\n raise AssertionError(f\"Unexpected request: {method} {endpoint}\")\n\n monkeypatch.setattr(service, \"_gitea_request\", fake_gitea_request)\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(\n service.create_gitea_pull_request(\n server_url=\"https://gitea.bebesh.ru\",\n pat=\"secret\",\n remote_url=\"https://gitea.bebesh.ru/busya/covid-vaccine-dashboard.git\",\n from_branch=\"dev\",\n to_branch=\"preprod\",\n title=\"Promote dev -> preprod\",\n description=\"\",\n )\n )\n\n assert exc_info.value.status_code == 400\n assert \"target branch 'preprod'\" in str(exc_info.value.detail)\n# [/DEF:test_create_gitea_pull_request_returns_branch_error_when_target_missing:Function]\n\n# [/DEF:TestGitServiceGiteaPr:Module]\n" }, { "contract_id": "test_derive_server_url_from_remote_strips_credentials", "contract_type": "Function", "file_path": "backend/tests/core/test_git_service_gitea_pr.py", - "start_line": 21, - "end_line": 32, + "start_line": 20, + "end_line": 31, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -98347,8 +66559,8 @@ "contract_id": "test_create_gitea_pull_request_retries_with_remote_host_on_404", "contract_type": "Function", "file_path": "backend/tests/core/test_git_service_gitea_pr.py", - "start_line": 35, - "end_line": 68, + "start_line": 34, + "end_line": 67, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -98400,8 +66612,8 @@ "contract_id": "test_create_gitea_pull_request_returns_branch_error_when_target_missing", "contract_type": "Function", "file_path": "backend/tests/core/test_git_service_gitea_pr.py", - "start_line": 71, - "end_line": 105, + "start_line": 70, + "end_line": 104, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -98454,11 +66666,10 @@ "contract_type": "Module", "file_path": "backend/tests/core/test_mapping_service.py", "start_line": 1, - "end_line": 345, - "tier": "TIER_2", - "complexity": 3, + "end_line": 344, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for the IdMappingService matching UUIDs to integer IDs." }, @@ -98470,16 +66681,35 @@ "target_ref": "[src.core.mapping_service.IdMappingService]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", - "body": "# [DEF:TestMappingService:Module]\n#\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.\n# @LAYER: Domain\n# @RELATION: VERIFIES ->[src.core.mapping_service.IdMappingService]\n#\nimport pytest\nfrom datetime import datetime, timezone\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nimport sys\nimport os\nfrom pathlib import Path\n\n# Add backend directory to sys.path so 'src' can be resolved\nbackend_dir = str(Path(__file__).parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.models.mapping import Base, ResourceMapping, ResourceType\nfrom src.core.mapping_service import IdMappingService\n\n\n@pytest.fixture\ndef db_session():\n # In-memory SQLite for testing\n engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n\n\nclass MockSupersetClient:\n def __init__(self, resources):\n self.resources = resources\n\n def get_all_resources(self, endpoint, since_dttm=None):\n return self.resources.get(endpoint, [])\n\n\n# [DEF:test_sync_environment_upserts_correctly:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_upserts_correctly(db_session):\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\n \"id\": 42,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"slice_name\": \"Test Chart\",\n }\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n mapping = db_session.query(ResourceMapping).first()\n assert mapping is not None\n assert mapping.environment_id == \"test-env\"\n assert mapping.resource_type == ResourceType.CHART\n assert mapping.uuid == \"123e4567-e89b-12d3-a456-426614174000\"\n assert mapping.remote_integer_id == \"42\"\n assert mapping.resource_name == \"Test Chart\"\n\n\n# [/DEF:test_sync_environment_upserts_correctly:Function]\n\n\n# [DEF:test_get_remote_id_returns_integer:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_integer(db_session):\n service = IdMappingService(db_session)\n mapping = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DATASET,\n uuid=\"uuid-1\",\n remote_integer_id=\"99\",\n resource_name=\"Test DS\",\n last_synced_at=datetime.now(timezone.utc),\n )\n db_session.add(mapping)\n db_session.commit()\n\n result = service.get_remote_id(\"test-env\", ResourceType.DATASET, \"uuid-1\")\n assert result == 99\n\n\n# [/DEF:test_get_remote_id_returns_integer:Function]\n\n\n# [DEF:test_get_remote_ids_batch_returns_dict:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_ids_batch_returns_dict(db_session):\n service = IdMappingService(db_session)\n m1 = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DASHBOARD,\n uuid=\"uuid-1\",\n remote_integer_id=\"11\",\n )\n m2 = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DASHBOARD,\n uuid=\"uuid-2\",\n remote_integer_id=\"22\",\n )\n db_session.add_all([m1, m2])\n db_session.commit()\n\n result = service.get_remote_ids_batch(\n \"test-env\", ResourceType.DASHBOARD, [\"uuid-1\", \"uuid-2\", \"uuid-missing\"]\n )\n\n assert len(result) == 2\n assert result[\"uuid-1\"] == 11\n assert result[\"uuid-2\"] == 22\n assert \"uuid-missing\" not in result\n\n\n# [/DEF:test_get_remote_ids_batch_returns_dict:Function]\n\n\n# [DEF:test_sync_environment_updates_existing_mapping:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_updates_existing_mapping(db_session):\n \"\"\"Verify that sync_environment updates an existing mapping (upsert UPDATE path).\"\"\"\n from src.models.mapping import ResourceMapping\n\n # Pre-populate a mapping\n existing = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.CHART,\n uuid=\"123e4567-e89b-12d3-a456-426614174000\",\n remote_integer_id=\"10\",\n resource_name=\"Old Name\",\n )\n db_session.add(existing)\n db_session.commit()\n\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\n \"id\": 42,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"slice_name\": \"Updated Name\",\n }\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n mapping = (\n db_session.query(ResourceMapping)\n .filter_by(uuid=\"123e4567-e89b-12d3-a456-426614174000\")\n .first()\n )\n assert mapping.remote_integer_id == \"42\"\n assert mapping.resource_name == \"Updated Name\"\n # Should still be only one record (updated, not duplicated)\n count = db_session.query(ResourceMapping).count()\n assert count == 1\n\n\n# [/DEF:test_sync_environment_updates_existing_mapping:Function]\n\n\n# [DEF:test_sync_environment_skips_resources_without_uuid:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_skips_resources_without_uuid(db_session):\n \"\"\"Resources missing uuid or having id=None should be silently skipped.\"\"\"\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 42, \"slice_name\": \"No UUID\"}, # Missing 'uuid' -> skipped\n {\n \"id\": None,\n \"uuid\": \"valid-uuid\",\n \"slice_name\": \"ID is None\",\n }, # id=None -> skipped\n {\n \"id\": None,\n \"uuid\": None,\n \"slice_name\": \"Both None\",\n }, # both None -> skipped\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n count = db_session.query(ResourceMapping).count()\n assert count == 0\n\n\n# [/DEF:test_sync_environment_skips_resources_without_uuid:Function]\n\n\n# [DEF:test_sync_environment_handles_api_error_gracefully:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_handles_api_error_gracefully(db_session):\n \"\"\"If one resource type fails, others should still sync.\"\"\"\n\n class FailingClient:\n def get_all_resources(self, endpoint, since_dttm=None):\n if endpoint == \"chart\":\n raise ConnectionError(\"API timeout\")\n if endpoint == \"dataset\":\n return [{\"id\": 99, \"uuid\": \"ds-uuid-1\", \"table_name\": \"users\"}]\n return []\n\n service = IdMappingService(db_session)\n service.sync_environment(\"test-env\", FailingClient())\n\n count = db_session.query(ResourceMapping).count()\n assert count == 1 # Only dataset was synced; chart error was swallowed\n mapping = db_session.query(ResourceMapping).first()\n assert mapping.resource_type == ResourceType.DATASET\n\n\n# [/DEF:test_sync_environment_handles_api_error_gracefully:Function]\n\n\n# [DEF:test_get_remote_id_returns_none_for_missing:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_none_for_missing(db_session):\n \"\"\"get_remote_id should return None when no mapping exists.\"\"\"\n service = IdMappingService(db_session)\n result = service.get_remote_id(\"test-env\", ResourceType.CHART, \"nonexistent-uuid\")\n assert result is None\n\n\n# [/DEF:test_get_remote_id_returns_none_for_missing:Function]\n\n\n# [DEF:test_get_remote_ids_batch_returns_empty_for_empty_input:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_ids_batch_returns_empty_for_empty_input(db_session):\n \"\"\"get_remote_ids_batch should return {} for an empty list of UUIDs.\"\"\"\n service = IdMappingService(db_session)\n result = service.get_remote_ids_batch(\"test-env\", ResourceType.CHART, [])\n assert result == {}\n\n\n# [/DEF:test_get_remote_ids_batch_returns_empty_for_empty_input:Function]\n\n\n# [DEF:test_mapping_service_alignment_with_test_data:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_mapping_service_alignment_with_test_data(db_session):\n \"\"\"**@TEST_DATA**: Verifies that the service aligns with the resource_mapping_record contract.\"\"\"\n # Contract: {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}\n contract_data = {\n \"environment_id\": \"prod-env-1\",\n \"resource_type\": ResourceType.CHART,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"remote_integer_id\": \"42\",\n }\n\n mapping = ResourceMapping(**contract_data)\n db_session.add(mapping)\n db_session.commit()\n\n service = IdMappingService(db_session)\n result = service.get_remote_id(\n contract_data[\"environment_id\"],\n contract_data[\"resource_type\"],\n contract_data[\"uuid\"],\n )\n\n assert result == 42\n\n\n# [/DEF:test_mapping_service_alignment_with_test_data:Function]\n\n\n# [DEF:test_sync_environment_requires_existing_env:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_requires_existing_env(db_session):\n \"\"\"**@PRE**: Verify behavior when environment_id is invalid/missing in DB.\n Note: The current implementation doesn't strictly check for environment existencia in the DB\n before polling, but it should handle it gracefully or follow the contract.\n \"\"\"\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient({\"chart\": []})\n\n # Even if environment doesn't exist in a hypothetical 'environments' table,\n # the service should still complete or fail according to defined error handling.\n # In GRACE-Poly, @PRE is a hard requirement. If we don't have an Env model check,\n # we simulate the intent.\n\n service.sync_environment(\"non-existent-env\", mock_client)\n # If no error raised, at least verify no mappings were created for other envs\n assert db_session.query(ResourceMapping).count() == 0\n\n\n# [/DEF:test_sync_environment_requires_existing_env:Function]\n\n\n# [DEF:test_sync_environment_deletes_stale_mappings:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_deletes_stale_mappings(db_session):\n \"\"\"Verify that mappings for resources deleted from the remote environment\n are removed from the local DB on the next sync cycle.\"\"\"\n service = IdMappingService(db_session)\n\n # First sync: 2 charts exist\n client_v1 = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 1, \"uuid\": \"aaa\", \"slice_name\": \"Chart A\"},\n {\"id\": 2, \"uuid\": \"bbb\", \"slice_name\": \"Chart B\"},\n ]\n }\n )\n service.sync_environment(\"env1\", client_v1)\n assert (\n db_session.query(ResourceMapping).filter_by(environment_id=\"env1\").count() == 2\n )\n\n # Second sync: user deleted Chart B from superset\n client_v2 = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 1, \"uuid\": \"aaa\", \"slice_name\": \"Chart A\"},\n ]\n }\n )\n service.sync_environment(\"env1\", client_v2)\n\n remaining = db_session.query(ResourceMapping).filter_by(environment_id=\"env1\").all()\n assert len(remaining) == 1\n assert remaining[0].uuid == \"aaa\"\n\n\n# [/DEF:test_sync_environment_deletes_stale_mappings:Function]\n# [/DEF:TestMappingService:Module]\n" + "body": "# [DEF:TestMappingService:Module]\n#\n# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.\n# @LAYER: Domain\n# @RELATION: VERIFIES ->[src.core.mapping_service.IdMappingService]\n#\nimport sys\nfrom datetime import UTC, datetime\nfrom pathlib import Path\n\nimport pytest\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\n# Add backend directory to sys.path so 'src' can be resolved\nbackend_dir = str(Path(__file__).parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.mapping_service import IdMappingService\nfrom src.models.mapping import Base, ResourceMapping, ResourceType\n\n\n@pytest.fixture\ndef db_session():\n # In-memory SQLite for testing\n engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n\n\nclass MockSupersetClient:\n def __init__(self, resources):\n self.resources = resources\n\n def get_all_resources(self, endpoint, since_dttm=None):\n return self.resources.get(endpoint, [])\n\n\n# [DEF:test_sync_environment_upserts_correctly:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_upserts_correctly(db_session):\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\n \"id\": 42,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"slice_name\": \"Test Chart\",\n }\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n mapping = db_session.query(ResourceMapping).first()\n assert mapping is not None\n assert mapping.environment_id == \"test-env\"\n assert mapping.resource_type == ResourceType.CHART\n assert mapping.uuid == \"123e4567-e89b-12d3-a456-426614174000\"\n assert mapping.remote_integer_id == \"42\"\n assert mapping.resource_name == \"Test Chart\"\n\n\n# [/DEF:test_sync_environment_upserts_correctly:Function]\n\n\n# [DEF:test_get_remote_id_returns_integer:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_integer(db_session):\n service = IdMappingService(db_session)\n mapping = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DATASET,\n uuid=\"uuid-1\",\n remote_integer_id=\"99\",\n resource_name=\"Test DS\",\n last_synced_at=datetime.now(UTC),\n )\n db_session.add(mapping)\n db_session.commit()\n\n result = service.get_remote_id(\"test-env\", ResourceType.DATASET, \"uuid-1\")\n assert result == 99\n\n\n# [/DEF:test_get_remote_id_returns_integer:Function]\n\n\n# [DEF:test_get_remote_ids_batch_returns_dict:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_ids_batch_returns_dict(db_session):\n service = IdMappingService(db_session)\n m1 = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DASHBOARD,\n uuid=\"uuid-1\",\n remote_integer_id=\"11\",\n )\n m2 = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DASHBOARD,\n uuid=\"uuid-2\",\n remote_integer_id=\"22\",\n )\n db_session.add_all([m1, m2])\n db_session.commit()\n\n result = service.get_remote_ids_batch(\n \"test-env\", ResourceType.DASHBOARD, [\"uuid-1\", \"uuid-2\", \"uuid-missing\"]\n )\n\n assert len(result) == 2\n assert result[\"uuid-1\"] == 11\n assert result[\"uuid-2\"] == 22\n assert \"uuid-missing\" not in result\n\n\n# [/DEF:test_get_remote_ids_batch_returns_dict:Function]\n\n\n# [DEF:test_sync_environment_updates_existing_mapping:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_updates_existing_mapping(db_session):\n \"\"\"Verify that sync_environment updates an existing mapping (upsert UPDATE path).\"\"\"\n from src.models.mapping import ResourceMapping\n\n # Pre-populate a mapping\n existing = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.CHART,\n uuid=\"123e4567-e89b-12d3-a456-426614174000\",\n remote_integer_id=\"10\",\n resource_name=\"Old Name\",\n )\n db_session.add(existing)\n db_session.commit()\n\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\n \"id\": 42,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"slice_name\": \"Updated Name\",\n }\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n mapping = (\n db_session.query(ResourceMapping)\n .filter_by(uuid=\"123e4567-e89b-12d3-a456-426614174000\")\n .first()\n )\n assert mapping.remote_integer_id == \"42\"\n assert mapping.resource_name == \"Updated Name\"\n # Should still be only one record (updated, not duplicated)\n count = db_session.query(ResourceMapping).count()\n assert count == 1\n\n\n# [/DEF:test_sync_environment_updates_existing_mapping:Function]\n\n\n# [DEF:test_sync_environment_skips_resources_without_uuid:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_skips_resources_without_uuid(db_session):\n \"\"\"Resources missing uuid or having id=None should be silently skipped.\"\"\"\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 42, \"slice_name\": \"No UUID\"}, # Missing 'uuid' -> skipped\n {\n \"id\": None,\n \"uuid\": \"valid-uuid\",\n \"slice_name\": \"ID is None\",\n }, # id=None -> skipped\n {\n \"id\": None,\n \"uuid\": None,\n \"slice_name\": \"Both None\",\n }, # both None -> skipped\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n count = db_session.query(ResourceMapping).count()\n assert count == 0\n\n\n# [/DEF:test_sync_environment_skips_resources_without_uuid:Function]\n\n\n# [DEF:test_sync_environment_handles_api_error_gracefully:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_handles_api_error_gracefully(db_session):\n \"\"\"If one resource type fails, others should still sync.\"\"\"\n\n class FailingClient:\n def get_all_resources(self, endpoint, since_dttm=None):\n if endpoint == \"chart\":\n raise ConnectionError(\"API timeout\")\n if endpoint == \"dataset\":\n return [{\"id\": 99, \"uuid\": \"ds-uuid-1\", \"table_name\": \"users\"}]\n return []\n\n service = IdMappingService(db_session)\n service.sync_environment(\"test-env\", FailingClient())\n\n count = db_session.query(ResourceMapping).count()\n assert count == 1 # Only dataset was synced; chart error was swallowed\n mapping = db_session.query(ResourceMapping).first()\n assert mapping.resource_type == ResourceType.DATASET\n\n\n# [/DEF:test_sync_environment_handles_api_error_gracefully:Function]\n\n\n# [DEF:test_get_remote_id_returns_none_for_missing:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_none_for_missing(db_session):\n \"\"\"get_remote_id should return None when no mapping exists.\"\"\"\n service = IdMappingService(db_session)\n result = service.get_remote_id(\"test-env\", ResourceType.CHART, \"nonexistent-uuid\")\n assert result is None\n\n\n# [/DEF:test_get_remote_id_returns_none_for_missing:Function]\n\n\n# [DEF:test_get_remote_ids_batch_returns_empty_for_empty_input:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_ids_batch_returns_empty_for_empty_input(db_session):\n \"\"\"get_remote_ids_batch should return {} for an empty list of UUIDs.\"\"\"\n service = IdMappingService(db_session)\n result = service.get_remote_ids_batch(\"test-env\", ResourceType.CHART, [])\n assert result == {}\n\n\n# [/DEF:test_get_remote_ids_batch_returns_empty_for_empty_input:Function]\n\n\n# [DEF:test_mapping_service_alignment_with_test_data:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_mapping_service_alignment_with_test_data(db_session):\n \"\"\"**@TEST_DATA**: Verifies that the service aligns with the resource_mapping_record contract.\"\"\"\n # Contract: {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}\n contract_data = {\n \"environment_id\": \"prod-env-1\",\n \"resource_type\": ResourceType.CHART,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"remote_integer_id\": \"42\",\n }\n\n mapping = ResourceMapping(**contract_data)\n db_session.add(mapping)\n db_session.commit()\n\n service = IdMappingService(db_session)\n result = service.get_remote_id(\n contract_data[\"environment_id\"],\n contract_data[\"resource_type\"],\n contract_data[\"uuid\"],\n )\n\n assert result == 42\n\n\n# [/DEF:test_mapping_service_alignment_with_test_data:Function]\n\n\n# [DEF:test_sync_environment_requires_existing_env:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_requires_existing_env(db_session):\n \"\"\"**@PRE**: Verify behavior when environment_id is invalid/missing in DB.\n Note: The current implementation doesn't strictly check for environment existencia in the DB\n before polling, but it should handle it gracefully or follow the contract.\n \"\"\"\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient({\"chart\": []})\n\n # Even if environment doesn't exist in a hypothetical 'environments' table,\n # the service should still complete or fail according to defined error handling.\n # In GRACE-Poly, @PRE is a hard requirement. If we don't have an Env model check,\n # we simulate the intent.\n\n service.sync_environment(\"non-existent-env\", mock_client)\n # If no error raised, at least verify no mappings were created for other envs\n assert db_session.query(ResourceMapping).count() == 0\n\n\n# [/DEF:test_sync_environment_requires_existing_env:Function]\n\n\n# [DEF:test_sync_environment_deletes_stale_mappings:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_sync_environment_deletes_stale_mappings(db_session):\n \"\"\"Verify that mappings for resources deleted from the remote environment\n are removed from the local DB on the next sync cycle.\"\"\"\n service = IdMappingService(db_session)\n\n # First sync: 2 charts exist\n client_v1 = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 1, \"uuid\": \"aaa\", \"slice_name\": \"Chart A\"},\n {\"id\": 2, \"uuid\": \"bbb\", \"slice_name\": \"Chart B\"},\n ]\n }\n )\n service.sync_environment(\"env1\", client_v1)\n assert (\n db_session.query(ResourceMapping).filter_by(environment_id=\"env1\").count() == 2\n )\n\n # Second sync: user deleted Chart B from superset\n client_v2 = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 1, \"uuid\": \"aaa\", \"slice_name\": \"Chart A\"},\n ]\n }\n )\n service.sync_environment(\"env1\", client_v2)\n\n remaining = db_session.query(ResourceMapping).filter_by(environment_id=\"env1\").all()\n assert len(remaining) == 1\n assert remaining[0].uuid == \"aaa\"\n\n\n# [/DEF:test_sync_environment_deletes_stale_mappings:Function]\n# [/DEF:TestMappingService:Module]\n" }, { "contract_id": "test_sync_environment_upserts_correctly", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 44, - "end_line": 71, + "start_line": 43, + "end_line": 70, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98518,8 +66748,8 @@ "contract_id": "test_get_remote_id_returns_integer", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 74, - "end_line": 93, + "start_line": 73, + "end_line": 92, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98552,14 +66782,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_get_remote_id_returns_integer:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_integer(db_session):\n service = IdMappingService(db_session)\n mapping = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DATASET,\n uuid=\"uuid-1\",\n remote_integer_id=\"99\",\n resource_name=\"Test DS\",\n last_synced_at=datetime.now(timezone.utc),\n )\n db_session.add(mapping)\n db_session.commit()\n\n result = service.get_remote_id(\"test-env\", ResourceType.DATASET, \"uuid-1\")\n assert result == 99\n\n\n# [/DEF:test_get_remote_id_returns_integer:Function]\n" + "body": "# [DEF:test_get_remote_id_returns_integer:Function]\n# @RELATION: BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_integer(db_session):\n service = IdMappingService(db_session)\n mapping = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DATASET,\n uuid=\"uuid-1\",\n remote_integer_id=\"99\",\n resource_name=\"Test DS\",\n last_synced_at=datetime.now(UTC),\n )\n db_session.add(mapping)\n db_session.commit()\n\n result = service.get_remote_id(\"test-env\", ResourceType.DATASET, \"uuid-1\")\n assert result == 99\n\n\n# [/DEF:test_get_remote_id_returns_integer:Function]\n" }, { "contract_id": "test_get_remote_ids_batch_returns_dict", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 96, - "end_line": 125, + "start_line": 95, + "end_line": 124, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98598,8 +66828,8 @@ "contract_id": "test_sync_environment_updates_existing_mapping", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 128, - "end_line": 172, + "start_line": 127, + "end_line": 171, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98638,8 +66868,8 @@ "contract_id": "test_sync_environment_skips_resources_without_uuid", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 175, - "end_line": 204, + "start_line": 174, + "end_line": 203, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98678,8 +66908,8 @@ "contract_id": "test_sync_environment_handles_api_error_gracefully", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 207, - "end_line": 229, + "start_line": 206, + "end_line": 228, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98718,8 +66948,8 @@ "contract_id": "test_get_remote_id_returns_none_for_missing", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 232, - "end_line": 241, + "start_line": 231, + "end_line": 240, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98758,8 +66988,8 @@ "contract_id": "test_get_remote_ids_batch_returns_empty_for_empty_input", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 244, - "end_line": 253, + "start_line": 243, + "end_line": 252, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98798,8 +67028,8 @@ "contract_id": "test_mapping_service_alignment_with_test_data", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 256, - "end_line": 282, + "start_line": 255, + "end_line": 281, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98838,8 +67068,8 @@ "contract_id": "test_sync_environment_requires_existing_env", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 285, - "end_line": 305, + "start_line": 284, + "end_line": 304, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98878,8 +67108,8 @@ "contract_id": "test_sync_environment_deletes_stale_mappings", "contract_type": "Function", "file_path": "backend/tests/core/test_mapping_service.py", - "start_line": 308, - "end_line": 344, + "start_line": 307, + "end_line": 343, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -98919,11 +67149,10 @@ "contract_type": "Module", "file_path": "backend/tests/core/test_migration_engine.py", "start_line": 1, - "end_line": 365, - "tier": "TIER_2", - "complexity": 3, + "end_line": 360, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Unit tests for MigrationEngine's cross-filter patching algorithms." }, @@ -98935,20 +67164,38 @@ "target_ref": "[src.core.migration_engine:Module]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", - "body": "# [DEF:TestMigrationEngine:Module]\n#\n# @COMPLEXITY: 3\n# @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms.\n# @LAYER: Domain\n# @RELATION: VERIFIES -> [src.core.migration_engine:Module]\n#\nimport pytest\nimport tempfile\nimport json\nimport yaml\nimport zipfile\nimport sys\nimport os\nfrom pathlib import Path\nfrom unittest.mock import MagicMock\n\nbackend_dir = str(Path(__file__).parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.migration_engine import MigrationEngine\nfrom src.core.mapping_service import IdMappingService\nfrom src.models.mapping import ResourceType\n\n\n# --- Fixtures ---\n\n\n# [DEF:MockMappingService:Class]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.\n# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.\nclass MockMappingService:\n \"\"\"Mock that simulates IdMappingService.get_remote_ids_batch.\"\"\"\n\n def __init__(self, mappings: dict):\n self.mappings = mappings\n\n def get_remote_ids_batch(self, env_id, resource_type, uuids):\n # @INVARIANT_VIOLATION: resource_type parameter is silently ignored. Lookups are UUID-only; chart and dataset UUIDs share the same namespace. Tests that mix resource types will produce false positives.\n result = {}\n for uuid in uuids:\n if uuid in self.mappings:\n result[uuid] = self.mappings[uuid]\n return result\n\n\n# [/DEF:MockMappingService:Class]\n\n\n# [DEF:_write_dashboard_yaml:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Serialize dashboard metadata into YAML fixture with json_metadata payload for patch tests.\ndef _write_dashboard_yaml(dir_path: Path, metadata: dict) -> Path:\n \"\"\"Helper: writes a dashboard YAML file with json_metadata.\"\"\"\n file_path = dir_path / \"dash.yaml\"\n with open(file_path, \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n return file_path\n\n\n# --- _patch_dashboard_metadata tests ---\n\n# [/DEF:_write_dashboard_yaml:Function]\n\n\n# [DEF:test_patch_dashboard_metadata_replaces_chart_ids:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify native filter target chartId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_chart_ids():\n \"\"\"Verifies that chartId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-chart-A\": 999})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 42}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {42: \"uuid-chart-A\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"] == 999\n )\n\n\n# [/DEF:test_patch_dashboard_metadata_replaces_chart_ids:Function]\n\n\n# [DEF:test_patch_dashboard_metadata_replaces_dataset_ids:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify native filter target datasetId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_dataset_ids():\n \"\"\"Verifies that datasetId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-ds-B\": 500})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"datasetId\": 10}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {10: \"uuid-ds-B\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"datasetId\"]\n == 500\n )\n\n\n# [/DEF:test_patch_dashboard_metadata_replaces_dataset_ids:Function]\n\n\n# [DEF:test_patch_dashboard_metadata_skips_when_no_metadata:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure dashboard files without json_metadata are left unchanged by metadata patching.\ndef test_patch_dashboard_metadata_skips_when_no_metadata():\n \"\"\"Verifies early return when json_metadata key is absent.\"\"\"\n mock_service = MockMappingService({})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dash.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"title\": \"No metadata here\"}, f)\n\n engine._patch_dashboard_metadata(fp, \"target-env\", {})\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n assert \"json_metadata\" not in data\n\n\n# [/DEF:test_patch_dashboard_metadata_skips_when_no_metadata:Function]\n\n\n# [DEF:test_patch_dashboard_metadata_handles_missing_targets:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify patching updates mapped targets while preserving unmapped native filter IDs.\ndef test_patch_dashboard_metadata_handles_missing_targets():\n \"\"\"When some source IDs have no target mapping, patches what it can and leaves the rest.\"\"\"\n mock_service = MockMappingService({\"uuid-A\": 100}) # Only uuid-A maps\n engine = MigrationEngine(mock_service)\n\n metadata = {\n \"native_filter_configuration\": [\n {\"targets\": [{\"datasetId\": 1}, {\"datasetId\": 2}]}\n ]\n }\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {1: \"uuid-A\", 2: \"uuid-MISSING\"} # uuid-MISSING won't resolve\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n targets = result[\"native_filter_configuration\"][0][\"targets\"]\n # ID 1 should be replaced to 100; ID 2 should remain 2\n assert targets[0][\"datasetId\"] == 100\n assert targets[1][\"datasetId\"] == 2\n\n\n# --- _extract_chart_uuids_from_archive tests ---\n\n# [/DEF:test_patch_dashboard_metadata_handles_missing_targets:Function]\n\n\n# [DEF:test_extract_chart_uuids_from_archive:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify chart archive scan returns complete local chart id-to-uuid mapping.\ndef test_extract_chart_uuids_from_archive():\n \"\"\"Verifies that chart YAML files are parsed for id->uuid mappings.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n charts_dir = Path(td) / \"charts\"\n charts_dir.mkdir()\n\n chart1 = {\"id\": 42, \"uuid\": \"uuid-42\", \"slice_name\": \"Chart One\"}\n chart2 = {\"id\": 99, \"uuid\": \"uuid-99\", \"slice_name\": \"Chart Two\"}\n\n with open(charts_dir / \"chart1.yaml\", \"w\") as f:\n yaml.dump(chart1, f)\n with open(charts_dir / \"chart2.yaml\", \"w\") as f:\n yaml.dump(chart2, f)\n\n result = engine._extract_chart_uuids_from_archive(Path(td))\n\n assert result == {42: \"uuid-42\", 99: \"uuid-99\"}\n\n\n# --- _transform_yaml tests ---\n\n# [/DEF:test_extract_chart_uuids_from_archive:Function]\n\n\n# [DEF:test_transform_yaml_replaces_database_uuid:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure dataset YAML database_uuid fields are replaced when source UUID mapping exists.\ndef test_transform_yaml_replaces_database_uuid():\n \"\"\"Verifies that database_uuid in a dataset YAML is replaced.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-uuid-abc\", \"table_name\": \"my_table\"}, f)\n\n engine._transform_yaml(fp, {\"source-uuid-abc\": \"target-uuid-xyz\"})\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"target-uuid-xyz\"\n assert data[\"table_name\"] == \"my_table\"\n\n\n# [/DEF:test_transform_yaml_replaces_database_uuid:Function]\n\n\n# [DEF:test_transform_yaml_ignores_unmapped_uuid:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure transform_yaml leaves dataset files untouched when database_uuid is not mapped.\ndef test_transform_yaml_ignores_unmapped_uuid():\n \"\"\"Verifies no changes when UUID is not in the mapping.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n original = {\"database_uuid\": \"unknown-uuid\", \"table_name\": \"test\"}\n with open(fp, \"w\") as f:\n yaml.dump(original, f)\n\n engine._transform_yaml(fp, {\"other-uuid\": \"replacement\"})\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"unknown-uuid\"\n\n\n# --- [NEW] transform_zip E2E tests ---\n\n# [/DEF:test_transform_yaml_ignores_unmapped_uuid:Function]\n\n\n# [DEF:test_transform_zip_end_to_end:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Validate full ZIP transform pipeline remaps datasets and dashboard cross-filter chart IDs.\ndef test_transform_zip_end_to_end():\n \"\"\"Verifies full orchestration: extraction, transformation, patching, and re-packaging.\"\"\"\n mock_service = MockMappingService({\"char-uuid\": 101, \"ds-uuid\": 202})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"source.zip\"\n output_path = td_path / \"target.zip\"\n\n # Create source ZIP structure\n with tempfile.TemporaryDirectory() as src_dir:\n src_path = Path(src_dir)\n\n # 1. Dataset\n ds_dir = src_path / \"datasets\"\n ds_dir.mkdir()\n with open(ds_dir / \"ds.yaml\", \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-db-uuid\", \"table_name\": \"users\"}, f)\n\n # 2. Chart\n ch_dir = src_path / \"charts\"\n ch_dir.mkdir()\n with open(ch_dir / \"ch.yaml\", \"w\") as f:\n yaml.dump({\"id\": 10, \"uuid\": \"char-uuid\"}, f)\n\n # 3. Dashboard\n db_dir = src_path / \"dashboards\"\n db_dir.mkdir()\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 10}]}]}\n with open(db_dir / \"db.yaml\", \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n\n with zipfile.ZipFile(zip_path, \"w\") as zf:\n for root, _, files in os.walk(src_path):\n for file in files:\n p = Path(root) / file\n zf.write(p, p.relative_to(src_path))\n\n db_mapping = {\"source-db-uuid\": \"target-db-uuid\"}\n\n # Execute transform\n success = engine.transform_zip(\n str(zip_path),\n str(output_path),\n db_mapping,\n target_env_id=\"test-target\",\n fix_cross_filters=True,\n )\n\n assert success is True\n assert output_path.exists()\n\n # Verify contents\n with tempfile.TemporaryDirectory() as out_dir:\n with zipfile.ZipFile(output_path, \"r\") as zf:\n zf.extractall(out_dir)\n\n out_path = Path(out_dir)\n\n # Verify dataset transformation\n with open(out_path / \"datasets\" / \"ds.yaml\", \"r\") as f:\n ds_data = yaml.safe_load(f)\n assert ds_data[\"database_uuid\"] == \"target-db-uuid\"\n\n # Verify dashboard patching\n with open(out_path / \"dashboards\" / \"db.yaml\", \"r\") as f:\n db_data = yaml.safe_load(f)\n meta = json.loads(db_data[\"json_metadata\"])\n assert (\n meta[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"]\n == 101\n )\n\n\n# [/DEF:test_transform_zip_end_to_end:Function]\n\n\n# [DEF:test_transform_zip_invalid_path:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify transform_zip returns False when source archive path does not exist.\ndef test_transform_zip_invalid_path():\n \"\"\"@PRE: Verify behavior (False) on invalid ZIP path.\"\"\"\n engine = MigrationEngine()\n success = engine.transform_zip(\"non_existent.zip\", \"output.zip\", {})\n assert success is False\n\n\n# [/DEF:test_transform_zip_invalid_path:Function]\n\n\n# [DEF:test_transform_yaml_nonexistent_file:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify transform_yaml raises FileNotFoundError for missing YAML source files.\ndef test_transform_yaml_nonexistent_file():\n \"\"\"@PRE: Verify behavior on non-existent YAML file.\"\"\"\n engine = MigrationEngine()\n # Should log error and not crash (implemented via try-except if wrapped,\n # but _transform_yaml itself might raise FileNotFoundError if not guarded)\n with pytest.raises(FileNotFoundError):\n engine._transform_yaml(Path(\"non_existent.yaml\"), {})\n\n\n# [/DEF:test_transform_yaml_nonexistent_file:Function]\n# [/DEF:TestMigrationEngine:Module]\n" + "body": "# [DEF:TestMigrationEngine:Module]\n#\n# @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms.\n# @LAYER: Domain\n# @RELATION: VERIFIES -> [src.core.migration_engine:Module]\n#\nimport json\nimport os\nimport sys\nimport tempfile\nimport zipfile\nfrom pathlib import Path\n\nimport pytest\nimport yaml\n\nbackend_dir = str(Path(__file__).parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.migration_engine import MigrationEngine\n\n# --- Fixtures ---\n\n\n# [DEF:MockMappingService:Class]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.\n# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.\nclass MockMappingService:\n \"\"\"Mock that simulates IdMappingService.get_remote_ids_batch.\"\"\"\n\n def __init__(self, mappings: dict):\n self.mappings = mappings\n\n def get_remote_ids_batch(self, env_id, resource_type, uuids):\n # @INVARIANT_VIOLATION: resource_type parameter is silently ignored. Lookups are UUID-only; chart and dataset UUIDs share the same namespace. Tests that mix resource types will produce false positives.\n result = {}\n for uuid in uuids:\n if uuid in self.mappings:\n result[uuid] = self.mappings[uuid]\n return result\n\n\n# [/DEF:MockMappingService:Class]\n\n\n# [DEF:_write_dashboard_yaml:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Serialize dashboard metadata into YAML fixture with json_metadata payload for patch tests.\ndef _write_dashboard_yaml(dir_path: Path, metadata: dict) -> Path:\n \"\"\"Helper: writes a dashboard YAML file with json_metadata.\"\"\"\n file_path = dir_path / \"dash.yaml\"\n with open(file_path, \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n return file_path\n\n\n# --- _patch_dashboard_metadata tests ---\n\n# [/DEF:_write_dashboard_yaml:Function]\n\n\n# [DEF:test_patch_dashboard_metadata_replaces_chart_ids:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify native filter target chartId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_chart_ids():\n \"\"\"Verifies that chartId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-chart-A\": 999})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 42}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {42: \"uuid-chart-A\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"] == 999\n )\n\n\n# [/DEF:test_patch_dashboard_metadata_replaces_chart_ids:Function]\n\n\n# [DEF:test_patch_dashboard_metadata_replaces_dataset_ids:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify native filter target datasetId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_dataset_ids():\n \"\"\"Verifies that datasetId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-ds-B\": 500})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"datasetId\": 10}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {10: \"uuid-ds-B\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"datasetId\"]\n == 500\n )\n\n\n# [/DEF:test_patch_dashboard_metadata_replaces_dataset_ids:Function]\n\n\n# [DEF:test_patch_dashboard_metadata_skips_when_no_metadata:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure dashboard files without json_metadata are left unchanged by metadata patching.\ndef test_patch_dashboard_metadata_skips_when_no_metadata():\n \"\"\"Verifies early return when json_metadata key is absent.\"\"\"\n mock_service = MockMappingService({})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dash.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"title\": \"No metadata here\"}, f)\n\n engine._patch_dashboard_metadata(fp, \"target-env\", {})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert \"json_metadata\" not in data\n\n\n# [/DEF:test_patch_dashboard_metadata_skips_when_no_metadata:Function]\n\n\n# [DEF:test_patch_dashboard_metadata_handles_missing_targets:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify patching updates mapped targets while preserving unmapped native filter IDs.\ndef test_patch_dashboard_metadata_handles_missing_targets():\n \"\"\"When some source IDs have no target mapping, patches what it can and leaves the rest.\"\"\"\n mock_service = MockMappingService({\"uuid-A\": 100}) # Only uuid-A maps\n engine = MigrationEngine(mock_service)\n\n metadata = {\n \"native_filter_configuration\": [\n {\"targets\": [{\"datasetId\": 1}, {\"datasetId\": 2}]}\n ]\n }\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {1: \"uuid-A\", 2: \"uuid-MISSING\"} # uuid-MISSING won't resolve\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n targets = result[\"native_filter_configuration\"][0][\"targets\"]\n # ID 1 should be replaced to 100; ID 2 should remain 2\n assert targets[0][\"datasetId\"] == 100\n assert targets[1][\"datasetId\"] == 2\n\n\n# --- _extract_chart_uuids_from_archive tests ---\n\n# [/DEF:test_patch_dashboard_metadata_handles_missing_targets:Function]\n\n\n# [DEF:test_extract_chart_uuids_from_archive:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify chart archive scan returns complete local chart id-to-uuid mapping.\ndef test_extract_chart_uuids_from_archive():\n \"\"\"Verifies that chart YAML files are parsed for id->uuid mappings.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n charts_dir = Path(td) / \"charts\"\n charts_dir.mkdir()\n\n chart1 = {\"id\": 42, \"uuid\": \"uuid-42\", \"slice_name\": \"Chart One\"}\n chart2 = {\"id\": 99, \"uuid\": \"uuid-99\", \"slice_name\": \"Chart Two\"}\n\n with open(charts_dir / \"chart1.yaml\", \"w\") as f:\n yaml.dump(chart1, f)\n with open(charts_dir / \"chart2.yaml\", \"w\") as f:\n yaml.dump(chart2, f)\n\n result = engine._extract_chart_uuids_from_archive(Path(td))\n\n assert result == {42: \"uuid-42\", 99: \"uuid-99\"}\n\n\n# --- _transform_yaml tests ---\n\n# [/DEF:test_extract_chart_uuids_from_archive:Function]\n\n\n# [DEF:test_transform_yaml_replaces_database_uuid:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure dataset YAML database_uuid fields are replaced when source UUID mapping exists.\ndef test_transform_yaml_replaces_database_uuid():\n \"\"\"Verifies that database_uuid in a dataset YAML is replaced.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-uuid-abc\", \"table_name\": \"my_table\"}, f)\n\n engine._transform_yaml(fp, {\"source-uuid-abc\": \"target-uuid-xyz\"})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"target-uuid-xyz\"\n assert data[\"table_name\"] == \"my_table\"\n\n\n# [/DEF:test_transform_yaml_replaces_database_uuid:Function]\n\n\n# [DEF:test_transform_yaml_ignores_unmapped_uuid:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure transform_yaml leaves dataset files untouched when database_uuid is not mapped.\ndef test_transform_yaml_ignores_unmapped_uuid():\n \"\"\"Verifies no changes when UUID is not in the mapping.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n original = {\"database_uuid\": \"unknown-uuid\", \"table_name\": \"test\"}\n with open(fp, \"w\") as f:\n yaml.dump(original, f)\n\n engine._transform_yaml(fp, {\"other-uuid\": \"replacement\"})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"unknown-uuid\"\n\n\n# --- [NEW] transform_zip E2E tests ---\n\n# [/DEF:test_transform_yaml_ignores_unmapped_uuid:Function]\n\n\n# [DEF:test_transform_zip_end_to_end:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Validate full ZIP transform pipeline remaps datasets and dashboard cross-filter chart IDs.\ndef test_transform_zip_end_to_end():\n \"\"\"Verifies full orchestration: extraction, transformation, patching, and re-packaging.\"\"\"\n mock_service = MockMappingService({\"char-uuid\": 101, \"ds-uuid\": 202})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"source.zip\"\n output_path = td_path / \"target.zip\"\n\n # Create source ZIP structure\n with tempfile.TemporaryDirectory() as src_dir:\n src_path = Path(src_dir)\n\n # 1. Dataset\n ds_dir = src_path / \"datasets\"\n ds_dir.mkdir()\n with open(ds_dir / \"ds.yaml\", \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-db-uuid\", \"table_name\": \"users\"}, f)\n\n # 2. Chart\n ch_dir = src_path / \"charts\"\n ch_dir.mkdir()\n with open(ch_dir / \"ch.yaml\", \"w\") as f:\n yaml.dump({\"id\": 10, \"uuid\": \"char-uuid\"}, f)\n\n # 3. Dashboard\n db_dir = src_path / \"dashboards\"\n db_dir.mkdir()\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 10}]}]}\n with open(db_dir / \"db.yaml\", \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n\n with zipfile.ZipFile(zip_path, \"w\") as zf:\n for root, _, files in os.walk(src_path):\n for file in files:\n p = Path(root) / file\n zf.write(p, p.relative_to(src_path))\n\n db_mapping = {\"source-db-uuid\": \"target-db-uuid\"}\n\n # Execute transform\n success = engine.transform_zip(\n str(zip_path),\n str(output_path),\n db_mapping,\n target_env_id=\"test-target\",\n fix_cross_filters=True,\n )\n\n assert success is True\n assert output_path.exists()\n\n # Verify contents\n with tempfile.TemporaryDirectory() as out_dir:\n with zipfile.ZipFile(output_path, \"r\") as zf:\n zf.extractall(out_dir)\n\n out_path = Path(out_dir)\n\n # Verify dataset transformation\n with open(out_path / \"datasets\" / \"ds.yaml\") as f:\n ds_data = yaml.safe_load(f)\n assert ds_data[\"database_uuid\"] == \"target-db-uuid\"\n\n # Verify dashboard patching\n with open(out_path / \"dashboards\" / \"db.yaml\") as f:\n db_data = yaml.safe_load(f)\n meta = json.loads(db_data[\"json_metadata\"])\n assert (\n meta[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"]\n == 101\n )\n\n\n# [/DEF:test_transform_zip_end_to_end:Function]\n\n\n# [DEF:test_transform_zip_invalid_path:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify transform_zip returns False when source archive path does not exist.\ndef test_transform_zip_invalid_path():\n \"\"\"@PRE: Verify behavior (False) on invalid ZIP path.\"\"\"\n engine = MigrationEngine()\n success = engine.transform_zip(\"non_existent.zip\", \"output.zip\", {})\n assert success is False\n\n\n# [/DEF:test_transform_zip_invalid_path:Function]\n\n\n# [DEF:test_transform_yaml_nonexistent_file:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify transform_yaml raises FileNotFoundError for missing YAML source files.\ndef test_transform_yaml_nonexistent_file():\n \"\"\"@PRE: Verify behavior on non-existent YAML file.\"\"\"\n engine = MigrationEngine()\n # Should log error and not crash (implemented via try-except if wrapped,\n # but _transform_yaml itself might raise FileNotFoundError if not guarded)\n with pytest.raises(FileNotFoundError):\n engine._transform_yaml(Path(\"non_existent.yaml\"), {})\n\n\n# [/DEF:test_transform_yaml_nonexistent_file:Function]\n# [/DEF:TestMigrationEngine:Module]\n" }, { "contract_id": "MockMappingService", "contract_type": "Class", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 30, - "end_line": 50, + "start_line": 26, + "end_line": 45, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "COMPLEXITY": "2", "INVARIANT": "Returns mappings only for requested UUID keys present in seeded map.", "INVARIANT_VIOLATION": "resource_type parameter is silently ignored. Lookups are UUID-only; chart and dataset UUIDs share the same namespace. Tests that mix resource types will produce false positives.", "PURPOSE": "Deterministic mapping service double for native filter ID remapping scenarios." @@ -98965,9 +67212,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C2", + "message": "@INVARIANT is forbidden for contract type 'Class' at C1", "detail": { - "actual_complexity": 2, + "actual_complexity": 1, "contract_type": "Class" } }, @@ -98979,23 +67226,32 @@ }, { "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C2", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", "detail": { - "actual_complexity": 2, + "actual_complexity": 1, + "contract_type": "Class" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Class" } } ], "anchor_syntax": "def", - "body": "# [DEF:MockMappingService:Class]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @COMPLEXITY: 2\n# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.\n# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.\nclass MockMappingService:\n \"\"\"Mock that simulates IdMappingService.get_remote_ids_batch.\"\"\"\n\n def __init__(self, mappings: dict):\n self.mappings = mappings\n\n def get_remote_ids_batch(self, env_id, resource_type, uuids):\n # @INVARIANT_VIOLATION: resource_type parameter is silently ignored. Lookups are UUID-only; chart and dataset UUIDs share the same namespace. Tests that mix resource types will produce false positives.\n result = {}\n for uuid in uuids:\n if uuid in self.mappings:\n result[uuid] = self.mappings[uuid]\n return result\n\n\n# [/DEF:MockMappingService:Class]\n" + "body": "# [DEF:MockMappingService:Class]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.\n# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.\nclass MockMappingService:\n \"\"\"Mock that simulates IdMappingService.get_remote_ids_batch.\"\"\"\n\n def __init__(self, mappings: dict):\n self.mappings = mappings\n\n def get_remote_ids_batch(self, env_id, resource_type, uuids):\n # @INVARIANT_VIOLATION: resource_type parameter is silently ignored. Lookups are UUID-only; chart and dataset UUIDs share the same namespace. Tests that mix resource types will produce false positives.\n result = {}\n for uuid in uuids:\n if uuid in self.mappings:\n result[uuid] = self.mappings[uuid]\n return result\n\n\n# [/DEF:MockMappingService:Class]\n" }, { "contract_id": "_write_dashboard_yaml", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 53, - "end_line": 66, + "start_line": 48, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -99036,8 +67292,8 @@ "contract_id": "test_patch_dashboard_metadata_replaces_chart_ids", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 69, - "end_line": 93, + "start_line": 64, + "end_line": 88, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99063,14 +67319,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_patch_dashboard_metadata_replaces_chart_ids:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify native filter target chartId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_chart_ids():\n \"\"\"Verifies that chartId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-chart-A\": 999})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 42}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {42: \"uuid-chart-A\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"] == 999\n )\n\n\n# [/DEF:test_patch_dashboard_metadata_replaces_chart_ids:Function]\n" + "body": "# [DEF:test_patch_dashboard_metadata_replaces_chart_ids:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify native filter target chartId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_chart_ids():\n \"\"\"Verifies that chartId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-chart-A\": 999})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 42}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {42: \"uuid-chart-A\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"] == 999\n )\n\n\n# [/DEF:test_patch_dashboard_metadata_replaces_chart_ids:Function]\n" }, { "contract_id": "test_patch_dashboard_metadata_replaces_dataset_ids", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 96, - "end_line": 121, + "start_line": 91, + "end_line": 116, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99096,14 +67352,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_patch_dashboard_metadata_replaces_dataset_ids:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify native filter target datasetId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_dataset_ids():\n \"\"\"Verifies that datasetId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-ds-B\": 500})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"datasetId\": 10}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {10: \"uuid-ds-B\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"datasetId\"]\n == 500\n )\n\n\n# [/DEF:test_patch_dashboard_metadata_replaces_dataset_ids:Function]\n" + "body": "# [DEF:test_patch_dashboard_metadata_replaces_dataset_ids:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify native filter target datasetId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_dataset_ids():\n \"\"\"Verifies that datasetId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-ds-B\": 500})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"datasetId\": 10}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {10: \"uuid-ds-B\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"datasetId\"]\n == 500\n )\n\n\n# [/DEF:test_patch_dashboard_metadata_replaces_dataset_ids:Function]\n" }, { "contract_id": "test_patch_dashboard_metadata_skips_when_no_metadata", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 124, - "end_line": 144, + "start_line": 119, + "end_line": 139, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99129,14 +67385,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_patch_dashboard_metadata_skips_when_no_metadata:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure dashboard files without json_metadata are left unchanged by metadata patching.\ndef test_patch_dashboard_metadata_skips_when_no_metadata():\n \"\"\"Verifies early return when json_metadata key is absent.\"\"\"\n mock_service = MockMappingService({})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dash.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"title\": \"No metadata here\"}, f)\n\n engine._patch_dashboard_metadata(fp, \"target-env\", {})\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n assert \"json_metadata\" not in data\n\n\n# [/DEF:test_patch_dashboard_metadata_skips_when_no_metadata:Function]\n" + "body": "# [DEF:test_patch_dashboard_metadata_skips_when_no_metadata:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure dashboard files without json_metadata are left unchanged by metadata patching.\ndef test_patch_dashboard_metadata_skips_when_no_metadata():\n \"\"\"Verifies early return when json_metadata key is absent.\"\"\"\n mock_service = MockMappingService({})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dash.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"title\": \"No metadata here\"}, f)\n\n engine._patch_dashboard_metadata(fp, \"target-env\", {})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert \"json_metadata\" not in data\n\n\n# [/DEF:test_patch_dashboard_metadata_skips_when_no_metadata:Function]\n" }, { "contract_id": "test_patch_dashboard_metadata_handles_missing_targets", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 147, - "end_line": 178, + "start_line": 142, + "end_line": 173, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99162,14 +67418,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_patch_dashboard_metadata_handles_missing_targets:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify patching updates mapped targets while preserving unmapped native filter IDs.\ndef test_patch_dashboard_metadata_handles_missing_targets():\n \"\"\"When some source IDs have no target mapping, patches what it can and leaves the rest.\"\"\"\n mock_service = MockMappingService({\"uuid-A\": 100}) # Only uuid-A maps\n engine = MigrationEngine(mock_service)\n\n metadata = {\n \"native_filter_configuration\": [\n {\"targets\": [{\"datasetId\": 1}, {\"datasetId\": 2}]}\n ]\n }\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {1: \"uuid-A\", 2: \"uuid-MISSING\"} # uuid-MISSING won't resolve\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n targets = result[\"native_filter_configuration\"][0][\"targets\"]\n # ID 1 should be replaced to 100; ID 2 should remain 2\n assert targets[0][\"datasetId\"] == 100\n assert targets[1][\"datasetId\"] == 2\n\n\n# --- _extract_chart_uuids_from_archive tests ---\n\n# [/DEF:test_patch_dashboard_metadata_handles_missing_targets:Function]\n" + "body": "# [DEF:test_patch_dashboard_metadata_handles_missing_targets:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Verify patching updates mapped targets while preserving unmapped native filter IDs.\ndef test_patch_dashboard_metadata_handles_missing_targets():\n \"\"\"When some source IDs have no target mapping, patches what it can and leaves the rest.\"\"\"\n mock_service = MockMappingService({\"uuid-A\": 100}) # Only uuid-A maps\n engine = MigrationEngine(mock_service)\n\n metadata = {\n \"native_filter_configuration\": [\n {\"targets\": [{\"datasetId\": 1}, {\"datasetId\": 2}]}\n ]\n }\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {1: \"uuid-A\", 2: \"uuid-MISSING\"} # uuid-MISSING won't resolve\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n targets = result[\"native_filter_configuration\"][0][\"targets\"]\n # ID 1 should be replaced to 100; ID 2 should remain 2\n assert targets[0][\"datasetId\"] == 100\n assert targets[1][\"datasetId\"] == 2\n\n\n# --- _extract_chart_uuids_from_archive tests ---\n\n# [/DEF:test_patch_dashboard_metadata_handles_missing_targets:Function]\n" }, { "contract_id": "test_extract_chart_uuids_from_archive", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 181, - "end_line": 207, + "start_line": 176, + "end_line": 202, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99201,8 +67457,8 @@ "contract_id": "test_transform_yaml_replaces_database_uuid", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 210, - "end_line": 230, + "start_line": 205, + "end_line": 225, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99228,14 +67484,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_transform_yaml_replaces_database_uuid:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure dataset YAML database_uuid fields are replaced when source UUID mapping exists.\ndef test_transform_yaml_replaces_database_uuid():\n \"\"\"Verifies that database_uuid in a dataset YAML is replaced.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-uuid-abc\", \"table_name\": \"my_table\"}, f)\n\n engine._transform_yaml(fp, {\"source-uuid-abc\": \"target-uuid-xyz\"})\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"target-uuid-xyz\"\n assert data[\"table_name\"] == \"my_table\"\n\n\n# [/DEF:test_transform_yaml_replaces_database_uuid:Function]\n" + "body": "# [DEF:test_transform_yaml_replaces_database_uuid:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure dataset YAML database_uuid fields are replaced when source UUID mapping exists.\ndef test_transform_yaml_replaces_database_uuid():\n \"\"\"Verifies that database_uuid in a dataset YAML is replaced.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-uuid-abc\", \"table_name\": \"my_table\"}, f)\n\n engine._transform_yaml(fp, {\"source-uuid-abc\": \"target-uuid-xyz\"})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"target-uuid-xyz\"\n assert data[\"table_name\"] == \"my_table\"\n\n\n# [/DEF:test_transform_yaml_replaces_database_uuid:Function]\n" }, { "contract_id": "test_transform_yaml_ignores_unmapped_uuid", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 233, - "end_line": 255, + "start_line": 228, + "end_line": 250, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99261,14 +67517,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_transform_yaml_ignores_unmapped_uuid:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure transform_yaml leaves dataset files untouched when database_uuid is not mapped.\ndef test_transform_yaml_ignores_unmapped_uuid():\n \"\"\"Verifies no changes when UUID is not in the mapping.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n original = {\"database_uuid\": \"unknown-uuid\", \"table_name\": \"test\"}\n with open(fp, \"w\") as f:\n yaml.dump(original, f)\n\n engine._transform_yaml(fp, {\"other-uuid\": \"replacement\"})\n\n with open(fp, \"r\") as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"unknown-uuid\"\n\n\n# --- [NEW] transform_zip E2E tests ---\n\n# [/DEF:test_transform_yaml_ignores_unmapped_uuid:Function]\n" + "body": "# [DEF:test_transform_yaml_ignores_unmapped_uuid:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Ensure transform_yaml leaves dataset files untouched when database_uuid is not mapped.\ndef test_transform_yaml_ignores_unmapped_uuid():\n \"\"\"Verifies no changes when UUID is not in the mapping.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n original = {\"database_uuid\": \"unknown-uuid\", \"table_name\": \"test\"}\n with open(fp, \"w\") as f:\n yaml.dump(original, f)\n\n engine._transform_yaml(fp, {\"other-uuid\": \"replacement\"})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"unknown-uuid\"\n\n\n# --- [NEW] transform_zip E2E tests ---\n\n# [/DEF:test_transform_yaml_ignores_unmapped_uuid:Function]\n" }, { "contract_id": "test_transform_zip_end_to_end", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 258, - "end_line": 336, + "start_line": 253, + "end_line": 331, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99294,14 +67550,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_transform_zip_end_to_end:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Validate full ZIP transform pipeline remaps datasets and dashboard cross-filter chart IDs.\ndef test_transform_zip_end_to_end():\n \"\"\"Verifies full orchestration: extraction, transformation, patching, and re-packaging.\"\"\"\n mock_service = MockMappingService({\"char-uuid\": 101, \"ds-uuid\": 202})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"source.zip\"\n output_path = td_path / \"target.zip\"\n\n # Create source ZIP structure\n with tempfile.TemporaryDirectory() as src_dir:\n src_path = Path(src_dir)\n\n # 1. Dataset\n ds_dir = src_path / \"datasets\"\n ds_dir.mkdir()\n with open(ds_dir / \"ds.yaml\", \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-db-uuid\", \"table_name\": \"users\"}, f)\n\n # 2. Chart\n ch_dir = src_path / \"charts\"\n ch_dir.mkdir()\n with open(ch_dir / \"ch.yaml\", \"w\") as f:\n yaml.dump({\"id\": 10, \"uuid\": \"char-uuid\"}, f)\n\n # 3. Dashboard\n db_dir = src_path / \"dashboards\"\n db_dir.mkdir()\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 10}]}]}\n with open(db_dir / \"db.yaml\", \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n\n with zipfile.ZipFile(zip_path, \"w\") as zf:\n for root, _, files in os.walk(src_path):\n for file in files:\n p = Path(root) / file\n zf.write(p, p.relative_to(src_path))\n\n db_mapping = {\"source-db-uuid\": \"target-db-uuid\"}\n\n # Execute transform\n success = engine.transform_zip(\n str(zip_path),\n str(output_path),\n db_mapping,\n target_env_id=\"test-target\",\n fix_cross_filters=True,\n )\n\n assert success is True\n assert output_path.exists()\n\n # Verify contents\n with tempfile.TemporaryDirectory() as out_dir:\n with zipfile.ZipFile(output_path, \"r\") as zf:\n zf.extractall(out_dir)\n\n out_path = Path(out_dir)\n\n # Verify dataset transformation\n with open(out_path / \"datasets\" / \"ds.yaml\", \"r\") as f:\n ds_data = yaml.safe_load(f)\n assert ds_data[\"database_uuid\"] == \"target-db-uuid\"\n\n # Verify dashboard patching\n with open(out_path / \"dashboards\" / \"db.yaml\", \"r\") as f:\n db_data = yaml.safe_load(f)\n meta = json.loads(db_data[\"json_metadata\"])\n assert (\n meta[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"]\n == 101\n )\n\n\n# [/DEF:test_transform_zip_end_to_end:Function]\n" + "body": "# [DEF:test_transform_zip_end_to_end:Function]\n# @RELATION: BINDS_TO -> [TestMigrationEngine:Module]\n# @PURPOSE: Validate full ZIP transform pipeline remaps datasets and dashboard cross-filter chart IDs.\ndef test_transform_zip_end_to_end():\n \"\"\"Verifies full orchestration: extraction, transformation, patching, and re-packaging.\"\"\"\n mock_service = MockMappingService({\"char-uuid\": 101, \"ds-uuid\": 202})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"source.zip\"\n output_path = td_path / \"target.zip\"\n\n # Create source ZIP structure\n with tempfile.TemporaryDirectory() as src_dir:\n src_path = Path(src_dir)\n\n # 1. Dataset\n ds_dir = src_path / \"datasets\"\n ds_dir.mkdir()\n with open(ds_dir / \"ds.yaml\", \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-db-uuid\", \"table_name\": \"users\"}, f)\n\n # 2. Chart\n ch_dir = src_path / \"charts\"\n ch_dir.mkdir()\n with open(ch_dir / \"ch.yaml\", \"w\") as f:\n yaml.dump({\"id\": 10, \"uuid\": \"char-uuid\"}, f)\n\n # 3. Dashboard\n db_dir = src_path / \"dashboards\"\n db_dir.mkdir()\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 10}]}]}\n with open(db_dir / \"db.yaml\", \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n\n with zipfile.ZipFile(zip_path, \"w\") as zf:\n for root, _, files in os.walk(src_path):\n for file in files:\n p = Path(root) / file\n zf.write(p, p.relative_to(src_path))\n\n db_mapping = {\"source-db-uuid\": \"target-db-uuid\"}\n\n # Execute transform\n success = engine.transform_zip(\n str(zip_path),\n str(output_path),\n db_mapping,\n target_env_id=\"test-target\",\n fix_cross_filters=True,\n )\n\n assert success is True\n assert output_path.exists()\n\n # Verify contents\n with tempfile.TemporaryDirectory() as out_dir:\n with zipfile.ZipFile(output_path, \"r\") as zf:\n zf.extractall(out_dir)\n\n out_path = Path(out_dir)\n\n # Verify dataset transformation\n with open(out_path / \"datasets\" / \"ds.yaml\") as f:\n ds_data = yaml.safe_load(f)\n assert ds_data[\"database_uuid\"] == \"target-db-uuid\"\n\n # Verify dashboard patching\n with open(out_path / \"dashboards\" / \"db.yaml\") as f:\n db_data = yaml.safe_load(f)\n meta = json.loads(db_data[\"json_metadata\"])\n assert (\n meta[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"]\n == 101\n )\n\n\n# [/DEF:test_transform_zip_end_to_end:Function]\n" }, { "contract_id": "test_transform_zip_invalid_path", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 339, - "end_line": 349, + "start_line": 334, + "end_line": 344, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99333,8 +67589,8 @@ "contract_id": "test_transform_yaml_nonexistent_file", "contract_type": "Function", "file_path": "backend/tests/core/test_migration_engine.py", - "start_line": 352, - "end_line": 364, + "start_line": 347, + "end_line": 359, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99367,11 +67623,10 @@ "contract_type": "Module", "file_path": "backend/tests/scripts/test_clean_release_cli.py", "start_line": 1, - "end_line": 346, - "tier": "TIER_2", - "complexity": 3, + "end_line": 343, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Smoke tests for the redesigned clean release CLI." }, @@ -99384,6 +67639,15 @@ } ], "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" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -99404,14 +67668,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_clean_release_cli:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @PURPOSE: Smoke tests for the redesigned clean release CLI.\n# @LAYER: Domain\n\n\"\"\"Smoke tests for the redesigned clean release CLI commands.\"\"\"\n\nfrom types import SimpleNamespace\nimport json\n\nfrom src.dependencies import get_clean_release_repository, get_config_manager\nfrom datetime import datetime, timezone\nfrom uuid import uuid4\n\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n ComplianceReport,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision\nfrom src.scripts.clean_release_cli import main as cli_main\n\n\n# [DEF:test_cli_candidate_register_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify candidate-register command exits successfully for valid required arguments.\ndef test_cli_candidate_register_scaffold() -> None:\n \"\"\"Candidate register CLI command smoke test.\"\"\"\n exit_code = cli_main(\n [\n \"candidate-register\",\n \"--candidate-id\",\n \"cli-candidate-1\",\n \"--version\",\n \"1.0.0\",\n \"--source-snapshot-ref\",\n \"git:sha123\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert exit_code == 0\n\n\n# [/DEF:test_cli_candidate_register_scaffold:Function]\n\n\n# [DEF:test_cli_manifest_build_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify candidate-register/artifact-import/manifest-build smoke path succeeds end-to-end.\ndef test_cli_manifest_build_scaffold() -> None:\n \"\"\"Manifest build CLI command smoke test.\"\"\"\n register_exit = cli_main(\n [\n \"candidate-register\",\n \"--candidate-id\",\n \"cli-candidate-2\",\n \"--version\",\n \"1.0.0\",\n \"--source-snapshot-ref\",\n \"git:sha234\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert register_exit == 0\n\n import_exit = cli_main(\n [\n \"artifact-import\",\n \"--candidate-id\",\n \"cli-candidate-2\",\n \"--artifact-id\",\n \"artifact-2\",\n \"--path\",\n \"bin/app\",\n \"--sha256\",\n \"feedbeef\",\n \"--size\",\n \"24\",\n ]\n )\n assert import_exit == 0\n\n manifest_exit = cli_main(\n [\n \"manifest-build\",\n \"--candidate-id\",\n \"cli-candidate-2\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert manifest_exit == 0\n\n\n# [/DEF:test_cli_manifest_build_scaffold:Function]\n\n\n# [DEF:test_cli_compliance_run_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify compliance run/status/violations/report commands complete for prepared candidate.\ndef test_cli_compliance_run_scaffold() -> None:\n \"\"\"Compliance CLI command smoke test for run/status/report/violations.\"\"\"\n repository = get_clean_release_repository()\n config_manager = get_config_manager()\n\n registry = SourceRegistrySnapshot(\n id=\"cli-registry\",\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n policy = CleanPolicySnapshot(\n id=\"cli-policy\",\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=registry.id,\n immutable=True,\n )\n repository.save_registry(registry)\n repository.save_policy(policy)\n\n config = config_manager.get_config()\n if getattr(config, \"settings\", None) is None:\n # @INVARIANT: SimpleNamespace substitutes for GlobalSettings — any field rename in GlobalSettings will silently not propagate here; re-verify on GlobalSettings schema changes.\n config.settings = SimpleNamespace()\n config.settings.clean_release = SimpleNamespace(\n active_policy_id=policy.id,\n active_registry_id=registry.id,\n )\n\n register_exit = cli_main(\n [\n \"candidate-register\",\n \"--candidate-id\",\n \"cli-candidate-3\",\n \"--version\",\n \"1.0.0\",\n \"--source-snapshot-ref\",\n \"git:sha345\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert register_exit == 0\n\n import_exit = cli_main(\n [\n \"artifact-import\",\n \"--candidate-id\",\n \"cli-candidate-3\",\n \"--artifact-id\",\n \"artifact-1\",\n \"--path\",\n \"bin/app\",\n \"--sha256\",\n \"deadbeef\",\n \"--size\",\n \"42\",\n ]\n )\n assert import_exit == 0\n\n manifest_exit = cli_main(\n [\n \"manifest-build\",\n \"--candidate-id\",\n \"cli-candidate-3\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert manifest_exit == 0\n\n run_exit = cli_main(\n [\n \"compliance-run\",\n \"--candidate-id\",\n \"cli-candidate-3\",\n \"--actor\",\n \"cli-test\",\n \"--json\",\n ]\n )\n assert run_exit == 0\n\n run_id = next(\n run.id\n for run in repository.check_runs.values()\n if run.candidate_id == \"cli-candidate-3\"\n )\n\n status_exit = cli_main([\"compliance-status\", \"--run-id\", run_id, \"--json\"])\n assert status_exit == 0\n\n violations_exit = cli_main([\"compliance-violations\", \"--run-id\", run_id, \"--json\"])\n assert violations_exit == 0\n\n report_exit = cli_main([\"compliance-report\", \"--run-id\", run_id, \"--json\"])\n assert report_exit == 0\n\n\n# [/DEF:test_cli_compliance_run_scaffold:Function]\n\n\n# [DEF:test_cli_release_gate_commands_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify approve/reject/publish/revoke release-gate commands execute with valid fixtures.\ndef test_cli_release_gate_commands_scaffold() -> None:\n \"\"\"Release gate CLI smoke test for approve/reject/publish/revoke commands.\"\"\"\n repository = get_clean_release_repository()\n\n approved_candidate_id = f\"cli-release-approved-{uuid4()}\"\n rejected_candidate_id = f\"cli-release-rejected-{uuid4()}\"\n approved_report_id = f\"CCR-cli-release-approved-{uuid4()}\"\n rejected_report_id = f\"CCR-cli-release-rejected-{uuid4()}\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=approved_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approved\",\n created_by=\"cli-test\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_candidate(\n ReleaseCandidate(\n id=rejected_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-rejected\",\n created_by=\"cli-test\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=approved_report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=approved_candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=rejected_report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=rejected_candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n\n approve_exit = cli_main(\n [\n \"approve\",\n \"--candidate-id\",\n approved_candidate_id,\n \"--report-id\",\n approved_report_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"approve candidate\",\n \"--json\",\n ]\n )\n assert approve_exit == 0\n\n reject_exit = cli_main(\n [\n \"reject\",\n \"--candidate-id\",\n rejected_candidate_id,\n \"--report-id\",\n rejected_report_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"reject candidate\",\n \"--json\",\n ]\n )\n assert reject_exit == 0\n\n publish_exit = cli_main(\n [\n \"publish\",\n \"--candidate-id\",\n approved_candidate_id,\n \"--report-id\",\n approved_report_id,\n \"--actor\",\n \"cli-test\",\n \"--target-channel\",\n \"stable\",\n \"--publication-ref\",\n \"rel-cli-001\",\n \"--json\",\n ]\n )\n assert publish_exit == 0\n\n publication_records = getattr(repository, \"publication_records\", [])\n assert publication_records\n publication_id = publication_records[-1].id\n\n revoke_exit = cli_main(\n [\n \"revoke\",\n \"--publication-id\",\n publication_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"rollback\",\n \"--json\",\n ]\n )\n assert revoke_exit == 0\n\n\n# [/DEF:test_cli_release_gate_commands_scaffold:Function]\n# [/DEF:test_clean_release_cli:Module]\n" + "body": "# [DEF:test_clean_release_cli:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @PURPOSE: Smoke tests for the redesigned clean release CLI.\n# @LAYER: Domain\n\n\"\"\"Smoke tests for the redesigned clean release CLI commands.\"\"\"\n\nfrom datetime import UTC, datetime\nfrom types import SimpleNamespace\nfrom uuid import uuid4\n\nfrom src.dependencies import get_clean_release_repository, get_config_manager\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n ComplianceReport,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.scripts.clean_release_cli import main as cli_main\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision\n\n\n# [DEF:test_cli_candidate_register_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify candidate-register command exits successfully for valid required arguments.\ndef test_cli_candidate_register_scaffold() -> None:\n \"\"\"Candidate register CLI command smoke test.\"\"\"\n exit_code = cli_main(\n [\n \"candidate-register\",\n \"--candidate-id\",\n \"cli-candidate-1\",\n \"--version\",\n \"1.0.0\",\n \"--source-snapshot-ref\",\n \"git:sha123\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert exit_code == 0\n\n\n# [/DEF:test_cli_candidate_register_scaffold:Function]\n\n\n# [DEF:test_cli_manifest_build_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify candidate-register/artifact-import/manifest-build smoke path succeeds end-to-end.\ndef test_cli_manifest_build_scaffold() -> None:\n \"\"\"Manifest build CLI command smoke test.\"\"\"\n register_exit = cli_main(\n [\n \"candidate-register\",\n \"--candidate-id\",\n \"cli-candidate-2\",\n \"--version\",\n \"1.0.0\",\n \"--source-snapshot-ref\",\n \"git:sha234\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert register_exit == 0\n\n import_exit = cli_main(\n [\n \"artifact-import\",\n \"--candidate-id\",\n \"cli-candidate-2\",\n \"--artifact-id\",\n \"artifact-2\",\n \"--path\",\n \"bin/app\",\n \"--sha256\",\n \"feedbeef\",\n \"--size\",\n \"24\",\n ]\n )\n assert import_exit == 0\n\n manifest_exit = cli_main(\n [\n \"manifest-build\",\n \"--candidate-id\",\n \"cli-candidate-2\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert manifest_exit == 0\n\n\n# [/DEF:test_cli_manifest_build_scaffold:Function]\n\n\n# [DEF:test_cli_compliance_run_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify compliance run/status/violations/report commands complete for prepared candidate.\ndef test_cli_compliance_run_scaffold() -> None:\n \"\"\"Compliance CLI command smoke test for run/status/report/violations.\"\"\"\n repository = get_clean_release_repository()\n config_manager = get_config_manager()\n\n registry = SourceRegistrySnapshot(\n id=\"cli-registry\",\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n policy = CleanPolicySnapshot(\n id=\"cli-policy\",\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=registry.id,\n immutable=True,\n )\n repository.save_registry(registry)\n repository.save_policy(policy)\n\n config = config_manager.get_config()\n if getattr(config, \"settings\", None) is None:\n # @INVARIANT: SimpleNamespace substitutes for GlobalSettings — any field rename in GlobalSettings will silently not propagate here; re-verify on GlobalSettings schema changes.\n config.settings = SimpleNamespace()\n config.settings.clean_release = SimpleNamespace(\n active_policy_id=policy.id,\n active_registry_id=registry.id,\n )\n\n register_exit = cli_main(\n [\n \"candidate-register\",\n \"--candidate-id\",\n \"cli-candidate-3\",\n \"--version\",\n \"1.0.0\",\n \"--source-snapshot-ref\",\n \"git:sha345\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert register_exit == 0\n\n import_exit = cli_main(\n [\n \"artifact-import\",\n \"--candidate-id\",\n \"cli-candidate-3\",\n \"--artifact-id\",\n \"artifact-1\",\n \"--path\",\n \"bin/app\",\n \"--sha256\",\n \"deadbeef\",\n \"--size\",\n \"42\",\n ]\n )\n assert import_exit == 0\n\n manifest_exit = cli_main(\n [\n \"manifest-build\",\n \"--candidate-id\",\n \"cli-candidate-3\",\n \"--created-by\",\n \"cli-test\",\n ]\n )\n assert manifest_exit == 0\n\n run_exit = cli_main(\n [\n \"compliance-run\",\n \"--candidate-id\",\n \"cli-candidate-3\",\n \"--actor\",\n \"cli-test\",\n \"--json\",\n ]\n )\n assert run_exit == 0\n\n run_id = next(\n run.id\n for run in repository.check_runs.values()\n if run.candidate_id == \"cli-candidate-3\"\n )\n\n status_exit = cli_main([\"compliance-status\", \"--run-id\", run_id, \"--json\"])\n assert status_exit == 0\n\n violations_exit = cli_main([\"compliance-violations\", \"--run-id\", run_id, \"--json\"])\n assert violations_exit == 0\n\n report_exit = cli_main([\"compliance-report\", \"--run-id\", run_id, \"--json\"])\n assert report_exit == 0\n\n\n# [/DEF:test_cli_compliance_run_scaffold:Function]\n\n\n# [DEF:test_cli_release_gate_commands_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify approve/reject/publish/revoke release-gate commands execute with valid fixtures.\ndef test_cli_release_gate_commands_scaffold() -> None:\n \"\"\"Release gate CLI smoke test for approve/reject/publish/revoke commands.\"\"\"\n repository = get_clean_release_repository()\n\n approved_candidate_id = f\"cli-release-approved-{uuid4()}\"\n rejected_candidate_id = f\"cli-release-rejected-{uuid4()}\"\n approved_report_id = f\"CCR-cli-release-approved-{uuid4()}\"\n rejected_report_id = f\"CCR-cli-release-rejected-{uuid4()}\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=approved_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approved\",\n created_by=\"cli-test\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_candidate(\n ReleaseCandidate(\n id=rejected_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-rejected\",\n created_by=\"cli-test\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=approved_report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=approved_candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=rejected_report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=rejected_candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n approve_exit = cli_main(\n [\n \"approve\",\n \"--candidate-id\",\n approved_candidate_id,\n \"--report-id\",\n approved_report_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"approve candidate\",\n \"--json\",\n ]\n )\n assert approve_exit == 0\n\n reject_exit = cli_main(\n [\n \"reject\",\n \"--candidate-id\",\n rejected_candidate_id,\n \"--report-id\",\n rejected_report_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"reject candidate\",\n \"--json\",\n ]\n )\n assert reject_exit == 0\n\n publish_exit = cli_main(\n [\n \"publish\",\n \"--candidate-id\",\n approved_candidate_id,\n \"--report-id\",\n approved_report_id,\n \"--actor\",\n \"cli-test\",\n \"--target-channel\",\n \"stable\",\n \"--publication-ref\",\n \"rel-cli-001\",\n \"--json\",\n ]\n )\n assert publish_exit == 0\n\n publication_records = getattr(repository, \"publication_records\", [])\n assert publication_records\n publication_id = publication_records[-1].id\n\n revoke_exit = cli_main(\n [\n \"revoke\",\n \"--publication-id\",\n publication_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"rollback\",\n \"--json\",\n ]\n )\n assert revoke_exit == 0\n\n\n# [/DEF:test_cli_release_gate_commands_scaffold:Function]\n# [/DEF:test_clean_release_cli:Module]\n" }, { "contract_id": "test_cli_candidate_register_scaffold", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_cli.py", - "start_line": 26, - "end_line": 47, + "start_line": 23, + "end_line": 44, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99443,8 +67707,8 @@ "contract_id": "test_cli_manifest_build_scaffold", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_cli.py", - "start_line": 50, - "end_line": 99, + "start_line": 47, + "end_line": 96, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99476,8 +67740,8 @@ "contract_id": "test_cli_compliance_run_scaffold", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_cli.py", - "start_line": 102, - "end_line": 210, + "start_line": 99, + "end_line": 207, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99519,8 +67783,8 @@ "contract_id": "test_cli_release_gate_commands_scaffold", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_cli.py", - "start_line": 213, - "end_line": 345, + "start_line": 210, + "end_line": 342, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99546,18 +67810,17 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_cli_release_gate_commands_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify approve/reject/publish/revoke release-gate commands execute with valid fixtures.\ndef test_cli_release_gate_commands_scaffold() -> None:\n \"\"\"Release gate CLI smoke test for approve/reject/publish/revoke commands.\"\"\"\n repository = get_clean_release_repository()\n\n approved_candidate_id = f\"cli-release-approved-{uuid4()}\"\n rejected_candidate_id = f\"cli-release-rejected-{uuid4()}\"\n approved_report_id = f\"CCR-cli-release-approved-{uuid4()}\"\n rejected_report_id = f\"CCR-cli-release-rejected-{uuid4()}\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=approved_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approved\",\n created_by=\"cli-test\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_candidate(\n ReleaseCandidate(\n id=rejected_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-rejected\",\n created_by=\"cli-test\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=approved_report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=approved_candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=rejected_report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=rejected_candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n\n approve_exit = cli_main(\n [\n \"approve\",\n \"--candidate-id\",\n approved_candidate_id,\n \"--report-id\",\n approved_report_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"approve candidate\",\n \"--json\",\n ]\n )\n assert approve_exit == 0\n\n reject_exit = cli_main(\n [\n \"reject\",\n \"--candidate-id\",\n rejected_candidate_id,\n \"--report-id\",\n rejected_report_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"reject candidate\",\n \"--json\",\n ]\n )\n assert reject_exit == 0\n\n publish_exit = cli_main(\n [\n \"publish\",\n \"--candidate-id\",\n approved_candidate_id,\n \"--report-id\",\n approved_report_id,\n \"--actor\",\n \"cli-test\",\n \"--target-channel\",\n \"stable\",\n \"--publication-ref\",\n \"rel-cli-001\",\n \"--json\",\n ]\n )\n assert publish_exit == 0\n\n publication_records = getattr(repository, \"publication_records\", [])\n assert publication_records\n publication_id = publication_records[-1].id\n\n revoke_exit = cli_main(\n [\n \"revoke\",\n \"--publication-id\",\n publication_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"rollback\",\n \"--json\",\n ]\n )\n assert revoke_exit == 0\n\n\n# [/DEF:test_cli_release_gate_commands_scaffold:Function]\n" + "body": "# [DEF:test_cli_release_gate_commands_scaffold:Function]\n# @RELATION: BINDS_TO -> test_clean_release_cli\n# @PURPOSE: Verify approve/reject/publish/revoke release-gate commands execute with valid fixtures.\ndef test_cli_release_gate_commands_scaffold() -> None:\n \"\"\"Release gate CLI smoke test for approve/reject/publish/revoke commands.\"\"\"\n repository = get_clean_release_repository()\n\n approved_candidate_id = f\"cli-release-approved-{uuid4()}\"\n rejected_candidate_id = f\"cli-release-rejected-{uuid4()}\"\n approved_report_id = f\"CCR-cli-release-approved-{uuid4()}\"\n rejected_report_id = f\"CCR-cli-release-rejected-{uuid4()}\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=approved_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approved\",\n created_by=\"cli-test\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_candidate(\n ReleaseCandidate(\n id=rejected_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-rejected\",\n created_by=\"cli-test\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=approved_report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=approved_candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=rejected_report_id,\n run_id=f\"run-{uuid4()}\",\n candidate_id=rejected_candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\n \"operator_summary\": \"ok\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n approve_exit = cli_main(\n [\n \"approve\",\n \"--candidate-id\",\n approved_candidate_id,\n \"--report-id\",\n approved_report_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"approve candidate\",\n \"--json\",\n ]\n )\n assert approve_exit == 0\n\n reject_exit = cli_main(\n [\n \"reject\",\n \"--candidate-id\",\n rejected_candidate_id,\n \"--report-id\",\n rejected_report_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"reject candidate\",\n \"--json\",\n ]\n )\n assert reject_exit == 0\n\n publish_exit = cli_main(\n [\n \"publish\",\n \"--candidate-id\",\n approved_candidate_id,\n \"--report-id\",\n approved_report_id,\n \"--actor\",\n \"cli-test\",\n \"--target-channel\",\n \"stable\",\n \"--publication-ref\",\n \"rel-cli-001\",\n \"--json\",\n ]\n )\n assert publish_exit == 0\n\n publication_records = getattr(repository, \"publication_records\", [])\n assert publication_records\n publication_id = publication_records[-1].id\n\n revoke_exit = cli_main(\n [\n \"revoke\",\n \"--publication-id\",\n publication_id,\n \"--actor\",\n \"cli-test\",\n \"--comment\",\n \"rollback\",\n \"--json\",\n ]\n )\n assert revoke_exit == 0\n\n\n# [/DEF:test_cli_release_gate_commands_scaffold:Function]\n" }, { "contract_id": "TestCleanReleaseTui", "contract_type": "Module", "file_path": "backend/tests/scripts/test_clean_release_tui.py", "start_line": 1, - "end_line": 247, - "tier": "TIER_2", - "complexity": 3, + "end_line": 245, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "INVARIANT": "TUI initializes, handles hotkeys (F5, F10) and safely falls back without TTY.", "LAYER": "Scripts", "PURPOSE": "Unit tests for the interactive curses TUI of the clean release process.", @@ -99580,9 +67843,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 3, + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Module" } }, @@ -99606,14 +67887,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:TestCleanReleaseTui:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, tui, clean-release, curses\n# @PURPOSE: Unit tests for the interactive curses TUI of the clean release process.\n# @LAYER: Scripts\n# @INVARIANT: TUI initializes, handles hotkeys (F5, F10) and safely falls back without TTY.\n\nimport os\nimport sys\nimport curses\nimport json\nfrom unittest import mock\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom src.scripts.clean_release_tui import CleanReleaseTUI, main, tui_main\nfrom src.models.clean_release import CheckFinalStatus\n\n\n@pytest.fixture\ndef mock_stdscr() -> MagicMock:\n stdscr = MagicMock()\n stdscr.getmaxyx.return_value = (40, 100)\n stdscr.getch.return_value = -1\n return stdscr\n\n\n# [DEF:test_headless_fallback:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_headless_fallback(capsys):\n \"\"\"\n @TEST_EDGE: stdout_unavailable\n Tests that non-TTY startup is explicitly refused and wrapper is not invoked.\n \"\"\"\n with mock.patch(\"src.scripts.clean_release_tui.curses.wrapper\") as curses_wrapper_mock:\n with mock.patch(\"sys.stdout.isatty\", return_value=False):\n exit_code = main()\n\n curses_wrapper_mock.assert_not_called()\n\n assert exit_code == 2\n captured = capsys.readouterr()\n assert \"TTY is required for TUI mode\" in captured.err\n assert \"Use CLI/API workflow instead\" in captured.err\n\n\n# [/DEF:test_headless_fallback:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_initial_render:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_initial_render(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates the initial rendering cycle of the TUI application to ensure\n titles, headers, footers and the READY state are drawn appropriately.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n assert app.status == \"READY\"\n \n # We only want to run one loop iteration, so we mock getch to return F10\n mock_stdscr.getch.return_value = curses.KEY_F10\n \n app.loop()\n \n # Assert header was drawn\n addstr_calls = mock_stdscr.addstr.call_args_list\n assert any(\"Enterprise Clean Release Validator\" in str(call) for call in addstr_calls)\n assert any(\"Candidate: [2026.03.03-rc1]\" in str(call) for call in addstr_calls)\n \n # Assert checks list is shown\n assert any(\"Data Purity\" in str(call) for call in addstr_calls)\n assert any(\"Internal Sources Only\" in str(call) for call in addstr_calls)\n\n # Assert footer is shown\n assert any(\"F5 Run\" in str(call) for call in addstr_calls)\n\n\n# [/DEF:test_tui_initial_render:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_run_checks_f5:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_run_checks_f5(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F5 to transition into the RUNNING checks flow.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n \n # getch sequence:\n # 1. First loop: F5 (triggers run_checks)\n # 2. Next call after run_checks: F10 to exit\n mock_stdscr.f5_pressed = False\n def side_effect():\n if not mock_stdscr.f5_pressed:\n mock_stdscr.f5_pressed = True\n return curses.KEY_F5\n return curses.KEY_F10\n \n mock_stdscr.getch.side_effect = side_effect\n \n with mock.patch(\"time.sleep\", return_value=None):\n app.loop()\n \n # After F5 is pressed, status should be BLOCKED due to deliberate 'test-data' violation\n assert app.status == CheckFinalStatus.BLOCKED\n assert app.report_id is not None\n assert \"CCR-\" in app.report_id\n assert len(app.violations_list) > 0\n\n\n# [/DEF:test_tui_run_checks_f5:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_exit_f10:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_exit_f10(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F10 to exit the application immediately without running checks.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n \n app = CleanReleaseTUI(mock_stdscr)\n mock_stdscr.getch.return_value = curses.KEY_F10\n \n # loop() should return cleanly\n app.loop()\n \n assert app.status == \"READY\"\n\n\n# [/DEF:test_tui_exit_f10:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_clear_history_f7:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F7 to clear history.\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F7 = curses.KEY_F7\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n app.status = CheckFinalStatus.BLOCKED\n app.report_id = \"SOME-REPORT\"\n \n # F7 then F10\n mock_stdscr.getch.side_effect = [curses.KEY_F7, curses.KEY_F10]\n \n app.loop()\n \n assert app.status == \"READY\"\n assert app.report_id is None\n assert len(app.checks_progress) == 0\n\n\n# [/DEF:test_tui_clear_history_f7:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_real_mode_bootstrap_imports_artifacts_catalog:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_real_mode_bootstrap_imports_artifacts_catalog(\n mock_curses_module,\n mock_stdscr: MagicMock,\n tmp_path,\n# [/DEF:test_tui_real_mode_bootstrap_imports_artifacts_catalog:Function]\n\n):\n \"\"\"\n @TEST_CONTRACT: bootstrap.json + artifacts.json -> candidate PREPARED with imported artifacts\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n bootstrap_path = tmp_path / \"bootstrap.json\"\n artifacts_path = tmp_path / \"artifacts.json\"\n bootstrap_path.write_text(\n json.dumps(\n {\n \"candidate_id\": \"real-candidate-1\",\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:release/1\",\n \"created_by\": \"operator\",\n \"allowed_hosts\": [\"repo.intra.company.local\"],\n }\n ),\n encoding=\"utf-8\",\n )\n artifacts_path.write_text(\n json.dumps(\n {\n \"artifacts\": [\n {\n \"id\": \"artifact-1\",\n \"path\": \"backend/dist/package.tar.gz\",\n \"sha256\": \"deadbeef\",\n \"size\": 1024,\n \"category\": \"core\",\n \"source_uri\": \"https://repo.intra.company.local/releases/package.tar.gz\",\n \"source_host\": \"repo.intra.company.local\",\n }\n ]\n }\n ),\n encoding=\"utf-8\",\n )\n\n with mock.patch.dict(\n os.environ,\n {\n \"CLEAN_TUI_MODE\": \"real\",\n \"CLEAN_TUI_BOOTSTRAP_JSON\": str(bootstrap_path),\n \"CLEAN_TUI_ARTIFACTS_JSON\": str(artifacts_path),\n },\n clear=False,\n ):\n app = CleanReleaseTUI(mock_stdscr)\n\n candidate = app.repo.get_candidate(\"real-candidate-1\")\n artifacts = app.repo.get_artifacts_by_candidate(\"real-candidate-1\")\n\n assert candidate is not None\n assert candidate.status == \"PREPARED\"\n assert len(artifacts) == 1\n assert artifacts[0].path == \"backend/dist/package.tar.gz\"\n assert artifacts[0].detected_category == \"core\"\n\n\n# [/DEF:TestCleanReleaseTui:Module]\n" + "body": "# [DEF:TestCleanReleaseTui:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: tests, tui, clean-release, curses\n# @PURPOSE: Unit tests for the interactive curses TUI of the clean release process.\n# @LAYER: Scripts\n# @INVARIANT: TUI initializes, handles hotkeys (F5, F10) and safely falls back without TTY.\n\nimport curses\nimport json\nimport os\nfrom unittest import mock\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom src.models.clean_release import CheckFinalStatus\nfrom src.scripts.clean_release_tui import CleanReleaseTUI, main\n\n\n@pytest.fixture\ndef mock_stdscr() -> MagicMock:\n stdscr = MagicMock()\n stdscr.getmaxyx.return_value = (40, 100)\n stdscr.getch.return_value = -1\n return stdscr\n\n\n# [DEF:test_headless_fallback:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_headless_fallback(capsys):\n \"\"\"\n @TEST_EDGE: stdout_unavailable\n Tests that non-TTY startup is explicitly refused and wrapper is not invoked.\n \"\"\"\n with mock.patch(\"src.scripts.clean_release_tui.curses.wrapper\") as curses_wrapper_mock:\n with mock.patch(\"sys.stdout.isatty\", return_value=False):\n exit_code = main()\n\n curses_wrapper_mock.assert_not_called()\n\n assert exit_code == 2\n captured = capsys.readouterr()\n assert \"TTY is required for TUI mode\" in captured.err\n assert \"Use CLI/API workflow instead\" in captured.err\n\n\n# [/DEF:test_headless_fallback:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_initial_render:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_initial_render(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates the initial rendering cycle of the TUI application to ensure\n titles, headers, footers and the READY state are drawn appropriately.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n assert app.status == \"READY\"\n\n # We only want to run one loop iteration, so we mock getch to return F10\n mock_stdscr.getch.return_value = curses.KEY_F10\n\n app.loop()\n\n # Assert header was drawn\n addstr_calls = mock_stdscr.addstr.call_args_list\n assert any(\"Enterprise Clean Release Validator\" in str(call) for call in addstr_calls)\n assert any(\"Candidate: [2026.03.03-rc1]\" in str(call) for call in addstr_calls)\n\n # Assert checks list is shown\n assert any(\"Data Purity\" in str(call) for call in addstr_calls)\n assert any(\"Internal Sources Only\" in str(call) for call in addstr_calls)\n\n # Assert footer is shown\n assert any(\"F5 Run\" in str(call) for call in addstr_calls)\n\n\n# [/DEF:test_tui_initial_render:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_run_checks_f5:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_run_checks_f5(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F5 to transition into the RUNNING checks flow.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n\n # getch sequence:\n # 1. First loop: F5 (triggers run_checks)\n # 2. Next call after run_checks: F10 to exit\n mock_stdscr.f5_pressed = False\n def side_effect():\n if not mock_stdscr.f5_pressed:\n mock_stdscr.f5_pressed = True\n return curses.KEY_F5\n return curses.KEY_F10\n\n mock_stdscr.getch.side_effect = side_effect\n\n with mock.patch(\"time.sleep\", return_value=None):\n app.loop()\n\n # After F5 is pressed, status should be BLOCKED due to deliberate 'test-data' violation\n assert app.status == CheckFinalStatus.BLOCKED\n assert app.report_id is not None\n assert \"CCR-\" in app.report_id\n assert len(app.violations_list) > 0\n\n\n# [/DEF:test_tui_run_checks_f5:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_exit_f10:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_exit_f10(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F10 to exit the application immediately without running checks.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n\n app = CleanReleaseTUI(mock_stdscr)\n mock_stdscr.getch.return_value = curses.KEY_F10\n\n # loop() should return cleanly\n app.loop()\n\n assert app.status == \"READY\"\n\n\n# [/DEF:test_tui_exit_f10:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_clear_history_f7:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F7 to clear history.\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F7 = curses.KEY_F7\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n app.status = CheckFinalStatus.BLOCKED\n app.report_id = \"SOME-REPORT\"\n\n # F7 then F10\n mock_stdscr.getch.side_effect = [curses.KEY_F7, curses.KEY_F10]\n\n app.loop()\n\n assert app.status == \"READY\"\n assert app.report_id is None\n assert len(app.checks_progress) == 0\n\n\n# [/DEF:test_tui_clear_history_f7:Function]\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_real_mode_bootstrap_imports_artifacts_catalog:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_real_mode_bootstrap_imports_artifacts_catalog(\n mock_curses_module,\n mock_stdscr: MagicMock,\n tmp_path,\n# [/DEF:test_tui_real_mode_bootstrap_imports_artifacts_catalog:Function]\n\n):\n \"\"\"\n @TEST_CONTRACT: bootstrap.json + artifacts.json -> candidate PREPARED with imported artifacts\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n bootstrap_path = tmp_path / \"bootstrap.json\"\n artifacts_path = tmp_path / \"artifacts.json\"\n bootstrap_path.write_text(\n json.dumps(\n {\n \"candidate_id\": \"real-candidate-1\",\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:release/1\",\n \"created_by\": \"operator\",\n \"allowed_hosts\": [\"repo.intra.company.local\"],\n }\n ),\n encoding=\"utf-8\",\n )\n artifacts_path.write_text(\n json.dumps(\n {\n \"artifacts\": [\n {\n \"id\": \"artifact-1\",\n \"path\": \"backend/dist/package.tar.gz\",\n \"sha256\": \"deadbeef\",\n \"size\": 1024,\n \"category\": \"core\",\n \"source_uri\": \"https://repo.intra.company.local/releases/package.tar.gz\",\n \"source_host\": \"repo.intra.company.local\",\n }\n ]\n }\n ),\n encoding=\"utf-8\",\n )\n\n with mock.patch.dict(\n os.environ,\n {\n \"CLEAN_TUI_MODE\": \"real\",\n \"CLEAN_TUI_BOOTSTRAP_JSON\": str(bootstrap_path),\n \"CLEAN_TUI_ARTIFACTS_JSON\": str(artifacts_path),\n },\n clear=False,\n ):\n app = CleanReleaseTUI(mock_stdscr)\n\n candidate = app.repo.get_candidate(\"real-candidate-1\")\n artifacts = app.repo.get_artifacts_by_candidate(\"real-candidate-1\")\n\n assert candidate is not None\n assert candidate.status == \"PREPARED\"\n assert len(artifacts) == 1\n assert artifacts[0].path == \"backend/dist/package.tar.gz\"\n assert artifacts[0].detected_category == \"core\"\n\n\n# [/DEF:TestCleanReleaseTui:Module]\n" }, { "contract_id": "test_headless_fallback", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 30, - "end_line": 49, + "start_line": 28, + "end_line": 47, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -99652,8 +67933,8 @@ "contract_id": "test_tui_initial_render", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 52, - "end_line": 86, + "start_line": 50, + "end_line": 84, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -99686,14 +67967,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_tui_initial_render:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_initial_render(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates the initial rendering cycle of the TUI application to ensure\n titles, headers, footers and the READY state are drawn appropriately.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n assert app.status == \"READY\"\n \n # We only want to run one loop iteration, so we mock getch to return F10\n mock_stdscr.getch.return_value = curses.KEY_F10\n \n app.loop()\n \n # Assert header was drawn\n addstr_calls = mock_stdscr.addstr.call_args_list\n assert any(\"Enterprise Clean Release Validator\" in str(call) for call in addstr_calls)\n assert any(\"Candidate: [2026.03.03-rc1]\" in str(call) for call in addstr_calls)\n \n # Assert checks list is shown\n assert any(\"Data Purity\" in str(call) for call in addstr_calls)\n assert any(\"Internal Sources Only\" in str(call) for call in addstr_calls)\n\n # Assert footer is shown\n assert any(\"F5 Run\" in str(call) for call in addstr_calls)\n\n\n# [/DEF:test_tui_initial_render:Function]\n" + "body": "# [DEF:test_tui_initial_render:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_initial_render(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates the initial rendering cycle of the TUI application to ensure\n titles, headers, footers and the READY state are drawn appropriately.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n assert app.status == \"READY\"\n\n # We only want to run one loop iteration, so we mock getch to return F10\n mock_stdscr.getch.return_value = curses.KEY_F10\n\n app.loop()\n\n # Assert header was drawn\n addstr_calls = mock_stdscr.addstr.call_args_list\n assert any(\"Enterprise Clean Release Validator\" in str(call) for call in addstr_calls)\n assert any(\"Candidate: [2026.03.03-rc1]\" in str(call) for call in addstr_calls)\n\n # Assert checks list is shown\n assert any(\"Data Purity\" in str(call) for call in addstr_calls)\n assert any(\"Internal Sources Only\" in str(call) for call in addstr_calls)\n\n # Assert footer is shown\n assert any(\"F5 Run\" in str(call) for call in addstr_calls)\n\n\n# [/DEF:test_tui_initial_render:Function]\n" }, { "contract_id": "test_tui_run_checks_f5", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 89, - "end_line": 125, + "start_line": 87, + "end_line": 123, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -99726,14 +68007,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_tui_run_checks_f5:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_run_checks_f5(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F5 to transition into the RUNNING checks flow.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n \n # getch sequence:\n # 1. First loop: F5 (triggers run_checks)\n # 2. Next call after run_checks: F10 to exit\n mock_stdscr.f5_pressed = False\n def side_effect():\n if not mock_stdscr.f5_pressed:\n mock_stdscr.f5_pressed = True\n return curses.KEY_F5\n return curses.KEY_F10\n \n mock_stdscr.getch.side_effect = side_effect\n \n with mock.patch(\"time.sleep\", return_value=None):\n app.loop()\n \n # After F5 is pressed, status should be BLOCKED due to deliberate 'test-data' violation\n assert app.status == CheckFinalStatus.BLOCKED\n assert app.report_id is not None\n assert \"CCR-\" in app.report_id\n assert len(app.violations_list) > 0\n\n\n# [/DEF:test_tui_run_checks_f5:Function]\n" + "body": "# [DEF:test_tui_run_checks_f5:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_run_checks_f5(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F5 to transition into the RUNNING checks flow.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n\n # getch sequence:\n # 1. First loop: F5 (triggers run_checks)\n # 2. Next call after run_checks: F10 to exit\n mock_stdscr.f5_pressed = False\n def side_effect():\n if not mock_stdscr.f5_pressed:\n mock_stdscr.f5_pressed = True\n return curses.KEY_F5\n return curses.KEY_F10\n\n mock_stdscr.getch.side_effect = side_effect\n\n with mock.patch(\"time.sleep\", return_value=None):\n app.loop()\n\n # After F5 is pressed, status should be BLOCKED due to deliberate 'test-data' violation\n assert app.status == CheckFinalStatus.BLOCKED\n assert app.report_id is not None\n assert \"CCR-\" in app.report_id\n assert len(app.violations_list) > 0\n\n\n# [/DEF:test_tui_run_checks_f5:Function]\n" }, { "contract_id": "test_tui_exit_f10", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 128, - "end_line": 146, + "start_line": 126, + "end_line": 144, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -99766,14 +68047,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_tui_exit_f10:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_exit_f10(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F10 to exit the application immediately without running checks.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n \n app = CleanReleaseTUI(mock_stdscr)\n mock_stdscr.getch.return_value = curses.KEY_F10\n \n # loop() should return cleanly\n app.loop()\n \n assert app.status == \"READY\"\n\n\n# [/DEF:test_tui_exit_f10:Function]\n" + "body": "# [DEF:test_tui_exit_f10:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_exit_f10(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F10 to exit the application immediately without running checks.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n\n app = CleanReleaseTUI(mock_stdscr)\n mock_stdscr.getch.return_value = curses.KEY_F10\n\n # loop() should return cleanly\n app.loop()\n\n assert app.status == \"READY\"\n\n\n# [/DEF:test_tui_exit_f10:Function]\n" }, { "contract_id": "test_tui_clear_history_f7", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 149, - "end_line": 174, + "start_line": 147, + "end_line": 172, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -99806,14 +68087,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_tui_clear_history_f7:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F7 to clear history.\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F7 = curses.KEY_F7\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n app.status = CheckFinalStatus.BLOCKED\n app.report_id = \"SOME-REPORT\"\n \n # F7 then F10\n mock_stdscr.getch.side_effect = [curses.KEY_F7, curses.KEY_F10]\n \n app.loop()\n \n assert app.status == \"READY\"\n assert app.report_id is None\n assert len(app.checks_progress) == 0\n\n\n# [/DEF:test_tui_clear_history_f7:Function]\n" + "body": "# [DEF:test_tui_clear_history_f7:Function]\n# @RELATION: BINDS_TO -> TestCleanReleaseTui\ndef test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F7 to clear history.\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F7 = curses.KEY_F7\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n app.status = CheckFinalStatus.BLOCKED\n app.report_id = \"SOME-REPORT\"\n\n # F7 then F10\n mock_stdscr.getch.side_effect = [curses.KEY_F7, curses.KEY_F10]\n\n app.loop()\n\n assert app.status == \"READY\"\n assert app.report_id is None\n assert len(app.checks_progress) == 0\n\n\n# [/DEF:test_tui_clear_history_f7:Function]\n" }, { "contract_id": "test_tui_real_mode_bootstrap_imports_artifacts_catalog", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 177, - "end_line": 183, + "start_line": 175, + "end_line": 181, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -99853,11 +68134,10 @@ "contract_type": "Module", "file_path": "backend/tests/scripts/test_clean_release_tui_v2.py", "start_line": 1, - "end_line": 125, - "tier": "TIER_2", - "complexity": 3, + "end_line": 124, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": "3", "LAYER": "Domain", "PURPOSE": "Smoke tests for thin-client TUI action dispatch and blocked transition behavior." }, @@ -99870,6 +68150,15 @@ } ], "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" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -99890,14 +68179,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_clean_release_tui_v2:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 3\n# @PURPOSE: Smoke tests for thin-client TUI action dispatch and blocked transition behavior.\n# @LAYER: Domain\n\n\"\"\"Smoke tests for the redesigned clean release TUI.\"\"\"\n\nfrom __future__ import annotations\n\nimport curses\nfrom unittest.mock import MagicMock, patch\n\nfrom src.models.clean_release import CheckFinalStatus\nfrom src.scripts.clean_release_tui import CleanReleaseTUI, main\n\n\n# [DEF:_build_mock_stdscr:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Build deterministic curses screen mock with default terminal geometry and exit key.\ndef _build_mock_stdscr() -> MagicMock:\n stdscr = MagicMock()\n stdscr.getmaxyx.return_value = (40, 120)\n stdscr.getch.return_value = curses.KEY_F10\n return stdscr\n\n\n# [/DEF:_build_mock_stdscr:Function]\n\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_f5_dispatches_run_action:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Verify F5 key dispatch invokes run_checks exactly once before graceful exit.\ndef test_tui_f5_dispatches_run_action(mock_curses_module: MagicMock) -> None:\n \"\"\"F5 should dispatch run action from TUI loop.\"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda value: value\n mock_curses_module.A_BOLD = 0\n\n stdscr = _build_mock_stdscr()\n app = CleanReleaseTUI(stdscr)\n\n stdscr.getch.side_effect = [curses.KEY_F5, curses.KEY_F10]\n with patch.object(app, \"run_checks\", autospec=True) as run_checks_mock:\n app.loop()\n\n run_checks_mock.assert_called_once_with()\n\n\n# [/DEF:test_tui_f5_dispatches_run_action:Function]\n\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_f5_run_smoke_reports_blocked_state:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Verify blocked compliance state is surfaced after F5-triggered run action.\ndef test_tui_f5_run_smoke_reports_blocked_state(mock_curses_module: MagicMock) -> None:\n \"\"\"F5 smoke test should expose blocked outcome state after run action.\"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda value: value\n mock_curses_module.A_BOLD = 0\n\n stdscr = _build_mock_stdscr()\n app = CleanReleaseTUI(stdscr)\n stdscr.getch.side_effect = [curses.KEY_F5, curses.KEY_F10]\n\n def _set_blocked_state() -> None:\n app.status = CheckFinalStatus.BLOCKED\n app.report_id = \"CCR-smoke-blocked\"\n app.violations_list = [object()]\n\n with patch.object(app, \"run_checks\", side_effect=_set_blocked_state, autospec=True):\n app.loop()\n\n assert app.status == CheckFinalStatus.BLOCKED\n assert app.report_id == \"CCR-smoke-blocked\"\n assert app.violations_list\n\n\n# [/DEF:test_tui_f5_run_smoke_reports_blocked_state:Function]\n\n\n# [DEF:test_tui_non_tty_refuses_startup:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Verify non-TTY execution returns exit code 2 with actionable stderr guidance.\ndef test_tui_non_tty_refuses_startup(capsys) -> None:\n \"\"\"Non-TTY startup must refuse TUI mode and redirect operator to CLI/API flow.\"\"\"\n with patch(\"sys.stdout.isatty\", return_value=False):\n exit_code = main()\n\n captured = capsys.readouterr()\n assert exit_code == 2\n assert \"TTY is required for TUI mode\" in captured.err\n assert \"Use CLI/API workflow instead\" in captured.err\n\n\n# [/DEF:test_tui_non_tty_refuses_startup:Function]\n\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_f8_blocked_without_facade_binding:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Verify F8 path reports disabled action instead of mutating hidden facade state.\ndef test_tui_f8_blocked_without_facade_binding(mock_curses_module: MagicMock) -> None:\n \"\"\"F8 should not perform hidden state mutation when facade action is not bound.\"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F8 = curses.KEY_F8\n mock_curses_module.color_pair.side_effect = lambda value: value\n mock_curses_module.A_BOLD = 0\n\n stdscr = _build_mock_stdscr()\n app = CleanReleaseTUI(stdscr)\n stdscr.getch.side_effect = [curses.KEY_F8, curses.KEY_F10]\n\n app.loop()\n\n assert app.last_error is not None\n assert \"F8 disabled\" in app.last_error\n\n\n# [/DEF:test_tui_f8_blocked_without_facade_binding:Function]\n# [/DEF:test_clean_release_tui_v2:Module]\n" + "body": "# [DEF:test_clean_release_tui_v2:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @PURPOSE: Smoke tests for thin-client TUI action dispatch and blocked transition behavior.\n# @LAYER: Domain\n\n\"\"\"Smoke tests for the redesigned clean release TUI.\"\"\"\n\nfrom __future__ import annotations\n\nimport curses\nfrom unittest.mock import MagicMock, patch\n\nfrom src.models.clean_release import CheckFinalStatus\nfrom src.scripts.clean_release_tui import CleanReleaseTUI, main\n\n\n# [DEF:_build_mock_stdscr:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Build deterministic curses screen mock with default terminal geometry and exit key.\ndef _build_mock_stdscr() -> MagicMock:\n stdscr = MagicMock()\n stdscr.getmaxyx.return_value = (40, 120)\n stdscr.getch.return_value = curses.KEY_F10\n return stdscr\n\n\n# [/DEF:_build_mock_stdscr:Function]\n\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_f5_dispatches_run_action:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Verify F5 key dispatch invokes run_checks exactly once before graceful exit.\ndef test_tui_f5_dispatches_run_action(mock_curses_module: MagicMock) -> None:\n \"\"\"F5 should dispatch run action from TUI loop.\"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda value: value\n mock_curses_module.A_BOLD = 0\n\n stdscr = _build_mock_stdscr()\n app = CleanReleaseTUI(stdscr)\n\n stdscr.getch.side_effect = [curses.KEY_F5, curses.KEY_F10]\n with patch.object(app, \"run_checks\", autospec=True) as run_checks_mock:\n app.loop()\n\n run_checks_mock.assert_called_once_with()\n\n\n# [/DEF:test_tui_f5_dispatches_run_action:Function]\n\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_f5_run_smoke_reports_blocked_state:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Verify blocked compliance state is surfaced after F5-triggered run action.\ndef test_tui_f5_run_smoke_reports_blocked_state(mock_curses_module: MagicMock) -> None:\n \"\"\"F5 smoke test should expose blocked outcome state after run action.\"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda value: value\n mock_curses_module.A_BOLD = 0\n\n stdscr = _build_mock_stdscr()\n app = CleanReleaseTUI(stdscr)\n stdscr.getch.side_effect = [curses.KEY_F5, curses.KEY_F10]\n\n def _set_blocked_state() -> None:\n app.status = CheckFinalStatus.BLOCKED\n app.report_id = \"CCR-smoke-blocked\"\n app.violations_list = [object()]\n\n with patch.object(app, \"run_checks\", side_effect=_set_blocked_state, autospec=True):\n app.loop()\n\n assert app.status == CheckFinalStatus.BLOCKED\n assert app.report_id == \"CCR-smoke-blocked\"\n assert app.violations_list\n\n\n# [/DEF:test_tui_f5_run_smoke_reports_blocked_state:Function]\n\n\n# [DEF:test_tui_non_tty_refuses_startup:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Verify non-TTY execution returns exit code 2 with actionable stderr guidance.\ndef test_tui_non_tty_refuses_startup(capsys) -> None:\n \"\"\"Non-TTY startup must refuse TUI mode and redirect operator to CLI/API flow.\"\"\"\n with patch(\"sys.stdout.isatty\", return_value=False):\n exit_code = main()\n\n captured = capsys.readouterr()\n assert exit_code == 2\n assert \"TTY is required for TUI mode\" in captured.err\n assert \"Use CLI/API workflow instead\" in captured.err\n\n\n# [/DEF:test_tui_non_tty_refuses_startup:Function]\n\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# [DEF:test_tui_f8_blocked_without_facade_binding:Function]\n# @RELATION: BINDS_TO -> test_clean_release_tui_v2\n# @PURPOSE: Verify F8 path reports disabled action instead of mutating hidden facade state.\ndef test_tui_f8_blocked_without_facade_binding(mock_curses_module: MagicMock) -> None:\n \"\"\"F8 should not perform hidden state mutation when facade action is not bound.\"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F8 = curses.KEY_F8\n mock_curses_module.color_pair.side_effect = lambda value: value\n mock_curses_module.A_BOLD = 0\n\n stdscr = _build_mock_stdscr()\n app = CleanReleaseTUI(stdscr)\n stdscr.getch.side_effect = [curses.KEY_F8, curses.KEY_F10]\n\n app.loop()\n\n assert app.last_error is not None\n assert \"F8 disabled\" in app.last_error\n\n\n# [/DEF:test_tui_f8_blocked_without_facade_binding:Function]\n# [/DEF:test_clean_release_tui_v2:Module]\n" }, { "contract_id": "_build_mock_stdscr", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui_v2.py", - "start_line": 18, - "end_line": 28, + "start_line": 17, + "end_line": 27, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -99938,8 +68227,8 @@ "contract_id": "test_tui_f5_dispatches_run_action", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui_v2.py", - "start_line": 32, - "end_line": 52, + "start_line": 31, + "end_line": 51, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -99971,8 +68260,8 @@ "contract_id": "test_tui_f5_run_smoke_reports_blocked_state", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui_v2.py", - "start_line": 56, - "end_line": 83, + "start_line": 55, + "end_line": 82, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -100004,8 +68293,8 @@ "contract_id": "test_tui_non_tty_refuses_startup", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui_v2.py", - "start_line": 86, - "end_line": 100, + "start_line": 85, + "end_line": 99, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -100037,8 +68326,8 @@ "contract_id": "test_tui_f8_blocked_without_facade_binding", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui_v2.py", - "start_line": 104, - "end_line": 124, + "start_line": 103, + "end_line": 123, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -100071,11 +68360,10 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_approval_service.py", "start_line": 1, - "end_line": 203, - "tier": "TIER_3", - "complexity": 5, + "end_line": 202, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "INVARIANT": "Approval is allowed only for PASSED report bound to candidate; duplicate approve and foreign report must be rejected.", "LAYER": "Tests", "PURPOSE": "Define approval gate contracts for approve/reject operations over immutable compliance evidence.", @@ -100097,56 +68385,29 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, @@ -100170,14 +68431,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:TestApprovalService:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 5\n# @SEMANTICS: tests, clean-release, approval, lifecycle, gate\n# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.\n# @LAYER: Tests\n# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; duplicate approve and foreign report must be rejected.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nimport pytest\n\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import ApprovalDecisionType, CandidateStatus, ComplianceDecision\nfrom src.services.clean_release.exceptions import ApprovalGateError\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_candidate_with_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Seed candidate and report fixtures for approval gate tests.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and report linked by candidate_id.\ndef _seed_candidate_with_report(\n *,\n candidate_id: str = \"cand-approve-1\",\n report_id: str = \"CCR-approve-1\",\n report_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approve-1\",\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-approve-1\",\n candidate_id=candidate_id,\n final_status=report_status.value,\n summary_json={\n \"operator_summary\": \"seed\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0 if report_status == ComplianceDecision.PASSED else 1,\n },\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# [/DEF:_seed_candidate_with_report:Function]\n\n\n# [DEF:test_approve_rejects_blocked_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when latest report final status is not PASSED.\n# @PRE: Candidate has BLOCKED report.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_blocked_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report(\n report_status=ComplianceDecision.BLOCKED,\n )\n\n with pytest.raises(ApprovalGateError, match=\"PASSED\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"blocked report cannot be approved\",\n )\n# [/DEF:test_approve_rejects_blocked_report:Function]\n\n\n# [DEF:test_approve_rejects_foreign_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when report belongs to another candidate.\n# @PRE: Candidate exists, report candidate_id differs.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_foreign_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, _ = _seed_candidate_with_report()\n foreign_report = ComplianceReport(\n id=\"CCR-foreign-1\",\n run_id=\"run-foreign-1\",\n candidate_id=\"cand-foreign-1\",\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"foreign\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n repository.save_report(foreign_report)\n\n with pytest.raises(ApprovalGateError, match=\"belongs to another candidate\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=foreign_report.id,\n decided_by=\"approver\",\n comment=\"foreign report\",\n )\n# [/DEF:test_approve_rejects_foreign_report:Function]\n\n\n# [DEF:test_approve_rejects_duplicate_approve:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure repeated approve decision for same candidate is blocked.\n# @PRE: Candidate has already been approved once.\n# @POST: Second approve_candidate call raises ApprovalGateError.\ndef test_approve_rejects_duplicate_approve():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n first = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"first approval\",\n )\n assert first.decision == ApprovalDecisionType.APPROVED.value\n assert repository.get_candidate(candidate_id).status == CandidateStatus.APPROVED.value\n\n with pytest.raises(ApprovalGateError, match=\"already approved\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"duplicate approval\",\n )\n# [/DEF:test_approve_rejects_duplicate_approve:Function]\n\n\n# [DEF:test_reject_persists_decision_without_promoting_candidate_state:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure reject decision is immutable and does not promote candidate to APPROVED.\n# @PRE: Candidate has PASSED report and CHECK_PASSED lifecycle state.\n# @POST: reject_candidate persists REJECTED decision; candidate status remains unchanged.\ndef test_reject_persists_decision_without_promoting_candidate_state():\n from src.services.clean_release.approval_service import reject_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"manual rejection\",\n )\n\n candidate = repository.get_candidate(candidate_id)\n assert decision.decision == ApprovalDecisionType.REJECTED.value\n assert candidate is not None\n assert candidate.status == CandidateStatus.CHECK_PASSED.value\n# [/DEF:test_reject_persists_decision_without_promoting_candidate_state:Function]\n\n\n# [DEF:test_reject_then_publish_is_blocked:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure latest REJECTED decision blocks publication gate.\n# @PRE: Candidate is rejected for passed report.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_reject_then_publish_is_blocked():\n from src.services.clean_release.approval_service import reject_candidate\n from src.services.clean_release.publication_service import publish_candidate\n from src.services.clean_release.exceptions import PublicationGateError\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"rejected before publish\",\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-blocked\",\n )\n# [/DEF:test_reject_then_publish_is_blocked:Function]\n\n# [/DEF:TestApprovalService:Module]\n" + "body": "# [DEF:TestApprovalService:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, approval, lifecycle, gate\n# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.\n# @LAYER: Tests\n# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; duplicate approve and foreign report must be rejected.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nimport pytest\n\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import ApprovalDecisionType, CandidateStatus, ComplianceDecision\nfrom src.services.clean_release.exceptions import ApprovalGateError\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_candidate_with_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Seed candidate and report fixtures for approval gate tests.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and report linked by candidate_id.\ndef _seed_candidate_with_report(\n *,\n candidate_id: str = \"cand-approve-1\",\n report_id: str = \"CCR-approve-1\",\n report_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approve-1\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-approve-1\",\n candidate_id=candidate_id,\n final_status=report_status.value,\n summary_json={\n \"operator_summary\": \"seed\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0 if report_status == ComplianceDecision.PASSED else 1,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# [/DEF:_seed_candidate_with_report:Function]\n\n\n# [DEF:test_approve_rejects_blocked_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when latest report final status is not PASSED.\n# @PRE: Candidate has BLOCKED report.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_blocked_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report(\n report_status=ComplianceDecision.BLOCKED,\n )\n\n with pytest.raises(ApprovalGateError, match=\"PASSED\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"blocked report cannot be approved\",\n )\n# [/DEF:test_approve_rejects_blocked_report:Function]\n\n\n# [DEF:test_approve_rejects_foreign_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when report belongs to another candidate.\n# @PRE: Candidate exists, report candidate_id differs.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_foreign_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, _ = _seed_candidate_with_report()\n foreign_report = ComplianceReport(\n id=\"CCR-foreign-1\",\n run_id=\"run-foreign-1\",\n candidate_id=\"cand-foreign-1\",\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"foreign\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n repository.save_report(foreign_report)\n\n with pytest.raises(ApprovalGateError, match=\"belongs to another candidate\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=foreign_report.id,\n decided_by=\"approver\",\n comment=\"foreign report\",\n )\n# [/DEF:test_approve_rejects_foreign_report:Function]\n\n\n# [DEF:test_approve_rejects_duplicate_approve:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure repeated approve decision for same candidate is blocked.\n# @PRE: Candidate has already been approved once.\n# @POST: Second approve_candidate call raises ApprovalGateError.\ndef test_approve_rejects_duplicate_approve():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n first = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"first approval\",\n )\n assert first.decision == ApprovalDecisionType.APPROVED.value\n assert repository.get_candidate(candidate_id).status == CandidateStatus.APPROVED.value\n\n with pytest.raises(ApprovalGateError, match=\"already approved\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"duplicate approval\",\n )\n# [/DEF:test_approve_rejects_duplicate_approve:Function]\n\n\n# [DEF:test_reject_persists_decision_without_promoting_candidate_state:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure reject decision is immutable and does not promote candidate to APPROVED.\n# @PRE: Candidate has PASSED report and CHECK_PASSED lifecycle state.\n# @POST: reject_candidate persists REJECTED decision; candidate status remains unchanged.\ndef test_reject_persists_decision_without_promoting_candidate_state():\n from src.services.clean_release.approval_service import reject_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"manual rejection\",\n )\n\n candidate = repository.get_candidate(candidate_id)\n assert decision.decision == ApprovalDecisionType.REJECTED.value\n assert candidate is not None\n assert candidate.status == CandidateStatus.CHECK_PASSED.value\n# [/DEF:test_reject_persists_decision_without_promoting_candidate_state:Function]\n\n\n# [DEF:test_reject_then_publish_is_blocked:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure latest REJECTED decision blocks publication gate.\n# @PRE: Candidate is rejected for passed report.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_reject_then_publish_is_blocked():\n from src.services.clean_release.approval_service import reject_candidate\n from src.services.clean_release.exceptions import PublicationGateError\n from src.services.clean_release.publication_service import publish_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"rejected before publish\",\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-blocked\",\n )\n# [/DEF:test_reject_then_publish_is_blocked:Function]\n\n# [/DEF:TestApprovalService:Module]\n" }, { "contract_id": "_seed_candidate_with_report", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 21, - "end_line": 59, + "start_line": 20, + "end_line": 58, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -100232,14 +68493,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:_seed_candidate_with_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Seed candidate and report fixtures for approval gate tests.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and report linked by candidate_id.\ndef _seed_candidate_with_report(\n *,\n candidate_id: str = \"cand-approve-1\",\n report_id: str = \"CCR-approve-1\",\n report_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approve-1\",\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-approve-1\",\n candidate_id=candidate_id,\n final_status=report_status.value,\n summary_json={\n \"operator_summary\": \"seed\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0 if report_status == ComplianceDecision.PASSED else 1,\n },\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# [/DEF:_seed_candidate_with_report:Function]\n" + "body": "# [DEF:_seed_candidate_with_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Seed candidate and report fixtures for approval gate tests.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and report linked by candidate_id.\ndef _seed_candidate_with_report(\n *,\n candidate_id: str = \"cand-approve-1\",\n report_id: str = \"CCR-approve-1\",\n report_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approve-1\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-approve-1\",\n candidate_id=candidate_id,\n final_status=report_status.value,\n summary_json={\n \"operator_summary\": \"seed\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0 if report_status == ComplianceDecision.PASSED else 1,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# [/DEF:_seed_candidate_with_report:Function]\n" }, { "contract_id": "test_approve_rejects_blocked_report", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 62, - "end_line": 82, + "start_line": 61, + "end_line": 81, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -100291,8 +68552,8 @@ "contract_id": "test_approve_rejects_foreign_report", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 85, - "end_line": 113, + "start_line": 84, + "end_line": 112, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -100338,14 +68599,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_approve_rejects_foreign_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when report belongs to another candidate.\n# @PRE: Candidate exists, report candidate_id differs.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_foreign_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, _ = _seed_candidate_with_report()\n foreign_report = ComplianceReport(\n id=\"CCR-foreign-1\",\n run_id=\"run-foreign-1\",\n candidate_id=\"cand-foreign-1\",\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"foreign\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n repository.save_report(foreign_report)\n\n with pytest.raises(ApprovalGateError, match=\"belongs to another candidate\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=foreign_report.id,\n decided_by=\"approver\",\n comment=\"foreign report\",\n )\n# [/DEF:test_approve_rejects_foreign_report:Function]\n" + "body": "# [DEF:test_approve_rejects_foreign_report:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when report belongs to another candidate.\n# @PRE: Candidate exists, report candidate_id differs.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_foreign_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, _ = _seed_candidate_with_report()\n foreign_report = ComplianceReport(\n id=\"CCR-foreign-1\",\n run_id=\"run-foreign-1\",\n candidate_id=\"cand-foreign-1\",\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"foreign\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n repository.save_report(foreign_report)\n\n with pytest.raises(ApprovalGateError, match=\"belongs to another candidate\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=foreign_report.id,\n decided_by=\"approver\",\n comment=\"foreign report\",\n )\n# [/DEF:test_approve_rejects_foreign_report:Function]\n" }, { "contract_id": "test_approve_rejects_duplicate_approve", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 116, - "end_line": 144, + "start_line": 115, + "end_line": 143, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -100397,8 +68658,8 @@ "contract_id": "test_reject_persists_decision_without_promoting_candidate_state", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 147, - "end_line": 169, + "start_line": 146, + "end_line": 168, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -100450,8 +68711,8 @@ "contract_id": "test_reject_then_publish_is_blocked", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 172, - "end_line": 201, + "start_line": 171, + "end_line": 200, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -100497,7 +68758,7 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_reject_then_publish_is_blocked:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure latest REJECTED decision blocks publication gate.\n# @PRE: Candidate is rejected for passed report.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_reject_then_publish_is_blocked():\n from src.services.clean_release.approval_service import reject_candidate\n from src.services.clean_release.publication_service import publish_candidate\n from src.services.clean_release.exceptions import PublicationGateError\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"rejected before publish\",\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-blocked\",\n )\n# [/DEF:test_reject_then_publish_is_blocked:Function]\n" + "body": "# [DEF:test_reject_then_publish_is_blocked:Function]\n# @RELATION: BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure latest REJECTED decision blocks publication gate.\n# @PRE: Candidate is rejected for passed report.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_reject_then_publish_is_blocked():\n from src.services.clean_release.approval_service import reject_candidate\n from src.services.clean_release.exceptions import PublicationGateError\n from src.services.clean_release.publication_service import publish_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"rejected before publish\",\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-blocked\",\n )\n# [/DEF:test_reject_then_publish_is_blocked:Function]\n" }, { "contract_id": "test_candidate_manifest_services", @@ -100505,10 +68766,9 @@ "file_path": "backend/tests/services/clean_release/test_candidate_manifest_services.py", "start_line": 1, "end_line": 290, - "tier": "TIER_2", - "complexity": 3, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": "3", "LAYER": "Tests", "PURPOSE": "Test lifecycle and manifest versioning for release candidates." }, @@ -100521,6 +68781,15 @@ } ], "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" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -100541,7 +68810,7 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_candidate_manifest_services:Module]\n# @RELATION: BELONGS_TO -> [SrcRoot:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Test lifecycle and manifest versioning for release candidates.\n# @LAYER: Tests\n\nimport pytest\nfrom datetime import datetime, timezone\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom src.core.database import Base\nfrom src.models.clean_release import (\n ReleaseCandidate,\n DistributionManifest,\n CandidateArtifact,\n)\nfrom src.services.clean_release.enums import CandidateStatus\nfrom src.services.clean_release.candidate_service import register_candidate\nfrom src.services.clean_release.manifest_service import build_manifest_snapshot\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n\n\n# [DEF:test_candidate_lifecycle_transitions:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify release candidate allows legal status transitions and rejects forbidden back-transitions.\ndef test_candidate_lifecycle_transitions(db_session):\n \"\"\"\n @PURPOSE: Verify legal state transitions for ReleaseCandidate.\n \"\"\"\n candidate = ReleaseCandidate(\n id=\"test-candidate-1\",\n name=\"Test Candidate\",\n version=\"1.0.0\",\n source_snapshot_ref=\"ref-1\",\n created_by=\"operator\",\n status=CandidateStatus.DRAFT,\n )\n db_session.add(candidate)\n db_session.commit()\n\n # Valid transition: DRAFT -> PREPARED\n candidate.transition_to(CandidateStatus.PREPARED)\n assert candidate.status == CandidateStatus.PREPARED\n\n # Invalid transition: PREPARED -> DRAFT (should raise IllegalTransitionError)\n from src.services.clean_release.exceptions import IllegalTransitionError\n\n with pytest.raises(IllegalTransitionError, match=\"Forbidden transition\"):\n candidate.transition_to(CandidateStatus.DRAFT)\n\n\n# [/DEF:test_candidate_lifecycle_transitions:Function]\n\n\n# [DEF:test_manifest_versioning_and_immutability:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify manifest versions increment monotonically and older snapshots remain queryable.\ndef test_manifest_versioning_and_immutability(db_session):\n \"\"\"\n @PURPOSE: Verify manifest versioning and immutability invariants.\n \"\"\"\n candidate_id = \"test-candidate-2\"\n\n # Create version 1\n m1 = DistributionManifest(\n id=\"manifest-v1\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash1\",\n artifacts_digest=\"hash1\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(timezone.utc),\n created_by=\"operator\",\n )\n db_session.add(m1)\n\n # Create version 2\n m2 = DistributionManifest(\n id=\"manifest-v2\",\n candidate_id=candidate_id,\n manifest_version=2,\n manifest_digest=\"hash2\",\n artifacts_digest=\"hash2\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(timezone.utc),\n created_by=\"operator\",\n )\n db_session.add(m2)\n db_session.commit()\n\n latest = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n assert latest.manifest_version == 2\n assert latest.id == \"manifest-v2\"\n\n all_manifests = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .all()\n )\n assert len(all_manifests) == 2\n\n\n# [/DEF:test_manifest_versioning_and_immutability:Function]\n\n\n# [DEF:_valid_artifacts:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Provide canonical valid artifact payload used by candidate registration tests.\ndef _valid_artifacts():\n return [\n {\n \"id\": \"art-1\",\n \"path\": \"bin/app\",\n \"sha256\": \"abc123\",\n \"size\": 42,\n }\n ]\n\n\n# [/DEF:_valid_artifacts:Function]\n\n\n# [DEF:test_register_candidate_rejects_duplicate_candidate_id:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify duplicate candidate_id registration is rejected by service invariants.\ndef test_register_candidate_rejects_duplicate_candidate_id():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"dup-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha1\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n with pytest.raises(ValueError, match=\"already exists\"):\n register_candidate(\n repository=repository,\n candidate_id=\"dup-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha1\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n\n# [/DEF:test_register_candidate_rejects_duplicate_candidate_id:Function]\n\n\n# [DEF:test_register_candidate_rejects_malformed_artifact_input:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify candidate registration rejects artifact payloads missing required fields.\ndef test_register_candidate_rejects_malformed_artifact_input():\n repository = CleanReleaseRepository()\n bad_artifacts = [{\"id\": \"art-1\", \"path\": \"bin/app\", \"size\": 42}] # missing sha256\n\n with pytest.raises(ValueError, match=\"missing required field 'sha256'\"):\n register_candidate(\n repository=repository,\n candidate_id=\"bad-art-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha2\",\n created_by=\"operator\",\n artifacts=bad_artifacts,\n )\n\n\n# [/DEF:test_register_candidate_rejects_malformed_artifact_input:Function]\n\n\n# [DEF:test_register_candidate_rejects_empty_artifact_set:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify candidate registration rejects empty artifact collections.\ndef test_register_candidate_rejects_empty_artifact_set():\n repository = CleanReleaseRepository()\n\n with pytest.raises(ValueError, match=\"artifacts must not be empty\"):\n register_candidate(\n repository=repository,\n candidate_id=\"empty-art-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha3\",\n created_by=\"operator\",\n artifacts=[],\n )\n\n\n# [/DEF:test_register_candidate_rejects_empty_artifact_set:Function]\n\n\n# [DEF:test_manifest_service_rebuild_creates_new_version:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify repeated manifest build creates a new incremented immutable version.\ndef test_manifest_service_rebuild_creates_new_version():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"manifest-version-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha10\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n first = build_manifest_snapshot(\n repository=repository, candidate_id=\"manifest-version-1\", created_by=\"operator\"\n )\n second = build_manifest_snapshot(\n repository=repository, candidate_id=\"manifest-version-1\", created_by=\"operator\"\n )\n\n assert first.manifest_version == 1\n assert second.manifest_version == 2\n assert first.id != second.id\n\n\n# [/DEF:test_manifest_service_rebuild_creates_new_version:Function]\n\n\n# [DEF:test_manifest_service_existing_manifest_cannot_be_mutated:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify existing manifest snapshot remains immutable when rebuilding newer manifest version.\ndef test_manifest_service_existing_manifest_cannot_be_mutated():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha11\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n created = build_manifest_snapshot(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n created_by=\"operator\",\n )\n original_digest = created.manifest_digest\n\n rebuilt = build_manifest_snapshot(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n created_by=\"operator\",\n )\n old_manifest = repository.get_manifest(created.id)\n\n assert old_manifest is not None\n assert old_manifest.manifest_digest == original_digest\n assert old_manifest.id == created.id\n assert rebuilt.id != created.id\n\n\n# [/DEF:test_manifest_service_existing_manifest_cannot_be_mutated:Function]\n\n\n# [DEF:test_manifest_service_rejects_missing_candidate:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify manifest build fails with missing candidate identifier.\ndef test_manifest_service_rejects_missing_candidate():\n repository = CleanReleaseRepository()\n\n with pytest.raises(ValueError, match=\"not found\"):\n build_manifest_snapshot(\n repository=repository,\n candidate_id=\"missing-candidate\",\n created_by=\"operator\",\n )\n\n\n# [/DEF:test_manifest_service_rejects_missing_candidate:Function]\n# [/DEF:test_candidate_manifest_services:Module]\n" + "body": "# [DEF:test_candidate_manifest_services:Module]\n# @RELATION: BELONGS_TO -> [SrcRoot:Module]\n# @PURPOSE: Test lifecycle and manifest versioning for release candidates.\n# @LAYER: Tests\n\nfrom datetime import UTC, datetime\n\nimport pytest\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.database import Base\nfrom src.models.clean_release import (\n DistributionManifest,\n ReleaseCandidate,\n)\nfrom src.services.clean_release.candidate_service import register_candidate\nfrom src.services.clean_release.enums import CandidateStatus\nfrom src.services.clean_release.manifest_service import build_manifest_snapshot\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n\n\n# [DEF:test_candidate_lifecycle_transitions:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify release candidate allows legal status transitions and rejects forbidden back-transitions.\ndef test_candidate_lifecycle_transitions(db_session):\n \"\"\"\n @PURPOSE: Verify legal state transitions for ReleaseCandidate.\n \"\"\"\n candidate = ReleaseCandidate(\n id=\"test-candidate-1\",\n name=\"Test Candidate\",\n version=\"1.0.0\",\n source_snapshot_ref=\"ref-1\",\n created_by=\"operator\",\n status=CandidateStatus.DRAFT,\n )\n db_session.add(candidate)\n db_session.commit()\n\n # Valid transition: DRAFT -> PREPARED\n candidate.transition_to(CandidateStatus.PREPARED)\n assert candidate.status == CandidateStatus.PREPARED\n\n # Invalid transition: PREPARED -> DRAFT (should raise IllegalTransitionError)\n from src.services.clean_release.exceptions import IllegalTransitionError\n\n with pytest.raises(IllegalTransitionError, match=\"Forbidden transition\"):\n candidate.transition_to(CandidateStatus.DRAFT)\n\n\n# [/DEF:test_candidate_lifecycle_transitions:Function]\n\n\n# [DEF:test_manifest_versioning_and_immutability:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify manifest versions increment monotonically and older snapshots remain queryable.\ndef test_manifest_versioning_and_immutability(db_session):\n \"\"\"\n @PURPOSE: Verify manifest versioning and immutability invariants.\n \"\"\"\n candidate_id = \"test-candidate-2\"\n\n # Create version 1\n m1 = DistributionManifest(\n id=\"manifest-v1\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash1\",\n artifacts_digest=\"hash1\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(UTC),\n created_by=\"operator\",\n )\n db_session.add(m1)\n\n # Create version 2\n m2 = DistributionManifest(\n id=\"manifest-v2\",\n candidate_id=candidate_id,\n manifest_version=2,\n manifest_digest=\"hash2\",\n artifacts_digest=\"hash2\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(UTC),\n created_by=\"operator\",\n )\n db_session.add(m2)\n db_session.commit()\n\n latest = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n assert latest.manifest_version == 2\n assert latest.id == \"manifest-v2\"\n\n all_manifests = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .all()\n )\n assert len(all_manifests) == 2\n\n\n# [/DEF:test_manifest_versioning_and_immutability:Function]\n\n\n# [DEF:_valid_artifacts:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Provide canonical valid artifact payload used by candidate registration tests.\ndef _valid_artifacts():\n return [\n {\n \"id\": \"art-1\",\n \"path\": \"bin/app\",\n \"sha256\": \"abc123\",\n \"size\": 42,\n }\n ]\n\n\n# [/DEF:_valid_artifacts:Function]\n\n\n# [DEF:test_register_candidate_rejects_duplicate_candidate_id:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify duplicate candidate_id registration is rejected by service invariants.\ndef test_register_candidate_rejects_duplicate_candidate_id():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"dup-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha1\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n with pytest.raises(ValueError, match=\"already exists\"):\n register_candidate(\n repository=repository,\n candidate_id=\"dup-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha1\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n\n# [/DEF:test_register_candidate_rejects_duplicate_candidate_id:Function]\n\n\n# [DEF:test_register_candidate_rejects_malformed_artifact_input:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify candidate registration rejects artifact payloads missing required fields.\ndef test_register_candidate_rejects_malformed_artifact_input():\n repository = CleanReleaseRepository()\n bad_artifacts = [{\"id\": \"art-1\", \"path\": \"bin/app\", \"size\": 42}] # missing sha256\n\n with pytest.raises(ValueError, match=\"missing required field 'sha256'\"):\n register_candidate(\n repository=repository,\n candidate_id=\"bad-art-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha2\",\n created_by=\"operator\",\n artifacts=bad_artifacts,\n )\n\n\n# [/DEF:test_register_candidate_rejects_malformed_artifact_input:Function]\n\n\n# [DEF:test_register_candidate_rejects_empty_artifact_set:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify candidate registration rejects empty artifact collections.\ndef test_register_candidate_rejects_empty_artifact_set():\n repository = CleanReleaseRepository()\n\n with pytest.raises(ValueError, match=\"artifacts must not be empty\"):\n register_candidate(\n repository=repository,\n candidate_id=\"empty-art-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha3\",\n created_by=\"operator\",\n artifacts=[],\n )\n\n\n# [/DEF:test_register_candidate_rejects_empty_artifact_set:Function]\n\n\n# [DEF:test_manifest_service_rebuild_creates_new_version:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify repeated manifest build creates a new incremented immutable version.\ndef test_manifest_service_rebuild_creates_new_version():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"manifest-version-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha10\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n first = build_manifest_snapshot(\n repository=repository, candidate_id=\"manifest-version-1\", created_by=\"operator\"\n )\n second = build_manifest_snapshot(\n repository=repository, candidate_id=\"manifest-version-1\", created_by=\"operator\"\n )\n\n assert first.manifest_version == 1\n assert second.manifest_version == 2\n assert first.id != second.id\n\n\n# [/DEF:test_manifest_service_rebuild_creates_new_version:Function]\n\n\n# [DEF:test_manifest_service_existing_manifest_cannot_be_mutated:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify existing manifest snapshot remains immutable when rebuilding newer manifest version.\ndef test_manifest_service_existing_manifest_cannot_be_mutated():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha11\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n created = build_manifest_snapshot(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n created_by=\"operator\",\n )\n original_digest = created.manifest_digest\n\n rebuilt = build_manifest_snapshot(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n created_by=\"operator\",\n )\n old_manifest = repository.get_manifest(created.id)\n\n assert old_manifest is not None\n assert old_manifest.manifest_digest == original_digest\n assert old_manifest.id == created.id\n assert rebuilt.id != created.id\n\n\n# [/DEF:test_manifest_service_existing_manifest_cannot_be_mutated:Function]\n\n\n# [DEF:test_manifest_service_rejects_missing_candidate:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify manifest build fails with missing candidate identifier.\ndef test_manifest_service_rejects_missing_candidate():\n repository = CleanReleaseRepository()\n\n with pytest.raises(ValueError, match=\"not found\"):\n build_manifest_snapshot(\n repository=repository,\n candidate_id=\"missing-candidate\",\n created_by=\"operator\",\n )\n\n\n# [/DEF:test_manifest_service_rejects_missing_candidate:Function]\n# [/DEF:test_candidate_manifest_services:Module]\n" }, { "contract_id": "test_candidate_lifecycle_transitions", @@ -100607,7 +68876,7 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_manifest_versioning_and_immutability:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify manifest versions increment monotonically and older snapshots remain queryable.\ndef test_manifest_versioning_and_immutability(db_session):\n \"\"\"\n @PURPOSE: Verify manifest versioning and immutability invariants.\n \"\"\"\n candidate_id = \"test-candidate-2\"\n\n # Create version 1\n m1 = DistributionManifest(\n id=\"manifest-v1\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash1\",\n artifacts_digest=\"hash1\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(timezone.utc),\n created_by=\"operator\",\n )\n db_session.add(m1)\n\n # Create version 2\n m2 = DistributionManifest(\n id=\"manifest-v2\",\n candidate_id=candidate_id,\n manifest_version=2,\n manifest_digest=\"hash2\",\n artifacts_digest=\"hash2\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(timezone.utc),\n created_by=\"operator\",\n )\n db_session.add(m2)\n db_session.commit()\n\n latest = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n assert latest.manifest_version == 2\n assert latest.id == \"manifest-v2\"\n\n all_manifests = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .all()\n )\n assert len(all_manifests) == 2\n\n\n# [/DEF:test_manifest_versioning_and_immutability:Function]\n" + "body": "# [DEF:test_manifest_versioning_and_immutability:Function]\n# @RELATION: BINDS_TO -> [test_candidate_manifest_services:Module]\n# @PURPOSE: Verify manifest versions increment monotonically and older snapshots remain queryable.\ndef test_manifest_versioning_and_immutability(db_session):\n \"\"\"\n @PURPOSE: Verify manifest versioning and immutability invariants.\n \"\"\"\n candidate_id = \"test-candidate-2\"\n\n # Create version 1\n m1 = DistributionManifest(\n id=\"manifest-v1\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash1\",\n artifacts_digest=\"hash1\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(UTC),\n created_by=\"operator\",\n )\n db_session.add(m1)\n\n # Create version 2\n m2 = DistributionManifest(\n id=\"manifest-v2\",\n candidate_id=candidate_id,\n manifest_version=2,\n manifest_digest=\"hash2\",\n artifacts_digest=\"hash2\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(UTC),\n created_by=\"operator\",\n )\n db_session.add(m2)\n db_session.commit()\n\n latest = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n assert latest.manifest_version == 2\n assert latest.id == \"manifest-v2\"\n\n all_manifests = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .all()\n )\n assert len(all_manifests) == 2\n\n\n# [/DEF:test_manifest_versioning_and_immutability:Function]\n" }, { "contract_id": "_valid_artifacts", @@ -100854,11 +69123,10 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", "start_line": 1, - "end_line": 176, - "tier": "TIER_3", - "complexity": 5, + "end_line": 175, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "INVARIANT": "Missing manifest prevents run startup; failed execution cannot finalize as PASSED.", "LAYER": "Tests", "PURPOSE": "Validate stage pipeline and run finalization contracts for compliance execution.", @@ -100880,56 +69148,29 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, @@ -100953,14 +69194,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:TestComplianceExecutionService:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 5\n# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization\n# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.\n# @LAYER: Tests\n# @INVARIANT: Missing manifest prevents run startup; failed execution cannot finalize as PASSED.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nimport pytest\n\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n ComplianceDecision,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_with_candidate_policy_registry:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Build deterministic repository state for run startup tests.\n# @PRE: candidate_id and snapshot ids are non-empty.\n# @POST: Returns repository with candidate, policy and registry; manifest is optional.\ndef _seed_with_candidate_policy_registry(\n *,\n with_manifest: bool,\n prohibited_detected_count: int = 0,\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-us2-1\"\n policy_id = \"policy-us2-1\"\n registry_id = \"registry-us2-1\"\n manifest_id = \"manifest-us2-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-us2\",\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=registry_id,\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=registry_id,\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-us2-1\",\n artifacts_digest=\"digest-us2-1\",\n source_snapshot_ref=\"git:sha-us2\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0 if prohibited_detected_count == 0 else prohibited_detected_count,\n \"prohibited_detected_count\": prohibited_detected_count,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n# [/DEF:_seed_with_candidate_policy_registry:Function]\n\n\n# [DEF:test_run_without_manifest_rejected:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure compliance run cannot start when manifest is unresolved.\n# @PRE: Candidate/policy exist but manifest is missing.\n# @POST: start_check_run raises ValueError and no run is persisted.\ndef test_run_without_manifest_rejected():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=False)\n orchestrator = CleanComplianceOrchestrator(repository)\n\n with pytest.raises(ValueError, match=\"Manifest or Policy not found\"):\n orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n\n assert len(repository.check_runs) == 0\n# [/DEF:test_run_without_manifest_rejected:Function]\n\n\n# [DEF:test_task_crash_mid_run_marks_failed:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure execution crash conditions force FAILED run status.\n# @PRE: Run exists, then required dependency becomes unavailable before execute_stages.\n# @POST: execute_stages persists run with FAILED status.\ndef test_task_crash_mid_run_marks_failed():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=True)\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n\n # Simulate mid-run crash dependency loss: registry snapshot disappears.\n repository.registries.clear()\n\n failed = orchestrator.execute_stages(run)\n assert failed.status == RunStatus.FAILED\n# [/DEF:test_task_crash_mid_run_marks_failed:Function]\n\n\n# [DEF:test_blocked_run_finalization_blocks_report_builder:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure blocked runs require blocking violations before report creation.\n# @PRE: Manifest contains prohibited artifacts leading to BLOCKED decision.\n# @POST: finalize keeps BLOCKED and report_builder rejects zero blocking violations.\ndef test_blocked_run_finalization_blocks_report_builder():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(\n with_manifest=True,\n prohibited_detected_count=1,\n )\n orchestrator = CleanComplianceOrchestrator(repository)\n builder = ComplianceReportBuilder(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == ComplianceDecision.BLOCKED\n assert run.status == RunStatus.SUCCEEDED\n\n with pytest.raises(ValueError, match=\"Blocked run requires at least one blocking violation\"):\n builder.build_report_payload(run, [])\n# [/DEF:test_blocked_run_finalization_blocks_report_builder:Function]\n\n# [/DEF:TestComplianceExecutionService:Module]\n" + "body": "# [DEF:TestComplianceExecutionService:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization\n# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.\n# @LAYER: Tests\n# @INVARIANT: Missing manifest prevents run startup; failed execution cannot finalize as PASSED.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nimport pytest\n\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n ComplianceDecision,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_with_candidate_policy_registry:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Build deterministic repository state for run startup tests.\n# @PRE: candidate_id and snapshot ids are non-empty.\n# @POST: Returns repository with candidate, policy and registry; manifest is optional.\ndef _seed_with_candidate_policy_registry(\n *,\n with_manifest: bool,\n prohibited_detected_count: int = 0,\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-us2-1\"\n policy_id = \"policy-us2-1\"\n registry_id = \"registry-us2-1\"\n manifest_id = \"manifest-us2-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-us2\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=registry_id,\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=registry_id,\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-us2-1\",\n artifacts_digest=\"digest-us2-1\",\n source_snapshot_ref=\"git:sha-us2\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0 if prohibited_detected_count == 0 else prohibited_detected_count,\n \"prohibited_detected_count\": prohibited_detected_count,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n# [/DEF:_seed_with_candidate_policy_registry:Function]\n\n\n# [DEF:test_run_without_manifest_rejected:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure compliance run cannot start when manifest is unresolved.\n# @PRE: Candidate/policy exist but manifest is missing.\n# @POST: start_check_run raises ValueError and no run is persisted.\ndef test_run_without_manifest_rejected():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=False)\n orchestrator = CleanComplianceOrchestrator(repository)\n\n with pytest.raises(ValueError, match=\"Manifest or Policy not found\"):\n orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n\n assert len(repository.check_runs) == 0\n# [/DEF:test_run_without_manifest_rejected:Function]\n\n\n# [DEF:test_task_crash_mid_run_marks_failed:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure execution crash conditions force FAILED run status.\n# @PRE: Run exists, then required dependency becomes unavailable before execute_stages.\n# @POST: execute_stages persists run with FAILED status.\ndef test_task_crash_mid_run_marks_failed():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=True)\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n\n # Simulate mid-run crash dependency loss: registry snapshot disappears.\n repository.registries.clear()\n\n failed = orchestrator.execute_stages(run)\n assert failed.status == RunStatus.FAILED\n# [/DEF:test_task_crash_mid_run_marks_failed:Function]\n\n\n# [DEF:test_blocked_run_finalization_blocks_report_builder:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure blocked runs require blocking violations before report creation.\n# @PRE: Manifest contains prohibited artifacts leading to BLOCKED decision.\n# @POST: finalize keeps BLOCKED and report_builder rejects zero blocking violations.\ndef test_blocked_run_finalization_blocks_report_builder():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(\n with_manifest=True,\n prohibited_detected_count=1,\n )\n orchestrator = CleanComplianceOrchestrator(repository)\n builder = ComplianceReportBuilder(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == ComplianceDecision.BLOCKED\n assert run.status == RunStatus.SUCCEEDED\n\n with pytest.raises(ValueError, match=\"Blocked run requires at least one blocking violation\"):\n builder.build_report_payload(run, [])\n# [/DEF:test_blocked_run_finalization_blocks_report_builder:Function]\n\n# [/DEF:TestComplianceExecutionService:Module]\n" }, { "contract_id": "_seed_with_candidate_policy_registry", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", - "start_line": 28, - "end_line": 99, + "start_line": 27, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -101015,14 +69256,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:_seed_with_candidate_policy_registry:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Build deterministic repository state for run startup tests.\n# @PRE: candidate_id and snapshot ids are non-empty.\n# @POST: Returns repository with candidate, policy and registry; manifest is optional.\ndef _seed_with_candidate_policy_registry(\n *,\n with_manifest: bool,\n prohibited_detected_count: int = 0,\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-us2-1\"\n policy_id = \"policy-us2-1\"\n registry_id = \"registry-us2-1\"\n manifest_id = \"manifest-us2-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-us2\",\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=registry_id,\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=registry_id,\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-us2-1\",\n artifacts_digest=\"digest-us2-1\",\n source_snapshot_ref=\"git:sha-us2\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0 if prohibited_detected_count == 0 else prohibited_detected_count,\n \"prohibited_detected_count\": prohibited_detected_count,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n# [/DEF:_seed_with_candidate_policy_registry:Function]\n" + "body": "# [DEF:_seed_with_candidate_policy_registry:Function]\n# @RELATION: BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Build deterministic repository state for run startup tests.\n# @PRE: candidate_id and snapshot ids are non-empty.\n# @POST: Returns repository with candidate, policy and registry; manifest is optional.\ndef _seed_with_candidate_policy_registry(\n *,\n with_manifest: bool,\n prohibited_detected_count: int = 0,\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-us2-1\"\n policy_id = \"policy-us2-1\"\n registry_id = \"registry-us2-1\"\n manifest_id = \"manifest-us2-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-us2\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=registry_id,\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=registry_id,\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-us2-1\",\n artifacts_digest=\"digest-us2-1\",\n source_snapshot_ref=\"git:sha-us2\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0 if prohibited_detected_count == 0 else prohibited_detected_count,\n \"prohibited_detected_count\": prohibited_detected_count,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n# [/DEF:_seed_with_candidate_policy_registry:Function]\n" }, { "contract_id": "test_run_without_manifest_rejected", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", - "start_line": 102, - "end_line": 120, + "start_line": 101, + "end_line": 119, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -101074,8 +69315,8 @@ "contract_id": "test_task_crash_mid_run_marks_failed", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", - "start_line": 123, - "end_line": 144, + "start_line": 122, + "end_line": 143, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -101127,8 +69368,8 @@ "contract_id": "test_blocked_run_finalization_blocks_report_builder", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", - "start_line": 147, - "end_line": 174, + "start_line": 146, + "end_line": 173, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -101181,11 +69422,10 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", "start_line": 1, - "end_line": 309, - "tier": "TIER_3", - "complexity": 5, + "end_line": 307, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "INVARIANT": "Compliance execution triggered as task produces terminal task status and persists run evidence.", "LAYER": "Tests", "PURPOSE": "Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.", @@ -101207,56 +69447,29 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, @@ -101280,14 +69493,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:TestComplianceTaskIntegration:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 5\n# @SEMANTICS: tests, clean-release, compliance, task-manager, integration\n# @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.\n# @LAYER: Tests\n# @INVARIANT: Compliance execution triggered as task produces terminal task status and persists run evidence.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom src.core.task_manager.manager import TaskManager\nfrom src.core.task_manager.models import TaskStatus\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.compliance_orchestrator import (\n CleanComplianceOrchestrator,\n)\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_repository:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Prepare deterministic candidate/policy/registry/manifest fixtures for task integration tests.\n# @PRE: with_manifest controls manifest availability.\n# @POST: Returns initialized repository and identifiers for compliance run startup.\ndef _seed_repository(\n *, with_manifest: bool\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-task-int-1\"\n policy_id = \"policy-task-int-1\"\n manifest_id = \"manifest-task-int-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-task-int\",\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-task-int-1\",\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-task-int-1\",\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-task-int\",\n artifacts_digest=\"digest-task-int\",\n source_snapshot_ref=\"git:sha-task-int\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0,\n \"prohibited_detected_count\": 0,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n\n\n# [/DEF:_seed_repository:Function]\n\n\n# [DEF:CleanReleaseCompliancePlugin:Class]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: TaskManager plugin shim that executes clean release compliance orchestration.\nclass CleanReleaseCompliancePlugin:\n @property\n def id(self) -> str:\n return \"clean-release-compliance\"\n\n @property\n def name(self) -> str:\n return \"clean_release_compliance\"\n\n def execute(self, params: Dict[str, Any], context=None):\n orchestrator = CleanComplianceOrchestrator(params[\"repository\"])\n run = orchestrator.start_check_run(\n candidate_id=params[\"candidate_id\"],\n policy_id=params[\"policy_id\"],\n requested_by=params.get(\"requested_by\", \"tester\"),\n manifest_id=params[\"manifest_id\"],\n )\n run.task_id = params[\"_task_id\"]\n params[\"repository\"].save_check_run(run)\n\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n if context is not None:\n context.logger.info(\"Compliance run completed via TaskManager plugin\")\n\n return {\n \"run_id\": run.id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n }\n\n\n# [/DEF:CleanReleaseCompliancePlugin:Class]\n\n\n# [DEF:_PluginLoaderStub:Class]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @COMPLEXITY: 2\n# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.\n# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.\nclass _PluginLoaderStub:\n # @CONTRACT: Partial PluginLoader stub. Implements: has_plugin, get_plugin. Stubs (NotImplementedError): list_plugins, get_all_plugins, get_all_plugin_configs.\n def __init__(self, plugin: CleanReleaseCompliancePlugin):\n self._plugin = plugin\n\n def has_plugin(self, plugin_id: str) -> bool:\n return plugin_id == self._plugin.id\n\n def get_plugin(self, plugin_id: str):\n if plugin_id != self._plugin.id:\n raise ValueError(\"Plugin not found\")\n return self._plugin\n\n def list_plugins(self):\n raise NotImplementedError(\n \"list_plugins not implemented in _PluginLoaderStub; add if test path requires plugin enumeration\"\n )\n\n def get_all_plugins(self):\n raise NotImplementedError(\n \"get_all_plugins not implemented in _PluginLoaderStub; add if test path requires full plugin set\"\n )\n\n def get_all_plugin_configs(self):\n raise NotImplementedError(\n \"get_all_plugin_configs not implemented in _PluginLoaderStub; add if test path requires plugin configs\"\n )\n\n\n# [/DEF:_PluginLoaderStub:Class]\n\n\n# [DEF:_make_task_manager:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Build TaskManager with mocked persistence services for isolated integration tests.\n# @POST: Returns TaskManager ready for async task execution.\ndef _make_task_manager() -> TaskManager:\n plugin_loader = _PluginLoaderStub(CleanReleaseCompliancePlugin())\n\n with (\n patch(\n \"src.core.task_manager.manager.TaskPersistenceService\"\n ) as mock_persistence,\n patch(\n \"src.core.task_manager.manager.TaskLogPersistenceService\"\n ) as mock_log_persistence,\n ):\n mock_persistence.return_value.load_tasks.return_value = []\n mock_persistence.return_value.persist_task = MagicMock()\n mock_log_persistence.return_value.add_logs = MagicMock()\n mock_log_persistence.return_value.get_logs = MagicMock(return_value=[])\n mock_log_persistence.return_value.get_log_stats = MagicMock()\n mock_log_persistence.return_value.get_sources = MagicMock(return_value=[])\n\n return TaskManager(plugin_loader)\n\n\n# [/DEF:_make_task_manager:Function]\n\n\n# [DEF:_wait_for_terminal_task:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Poll task registry until target task reaches terminal status.\n# @PRE: task_id exists in manager registry.\n# @POST: Returns task with SUCCESS or FAILED status, otherwise raises TimeoutError.\nasync def _wait_for_terminal_task(\n manager: TaskManager, task_id: str, timeout_seconds: float = 3.0\n):\n started = asyncio.get_running_loop().time()\n while True:\n task = manager.get_task(task_id)\n if task and task.status in {TaskStatus.SUCCESS, TaskStatus.FAILED}:\n return task\n if asyncio.get_running_loop().time() - started > timeout_seconds:\n raise TimeoutError(f\"Task {task_id} did not reach terminal status\")\n await asyncio.sleep(0.05)\n\n\n# [/DEF:_wait_for_terminal_task:Function]\n\n\n# [DEF:test_compliance_run_executes_as_task_manager_task:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Verify successful compliance execution is observable as TaskManager SUCCESS task.\n# @PRE: Candidate, policy and manifest are available in repository.\n# @POST: Task ends with SUCCESS; run is persisted with SUCCEEDED status and task binding.\n@pytest.mark.asyncio\nasync def test_compliance_run_executes_as_task_manager_task():\n repository, candidate_id, policy_id, manifest_id = _seed_repository(\n with_manifest=True\n )\n manager = _make_task_manager()\n\n try:\n task = await manager.create_task(\n \"clean-release-compliance\",\n {\n \"repository\": repository,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"manifest_id\": manifest_id,\n \"requested_by\": \"integration-tester\",\n },\n )\n finished = await _wait_for_terminal_task(manager, task.id)\n\n assert finished.status == TaskStatus.SUCCESS\n assert isinstance(finished.result, dict)\n\n run_id = finished.result[\"run_id\"]\n run = repository.get_check_run(run_id)\n assert run is not None\n assert run.status == RunStatus.SUCCEEDED\n assert run.task_id == task.id\n finally:\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# [/DEF:test_compliance_run_executes_as_task_manager_task:Function]\n\n\n# [DEF:test_compliance_run_missing_manifest_marks_task_failed:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Verify missing manifest startup failure is surfaced as TaskManager FAILED task.\n# @PRE: Candidate/policy exist but manifest is absent.\n# @POST: Task ends with FAILED and run history remains empty.\n@pytest.mark.asyncio\nasync def test_compliance_run_missing_manifest_marks_task_failed():\n repository, candidate_id, policy_id, manifest_id = _seed_repository(\n with_manifest=False\n )\n manager = _make_task_manager()\n\n try:\n task = await manager.create_task(\n \"clean-release-compliance\",\n {\n \"repository\": repository,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"manifest_id\": manifest_id,\n \"requested_by\": \"integration-tester\",\n },\n )\n finished = await _wait_for_terminal_task(manager, task.id)\n\n assert finished.status == TaskStatus.FAILED\n assert len(repository.check_runs) == 0\n assert any(\n \"Manifest or Policy not found\" in log.message for log in finished.logs\n )\n finally:\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# [/DEF:test_compliance_run_missing_manifest_marks_task_failed:Function]\n\n# [/DEF:TestComplianceTaskIntegration:Module]\n" + "body": "# [DEF:TestComplianceTaskIntegration:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, compliance, task-manager, integration\n# @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.\n# @LAYER: Tests\n# @INVARIANT: Compliance execution triggered as task produces terminal task status and persists run evidence.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import UTC, datetime\nfrom typing import Any\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom src.core.task_manager.manager import TaskManager\nfrom src.core.task_manager.models import TaskStatus\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.compliance_orchestrator import (\n CleanComplianceOrchestrator,\n)\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_repository:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Prepare deterministic candidate/policy/registry/manifest fixtures for task integration tests.\n# @PRE: with_manifest controls manifest availability.\n# @POST: Returns initialized repository and identifiers for compliance run startup.\ndef _seed_repository(\n *, with_manifest: bool\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-task-int-1\"\n policy_id = \"policy-task-int-1\"\n manifest_id = \"manifest-task-int-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-task-int\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-task-int-1\",\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-task-int-1\",\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-task-int\",\n artifacts_digest=\"digest-task-int\",\n source_snapshot_ref=\"git:sha-task-int\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0,\n \"prohibited_detected_count\": 0,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n\n\n# [/DEF:_seed_repository:Function]\n\n\n# [DEF:CleanReleaseCompliancePlugin:Class]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: TaskManager plugin shim that executes clean release compliance orchestration.\nclass CleanReleaseCompliancePlugin:\n @property\n def id(self) -> str:\n return \"clean-release-compliance\"\n\n @property\n def name(self) -> str:\n return \"clean_release_compliance\"\n\n def execute(self, params: dict[str, Any], context=None):\n orchestrator = CleanComplianceOrchestrator(params[\"repository\"])\n run = orchestrator.start_check_run(\n candidate_id=params[\"candidate_id\"],\n policy_id=params[\"policy_id\"],\n requested_by=params.get(\"requested_by\", \"tester\"),\n manifest_id=params[\"manifest_id\"],\n )\n run.task_id = params[\"_task_id\"]\n params[\"repository\"].save_check_run(run)\n\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n if context is not None:\n context.logger.info(\"Compliance run completed via TaskManager plugin\")\n\n return {\n \"run_id\": run.id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n }\n\n\n# [/DEF:CleanReleaseCompliancePlugin:Class]\n\n\n# [DEF:_PluginLoaderStub:Class]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.\n# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.\nclass _PluginLoaderStub:\n # @CONTRACT: Partial PluginLoader stub. Implements: has_plugin, get_plugin. Stubs (NotImplementedError): list_plugins, get_all_plugins, get_all_plugin_configs.\n def __init__(self, plugin: CleanReleaseCompliancePlugin):\n self._plugin = plugin\n\n def has_plugin(self, plugin_id: str) -> bool:\n return plugin_id == self._plugin.id\n\n def get_plugin(self, plugin_id: str):\n if plugin_id != self._plugin.id:\n raise ValueError(\"Plugin not found\")\n return self._plugin\n\n def list_plugins(self):\n raise NotImplementedError(\n \"list_plugins not implemented in _PluginLoaderStub; add if test path requires plugin enumeration\"\n )\n\n def get_all_plugins(self):\n raise NotImplementedError(\n \"get_all_plugins not implemented in _PluginLoaderStub; add if test path requires full plugin set\"\n )\n\n def get_all_plugin_configs(self):\n raise NotImplementedError(\n \"get_all_plugin_configs not implemented in _PluginLoaderStub; add if test path requires plugin configs\"\n )\n\n\n# [/DEF:_PluginLoaderStub:Class]\n\n\n# [DEF:_make_task_manager:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Build TaskManager with mocked persistence services for isolated integration tests.\n# @POST: Returns TaskManager ready for async task execution.\ndef _make_task_manager() -> TaskManager:\n plugin_loader = _PluginLoaderStub(CleanReleaseCompliancePlugin())\n\n with (\n patch(\n \"src.core.task_manager.manager.TaskPersistenceService\"\n ) as mock_persistence,\n patch(\n \"src.core.task_manager.manager.TaskLogPersistenceService\"\n ) as mock_log_persistence,\n ):\n mock_persistence.return_value.load_tasks.return_value = []\n mock_persistence.return_value.persist_task = MagicMock()\n mock_log_persistence.return_value.add_logs = MagicMock()\n mock_log_persistence.return_value.get_logs = MagicMock(return_value=[])\n mock_log_persistence.return_value.get_log_stats = MagicMock()\n mock_log_persistence.return_value.get_sources = MagicMock(return_value=[])\n\n return TaskManager(plugin_loader)\n\n\n# [/DEF:_make_task_manager:Function]\n\n\n# [DEF:_wait_for_terminal_task:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Poll task registry until target task reaches terminal status.\n# @PRE: task_id exists in manager registry.\n# @POST: Returns task with SUCCESS or FAILED status, otherwise raises TimeoutError.\nasync def _wait_for_terminal_task(\n manager: TaskManager, task_id: str, timeout_seconds: float = 3.0\n):\n started = asyncio.get_running_loop().time()\n while True:\n task = manager.get_task(task_id)\n if task and task.status in {TaskStatus.SUCCESS, TaskStatus.FAILED}:\n return task\n if asyncio.get_running_loop().time() - started > timeout_seconds:\n raise TimeoutError(f\"Task {task_id} did not reach terminal status\")\n await asyncio.sleep(0.05)\n\n\n# [/DEF:_wait_for_terminal_task:Function]\n\n\n# [DEF:test_compliance_run_executes_as_task_manager_task:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Verify successful compliance execution is observable as TaskManager SUCCESS task.\n# @PRE: Candidate, policy and manifest are available in repository.\n# @POST: Task ends with SUCCESS; run is persisted with SUCCEEDED status and task binding.\n@pytest.mark.asyncio\nasync def test_compliance_run_executes_as_task_manager_task():\n repository, candidate_id, policy_id, manifest_id = _seed_repository(\n with_manifest=True\n )\n manager = _make_task_manager()\n\n try:\n task = await manager.create_task(\n \"clean-release-compliance\",\n {\n \"repository\": repository,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"manifest_id\": manifest_id,\n \"requested_by\": \"integration-tester\",\n },\n )\n finished = await _wait_for_terminal_task(manager, task.id)\n\n assert finished.status == TaskStatus.SUCCESS\n assert isinstance(finished.result, dict)\n\n run_id = finished.result[\"run_id\"]\n run = repository.get_check_run(run_id)\n assert run is not None\n assert run.status == RunStatus.SUCCEEDED\n assert run.task_id == task.id\n finally:\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# [/DEF:test_compliance_run_executes_as_task_manager_task:Function]\n\n\n# [DEF:test_compliance_run_missing_manifest_marks_task_failed:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Verify missing manifest startup failure is surfaced as TaskManager FAILED task.\n# @PRE: Candidate/policy exist but manifest is absent.\n# @POST: Task ends with FAILED and run history remains empty.\n@pytest.mark.asyncio\nasync def test_compliance_run_missing_manifest_marks_task_failed():\n repository, candidate_id, policy_id, manifest_id = _seed_repository(\n with_manifest=False\n )\n manager = _make_task_manager()\n\n try:\n task = await manager.create_task(\n \"clean-release-compliance\",\n {\n \"repository\": repository,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"manifest_id\": manifest_id,\n \"requested_by\": \"integration-tester\",\n },\n )\n finished = await _wait_for_terminal_task(manager, task.id)\n\n assert finished.status == TaskStatus.FAILED\n assert len(repository.check_runs) == 0\n assert any(\n \"Manifest or Policy not found\" in log.message for log in finished.logs\n )\n finally:\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# [/DEF:test_compliance_run_missing_manifest_marks_task_failed:Function]\n\n# [/DEF:TestComplianceTaskIntegration:Module]\n" }, { "contract_id": "_seed_repository", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 33, - "end_line": 103, + "start_line": 32, + "end_line": 102, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -101342,14 +69555,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:_seed_repository:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Prepare deterministic candidate/policy/registry/manifest fixtures for task integration tests.\n# @PRE: with_manifest controls manifest availability.\n# @POST: Returns initialized repository and identifiers for compliance run startup.\ndef _seed_repository(\n *, with_manifest: bool\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-task-int-1\"\n policy_id = \"policy-task-int-1\"\n manifest_id = \"manifest-task-int-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-task-int\",\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-task-int-1\",\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-task-int-1\",\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-task-int\",\n artifacts_digest=\"digest-task-int\",\n source_snapshot_ref=\"git:sha-task-int\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0,\n \"prohibited_detected_count\": 0,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n\n\n# [/DEF:_seed_repository:Function]\n" + "body": "# [DEF:_seed_repository:Function]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Prepare deterministic candidate/policy/registry/manifest fixtures for task integration tests.\n# @PRE: with_manifest controls manifest availability.\n# @POST: Returns initialized repository and identifiers for compliance run startup.\ndef _seed_repository(\n *, with_manifest: bool\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-task-int-1\"\n policy_id = \"policy-task-int-1\"\n manifest_id = \"manifest-task-int-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-task-int\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-task-int-1\",\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-task-int-1\",\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-task-int\",\n artifacts_digest=\"digest-task-int\",\n source_snapshot_ref=\"git:sha-task-int\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0,\n \"prohibited_detected_count\": 0,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n\n\n# [/DEF:_seed_repository:Function]\n" }, { "contract_id": "CleanReleaseCompliancePlugin", "contract_type": "Class", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 106, - "end_line": 142, + "start_line": 105, + "end_line": 141, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -101384,18 +69597,17 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:CleanReleaseCompliancePlugin:Class]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: TaskManager plugin shim that executes clean release compliance orchestration.\nclass CleanReleaseCompliancePlugin:\n @property\n def id(self) -> str:\n return \"clean-release-compliance\"\n\n @property\n def name(self) -> str:\n return \"clean_release_compliance\"\n\n def execute(self, params: Dict[str, Any], context=None):\n orchestrator = CleanComplianceOrchestrator(params[\"repository\"])\n run = orchestrator.start_check_run(\n candidate_id=params[\"candidate_id\"],\n policy_id=params[\"policy_id\"],\n requested_by=params.get(\"requested_by\", \"tester\"),\n manifest_id=params[\"manifest_id\"],\n )\n run.task_id = params[\"_task_id\"]\n params[\"repository\"].save_check_run(run)\n\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n if context is not None:\n context.logger.info(\"Compliance run completed via TaskManager plugin\")\n\n return {\n \"run_id\": run.id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n }\n\n\n# [/DEF:CleanReleaseCompliancePlugin:Class]\n" + "body": "# [DEF:CleanReleaseCompliancePlugin:Class]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: TaskManager plugin shim that executes clean release compliance orchestration.\nclass CleanReleaseCompliancePlugin:\n @property\n def id(self) -> str:\n return \"clean-release-compliance\"\n\n @property\n def name(self) -> str:\n return \"clean_release_compliance\"\n\n def execute(self, params: dict[str, Any], context=None):\n orchestrator = CleanComplianceOrchestrator(params[\"repository\"])\n run = orchestrator.start_check_run(\n candidate_id=params[\"candidate_id\"],\n policy_id=params[\"policy_id\"],\n requested_by=params.get(\"requested_by\", \"tester\"),\n manifest_id=params[\"manifest_id\"],\n )\n run.task_id = params[\"_task_id\"]\n params[\"repository\"].save_check_run(run)\n\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n if context is not None:\n context.logger.info(\"Compliance run completed via TaskManager plugin\")\n\n return {\n \"run_id\": run.id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n }\n\n\n# [/DEF:CleanReleaseCompliancePlugin:Class]\n" }, { "contract_id": "_PluginLoaderStub", "contract_type": "Class", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 145, - "end_line": 179, + "start_line": 144, + "end_line": 177, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "COMPLEXITY": "2", "CONTRACT": "Partial PluginLoader stub. Implements: has_plugin, get_plugin. Stubs (NotImplementedError): list_plugins, get_all_plugins, get_all_plugin_configs.", "INVARIANT": "has_plugin/get_plugin only acknowledge the seeded compliance plugin id.", "PURPOSE": "Provide minimal plugin loader contract used by TaskManager in integration tests." @@ -101418,31 +69630,40 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Class' at C2", + "message": "@INVARIANT is forbidden for contract type 'Class' at C1", "detail": { - "actual_complexity": 2, + "actual_complexity": 1, + "contract_type": "Class" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Class" } }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Class' at C2", + "message": "@RELATION is forbidden for contract type 'Class' at C1", "detail": { - "actual_complexity": 2, + "actual_complexity": 1, "contract_type": "Class" } } ], "anchor_syntax": "def", - "body": "# [DEF:_PluginLoaderStub:Class]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @COMPLEXITY: 2\n# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.\n# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.\nclass _PluginLoaderStub:\n # @CONTRACT: Partial PluginLoader stub. Implements: has_plugin, get_plugin. Stubs (NotImplementedError): list_plugins, get_all_plugins, get_all_plugin_configs.\n def __init__(self, plugin: CleanReleaseCompliancePlugin):\n self._plugin = plugin\n\n def has_plugin(self, plugin_id: str) -> bool:\n return plugin_id == self._plugin.id\n\n def get_plugin(self, plugin_id: str):\n if plugin_id != self._plugin.id:\n raise ValueError(\"Plugin not found\")\n return self._plugin\n\n def list_plugins(self):\n raise NotImplementedError(\n \"list_plugins not implemented in _PluginLoaderStub; add if test path requires plugin enumeration\"\n )\n\n def get_all_plugins(self):\n raise NotImplementedError(\n \"get_all_plugins not implemented in _PluginLoaderStub; add if test path requires full plugin set\"\n )\n\n def get_all_plugin_configs(self):\n raise NotImplementedError(\n \"get_all_plugin_configs not implemented in _PluginLoaderStub; add if test path requires plugin configs\"\n )\n\n\n# [/DEF:_PluginLoaderStub:Class]\n" + "body": "# [DEF:_PluginLoaderStub:Class]\n# @RELATION: BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.\n# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.\nclass _PluginLoaderStub:\n # @CONTRACT: Partial PluginLoader stub. Implements: has_plugin, get_plugin. Stubs (NotImplementedError): list_plugins, get_all_plugins, get_all_plugin_configs.\n def __init__(self, plugin: CleanReleaseCompliancePlugin):\n self._plugin = plugin\n\n def has_plugin(self, plugin_id: str) -> bool:\n return plugin_id == self._plugin.id\n\n def get_plugin(self, plugin_id: str):\n if plugin_id != self._plugin.id:\n raise ValueError(\"Plugin not found\")\n return self._plugin\n\n def list_plugins(self):\n raise NotImplementedError(\n \"list_plugins not implemented in _PluginLoaderStub; add if test path requires plugin enumeration\"\n )\n\n def get_all_plugins(self):\n raise NotImplementedError(\n \"get_all_plugins not implemented in _PluginLoaderStub; add if test path requires full plugin set\"\n )\n\n def get_all_plugin_configs(self):\n raise NotImplementedError(\n \"get_all_plugin_configs not implemented in _PluginLoaderStub; add if test path requires plugin configs\"\n )\n\n\n# [/DEF:_PluginLoaderStub:Class]\n" }, { "contract_id": "_make_task_manager", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 182, - "end_line": 207, + "start_line": 180, + "end_line": 205, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -101493,8 +69714,8 @@ "contract_id": "_wait_for_terminal_task", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 210, - "end_line": 228, + "start_line": 208, + "end_line": 226, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -101555,8 +69776,8 @@ "contract_id": "test_compliance_run_executes_as_task_manager_task", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 231, - "end_line": 269, + "start_line": 229, + "end_line": 267, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -101608,8 +69829,8 @@ "contract_id": "test_compliance_run_missing_manifest_marks_task_failed", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 272, - "end_line": 307, + "start_line": 270, + "end_line": 305, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -101662,11 +69883,10 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_demo_mode_isolation.py", "start_line": 1, - "end_line": 90, - "tier": "TIER_2", - "complexity": 3, + "end_line": 89, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Tests", "PURPOSE": "Verify demo and real mode namespace isolation contracts before TUI integration.", "SEMANTICS": [ @@ -101685,16 +69905,35 @@ "target_ref": "backend.src.services.clean_release.demo_data_service" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", - "body": "# [DEF:TestDemoModeIsolation:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository\n# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.\n# @LAYER: Tests\n# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.demo_data_service\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nfrom src.models.clean_release import ReleaseCandidate\nfrom src.services.clean_release.demo_data_service import (\n build_namespaced_id,\n create_isolated_repository,\n resolve_namespace,\n)\n\n\n# [DEF:test_resolve_namespace_separates_demo_and_real:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure namespace resolver returns deterministic and distinct namespaces.\n# @PRE: Mode names are provided as user/runtime strings.\n# @POST: Demo and real namespaces are different and stable.\ndef test_resolve_namespace_separates_demo_and_real() -> None:\n demo = resolve_namespace(\"demo\")\n real = resolve_namespace(\"real\")\n\n assert demo == \"clean-release:demo\"\n assert real == \"clean-release:real\"\n assert demo != real\n# [/DEF:test_resolve_namespace_separates_demo_and_real:Function]\n\n\n# [DEF:test_build_namespaced_id_prevents_cross_mode_collisions:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure ID generation prevents demo/real collisions for identical logical IDs.\n# @PRE: Same logical candidate id is used in two different namespaces.\n# @POST: Produced physical IDs differ by namespace prefix.\ndef test_build_namespaced_id_prevents_cross_mode_collisions() -> None:\n logical_id = \"2026.03.09-rc1\"\n demo_id = build_namespaced_id(resolve_namespace(\"demo\"), logical_id)\n real_id = build_namespaced_id(resolve_namespace(\"real\"), logical_id)\n\n assert demo_id != real_id\n assert demo_id.startswith(\"clean-release:demo::\")\n assert real_id.startswith(\"clean-release:real::\")\n# [/DEF:test_build_namespaced_id_prevents_cross_mode_collisions:Function]\n\n\n# [DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.\n# @PRE: Two repositories are created for distinct modes.\n# @POST: Candidate mutations in one mode are not visible in the other mode.\ndef test_create_isolated_repository_keeps_mode_data_separate() -> None:\n demo_repo = create_isolated_repository(\"demo\")\n real_repo = create_isolated_repository(\"real\")\n\n demo_candidate_id = build_namespaced_id(resolve_namespace(\"demo\"), \"candidate-1\")\n real_candidate_id = build_namespaced_id(resolve_namespace(\"real\"), \"candidate-1\")\n\n demo_repo.save_candidate(\n ReleaseCandidate(\n id=demo_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-demo\",\n created_by=\"demo-operator\",\n created_at=datetime.now(timezone.utc),\n status=\"DRAFT\",\n )\n )\n real_repo.save_candidate(\n ReleaseCandidate(\n id=real_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-real\",\n created_by=\"real-operator\",\n created_at=datetime.now(timezone.utc),\n status=\"DRAFT\",\n )\n )\n\n assert demo_repo.get_candidate(demo_candidate_id) is not None\n assert demo_repo.get_candidate(real_candidate_id) is None\n assert real_repo.get_candidate(real_candidate_id) is not None\n assert real_repo.get_candidate(demo_candidate_id) is None\n# [/DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n\n# [/DEF:TestDemoModeIsolation:Module]\n" + "body": "# [DEF:TestDemoModeIsolation:Module]\n# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository\n# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.\n# @LAYER: Tests\n# @RELATION: DEPENDS_ON -> backend.src.services.clean_release.demo_data_service\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nfrom src.models.clean_release import ReleaseCandidate\nfrom src.services.clean_release.demo_data_service import (\n build_namespaced_id,\n create_isolated_repository,\n resolve_namespace,\n)\n\n\n# [DEF:test_resolve_namespace_separates_demo_and_real:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure namespace resolver returns deterministic and distinct namespaces.\n# @PRE: Mode names are provided as user/runtime strings.\n# @POST: Demo and real namespaces are different and stable.\ndef test_resolve_namespace_separates_demo_and_real() -> None:\n demo = resolve_namespace(\"demo\")\n real = resolve_namespace(\"real\")\n\n assert demo == \"clean-release:demo\"\n assert real == \"clean-release:real\"\n assert demo != real\n# [/DEF:test_resolve_namespace_separates_demo_and_real:Function]\n\n\n# [DEF:test_build_namespaced_id_prevents_cross_mode_collisions:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Ensure ID generation prevents demo/real collisions for identical logical IDs.\n# @PRE: Same logical candidate id is used in two different namespaces.\n# @POST: Produced physical IDs differ by namespace prefix.\ndef test_build_namespaced_id_prevents_cross_mode_collisions() -> None:\n logical_id = \"2026.03.09-rc1\"\n demo_id = build_namespaced_id(resolve_namespace(\"demo\"), logical_id)\n real_id = build_namespaced_id(resolve_namespace(\"real\"), logical_id)\n\n assert demo_id != real_id\n assert demo_id.startswith(\"clean-release:demo::\")\n assert real_id.startswith(\"clean-release:real::\")\n# [/DEF:test_build_namespaced_id_prevents_cross_mode_collisions:Function]\n\n\n# [DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.\n# @PRE: Two repositories are created for distinct modes.\n# @POST: Candidate mutations in one mode are not visible in the other mode.\ndef test_create_isolated_repository_keeps_mode_data_separate() -> None:\n demo_repo = create_isolated_repository(\"demo\")\n real_repo = create_isolated_repository(\"real\")\n\n demo_candidate_id = build_namespaced_id(resolve_namespace(\"demo\"), \"candidate-1\")\n real_candidate_id = build_namespaced_id(resolve_namespace(\"real\"), \"candidate-1\")\n\n demo_repo.save_candidate(\n ReleaseCandidate(\n id=demo_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-demo\",\n created_by=\"demo-operator\",\n created_at=datetime.now(UTC),\n status=\"DRAFT\",\n )\n )\n real_repo.save_candidate(\n ReleaseCandidate(\n id=real_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-real\",\n created_by=\"real-operator\",\n created_at=datetime.now(UTC),\n status=\"DRAFT\",\n )\n )\n\n assert demo_repo.get_candidate(demo_candidate_id) is not None\n assert demo_repo.get_candidate(real_candidate_id) is None\n assert real_repo.get_candidate(real_candidate_id) is not None\n assert real_repo.get_candidate(demo_candidate_id) is None\n# [/DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n\n# [/DEF:TestDemoModeIsolation:Module]\n" }, { "contract_id": "test_resolve_namespace_separates_demo_and_real", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_demo_mode_isolation.py", - "start_line": 20, - "end_line": 32, + "start_line": 19, + "end_line": 31, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -101746,8 +69985,8 @@ "contract_id": "test_build_namespaced_id_prevents_cross_mode_collisions", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_demo_mode_isolation.py", - "start_line": 35, - "end_line": 48, + "start_line": 34, + "end_line": 47, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -101799,8 +70038,8 @@ "contract_id": "test_create_isolated_repository_keeps_mode_data_separate", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_demo_mode_isolation.py", - "start_line": 51, - "end_line": 88, + "start_line": 50, + "end_line": 87, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -101846,18 +70085,17 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.\n# @PRE: Two repositories are created for distinct modes.\n# @POST: Candidate mutations in one mode are not visible in the other mode.\ndef test_create_isolated_repository_keeps_mode_data_separate() -> None:\n demo_repo = create_isolated_repository(\"demo\")\n real_repo = create_isolated_repository(\"real\")\n\n demo_candidate_id = build_namespaced_id(resolve_namespace(\"demo\"), \"candidate-1\")\n real_candidate_id = build_namespaced_id(resolve_namespace(\"real\"), \"candidate-1\")\n\n demo_repo.save_candidate(\n ReleaseCandidate(\n id=demo_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-demo\",\n created_by=\"demo-operator\",\n created_at=datetime.now(timezone.utc),\n status=\"DRAFT\",\n )\n )\n real_repo.save_candidate(\n ReleaseCandidate(\n id=real_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-real\",\n created_by=\"real-operator\",\n created_at=datetime.now(timezone.utc),\n status=\"DRAFT\",\n )\n )\n\n assert demo_repo.get_candidate(demo_candidate_id) is not None\n assert demo_repo.get_candidate(real_candidate_id) is None\n assert real_repo.get_candidate(real_candidate_id) is not None\n assert real_repo.get_candidate(demo_candidate_id) is None\n# [/DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n" + "body": "# [DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n# @RELATION: BINDS_TO -> TestDemoModeIsolation\n# @PURPOSE: Verify demo and real repositories do not leak state across mode boundaries.\n# @PRE: Two repositories are created for distinct modes.\n# @POST: Candidate mutations in one mode are not visible in the other mode.\ndef test_create_isolated_repository_keeps_mode_data_separate() -> None:\n demo_repo = create_isolated_repository(\"demo\")\n real_repo = create_isolated_repository(\"real\")\n\n demo_candidate_id = build_namespaced_id(resolve_namespace(\"demo\"), \"candidate-1\")\n real_candidate_id = build_namespaced_id(resolve_namespace(\"real\"), \"candidate-1\")\n\n demo_repo.save_candidate(\n ReleaseCandidate(\n id=demo_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-demo\",\n created_by=\"demo-operator\",\n created_at=datetime.now(UTC),\n status=\"DRAFT\",\n )\n )\n real_repo.save_candidate(\n ReleaseCandidate(\n id=real_candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-real\",\n created_by=\"real-operator\",\n created_at=datetime.now(UTC),\n status=\"DRAFT\",\n )\n )\n\n assert demo_repo.get_candidate(demo_candidate_id) is not None\n assert demo_repo.get_candidate(real_candidate_id) is None\n assert real_repo.get_candidate(real_candidate_id) is not None\n assert real_repo.get_candidate(demo_candidate_id) is None\n# [/DEF:test_create_isolated_repository_keeps_mode_data_separate:Function]\n" }, { "contract_id": "TestPolicyResolutionService", "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", "start_line": 1, - "end_line": 123, - "tier": "TIER_3", - "complexity": 5, + "end_line": 121, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "INVARIANT": "Resolution uses only ConfigManager active IDs and rejects runtime override attempts.", "LAYER": "Tests", "PURPOSE": "Verify trusted policy snapshot resolution contract and error guards.", @@ -101890,73 +70128,45 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } } ], "anchor_syntax": "def", - "body": "# [DEF:TestPolicyResolutionService:Module]\n# @COMPLEXITY: 5\n# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts\n# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.\n# @LAYER: Tests\n# @RELATION: DEPENDS_ON -> [policy_resolution_service]\n# @RELATION: DEPENDS_ON -> [repository]\n# @RELATION: DEPENDS_ON -> [clean_release_exceptions]\n# @INVARIANT: Resolution uses only ConfigManager active IDs and rejects runtime override attempts.\n\nfrom __future__ import annotations\n\nfrom types import SimpleNamespace\n\nimport pytest\n\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom src.services.clean_release.exceptions import PolicyResolutionError\nfrom src.services.clean_release.policy_resolution_service import (\n resolve_trusted_policy_snapshots,\n)\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_config_manager:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @COMPLEXITY: 1\n# @PURPOSE: Build deterministic ConfigManager-like stub for tests.\n# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.\n# @PRE: policy_id and registry_id may be None or non-empty strings.\n# @POST: Returns object exposing get_config().settings.clean_release active IDs.\ndef _config_manager(policy_id, registry_id):\n clean_release = SimpleNamespace(\n active_policy_id=policy_id, active_registry_id=registry_id\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n\n\n# [/DEF:_config_manager:Function]\n\n\n# [DEF:test_resolve_trusted_policy_snapshots_missing_profile:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @PURPOSE: Ensure resolution fails when trusted profile is not configured.\n# @PRE: active_policy_id is None.\n# @POST: Raises PolicyResolutionError with missing trusted profile reason.\ndef test_resolve_trusted_policy_snapshots_missing_profile():\n repository = CleanReleaseRepository()\n config_manager = _config_manager(policy_id=None, registry_id=\"registry-1\")\n\n with pytest.raises(PolicyResolutionError, match=\"missing trusted profile\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n )\n\n\n# [/DEF:test_resolve_trusted_policy_snapshots_missing_profile:Function]\n\n\n# [DEF:test_resolve_trusted_policy_snapshots_missing_registry:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @PURPOSE: Ensure resolution fails when trusted registry is not configured.\n# @PRE: active_registry_id is None and active_policy_id is set.\n# @POST: Raises PolicyResolutionError with missing trusted registry reason.\ndef test_resolve_trusted_policy_snapshots_missing_registry():\n repository = CleanReleaseRepository()\n config_manager = _config_manager(policy_id=\"policy-1\", registry_id=None)\n\n with pytest.raises(PolicyResolutionError, match=\"missing trusted registry\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n )\n\n\n# [/DEF:test_resolve_trusted_policy_snapshots_missing_registry:Function]\n\n\n# [DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @PURPOSE: Ensure runtime override attempt is rejected even if snapshots exist.\n# @PRE: valid trusted snapshots exist in repository and override is provided.\n# @POST: Raises PolicyResolutionError with override forbidden reason.\ndef test_resolve_trusted_policy_snapshots_rejects_override_attempt():\n repository = CleanReleaseRepository()\n repository.save_policy(\n CleanPolicySnapshot(\n id=\"policy-1\",\n policy_id=\"baseline\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-1\",\n immutable=True,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-1\",\n registry_id=\"trusted\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n\n config_manager = _config_manager(policy_id=\"policy-1\", registry_id=\"registry-1\")\n\n with pytest.raises(PolicyResolutionError, match=\"override attempt is forbidden\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n policy_id_override=\"policy-override\",\n )\n\n\n# [/DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]\n\n# [/DEF:TestPolicyResolutionService:Module]\n" + "body": "# [DEF:TestPolicyResolutionService:Module]\n# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts\n# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.\n# @LAYER: Tests\n# @RELATION: DEPENDS_ON -> [policy_resolution_service]\n# @RELATION: DEPENDS_ON -> [repository]\n# @RELATION: DEPENDS_ON -> [clean_release_exceptions]\n# @INVARIANT: Resolution uses only ConfigManager active IDs and rejects runtime override attempts.\n\nfrom __future__ import annotations\n\nfrom types import SimpleNamespace\n\nimport pytest\n\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom src.services.clean_release.exceptions import PolicyResolutionError\nfrom src.services.clean_release.policy_resolution_service import (\n resolve_trusted_policy_snapshots,\n)\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_config_manager:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @PURPOSE: Build deterministic ConfigManager-like stub for tests.\n# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.\n# @PRE: policy_id and registry_id may be None or non-empty strings.\n# @POST: Returns object exposing get_config().settings.clean_release active IDs.\ndef _config_manager(policy_id, registry_id):\n clean_release = SimpleNamespace(\n active_policy_id=policy_id, active_registry_id=registry_id\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n\n\n# [/DEF:_config_manager:Function]\n\n\n# [DEF:test_resolve_trusted_policy_snapshots_missing_profile:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @PURPOSE: Ensure resolution fails when trusted profile is not configured.\n# @PRE: active_policy_id is None.\n# @POST: Raises PolicyResolutionError with missing trusted profile reason.\ndef test_resolve_trusted_policy_snapshots_missing_profile():\n repository = CleanReleaseRepository()\n config_manager = _config_manager(policy_id=None, registry_id=\"registry-1\")\n\n with pytest.raises(PolicyResolutionError, match=\"missing trusted profile\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n )\n\n\n# [/DEF:test_resolve_trusted_policy_snapshots_missing_profile:Function]\n\n\n# [DEF:test_resolve_trusted_policy_snapshots_missing_registry:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @PURPOSE: Ensure resolution fails when trusted registry is not configured.\n# @PRE: active_registry_id is None and active_policy_id is set.\n# @POST: Raises PolicyResolutionError with missing trusted registry reason.\ndef test_resolve_trusted_policy_snapshots_missing_registry():\n repository = CleanReleaseRepository()\n config_manager = _config_manager(policy_id=\"policy-1\", registry_id=None)\n\n with pytest.raises(PolicyResolutionError, match=\"missing trusted registry\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n )\n\n\n# [/DEF:test_resolve_trusted_policy_snapshots_missing_registry:Function]\n\n\n# [DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @PURPOSE: Ensure runtime override attempt is rejected even if snapshots exist.\n# @PRE: valid trusted snapshots exist in repository and override is provided.\n# @POST: Raises PolicyResolutionError with override forbidden reason.\ndef test_resolve_trusted_policy_snapshots_rejects_override_attempt():\n repository = CleanReleaseRepository()\n repository.save_policy(\n CleanPolicySnapshot(\n id=\"policy-1\",\n policy_id=\"baseline\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-1\",\n immutable=True,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-1\",\n registry_id=\"trusted\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n\n config_manager = _config_manager(policy_id=\"policy-1\", registry_id=\"registry-1\")\n\n with pytest.raises(PolicyResolutionError, match=\"override attempt is forbidden\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n policy_id_override=\"policy-override\",\n )\n\n\n# [/DEF:test_resolve_trusted_policy_snapshots_rejects_override_attempt:Function]\n\n# [/DEF:TestPolicyResolutionService:Module]\n" }, { "contract_id": "_config_manager", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", - "start_line": 25, - "end_line": 41, + "start_line": 24, + "end_line": 39, "tier": "TIER_1", "complexity": 1, "metadata": { - "COMPLEXITY": "1", "INVARIANT": "Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.", "POST": "Returns object exposing get_config().settings.clean_release active IDs.", "PRE": "policy_id and registry_id may be None or non-empty strings.", @@ -102018,14 +70228,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:_config_manager:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @COMPLEXITY: 1\n# @PURPOSE: Build deterministic ConfigManager-like stub for tests.\n# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.\n# @PRE: policy_id and registry_id may be None or non-empty strings.\n# @POST: Returns object exposing get_config().settings.clean_release active IDs.\ndef _config_manager(policy_id, registry_id):\n clean_release = SimpleNamespace(\n active_policy_id=policy_id, active_registry_id=registry_id\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n\n\n# [/DEF:_config_manager:Function]\n" + "body": "# [DEF:_config_manager:Function]\n# @RELATION: BINDS_TO -> [TestPolicyResolutionService]\n# @PURPOSE: Build deterministic ConfigManager-like stub for tests.\n# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.\n# @PRE: policy_id and registry_id may be None or non-empty strings.\n# @POST: Returns object exposing get_config().settings.clean_release active IDs.\ndef _config_manager(policy_id, registry_id):\n clean_release = SimpleNamespace(\n active_policy_id=policy_id, active_registry_id=registry_id\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n\n\n# [/DEF:_config_manager:Function]\n" }, { "contract_id": "test_resolve_trusted_policy_snapshots_missing_profile", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", - "start_line": 44, - "end_line": 60, + "start_line": 42, + "end_line": 58, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102077,8 +70287,8 @@ "contract_id": "test_resolve_trusted_policy_snapshots_missing_registry", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", - "start_line": 63, - "end_line": 79, + "start_line": 61, + "end_line": 77, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102130,8 +70340,8 @@ "contract_id": "test_resolve_trusted_policy_snapshots_rejects_override_attempt", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", - "start_line": 82, - "end_line": 121, + "start_line": 80, + "end_line": 119, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102184,11 +70394,10 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_publication_service.py", "start_line": 1, - "end_line": 150, - "tier": "TIER_3", - "complexity": 5, + "end_line": 149, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "INVARIANT": "Publish requires approval; revoke requires existing publication; republish after revoke is allowed as a new record.", "LAYER": "Tests", "PURPOSE": "Define publication gate contracts over approved candidates and immutable publication records.", @@ -102210,56 +70419,29 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, @@ -102283,14 +70465,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:TestPublicationService:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 5\n# @SEMANTICS: tests, clean-release, publication, revoke, gate\n# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.\n# @LAYER: Tests\n# @INVARIANT: Publish requires approval; revoke requires existing publication; republish after revoke is allowed as a new record.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timezone\n\nimport pytest\n\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision, PublicationStatus\nfrom src.services.clean_release.exceptions import PublicationGateError\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_candidate_with_passed_report:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Seed candidate/report fixtures for publication gate scenarios.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and PASSED report.\ndef _seed_candidate_with_passed_report(\n *,\n candidate_id: str = \"cand-publish-1\",\n report_id: str = \"CCR-publish-1\",\n candidate_status: CandidateStatus = CandidateStatus.CHECK_PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-publish-1\",\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n status=candidate_status.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-publish-1\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"seed\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# [/DEF:_seed_candidate_with_passed_report:Function]\n\n\n# [DEF:test_publish_without_approval_rejected:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure publish action is blocked until candidate is approved.\n# @PRE: Candidate has PASSED report but status is not APPROVED.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_publish_without_approval_rejected():\n from src.services.clean_release.publication_service import publish_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_passed_report(\n candidate_status=CandidateStatus.CHECK_PASSED,\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-1\",\n )\n# [/DEF:test_publish_without_approval_rejected:Function]\n\n\n# [DEF:test_revoke_unknown_publication_rejected:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure revocation is rejected for unknown publication id.\n# @PRE: Repository has no matching publication record.\n# @POST: revoke_publication raises PublicationGateError.\ndef test_revoke_unknown_publication_rejected():\n from src.services.clean_release.publication_service import revoke_publication\n\n repository, _, _ = _seed_candidate_with_passed_report()\n\n with pytest.raises(PublicationGateError, match=\"not found\"):\n revoke_publication(\n repository=repository,\n publication_id=\"missing-publication\",\n revoked_by=\"publisher\",\n comment=\"unknown publication id\",\n )\n# [/DEF:test_revoke_unknown_publication_rejected:Function]\n\n\n# [DEF:test_republish_after_revoke_creates_new_active_record:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure republish after revoke is allowed and creates a new ACTIVE record.\n# @PRE: Candidate is APPROVED and first publication has been revoked.\n# @POST: New publish call returns distinct publication id with ACTIVE status.\ndef test_republish_after_revoke_creates_new_active_record():\n from src.services.clean_release.approval_service import approve_candidate\n from src.services.clean_release.publication_service import publish_candidate, revoke_publication\n\n repository, candidate_id, report_id = _seed_candidate_with_passed_report(\n candidate_status=CandidateStatus.CHECK_PASSED,\n )\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"approval before publication\",\n )\n\n first = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"release-1\",\n )\n revoked = revoke_publication(\n repository=repository,\n publication_id=first.id,\n revoked_by=\"publisher\",\n comment=\"rollback\",\n )\n second = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"release-2\",\n )\n\n assert first.id != second.id\n assert revoked.status == PublicationStatus.REVOKED.value\n assert second.status == PublicationStatus.ACTIVE.value\n# [/DEF:test_republish_after_revoke_creates_new_active_record:Function]\n\n# [/DEF:TestPublicationService:Module]\n" + "body": "# [DEF:TestPublicationService:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, publication, revoke, gate\n# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.\n# @LAYER: Tests\n# @INVARIANT: Publish requires approval; revoke requires existing publication; republish after revoke is allowed as a new record.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nimport pytest\n\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision, PublicationStatus\nfrom src.services.clean_release.exceptions import PublicationGateError\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# [DEF:_seed_candidate_with_passed_report:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Seed candidate/report fixtures for publication gate scenarios.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and PASSED report.\ndef _seed_candidate_with_passed_report(\n *,\n candidate_id: str = \"cand-publish-1\",\n report_id: str = \"CCR-publish-1\",\n candidate_status: CandidateStatus = CandidateStatus.CHECK_PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-publish-1\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=candidate_status.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-publish-1\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"seed\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# [/DEF:_seed_candidate_with_passed_report:Function]\n\n\n# [DEF:test_publish_without_approval_rejected:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure publish action is blocked until candidate is approved.\n# @PRE: Candidate has PASSED report but status is not APPROVED.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_publish_without_approval_rejected():\n from src.services.clean_release.publication_service import publish_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_passed_report(\n candidate_status=CandidateStatus.CHECK_PASSED,\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-1\",\n )\n# [/DEF:test_publish_without_approval_rejected:Function]\n\n\n# [DEF:test_revoke_unknown_publication_rejected:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure revocation is rejected for unknown publication id.\n# @PRE: Repository has no matching publication record.\n# @POST: revoke_publication raises PublicationGateError.\ndef test_revoke_unknown_publication_rejected():\n from src.services.clean_release.publication_service import revoke_publication\n\n repository, _, _ = _seed_candidate_with_passed_report()\n\n with pytest.raises(PublicationGateError, match=\"not found\"):\n revoke_publication(\n repository=repository,\n publication_id=\"missing-publication\",\n revoked_by=\"publisher\",\n comment=\"unknown publication id\",\n )\n# [/DEF:test_revoke_unknown_publication_rejected:Function]\n\n\n# [DEF:test_republish_after_revoke_creates_new_active_record:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure republish after revoke is allowed and creates a new ACTIVE record.\n# @PRE: Candidate is APPROVED and first publication has been revoked.\n# @POST: New publish call returns distinct publication id with ACTIVE status.\ndef test_republish_after_revoke_creates_new_active_record():\n from src.services.clean_release.approval_service import approve_candidate\n from src.services.clean_release.publication_service import publish_candidate, revoke_publication\n\n repository, candidate_id, report_id = _seed_candidate_with_passed_report(\n candidate_status=CandidateStatus.CHECK_PASSED,\n )\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"approval before publication\",\n )\n\n first = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"release-1\",\n )\n revoked = revoke_publication(\n repository=repository,\n publication_id=first.id,\n revoked_by=\"publisher\",\n comment=\"rollback\",\n )\n second = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"release-2\",\n )\n\n assert first.id != second.id\n assert revoked.status == PublicationStatus.REVOKED.value\n assert second.status == PublicationStatus.ACTIVE.value\n# [/DEF:test_republish_after_revoke_creates_new_active_record:Function]\n\n# [/DEF:TestPublicationService:Module]\n" }, { "contract_id": "_seed_candidate_with_passed_report", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_publication_service.py", - "start_line": 21, - "end_line": 55, + "start_line": 20, + "end_line": 54, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -102345,14 +70527,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:_seed_candidate_with_passed_report:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Seed candidate/report fixtures for publication gate scenarios.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and PASSED report.\ndef _seed_candidate_with_passed_report(\n *,\n candidate_id: str = \"cand-publish-1\",\n report_id: str = \"CCR-publish-1\",\n candidate_status: CandidateStatus = CandidateStatus.CHECK_PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-publish-1\",\n created_by=\"tester\",\n created_at=datetime.now(timezone.utc),\n status=candidate_status.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-publish-1\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"seed\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(timezone.utc),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# [/DEF:_seed_candidate_with_passed_report:Function]\n" + "body": "# [DEF:_seed_candidate_with_passed_report:Function]\n# @RELATION: BINDS_TO -> TestPublicationService\n# @PURPOSE: Seed candidate/report fixtures for publication gate scenarios.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and PASSED report.\ndef _seed_candidate_with_passed_report(\n *,\n candidate_id: str = \"cand-publish-1\",\n report_id: str = \"CCR-publish-1\",\n candidate_status: CandidateStatus = CandidateStatus.CHECK_PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-publish-1\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=candidate_status.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-publish-1\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"seed\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# [/DEF:_seed_candidate_with_passed_report:Function]\n" }, { "contract_id": "test_publish_without_approval_rejected", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_publication_service.py", - "start_line": 58, - "end_line": 79, + "start_line": 57, + "end_line": 78, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102404,8 +70586,8 @@ "contract_id": "test_revoke_unknown_publication_rejected", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_publication_service.py", - "start_line": 82, - "end_line": 99, + "start_line": 81, + "end_line": 98, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102457,8 +70639,8 @@ "contract_id": "test_republish_after_revoke_creates_new_active_record", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_publication_service.py", - "start_line": 102, - "end_line": 148, + "start_line": 101, + "end_line": 147, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102511,11 +70693,10 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", "start_line": 1, - "end_line": 143, - "tier": "TIER_3", - "complexity": 5, + "end_line": 140, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "INVARIANT": "Built reports are immutable snapshots; audit hooks produce append-only event traces.", "LAYER": "Tests", "PURPOSE": "Validate report snapshot immutability expectations and append-only audit hook behavior for US2.", @@ -102538,56 +70719,29 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "PRE", - "message": "@PRE is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Module" } }, @@ -102611,14 +70765,76 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:TestReportAuditImmutability:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 5\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 datetime, timezone\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom src.models.clean_release import (\n ComplianceReport,\n ComplianceRun,\n ComplianceViolation,\n)\nfrom src.services.clean_release.audit_service import (\n audit_check_run,\n audit_preparation,\n audit_report,\n audit_violation,\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(timezone.utc),\n started_at=datetime.now(timezone.utc),\n finished_at=datetime.now(timezone.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(timezone.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(timezone.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.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" + }, + { + "contract_id": "_terminal_run", + "contract_type": "Function", + "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", + "start_line": 29, + "end_line": 53, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Returns a terminal ComplianceRun suitable for report generation.", + "PRE": "final_status is a valid ComplianceDecision value.", + "PURPOSE": "Build deterministic terminal run fixture for report snapshot tests." + }, + "relations": [ + { + "source_id": "_terminal_run", + "relation_type": "BINDS_TO", + "target_id": "TestReportAuditImmutability", + "target_ref": "TestReportAuditImmutability" + } + ], + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "def", + "body": "# [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" }, { "contract_id": "test_report_builder_sets_immutable_snapshot_flag", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", - "start_line": 59, - "end_line": 77, + "start_line": 56, + "end_line": 74, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102670,8 +70886,8 @@ "contract_id": "test_repository_rejects_report_overwrite_for_same_report_id", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", - "start_line": 80, - "end_line": 120, + "start_line": 77, + "end_line": 117, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102717,14 +70933,14 @@ } ], "anchor_syntax": "def", - "body": "# [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(timezone.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(timezone.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" + "body": "# [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" }, { "contract_id": "test_audit_hooks_emit_append_only_event_stream", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", - "start_line": 123, - "end_line": 141, + "start_line": 120, + "end_line": 138, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -102777,11 +70993,10 @@ "contract_type": "Module", "file_path": "backend/tests/services/dataset_review/test_superset_matrix.py", "start_line": 1, - "end_line": 146, - "tier": "TIER_2", - "complexity": 3, + "end_line": 139, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Tests", "PURPOSE": "Verifies Superset preview and SQL Lab endpoint fallback strategy used by dataset-review orchestration.", "SEMANTICS": [ @@ -102808,6 +71023,24 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -102846,18 +71079,17 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:SupersetCompatibilityMatrixTests:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: dataset_review, superset, compatibility_matrix, preview, sql_lab, tests\n# @PURPOSE: Verifies Superset preview and SQL Lab endpoint fallback strategy used by dataset-review orchestration.\n# @LAYER: Tests\n# @RELATION: [DEPENDS_ON] ->[backend.src.core.superset_client.SupersetClient]\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\n\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock\n\n# Import models to ensure proper SQLAlchemy registration\nfrom src.models.auth import User\nfrom src.models.dataset_review import CompiledPreview\n\nfrom src.core.utils.superset_compilation_adapter import (\n PreviewCompilationPayload,\n SqlLabLaunchPayload,\n SupersetCompilationAdapter,\n)\n\n\n# [DEF:make_adapter:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build an adapter with a mock Superset client and deterministic environment for compatibility tests.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef make_adapter():\n environment = SimpleNamespace(\n id=\"env-1\",\n name=\"Test Env\",\n url=\"http://superset.example\",\n username=\"user\",\n password=\"pass\",\n verify_ssl=True,\n timeout=30,\n )\n client = MagicMock()\n client.network = MagicMock()\n return SupersetCompilationAdapter(environment=environment, client=client), client\n\n\n# [/DEF:make_adapter:Function]\n\n\n# [DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Confirms preview compilation uses a supported client method first when the capability exists.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_preview_prefers_supported_client_method_before_network_fallback():\n adapter, client = make_adapter()\n client.compile_preview = MagicMock(return_value={\"compiled_sql\": \"SELECT 1\"})\n payload = PreviewCompilationPayload(\n session_id=\"sess-1\",\n dataset_id=42,\n preview_fingerprint=\"fp-1\",\n template_params={\"country\": \"RU\"},\n effective_filters=[{\"name\": \"country\", \"value\": \"RU\"}],\n )\n\n preview = adapter.compile_preview(payload)\n\n assert preview.preview_status.value == \"ready\"\n assert preview.compiled_sql == \"SELECT 1\"\n client.compile_preview.assert_called_once()\n client.network.request.assert_not_called()\n\n\n# [/DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]\n\n\n# [DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql():\n adapter, client = make_adapter()\n payload = PreviewCompilationPayload(\n session_id=\"sess-2\",\n dataset_id=77,\n preview_fingerprint=\"fp-2\",\n template_params={\"region\": \"emea\"},\n effective_filters=[],\n )\n\n client.network.request.side_effect = [\n RuntimeError(\"preview endpoint unavailable\"),\n {\"result\": {\"sql\": \"SELECT * FROM dataset_77\"}},\n ]\n\n preview = adapter.compile_preview(payload)\n\n assert preview.preview_status.value == \"ready\"\n assert preview.compiled_sql == \"SELECT * FROM dataset_77\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/dataset/77/preview\"\n assert second_call[\"endpoint\"] == \"/dataset/77/sql\"\n\n\n# [/DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]\n\n\n# [DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_sql_lab_launch_falls_back_to_legacy_execute_endpoint():\n adapter, client = make_adapter()\n client.get_dataset.return_value = {\n \"result\": {\n \"id\": 55,\n \"schema\": \"public\",\n \"database\": {\"id\": 9},\n }\n }\n client.network.request.side_effect = [\n RuntimeError(\"sqllab execute unavailable\"),\n {\"result\": {\"id\": \"query-123\"}},\n ]\n payload = SqlLabLaunchPayload(\n session_id=\"sess-3\",\n dataset_id=55,\n preview_id=\"preview-9\",\n compiled_sql=\"SELECT * FROM sales\",\n template_params={\"limit\": 10},\n )\n\n sql_lab_ref = adapter.create_sql_lab_session(payload)\n\n assert sql_lab_ref == \"query-123\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/sqllab/execute/\"\n assert second_call[\"endpoint\"] == \"/sql_lab/execute/\"\n\n\n# [/DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]\n\n\n# [/DEF:SupersetCompatibilityMatrixTests:Module]\n" + "body": "# [DEF:SupersetCompatibilityMatrixTests:Module]\n# @SEMANTICS: dataset_review, superset, compatibility_matrix, preview, sql_lab, tests\n# @PURPOSE: Verifies Superset preview and SQL Lab endpoint fallback strategy used by dataset-review orchestration.\n# @LAYER: Tests\n# @RELATION: [DEPENDS_ON] ->[backend.src.core.superset_client.SupersetClient]\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\n\nfrom types import SimpleNamespace\nfrom unittest.mock import MagicMock\n\nfrom src.core.utils.superset_compilation_adapter import (\n PreviewCompilationPayload,\n SqlLabLaunchPayload,\n SupersetCompilationAdapter,\n)\n\n# Import models to ensure proper SQLAlchemy registration\n\n\n# [DEF:make_adapter:Function]\n# @PURPOSE: Build an adapter with a mock Superset client and deterministic environment for compatibility tests.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef make_adapter():\n environment = SimpleNamespace(\n id=\"env-1\",\n name=\"Test Env\",\n url=\"http://superset.example\",\n username=\"user\",\n password=\"pass\",\n verify_ssl=True,\n timeout=30,\n )\n client = MagicMock()\n client.network = MagicMock()\n return SupersetCompilationAdapter(environment=environment, client=client), client\n\n\n# [/DEF:make_adapter:Function]\n\n\n# [DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]\n# @PURPOSE: Confirms preview compilation uses a supported client method first when the capability exists.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_preview_prefers_supported_client_method_before_network_fallback():\n adapter, client = make_adapter()\n client.compile_preview = MagicMock(return_value={\"compiled_sql\": \"SELECT 1\"})\n payload = PreviewCompilationPayload(\n session_id=\"sess-1\",\n dataset_id=42,\n preview_fingerprint=\"fp-1\",\n template_params={\"country\": \"RU\"},\n effective_filters=[{\"name\": \"country\", \"value\": \"RU\"}],\n )\n\n preview = adapter.compile_preview(payload)\n\n assert preview.preview_status.value == \"ready\"\n assert preview.compiled_sql == \"SELECT 1\"\n client.compile_preview.assert_called_once()\n client.network.request.assert_not_called()\n\n\n# [/DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]\n\n\n# [DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]\n# @PURPOSE: Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql():\n adapter, client = make_adapter()\n payload = PreviewCompilationPayload(\n session_id=\"sess-2\",\n dataset_id=77,\n preview_fingerprint=\"fp-2\",\n template_params={\"region\": \"emea\"},\n effective_filters=[],\n )\n\n client.network.request.side_effect = [\n RuntimeError(\"preview endpoint unavailable\"),\n {\"result\": {\"sql\": \"SELECT * FROM dataset_77\"}},\n ]\n\n preview = adapter.compile_preview(payload)\n\n assert preview.preview_status.value == \"ready\"\n assert preview.compiled_sql == \"SELECT * FROM dataset_77\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/dataset/77/preview\"\n assert second_call[\"endpoint\"] == \"/dataset/77/sql\"\n\n\n# [/DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]\n\n\n# [DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]\n# @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_sql_lab_launch_falls_back_to_legacy_execute_endpoint():\n adapter, client = make_adapter()\n client.get_dataset.return_value = {\n \"result\": {\n \"id\": 55,\n \"schema\": \"public\",\n \"database\": {\"id\": 9},\n }\n }\n client.network.request.side_effect = [\n RuntimeError(\"sqllab execute unavailable\"),\n {\"result\": {\"id\": \"query-123\"}},\n ]\n payload = SqlLabLaunchPayload(\n session_id=\"sess-3\",\n dataset_id=55,\n preview_id=\"preview-9\",\n compiled_sql=\"SELECT * FROM sales\",\n template_params={\"limit\": 10},\n )\n\n sql_lab_ref = adapter.create_sql_lab_session(payload)\n\n assert sql_lab_ref == \"query-123\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/sqllab/execute/\"\n assert second_call[\"endpoint\"] == \"/sql_lab/execute/\"\n\n\n# [/DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]\n\n\n# [/DEF:SupersetCompatibilityMatrixTests:Module]\n" }, { "contract_id": "make_adapter", "contract_type": "Function", "file_path": "backend/tests/services/dataset_review/test_superset_matrix.py", - "start_line": 23, - "end_line": 42, + "start_line": 20, + "end_line": 38, "tier": "TIER_1", - "complexity": 2, + "complexity": 1, "metadata": { - "COMPLEXITY": "2", "PURPOSE": "Build an adapter with a mock Superset client and deterministic environment for compatibility tests." }, "relations": [ @@ -102871,10 +71103,19 @@ "schema_warnings": [ { "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Function' at C2", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 2, + "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" } }, @@ -102898,18 +71139,17 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:make_adapter:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Build an adapter with a mock Superset client and deterministic environment for compatibility tests.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef make_adapter():\n environment = SimpleNamespace(\n id=\"env-1\",\n name=\"Test Env\",\n url=\"http://superset.example\",\n username=\"user\",\n password=\"pass\",\n verify_ssl=True,\n timeout=30,\n )\n client = MagicMock()\n client.network = MagicMock()\n return SupersetCompilationAdapter(environment=environment, client=client), client\n\n\n# [/DEF:make_adapter:Function]\n" + "body": "# [DEF:make_adapter:Function]\n# @PURPOSE: Build an adapter with a mock Superset client and deterministic environment for compatibility tests.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef make_adapter():\n environment = SimpleNamespace(\n id=\"env-1\",\n name=\"Test Env\",\n url=\"http://superset.example\",\n username=\"user\",\n password=\"pass\",\n verify_ssl=True,\n timeout=30,\n )\n client = MagicMock()\n client.network = MagicMock()\n return SupersetCompilationAdapter(environment=environment, client=client), client\n\n\n# [/DEF:make_adapter:Function]\n" }, { "contract_id": "test_preview_prefers_supported_client_method_before_network_fallback", "contract_type": "Function", "file_path": "backend/tests/services/dataset_review/test_superset_matrix.py", - "start_line": 45, - "end_line": 68, + "start_line": 41, + "end_line": 63, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", "PURPOSE": "Confirms preview compilation uses a supported client method first when the capability exists." }, "relations": [ @@ -102950,18 +71190,17 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]\n# @COMPLEXITY: 2\n# @PURPOSE: Confirms preview compilation uses a supported client method first when the capability exists.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_preview_prefers_supported_client_method_before_network_fallback():\n adapter, client = make_adapter()\n client.compile_preview = MagicMock(return_value={\"compiled_sql\": \"SELECT 1\"})\n payload = PreviewCompilationPayload(\n session_id=\"sess-1\",\n dataset_id=42,\n preview_fingerprint=\"fp-1\",\n template_params={\"country\": \"RU\"},\n effective_filters=[{\"name\": \"country\", \"value\": \"RU\"}],\n )\n\n preview = adapter.compile_preview(payload)\n\n assert preview.preview_status.value == \"ready\"\n assert preview.compiled_sql == \"SELECT 1\"\n client.compile_preview.assert_called_once()\n client.network.request.assert_not_called()\n\n\n# [/DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]\n" + "body": "# [DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]\n# @PURPOSE: Confirms preview compilation uses a supported client method first when the capability exists.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_preview_prefers_supported_client_method_before_network_fallback():\n adapter, client = make_adapter()\n client.compile_preview = MagicMock(return_value={\"compiled_sql\": \"SELECT 1\"})\n payload = PreviewCompilationPayload(\n session_id=\"sess-1\",\n dataset_id=42,\n preview_fingerprint=\"fp-1\",\n template_params={\"country\": \"RU\"},\n effective_filters=[{\"name\": \"country\", \"value\": \"RU\"}],\n )\n\n preview = adapter.compile_preview(payload)\n\n assert preview.preview_status.value == \"ready\"\n assert preview.compiled_sql == \"SELECT 1\"\n client.compile_preview.assert_called_once()\n client.network.request.assert_not_called()\n\n\n# [/DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]\n" }, { "contract_id": "test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql", "contract_type": "Function", "file_path": "backend/tests/services/dataset_review/test_superset_matrix.py", - "start_line": 71, - "end_line": 103, - "tier": "TIER_2", - "complexity": 3, + "start_line": 66, + "end_line": 97, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": "3", "FRAGILE": "Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.", "PURPOSE": "Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL." }, @@ -102981,54 +71220,13 @@ "detail": null }, { - "code": "invalid_relation_predicate", + "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "Predicate [DEPENDS_ON] is not in allowed_predicates", + "message": "@RELATION is forbidden for contract type 'Function' at C2", "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[DEPENDS_ON]" + "actual_complexity": 2, + "contract_type": "Function" } - } - ], - "anchor_syntax": "def", - "body": "# [DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql():\n adapter, client = make_adapter()\n payload = PreviewCompilationPayload(\n session_id=\"sess-2\",\n dataset_id=77,\n preview_fingerprint=\"fp-2\",\n template_params={\"region\": \"emea\"},\n effective_filters=[],\n )\n\n client.network.request.side_effect = [\n RuntimeError(\"preview endpoint unavailable\"),\n {\"result\": {\"sql\": \"SELECT * FROM dataset_77\"}},\n ]\n\n preview = adapter.compile_preview(payload)\n\n assert preview.preview_status.value == \"ready\"\n assert preview.compiled_sql == \"SELECT * FROM dataset_77\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/dataset/77/preview\"\n assert second_call[\"endpoint\"] == \"/dataset/77/sql\"\n\n\n# [/DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]\n" - }, - { - "contract_id": "test_sql_lab_launch_falls_back_to_legacy_execute_endpoint", - "contract_type": "Function", - "file_path": "backend/tests/services/dataset_review/test_superset_matrix.py", - "start_line": 106, - "end_line": 143, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "FRAGILE": "Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.", - "PURPOSE": "Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction." - }, - "relations": [ - { - "source_id": "test_sql_lab_launch_falls_back_to_legacy_execute_endpoint", - "relation_type": "[DEPENDS_ON]", - "target_id": "SupersetCompilationAdapter", - "target_ref": "[SupersetCompilationAdapter]" - } - ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "FRAGILE", - "message": "@FRAGILE is not defined in axiom_config.yaml tags", - "detail": null }, { "code": "invalid_relation_predicate", @@ -103050,7 +71248,65 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]\n# @COMPLEXITY: 3\n# @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_sql_lab_launch_falls_back_to_legacy_execute_endpoint():\n adapter, client = make_adapter()\n client.get_dataset.return_value = {\n \"result\": {\n \"id\": 55,\n \"schema\": \"public\",\n \"database\": {\"id\": 9},\n }\n }\n client.network.request.side_effect = [\n RuntimeError(\"sqllab execute unavailable\"),\n {\"result\": {\"id\": \"query-123\"}},\n ]\n payload = SqlLabLaunchPayload(\n session_id=\"sess-3\",\n dataset_id=55,\n preview_id=\"preview-9\",\n compiled_sql=\"SELECT * FROM sales\",\n template_params={\"limit\": 10},\n )\n\n sql_lab_ref = adapter.create_sql_lab_session(payload)\n\n assert sql_lab_ref == \"query-123\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/sqllab/execute/\"\n assert second_call[\"endpoint\"] == \"/sql_lab/execute/\"\n\n\n# [/DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]\n" + "body": "# [DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]\n# @PURPOSE: Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql():\n adapter, client = make_adapter()\n payload = PreviewCompilationPayload(\n session_id=\"sess-2\",\n dataset_id=77,\n preview_fingerprint=\"fp-2\",\n template_params={\"region\": \"emea\"},\n effective_filters=[],\n )\n\n client.network.request.side_effect = [\n RuntimeError(\"preview endpoint unavailable\"),\n {\"result\": {\"sql\": \"SELECT * FROM dataset_77\"}},\n ]\n\n preview = adapter.compile_preview(payload)\n\n assert preview.preview_status.value == \"ready\"\n assert preview.compiled_sql == \"SELECT * FROM dataset_77\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/dataset/77/preview\"\n assert second_call[\"endpoint\"] == \"/dataset/77/sql\"\n\n\n# [/DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]\n" + }, + { + "contract_id": "test_sql_lab_launch_falls_back_to_legacy_execute_endpoint", + "contract_type": "Function", + "file_path": "backend/tests/services/dataset_review/test_superset_matrix.py", + "start_line": 100, + "end_line": 136, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "FRAGILE": "Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.", + "PURPOSE": "Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction." + }, + "relations": [ + { + "source_id": "test_sql_lab_launch_falls_back_to_legacy_execute_endpoint", + "relation_type": "[DEPENDS_ON]", + "target_id": "SupersetCompilationAdapter", + "target_ref": "[SupersetCompilationAdapter]" + } + ], + "schema_warnings": [ + { + "code": "unknown_tag", + "tag": "FRAGILE", + "message": "@FRAGILE is not defined in axiom_config.yaml tags", + "detail": null + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C2", + "detail": { + "actual_complexity": 2, + "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]" + } + } + ], + "anchor_syntax": "def", + "body": "# [DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]\n# @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction.\n# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]\ndef test_sql_lab_launch_falls_back_to_legacy_execute_endpoint():\n adapter, client = make_adapter()\n client.get_dataset.return_value = {\n \"result\": {\n \"id\": 55,\n \"schema\": \"public\",\n \"database\": {\"id\": 9},\n }\n }\n client.network.request.side_effect = [\n RuntimeError(\"sqllab execute unavailable\"),\n {\"result\": {\"id\": \"query-123\"}},\n ]\n payload = SqlLabLaunchPayload(\n session_id=\"sess-3\",\n dataset_id=55,\n preview_id=\"preview-9\",\n compiled_sql=\"SELECT * FROM sales\",\n template_params={\"limit\": 10},\n )\n\n sql_lab_ref = adapter.create_sql_lab_session(payload)\n\n assert sql_lab_ref == \"query-123\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/sqllab/execute/\"\n assert second_call[\"endpoint\"] == \"/sql_lab/execute/\"\n\n\n# [/DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]\n" }, { "contract_id": "TestAuth", @@ -103058,10 +71314,9 @@ "file_path": "backend/tests/test_auth.py", "start_line": 1, "end_line": 332, - "tier": "TIER_2", - "complexity": 3, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Test", "PURPOSE": "Covers authentication service/repository behavior and auth bootstrap helpers." }, @@ -103092,6 +71347,24 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "invalid_relation_predicate", "tag": "RELATION", @@ -103166,7 +71439,7 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:TestAuth:Module]\n# @COMPLEXITY: 3\n# @PURPOSE: Covers authentication service/repository behavior and auth bootstrap helpers.\n# @LAYER: Test\n# @RELATION: TESTS -> AuthService\n# @RELATION: TESTS -> AuthRepository\n# @RELATION: TESTS -> create_admin\n# @RELATION: TESTS -> ensure_encryption_key\n\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom cryptography.fernet import Fernet\nfrom src.core.database import Base\nfrom src.models.auth import User, Role, Permission, ADGroupMapping\nfrom src.services.auth_service import AuthService\nfrom src.core.auth.repository import AuthRepository\nfrom src.core.auth.security import verify_password, get_password_hash\nfrom src.scripts.create_admin import create_admin\nfrom src.scripts.init_auth_db import ensure_encryption_key\n\n# Create in-memory SQLite database for testing\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\n\nengine = create_engine(\n SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False}\n)\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n# Create all tables\nBase.metadata.create_all(bind=engine)\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"Create a new database session with a transaction, rollback after test\"\"\"\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n yield session\n\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef auth_service(db_session):\n return AuthService(db_session)\n\n\n@pytest.fixture\ndef auth_repo(db_session):\n return AuthRepository(db_session)\n\n\n# [DEF:test_create_user:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_create_user(auth_repo):\n \"\"\"Test user creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"testuser\")\n assert retrieved_user is not None\n assert retrieved_user.username == \"testuser\"\n assert retrieved_user.email == \"test@example.com\"\n assert verify_password(\"testpassword123\", retrieved_user.password_hash)\n\n\n# [/DEF:test_create_user:Function]\n\n\n# [DEF:test_authenticate_user:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_authenticate_user(auth_service, auth_repo):\n \"\"\"Test user authentication with valid and invalid credentials\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n # Test valid credentials\n authenticated_user = auth_service.authenticate_user(\"testuser\", \"testpassword123\")\n assert authenticated_user is not None\n assert authenticated_user.username == \"testuser\"\n\n # Test invalid password\n invalid_user = auth_service.authenticate_user(\"testuser\", \"wrongpassword\")\n assert invalid_user is None\n\n # Test invalid username\n invalid_user = auth_service.authenticate_user(\"nonexistent\", \"testpassword123\")\n assert invalid_user is None\n\n\n# [/DEF:test_authenticate_user:Function]\n\n\n# [DEF:test_create_session:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_create_session(auth_service, auth_repo):\n \"\"\"Test session token creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n session = auth_service.create_session(user)\n assert \"access_token\" in session\n assert \"token_type\" in session\n assert session[\"token_type\"] == \"bearer\"\n assert len(session[\"access_token\"]) > 0\n\n\n# [/DEF:test_create_session:Function]\n\n\n# [DEF:test_role_permission_association:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_role_permission_association(auth_repo):\n \"\"\"Test role and permission association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n perm1 = Permission(resource=\"admin:users\", action=\"READ\")\n perm2 = Permission(resource=\"admin:users\", action=\"WRITE\")\n\n role.permissions.extend([perm1, perm2])\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n retrieved_role = auth_repo.get_role_by_name(\"Admin\")\n assert retrieved_role is not None\n assert len(retrieved_role.permissions) == 2\n\n permissions = [f\"{p.resource}:{p.action}\" for p in retrieved_role.permissions]\n assert \"admin:users:READ\" in permissions\n assert \"admin:users:WRITE\" in permissions\n\n\n# [/DEF:test_role_permission_association:Function]\n\n\n# [DEF:test_user_role_association:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_user_role_association(auth_repo):\n \"\"\"Test user and role association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n user = User(\n username=\"adminuser\",\n email=\"admin@example.com\",\n password_hash=get_password_hash(\"adminpass123\"),\n auth_source=\"LOCAL\",\n )\n\n user.roles.append(role)\n\n auth_repo.db.add(role)\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"adminuser\")\n assert retrieved_user is not None\n assert len(retrieved_user.roles) == 1\n assert retrieved_user.roles[0].name == \"Admin\"\n\n\n# [/DEF:test_user_role_association:Function]\n\n\n# [DEF:test_ad_group_mapping:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ad_group_mapping(auth_repo):\n \"\"\"Test AD group mapping\"\"\"\n role = Role(name=\"ADFS_Admin\", description=\"ADFS administrators\")\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\ADFS_Admins\", role_id=role.id)\n\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n\n retrieved_mapping = (\n auth_repo.db.query(ADGroupMapping)\n .filter_by(ad_group=\"DOMAIN\\\\ADFS_Admins\")\n .first()\n )\n assert retrieved_mapping is not None\n assert retrieved_mapping.role_id == role.id\n\n\n# [/DEF:test_ad_group_mapping:Function]\n\n\n# [DEF:test_create_admin_creates_user_with_optional_email:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_create_admin_creates_user_with_optional_email(monkeypatch, db_session):\n \"\"\"Test bootstrap admin creation stores optional email and Admin role\"\"\"\n monkeypatch.setattr(\"src.scripts.create_admin.AuthSessionLocal\", lambda: db_session)\n\n result = create_admin(\"bootstrap-admin\", \"bootstrap-pass\", \"admin@example.com\")\n\n created_user = (\n db_session.query(User).filter(User.username == \"bootstrap-admin\").first()\n )\n assert result == \"created\"\n assert created_user is not None\n assert created_user.email == \"admin@example.com\"\n assert created_user.roles[0].name == \"Admin\"\n\n\n# [/DEF:test_create_admin_creates_user_with_optional_email:Function]\n\n\n# [DEF:test_create_admin_is_idempotent_for_existing_user:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_create_admin_is_idempotent_for_existing_user(monkeypatch, db_session):\n \"\"\"Test bootstrap admin creation preserves existing user on repeated runs\"\"\"\n monkeypatch.setattr(\"src.scripts.create_admin.AuthSessionLocal\", lambda: db_session)\n\n first_result = create_admin(\"bootstrap-admin-2\", \"bootstrap-pass\")\n second_result = create_admin(\n \"bootstrap-admin-2\", \"new-password\", \"changed@example.com\"\n )\n\n created_user = (\n db_session.query(User).filter(User.username == \"bootstrap-admin-2\").first()\n )\n assert first_result == \"created\"\n assert second_result == \"exists\"\n assert created_user is not None\n assert created_user.email is None\n assert verify_password(\"bootstrap-pass\", created_user.password_hash)\n assert not verify_password(\"new-password\", created_user.password_hash)\n\n\n# [/DEF:test_create_admin_is_idempotent_for_existing_user:Function]\n\n\n# [DEF:test_ensure_encryption_key_generates_backend_env_file:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_generates_backend_env_file(monkeypatch, tmp_path):\n \"\"\"Test first-time initialization generates and persists a Fernet key.\"\"\"\n env_file = tmp_path / \".env\"\n monkeypatch.delenv(\"ENCRYPTION_KEY\", raising=False)\n\n generated_key = ensure_encryption_key(env_file)\n\n assert generated_key\n assert env_file.exists()\n assert (\n env_file.read_text(encoding=\"utf-8\").strip()\n == f\"ENCRYPTION_KEY={generated_key}\"\n )\n assert verify_fernet_key(generated_key)\n\n\n# [/DEF:test_ensure_encryption_key_generates_backend_env_file:Function]\n\n\n# [DEF:test_ensure_encryption_key_reuses_existing_env_file_value:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_reuses_existing_env_file_value(monkeypatch, tmp_path):\n \"\"\"Test persisted key is reused without rewriting file contents.\"\"\"\n env_file = tmp_path / \".env\"\n existing_key = Fernet.generate_key().decode()\n env_file.write_text(\n f\"ENCRYPTION_KEY={existing_key}\\nOTHER=value\\n\", encoding=\"utf-8\"\n )\n monkeypatch.delenv(\"ENCRYPTION_KEY\", raising=False)\n\n reused_key = ensure_encryption_key(env_file)\n\n assert reused_key == existing_key\n assert (\n env_file.read_text(encoding=\"utf-8\")\n == f\"ENCRYPTION_KEY={existing_key}\\nOTHER=value\\n\"\n )\n\n\n# [/DEF:test_ensure_encryption_key_reuses_existing_env_file_value:Function]\n\n\n# [DEF:test_ensure_encryption_key_prefers_process_environment:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_prefers_process_environment(monkeypatch, tmp_path):\n \"\"\"Test explicit process environment has priority over file generation.\"\"\"\n env_file = tmp_path / \".env\"\n runtime_key = Fernet.generate_key().decode()\n monkeypatch.setenv(\"ENCRYPTION_KEY\", runtime_key)\n\n resolved_key = ensure_encryption_key(env_file)\n\n assert resolved_key == runtime_key\n assert not env_file.exists()\n\n\n# [/DEF:test_ensure_encryption_key_prefers_process_environment:Function]\n\n\ndef verify_fernet_key(value: str) -> bool:\n Fernet(value.encode())\n return True\n\n\n# [/DEF:TestAuth:Module]\n" + "body": "# [DEF:TestAuth:Module]\n# @PURPOSE: Covers authentication service/repository behavior and auth bootstrap helpers.\n# @LAYER: Test\n# @RELATION: TESTS -> AuthService\n# @RELATION: TESTS -> AuthRepository\n# @RELATION: TESTS -> create_admin\n# @RELATION: TESTS -> ensure_encryption_key\n\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom cryptography.fernet import Fernet\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.auth.repository import AuthRepository\nfrom src.core.auth.security import get_password_hash, verify_password\nfrom src.core.database import Base\nfrom src.models.auth import ADGroupMapping, Permission, Role, User\nfrom src.scripts.create_admin import create_admin\nfrom src.scripts.init_auth_db import ensure_encryption_key\nfrom src.services.auth_service import AuthService\n\n# Create in-memory SQLite database for testing\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\n\nengine = create_engine(\n SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False}\n)\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n# Create all tables\nBase.metadata.create_all(bind=engine)\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"Create a new database session with a transaction, rollback after test\"\"\"\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n yield session\n\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef auth_service(db_session):\n return AuthService(db_session)\n\n\n@pytest.fixture\ndef auth_repo(db_session):\n return AuthRepository(db_session)\n\n\n# [DEF:test_create_user:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_create_user(auth_repo):\n \"\"\"Test user creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"testuser\")\n assert retrieved_user is not None\n assert retrieved_user.username == \"testuser\"\n assert retrieved_user.email == \"test@example.com\"\n assert verify_password(\"testpassword123\", retrieved_user.password_hash)\n\n\n# [/DEF:test_create_user:Function]\n\n\n# [DEF:test_authenticate_user:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_authenticate_user(auth_service, auth_repo):\n \"\"\"Test user authentication with valid and invalid credentials\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n # Test valid credentials\n authenticated_user = auth_service.authenticate_user(\"testuser\", \"testpassword123\")\n assert authenticated_user is not None\n assert authenticated_user.username == \"testuser\"\n\n # Test invalid password\n invalid_user = auth_service.authenticate_user(\"testuser\", \"wrongpassword\")\n assert invalid_user is None\n\n # Test invalid username\n invalid_user = auth_service.authenticate_user(\"nonexistent\", \"testpassword123\")\n assert invalid_user is None\n\n\n# [/DEF:test_authenticate_user:Function]\n\n\n# [DEF:test_create_session:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_create_session(auth_service, auth_repo):\n \"\"\"Test session token creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n session = auth_service.create_session(user)\n assert \"access_token\" in session\n assert \"token_type\" in session\n assert session[\"token_type\"] == \"bearer\"\n assert len(session[\"access_token\"]) > 0\n\n\n# [/DEF:test_create_session:Function]\n\n\n# [DEF:test_role_permission_association:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_role_permission_association(auth_repo):\n \"\"\"Test role and permission association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n perm1 = Permission(resource=\"admin:users\", action=\"READ\")\n perm2 = Permission(resource=\"admin:users\", action=\"WRITE\")\n\n role.permissions.extend([perm1, perm2])\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n retrieved_role = auth_repo.get_role_by_name(\"Admin\")\n assert retrieved_role is not None\n assert len(retrieved_role.permissions) == 2\n\n permissions = [f\"{p.resource}:{p.action}\" for p in retrieved_role.permissions]\n assert \"admin:users:READ\" in permissions\n assert \"admin:users:WRITE\" in permissions\n\n\n# [/DEF:test_role_permission_association:Function]\n\n\n# [DEF:test_user_role_association:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_user_role_association(auth_repo):\n \"\"\"Test user and role association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n user = User(\n username=\"adminuser\",\n email=\"admin@example.com\",\n password_hash=get_password_hash(\"adminpass123\"),\n auth_source=\"LOCAL\",\n )\n\n user.roles.append(role)\n\n auth_repo.db.add(role)\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"adminuser\")\n assert retrieved_user is not None\n assert len(retrieved_user.roles) == 1\n assert retrieved_user.roles[0].name == \"Admin\"\n\n\n# [/DEF:test_user_role_association:Function]\n\n\n# [DEF:test_ad_group_mapping:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ad_group_mapping(auth_repo):\n \"\"\"Test AD group mapping\"\"\"\n role = Role(name=\"ADFS_Admin\", description=\"ADFS administrators\")\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\ADFS_Admins\", role_id=role.id)\n\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n\n retrieved_mapping = (\n auth_repo.db.query(ADGroupMapping)\n .filter_by(ad_group=\"DOMAIN\\\\ADFS_Admins\")\n .first()\n )\n assert retrieved_mapping is not None\n assert retrieved_mapping.role_id == role.id\n\n\n# [/DEF:test_ad_group_mapping:Function]\n\n\n# [DEF:test_create_admin_creates_user_with_optional_email:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_create_admin_creates_user_with_optional_email(monkeypatch, db_session):\n \"\"\"Test bootstrap admin creation stores optional email and Admin role\"\"\"\n monkeypatch.setattr(\"src.scripts.create_admin.AuthSessionLocal\", lambda: db_session)\n\n result = create_admin(\"bootstrap-admin\", \"bootstrap-pass\", \"admin@example.com\")\n\n created_user = (\n db_session.query(User).filter(User.username == \"bootstrap-admin\").first()\n )\n assert result == \"created\"\n assert created_user is not None\n assert created_user.email == \"admin@example.com\"\n assert created_user.roles[0].name == \"Admin\"\n\n\n# [/DEF:test_create_admin_creates_user_with_optional_email:Function]\n\n\n# [DEF:test_create_admin_is_idempotent_for_existing_user:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_create_admin_is_idempotent_for_existing_user(monkeypatch, db_session):\n \"\"\"Test bootstrap admin creation preserves existing user on repeated runs\"\"\"\n monkeypatch.setattr(\"src.scripts.create_admin.AuthSessionLocal\", lambda: db_session)\n\n first_result = create_admin(\"bootstrap-admin-2\", \"bootstrap-pass\")\n second_result = create_admin(\n \"bootstrap-admin-2\", \"new-password\", \"changed@example.com\"\n )\n\n created_user = (\n db_session.query(User).filter(User.username == \"bootstrap-admin-2\").first()\n )\n assert first_result == \"created\"\n assert second_result == \"exists\"\n assert created_user is not None\n assert created_user.email is None\n assert verify_password(\"bootstrap-pass\", created_user.password_hash)\n assert not verify_password(\"new-password\", created_user.password_hash)\n\n\n# [/DEF:test_create_admin_is_idempotent_for_existing_user:Function]\n\n\n# [DEF:test_ensure_encryption_key_generates_backend_env_file:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_generates_backend_env_file(monkeypatch, tmp_path):\n \"\"\"Test first-time initialization generates and persists a Fernet key.\"\"\"\n env_file = tmp_path / \".env\"\n monkeypatch.delenv(\"ENCRYPTION_KEY\", raising=False)\n\n generated_key = ensure_encryption_key(env_file)\n\n assert generated_key\n assert env_file.exists()\n assert (\n env_file.read_text(encoding=\"utf-8\").strip()\n == f\"ENCRYPTION_KEY={generated_key}\"\n )\n assert verify_fernet_key(generated_key)\n\n\n# [/DEF:test_ensure_encryption_key_generates_backend_env_file:Function]\n\n\n# [DEF:test_ensure_encryption_key_reuses_existing_env_file_value:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_reuses_existing_env_file_value(monkeypatch, tmp_path):\n \"\"\"Test persisted key is reused without rewriting file contents.\"\"\"\n env_file = tmp_path / \".env\"\n existing_key = Fernet.generate_key().decode()\n env_file.write_text(\n f\"ENCRYPTION_KEY={existing_key}\\nOTHER=value\\n\", encoding=\"utf-8\"\n )\n monkeypatch.delenv(\"ENCRYPTION_KEY\", raising=False)\n\n reused_key = ensure_encryption_key(env_file)\n\n assert reused_key == existing_key\n assert (\n env_file.read_text(encoding=\"utf-8\")\n == f\"ENCRYPTION_KEY={existing_key}\\nOTHER=value\\n\"\n )\n\n\n# [/DEF:test_ensure_encryption_key_reuses_existing_env_file_value:Function]\n\n\n# [DEF:test_ensure_encryption_key_prefers_process_environment:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_prefers_process_environment(monkeypatch, tmp_path):\n \"\"\"Test explicit process environment has priority over file generation.\"\"\"\n env_file = tmp_path / \".env\"\n runtime_key = Fernet.generate_key().decode()\n monkeypatch.setenv(\"ENCRYPTION_KEY\", runtime_key)\n\n resolved_key = ensure_encryption_key(env_file)\n\n assert resolved_key == runtime_key\n assert not env_file.exists()\n\n\n# [/DEF:test_ensure_encryption_key_prefers_process_environment:Function]\n\n\ndef verify_fernet_key(value: str) -> bool:\n Fernet(value.encode())\n return True\n\n\n# [/DEF:TestAuth:Module]\n" }, { "contract_id": "test_create_admin_creates_user_with_optional_email", @@ -103368,16 +71641,111 @@ "anchor_syntax": "def", "body": "# [DEF:test_ensure_encryption_key_prefers_process_environment:Function]\n# @RELATION: BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_prefers_process_environment(monkeypatch, tmp_path):\n \"\"\"Test explicit process environment has priority over file generation.\"\"\"\n env_file = tmp_path / \".env\"\n runtime_key = Fernet.generate_key().decode()\n monkeypatch.setenv(\"ENCRYPTION_KEY\", runtime_key)\n\n resolved_key = ensure_encryption_key(env_file)\n\n assert resolved_key == runtime_key\n assert not env_file.exists()\n\n\n# [/DEF:test_ensure_encryption_key_prefers_process_environment:Function]\n" }, + { + "contract_id": "TestCoreScheduler", + "contract_type": "Module", + "file_path": "backend/tests/test_core_scheduler.py", + "start_line": 1, + "end_line": 175, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Verify SchedulerService load_schedules and translation job add/remove contracts.", + "TEST_EDGE": "add_remove_translation_job — job correctly registered and removed from APScheduler", + "TEST_INVARIANT": "translation_jobs_persist_across_reload -> VERIFIED_BY: [test_load_schedules_reloads_translation_schedules]" + }, + "relations": [ + { + "source_id": "TestCoreScheduler", + "relation_type": "BINDS_TO", + "target_id": "SchedulerService", + "target_ref": "[SchedulerService]" + } + ], + "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", + "body": "# #region TestCoreScheduler [C:3] [TYPE Module] [SEMANTICS test, scheduler, apscheduler, load, reload]\n# @BRIEF Verify SchedulerService load_schedules and translation job add/remove contracts.\n# @RELATION BINDS_TO -> [SchedulerService]\n# @TEST_EDGE: load_schedules_reloads — jobs survive remove_all_jobs when re-registered from DB\n# @TEST_EDGE: add_remove_translation_job — job correctly registered and removed from APScheduler\n# @TEST_INVARIANT: translation_jobs_persist_across_reload -> VERIFIED_BY: [test_load_schedules_reloads_translation_schedules]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nfrom unittest.mock import MagicMock, patch\n\nfrom apscheduler.schedulers.background import BackgroundScheduler\n\n# ---------------------------------------------------------------------------\n# Helpers — fresh scheduler & mock config\n# ---------------------------------------------------------------------------\n\ndef _make_mock_config_manager(backup_env_id=\"env1\", backup_cron=\"0 3 * * *\"):\n \"\"\"Build a mock ConfigManager returning a single backup-enabled environment.\"\"\"\n mgr = MagicMock()\n mock_env = MagicMock()\n mock_env.id = backup_env_id\n mock_env.backup_schedule = MagicMock()\n mock_env.backup_schedule.enabled = True\n mock_env.backup_schedule.cron_expression = backup_cron\n mock_config = MagicMock()\n mock_config.environments = [mock_env]\n mgr.get_config.return_value = mock_config\n return mgr\n\n\ndef _make_mock_task_manager():\n \"\"\"Build a mock TaskManager returning no active tasks.\"\"\"\n tm = MagicMock()\n tm.get_tasks.return_value = []\n return tm\n\n\n# ---------------------------------------------------------------------------\n# Fixture helpers — start/stop a real BackgroundScheduler\n# ---------------------------------------------------------------------------\n\ndef _start_scheduler():\n \"\"\"Create and start a BackgroundScheduler. Returns the scheduler instance.\"\"\"\n sched = BackgroundScheduler()\n sched.start()\n return sched\n\n\ndef _stop_scheduler(sched):\n \"\"\"Shutdown a BackgroundScheduler if running.\"\"\"\n if sched.running:\n sched.shutdown(wait=False)\n\n\n# #region test_load_schedules_reloads_translation_schedules [C:1] [TYPE Function]\ndef test_load_schedules_reloads_translation_schedules():\n \"\"\"Verify load_schedules re-registers translation jobs after remove_all_jobs.\"\"\"\n from src.core.scheduler import SchedulerService\n\n sched = _start_scheduler()\n try:\n cfg = _make_mock_config_manager()\n tm = _make_mock_task_manager()\n\n svc = SchedulerService.__new__(SchedulerService)\n svc.task_manager = tm\n svc.config_manager = cfg\n svc.scheduler = sched\n\n # Pre-register a translation job (simulates what happens before reload)\n svc.add_translation_job(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n cron_expression=\"0 2 * * *\",\n )\n\n # Verify it exists BEFORE reload\n job_before = sched.get_job(\"translate_sched-1\")\n assert job_before is not None, \"Translation job should exist before reload\"\n\n # Mock the DB query that load_schedules uses internally\n mock_schedule = MagicMock()\n mock_schedule.id = \"sched-1\"\n mock_schedule.job_id = \"job-1\"\n mock_schedule.cron_expression = \"0 2 * * *\"\n mock_schedule.timezone = \"UTC\"\n mock_schedule.execution_mode = \"full\"\n\n mock_db = MagicMock()\n mock_db.query.return_value.filter.return_value.all.return_value = [mock_schedule]\n\n with patch(\"src.core.scheduler.SessionLocal\", return_value=mock_db):\n svc.load_schedules()\n\n # After reload, the translation job should STILL exist\n job_after = sched.get_job(\"translate_sched-1\")\n assert job_after is not None, (\n \"Translation job should survive load_schedules (re-registered from DB)\"\n )\n\n # Backup job should also exist (added from config)\n backup_job = sched.get_job(\"backup_env1\")\n assert backup_job is not None, \"Backup job should be registered by load_schedules\"\n finally:\n _stop_scheduler(sched)\n# #endregion test_load_schedules_reloads_translation_schedules\n\n\n# #region test_add_and_remove_translation_job [C:1] [TYPE Function]\ndef test_add_and_remove_translation_job():\n \"\"\"Verify add_translation_job registers and remove_translation_job removes.\"\"\"\n from src.core.scheduler import SchedulerService\n\n sched = _start_scheduler()\n try:\n cfg = _make_mock_config_manager()\n tm = _make_mock_task_manager()\n\n svc = SchedulerService.__new__(SchedulerService)\n svc.task_manager = tm\n svc.config_manager = cfg\n svc.scheduler = sched\n\n # Should NOT exist before add\n assert sched.get_job(\"translate_sched-x\") is None\n\n svc.add_translation_job(\n schedule_id=\"sched-x\",\n job_id=\"job-x\",\n cron_expression=\"*/5 * * * *\",\n )\n\n # Should exist after add\n job = sched.get_job(\"translate_sched-x\")\n assert job is not None, \"Translation job should be registered after add\"\n assert job.id == \"translate_sched-x\"\n\n svc.remove_translation_job(\"sched-x\")\n\n # Should NOT exist after remove\n assert sched.get_job(\"translate_sched-x\") is None, (\n \"Translation job should be removed after remove_translation_job\"\n )\n finally:\n _stop_scheduler(sched)\n# #endregion test_add_and_remove_translation_job\n\n\n# #region test_remove_nonexistent_translation_job_no_error [C:1] [TYPE Function]\ndef test_remove_nonexistent_translation_job_no_error():\n \"\"\"Verify remove_translation_job handles non-existent job silently.\"\"\"\n from src.core.scheduler import SchedulerService\n\n sched = _start_scheduler()\n try:\n cfg = _make_mock_config_manager()\n tm = _make_mock_task_manager()\n\n svc = SchedulerService.__new__(SchedulerService)\n svc.task_manager = tm\n svc.config_manager = cfg\n svc.scheduler = sched\n\n # Should not raise — silent ignore\n svc.remove_translation_job(\"nonexistent-sched\")\n finally:\n _stop_scheduler(sched)\n# #endregion test_remove_nonexistent_translation_job_no_error\n\n\n# #endregion TestCoreScheduler\n" + }, + { + "contract_id": "test_load_schedules_reloads_translation_schedules", + "contract_type": "Function", + "file_path": "backend/tests/test_core_scheduler.py", + "start_line": 59, + "end_line": 110, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_load_schedules_reloads_translation_schedules [C:1] [TYPE Function]\ndef test_load_schedules_reloads_translation_schedules():\n \"\"\"Verify load_schedules re-registers translation jobs after remove_all_jobs.\"\"\"\n from src.core.scheduler import SchedulerService\n\n sched = _start_scheduler()\n try:\n cfg = _make_mock_config_manager()\n tm = _make_mock_task_manager()\n\n svc = SchedulerService.__new__(SchedulerService)\n svc.task_manager = tm\n svc.config_manager = cfg\n svc.scheduler = sched\n\n # Pre-register a translation job (simulates what happens before reload)\n svc.add_translation_job(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n cron_expression=\"0 2 * * *\",\n )\n\n # Verify it exists BEFORE reload\n job_before = sched.get_job(\"translate_sched-1\")\n assert job_before is not None, \"Translation job should exist before reload\"\n\n # Mock the DB query that load_schedules uses internally\n mock_schedule = MagicMock()\n mock_schedule.id = \"sched-1\"\n mock_schedule.job_id = \"job-1\"\n mock_schedule.cron_expression = \"0 2 * * *\"\n mock_schedule.timezone = \"UTC\"\n mock_schedule.execution_mode = \"full\"\n\n mock_db = MagicMock()\n mock_db.query.return_value.filter.return_value.all.return_value = [mock_schedule]\n\n with patch(\"src.core.scheduler.SessionLocal\", return_value=mock_db):\n svc.load_schedules()\n\n # After reload, the translation job should STILL exist\n job_after = sched.get_job(\"translate_sched-1\")\n assert job_after is not None, (\n \"Translation job should survive load_schedules (re-registered from DB)\"\n )\n\n # Backup job should also exist (added from config)\n backup_job = sched.get_job(\"backup_env1\")\n assert backup_job is not None, \"Backup job should be registered by load_schedules\"\n finally:\n _stop_scheduler(sched)\n# #endregion test_load_schedules_reloads_translation_schedules\n" + }, + { + "contract_id": "test_add_and_remove_translation_job", + "contract_type": "Function", + "file_path": "backend/tests/test_core_scheduler.py", + "start_line": 113, + "end_line": 150, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_add_and_remove_translation_job [C:1] [TYPE Function]\ndef test_add_and_remove_translation_job():\n \"\"\"Verify add_translation_job registers and remove_translation_job removes.\"\"\"\n from src.core.scheduler import SchedulerService\n\n sched = _start_scheduler()\n try:\n cfg = _make_mock_config_manager()\n tm = _make_mock_task_manager()\n\n svc = SchedulerService.__new__(SchedulerService)\n svc.task_manager = tm\n svc.config_manager = cfg\n svc.scheduler = sched\n\n # Should NOT exist before add\n assert sched.get_job(\"translate_sched-x\") is None\n\n svc.add_translation_job(\n schedule_id=\"sched-x\",\n job_id=\"job-x\",\n cron_expression=\"*/5 * * * *\",\n )\n\n # Should exist after add\n job = sched.get_job(\"translate_sched-x\")\n assert job is not None, \"Translation job should be registered after add\"\n assert job.id == \"translate_sched-x\"\n\n svc.remove_translation_job(\"sched-x\")\n\n # Should NOT exist after remove\n assert sched.get_job(\"translate_sched-x\") is None, (\n \"Translation job should be removed after remove_translation_job\"\n )\n finally:\n _stop_scheduler(sched)\n# #endregion test_add_and_remove_translation_job\n" + }, + { + "contract_id": "test_remove_nonexistent_translation_job_no_error", + "contract_type": "Function", + "file_path": "backend/tests/test_core_scheduler.py", + "start_line": 153, + "end_line": 172, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_remove_nonexistent_translation_job_no_error [C:1] [TYPE Function]\ndef test_remove_nonexistent_translation_job_no_error():\n \"\"\"Verify remove_translation_job handles non-existent job silently.\"\"\"\n from src.core.scheduler import SchedulerService\n\n sched = _start_scheduler()\n try:\n cfg = _make_mock_config_manager()\n tm = _make_mock_task_manager()\n\n svc = SchedulerService.__new__(SchedulerService)\n svc.task_manager = tm\n svc.config_manager = cfg\n svc.scheduler = sched\n\n # Should not raise — silent ignore\n svc.remove_translation_job(\"nonexistent-sched\")\n finally:\n _stop_scheduler(sched)\n# #endregion test_remove_nonexistent_translation_job_no_error\n" + }, { "contract_id": "TestDashboardsApi", "contract_type": "Module", "file_path": "backend/tests/test_dashboards_api.py", "start_line": 1, - "end_line": 586, - "tier": "TIER_2", - "complexity": 3, + "end_line": 587, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Domain (Tests)", "PURPOSE": "Comprehensive contract-driven tests for Dashboard Hub API", "SEMANTICS": [ @@ -103396,16 +71764,35 @@ "target_ref": "[src.api.routes.dashboards]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", - "body": "# [DEF:TestDashboardsApi:Module]\n# @RELATION: VERIFIES ->[src.api.routes.dashboards]\n# @COMPLEXITY: 3\n# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API\n# @LAYER: Domain (Tests)\n# @SEMANTICS: tests, dashboards, api, contract, remediation\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom unittest.mock import MagicMock, patch, AsyncMock\nfrom datetime import datetime, timezone\nfrom src.app import app\nfrom src.api.routes.dashboards import (\n DashboardsResponse,\n DashboardDetailResponse,\n DashboardTaskHistoryResponse,\n DatabaseMappingsResponse,\n)\nfrom src.dependencies import (\n get_current_user,\n has_permission,\n get_config_manager,\n get_task_manager,\n get_resource_service,\n get_mapping_service,\n)\n\n# Global mock user\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture(autouse=True)\ndef mock_deps():\n config_manager = MagicMock()\n task_manager = MagicMock()\n resource_service = MagicMock()\n mapping_service = MagicMock()\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n app.dependency_overrides[get_mapping_service] = lambda: mapping_service\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n # Overrides for specific permission checks\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:migration\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:backup\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"tasks\", \"READ\")] = lambda: mock_user\n app.dependency_overrides[has_permission(\"dashboards\", \"READ\")] = lambda: mock_user\n\n yield {\n \"config\": config_manager,\n \"task\": task_manager,\n \"resource\": resource_service,\n \"mapping\": mapping_service,\n }\n app.dependency_overrides.clear()\n\n\nclient = TestClient(app)\n\n# --- 1. get_dashboards tests ---\n\n\n# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_success(mock_deps):\n \"\"\"Uses @TEST_FIXTURE: dashboard_list_happy data.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n # @TEST_FIXTURE: dashboard_list_happy -> {\"id\": 1, \"title\": \"Main Revenue\"}\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Main Revenue\",\n \"slug\": \"main-revenue\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n }\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&page=1&page_size=10\")\n\n assert response.status_code == 200\n data = response.json()\n\n # exhaustive @POST assertions\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 1 # @TEST_FIXTURE: expected_count: 1\n assert data[\"dashboards\"][0][\"title\"] == \"Main Revenue\"\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 10\n assert data[\"total_pages\"] == 1\n\n # schema validation\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_success:Function]\n\n\n# [DEF:test_get_dashboards_with_search:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_with_search(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\"id\": 1, \"title\": \"Sales Report\", \"slug\": \"sales\"},\n {\"id\": 2, \"title\": \"Marketing\", \"slug\": \"marketing\"},\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&search=sales\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales Report\"\n\n\n# [/DEF:test_get_dashboards_with_search:Function]\n\n\n# [DEF:test_get_dashboards_empty:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_empty(mock_deps):\n \"\"\"@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"empty_env\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(return_value=[])\n\n response = client.get(\"/api/dashboards?env_id=empty_env\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert len(data[\"dashboards\"]) == 0\n assert data[\"total_pages\"] == 1\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_empty:Function]\n\n\n# [DEF:test_get_dashboards_superset_failure:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n\n response = client.get(\"/api/dashboards?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch dashboards\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_superset_failure:Function]\n\n\n# [DEF:test_get_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards?env_id=nonexistent\")\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_env_not_found:Function]\n\n\n# [DEF:test_get_dashboards_invalid_pagination:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # page < 1\n assert client.get(\"/api/dashboards?env_id=prod&page=0\").status_code == 400\n assert client.get(\"/api/dashboards?env_id=prod&page=-1\").status_code == 400\n\n # page_size < 1\n assert client.get(\"/api/dashboards?env_id=prod&page_size=0\").status_code == 400\n\n # page_size > 100\n assert client.get(\"/api/dashboards?env_id=prod&page_size=101\").status_code == 400\n\n\n# --- 2. get_database_mappings tests ---\n\n# [/DEF:test_get_dashboards_invalid_pagination:Function]\n\n\n# [DEF:test_get_database_mappings_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_database_mappings_success(mock_deps):\n mock_s = MagicMock()\n mock_s.id = \"s\"\n mock_t = MagicMock()\n mock_t.id = \"t\"\n mock_deps[\"config\"].get_environments.return_value = [mock_s, mock_t]\n\n mock_deps[\"mapping\"].get_suggestions = AsyncMock(\n return_value=[{\"source_db\": \"src\", \"target_db\": \"dst\", \"confidence\": 0.9}]\n )\n response = client.get(\"/api/dashboards/db-mappings?source_env_id=s&target_env_id=t\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"mappings\"]) == 1\n assert data[\"mappings\"][0][\"confidence\"] == 0.9\n DatabaseMappingsResponse(**data)\n\n\n# [/DEF:test_get_database_mappings_success:Function]\n\n\n# [DEF:test_get_database_mappings_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_database_mappings_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t\"\n )\n assert response.status_code == 404\n\n\n# --- 3. get_dashboard_detail tests ---\n\n# [/DEF:test_get_database_mappings_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_detail_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_detail_success(mock_deps):\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n detail_payload = {\n \"id\": 42,\n \"title\": \"Detail\",\n \"charts\": [],\n \"datasets\": [],\n \"chart_count\": 0,\n \"dataset_count\": 0,\n }\n mock_client.get_dashboard_detail.return_value = detail_payload\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == 42\n DashboardDetailResponse(**data)\n\n\n# [/DEF:test_get_dashboard_detail_success:Function]\n\n\n# [DEF:test_get_dashboard_detail_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_detail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards/42?env_id=missing\")\n assert response.status_code == 404\n\n\n# --- 4. get_dashboard_tasks_history tests ---\n\n# [/DEF:test_get_dashboard_detail_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_tasks_history_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_success(mock_deps):\n now = datetime.now(timezone.utc)\n task1 = MagicMock(\n id=\"t1\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=now,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n mock_deps[\"task\"].get_all_tasks.return_value = [task1]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 1\n DashboardTaskHistoryResponse(**data)\n\n\n# [/DEF:test_get_dashboard_tasks_history_success:Function]\n\n\n# [DEF:test_get_dashboard_tasks_history_sorting:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_sorting(mock_deps):\n \"\"\"@POST: Response contains sorted task history (newest first).\"\"\"\n from datetime import timedelta\n\n now = datetime.now(timezone.utc)\n older = now - timedelta(hours=2)\n newest = now\n\n task_old = MagicMock(\n id=\"t-old\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=older,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n task_new = MagicMock(\n id=\"t-new\",\n plugin_id=\"superset-backup\",\n status=\"RUNNING\",\n started_at=newest,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n\n # Provide in wrong order to verify the endpoint sorts\n mock_deps[\"task\"].get_all_tasks.return_value = [task_old, task_new]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"items\"]) == 2\n # Newest first\n assert data[\"items\"][0][\"id\"] == \"t-new\"\n assert data[\"items\"][1][\"id\"] == \"t-old\"\n\n\n# --- 5. get_dashboard_thumbnail tests ---\n\n# [/DEF:test_get_dashboard_tasks_history_sorting:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_success(mock_deps):\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_client = MagicMock()\n mock_response = MagicMock(\n status_code=200, content=b\"img\", headers={\"Content-Type\": \"image/png\"}\n )\n mock_client.network.request.side_effect = (\n lambda method, endpoint, **kw: {\"image_url\": \"url\"}\n if method == \"POST\"\n else mock_response\n )\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n assert response.status_code == 200\n assert response.content == b\"img\"\n\n\n# [/DEF:test_get_dashboard_thumbnail_success:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=missing\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_dashboard_thumbnail_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_202:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_202(mock_deps):\n \"\"\"@POST: Returns 202 when thumbnail is being prepared by Superset.\"\"\"\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_client = MagicMock()\n\n # POST cache_dashboard_screenshot returns image_url\n mock_client.network.request.side_effect = [\n {\"image_url\": \"/api/v1/dashboard/42/thumbnail/abc123/\"}, # POST\n MagicMock(\n status_code=202,\n json=lambda: {\"message\": \"Thumbnail is being generated\"},\n headers={\"Content-Type\": \"application/json\"},\n ), # GET thumbnail -> 202\n ]\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n assert response.status_code == 202\n assert \"Thumbnail is being generated\" in response.json()[\"message\"]\n\n\n# --- 6. migrate_dashboards tests ---\n\n# [/DEF:test_get_dashboard_thumbnail_202:Function]\n\n\n# [DEF:test_migrate_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_success(mock_deps):\n mock_s = MagicMock()\n mock_s.id = \"s\"\n mock_t = MagicMock()\n mock_t.id = \"t\"\n mock_deps[\"config\"].get_environments.return_value = [mock_s, mock_t]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"task-123\"))\n\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"s\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"task-123\"\n\n\n# [/DEF:test_migrate_dashboards_success:Function]\n\n\n# [DEF:test_migrate_dashboards_pre_checks:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_pre_checks(mock_deps):\n # Missing IDs\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"s\", \"target_env_id\": \"t\", \"dashboard_ids\": []},\n )\n assert response.status_code == 400\n assert \"At least one dashboard ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_pre_checks:Function]\n\n\n# [DEF:test_migrate_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id and target_env_id are valid environment IDs.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"ghost\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 404\n assert \"Source environment not found\" in response.json()[\"detail\"]\n\n\n# --- 7. backup_dashboards tests ---\n\n# [/DEF:test_migrate_dashboards_env_not_found:Function]\n\n\n# [DEF:test_backup_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_success(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"backup-123\"))\n\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"prod\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"backup-123\"\n\n\n# [/DEF:test_backup_dashboards_success:Function]\n\n\n# [DEF:test_backup_dashboards_pre_checks:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_pre_checks(mock_deps):\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"prod\", \"dashboard_ids\": []}\n )\n assert response.status_code == 400\n\n\n# [/DEF:test_backup_dashboards_pre_checks:Function]\n\n\n# [DEF:test_backup_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: env_id is a valid environment ID.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"ghost\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_backup_dashboards_env_not_found:Function]\n\n\n# [DEF:test_backup_dashboards_with_schedule:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_with_schedule(mock_deps):\n \"\"\"@POST: If schedule is provided, a scheduled task is created.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"sched-456\"))\n\n response = client.post(\n \"/api/dashboards/backup\",\n json={\"env_id\": \"prod\", \"dashboard_ids\": [1], \"schedule\": \"0 0 * * *\"},\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"sched-456\"\n\n # Verify schedule was propagated to create_task\n call_kwargs = mock_deps[\"task\"].create_task.call_args\n task_params = call_kwargs.kwargs.get(\"params\") or call_kwargs[1].get(\"params\", {})\n assert task_params[\"schedule\"] == \"0 0 * * *\"\n\n\n# --- 8. Internal logic: _task_matches_dashboard ---\n# [/DEF:test_backup_dashboards_with_schedule:Function]\n\nfrom src.api.routes.dashboards import _task_matches_dashboard\n\n\n# [DEF:test_task_matches_dashboard_logic:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_task_matches_dashboard_logic():\n task = MagicMock(\n plugin_id=\"superset-backup\", params={\"dashboards\": [42], \"env\": \"prod\"}\n )\n assert _task_matches_dashboard(task, 42, \"prod\") is True\n assert _task_matches_dashboard(task, 43, \"prod\") is False\n assert _task_matches_dashboard(task, 42, \"dev\") is False\n\n llm_task = MagicMock(\n plugin_id=\"llm_dashboard_validation\",\n params={\"dashboard_id\": 42, \"environment_id\": \"prod\"},\n )\n assert _task_matches_dashboard(llm_task, 42, \"prod\") is True\n assert _task_matches_dashboard(llm_task, 42, None) is True\n\n\n# [/DEF:test_task_matches_dashboard_logic:Function]\n# [/DEF:TestDashboardsApi:Module]\n" + "body": "# [DEF:TestDashboardsApi:Module]\n# @RELATION: VERIFIES ->[src.api.routes.dashboards]\n# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API\n# @LAYER: Domain (Tests)\n# @SEMANTICS: tests, dashboards, api, contract, remediation\nfrom datetime import UTC, datetime\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\nfrom src.api.routes.dashboards import (\n DashboardDetailResponse,\n DashboardsResponse,\n DashboardTaskHistoryResponse,\n DatabaseMappingsResponse,\n)\nfrom src.app import app\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_mapping_service,\n get_resource_service,\n get_task_manager,\n has_permission,\n)\n\n# Global mock user\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture(autouse=True)\ndef mock_deps():\n config_manager = MagicMock()\n task_manager = MagicMock()\n resource_service = MagicMock()\n mapping_service = MagicMock()\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n app.dependency_overrides[get_mapping_service] = lambda: mapping_service\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n # Overrides for specific permission checks\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:migration\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"plugin:backup\", \"EXECUTE\")] = (\n lambda: mock_user\n )\n app.dependency_overrides[has_permission(\"tasks\", \"READ\")] = lambda: mock_user\n app.dependency_overrides[has_permission(\"dashboards\", \"READ\")] = lambda: mock_user\n\n yield {\n \"config\": config_manager,\n \"task\": task_manager,\n \"resource\": resource_service,\n \"mapping\": mapping_service,\n }\n app.dependency_overrides.clear()\n\n\nclient = TestClient(app)\n\n# --- 1. get_dashboards tests ---\n\n\n# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_success(mock_deps):\n \"\"\"Uses @TEST_FIXTURE: dashboard_list_happy data.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n\n # @TEST_FIXTURE: dashboard_list_happy -> {\"id\": 1, \"title\": \"Main Revenue\"}\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Main Revenue\",\n \"slug\": \"main-revenue\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n }\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&page=1&page_size=10\")\n\n assert response.status_code == 200\n data = response.json()\n\n # exhaustive @POST assertions\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 1 # @TEST_FIXTURE: expected_count: 1\n assert data[\"dashboards\"][0][\"title\"] == \"Main Revenue\"\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 10\n assert data[\"total_pages\"] == 1\n\n # schema validation\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_success:Function]\n\n\n# [DEF:test_get_dashboards_with_search:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_with_search(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n return_value=[\n {\"id\": 1, \"title\": \"Sales Report\", \"slug\": \"sales\"},\n {\"id\": 2, \"title\": \"Marketing\", \"slug\": \"marketing\"},\n ]\n )\n\n response = client.get(\"/api/dashboards?env_id=prod&search=sales\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales Report\"\n\n\n# [/DEF:test_get_dashboards_with_search:Function]\n\n\n# [DEF:test_get_dashboards_empty:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_empty(mock_deps):\n \"\"\"@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"empty_env\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(return_value=[])\n\n response = client.get(\"/api/dashboards?env_id=empty_env\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert len(data[\"dashboards\"]) == 0\n assert data[\"total_pages\"] == 1\n DashboardsResponse(**data)\n\n\n# [/DEF:test_get_dashboards_empty:Function]\n\n\n# [DEF:test_get_dashboards_superset_failure:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_superset_failure(mock_deps):\n \"\"\"@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"bad_conn\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].get_all_tasks.return_value = []\n mock_deps[\"resource\"].get_dashboards_with_status = AsyncMock(\n side_effect=Exception(\"Connection refused\")\n )\n\n response = client.get(\"/api/dashboards?env_id=bad_conn\")\n assert response.status_code == 503\n assert \"Failed to fetch dashboards\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_superset_failure:Function]\n\n\n# [DEF:test_get_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards?env_id=nonexistent\")\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_env_not_found:Function]\n\n\n# [DEF:test_get_dashboards_invalid_pagination:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboards_invalid_pagination(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n # page < 1\n assert client.get(\"/api/dashboards?env_id=prod&page=0\").status_code == 400\n assert client.get(\"/api/dashboards?env_id=prod&page=-1\").status_code == 400\n\n # page_size < 1\n assert client.get(\"/api/dashboards?env_id=prod&page_size=0\").status_code == 400\n\n # page_size > 100\n assert client.get(\"/api/dashboards?env_id=prod&page_size=101\").status_code == 400\n\n\n# --- 2. get_database_mappings tests ---\n\n# [/DEF:test_get_dashboards_invalid_pagination:Function]\n\n\n# [DEF:test_get_database_mappings_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_database_mappings_success(mock_deps):\n mock_s = MagicMock()\n mock_s.id = \"s\"\n mock_t = MagicMock()\n mock_t.id = \"t\"\n mock_deps[\"config\"].get_environments.return_value = [mock_s, mock_t]\n\n mock_deps[\"mapping\"].get_suggestions = AsyncMock(\n return_value=[{\"source_db\": \"src\", \"target_db\": \"dst\", \"confidence\": 0.9}]\n )\n response = client.get(\"/api/dashboards/db-mappings?source_env_id=s&target_env_id=t\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"mappings\"]) == 1\n assert data[\"mappings\"][0][\"confidence\"] == 0.9\n DatabaseMappingsResponse(**data)\n\n\n# [/DEF:test_get_database_mappings_success:Function]\n\n\n# [DEF:test_get_database_mappings_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_database_mappings_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\n \"/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t\"\n )\n assert response.status_code == 404\n\n\n# --- 3. get_dashboard_detail tests ---\n\n# [/DEF:test_get_database_mappings_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_detail_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_detail_success(mock_deps):\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n\n mock_client = MagicMock()\n detail_payload = {\n \"id\": 42,\n \"title\": \"Detail\",\n \"charts\": [],\n \"datasets\": [],\n \"chart_count\": 0,\n \"dataset_count\": 0,\n }\n mock_client.get_dashboard_detail.return_value = detail_payload\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == 42\n DashboardDetailResponse(**data)\n\n\n# [/DEF:test_get_dashboard_detail_success:Function]\n\n\n# [DEF:test_get_dashboard_detail_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_detail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards/42?env_id=missing\")\n assert response.status_code == 404\n\n\n# --- 4. get_dashboard_tasks_history tests ---\n\n# [/DEF:test_get_dashboard_detail_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_tasks_history_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_success(mock_deps):\n now = datetime.now(UTC)\n task1 = MagicMock(\n id=\"t1\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=now,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n mock_deps[\"task\"].get_all_tasks.return_value = [task1]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 1\n DashboardTaskHistoryResponse(**data)\n\n\n# [/DEF:test_get_dashboard_tasks_history_success:Function]\n\n\n# [DEF:test_get_dashboard_tasks_history_sorting:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_sorting(mock_deps):\n \"\"\"@POST: Response contains sorted task history (newest first).\"\"\"\n from datetime import timedelta\n\n now = datetime.now(UTC)\n older = now - timedelta(hours=2)\n newest = now\n\n task_old = MagicMock(\n id=\"t-old\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=older,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n task_new = MagicMock(\n id=\"t-new\",\n plugin_id=\"superset-backup\",\n status=\"RUNNING\",\n started_at=newest,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n\n # Provide in wrong order to verify the endpoint sorts\n mock_deps[\"task\"].get_all_tasks.return_value = [task_old, task_new]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"items\"]) == 2\n # Newest first\n assert data[\"items\"][0][\"id\"] == \"t-new\"\n assert data[\"items\"][1][\"id\"] == \"t-old\"\n\n\n# --- 5. get_dashboard_thumbnail tests ---\n\n# [/DEF:test_get_dashboard_tasks_history_sorting:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_success(mock_deps):\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_client = MagicMock()\n mock_response = MagicMock(\n status_code=200, content=b\"img\", headers={\"Content-Type\": \"image/png\"}\n )\n mock_client.network.request.side_effect = (\n lambda method, endpoint, **kw: {\"image_url\": \"url\"}\n if method == \"POST\"\n else mock_response\n )\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n assert response.status_code == 200\n assert response.content == b\"img\"\n\n\n# [/DEF:test_get_dashboard_thumbnail_success:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_env_not_found(mock_deps):\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=missing\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_dashboard_thumbnail_env_not_found:Function]\n\n\n# [DEF:test_get_dashboard_thumbnail_202:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_thumbnail_202(mock_deps):\n \"\"\"@POST: Returns 202 when thumbnail is being prepared by Superset.\"\"\"\n with patch(\"src.api.routes.dashboards.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_client = MagicMock()\n\n # POST cache_dashboard_screenshot returns image_url\n mock_client.network.request.side_effect = [\n {\"image_url\": \"/api/v1/dashboard/42/thumbnail/abc123/\"}, # POST\n MagicMock(\n status_code=202,\n json=lambda: {\"message\": \"Thumbnail is being generated\"},\n headers={\"Content-Type\": \"application/json\"},\n ), # GET thumbnail -> 202\n ]\n mock_client_cls.return_value = mock_client\n\n response = client.get(\"/api/dashboards/42/thumbnail?env_id=prod\")\n assert response.status_code == 202\n assert \"Thumbnail is being generated\" in response.json()[\"message\"]\n\n\n# --- 6. migrate_dashboards tests ---\n\n# [/DEF:test_get_dashboard_thumbnail_202:Function]\n\n\n# [DEF:test_migrate_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_success(mock_deps):\n mock_s = MagicMock()\n mock_s.id = \"s\"\n mock_t = MagicMock()\n mock_t.id = \"t\"\n mock_deps[\"config\"].get_environments.return_value = [mock_s, mock_t]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"task-123\"))\n\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"s\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"task-123\"\n\n\n# [/DEF:test_migrate_dashboards_success:Function]\n\n\n# [DEF:test_migrate_dashboards_pre_checks:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_pre_checks(mock_deps):\n # Missing IDs\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"s\", \"target_env_id\": \"t\", \"dashboard_ids\": []},\n )\n assert response.status_code == 400\n assert \"At least one dashboard ID must be provided\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_migrate_dashboards_pre_checks:Function]\n\n\n# [DEF:test_migrate_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_migrate_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: source_env_id and target_env_id are valid environment IDs.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/migrate\",\n json={\"source_env_id\": \"ghost\", \"target_env_id\": \"t\", \"dashboard_ids\": [1]},\n )\n assert response.status_code == 404\n assert \"Source environment not found\" in response.json()[\"detail\"]\n\n\n# --- 7. backup_dashboards tests ---\n\n# [/DEF:test_migrate_dashboards_env_not_found:Function]\n\n\n# [DEF:test_backup_dashboards_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_success(mock_deps):\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"backup-123\"))\n\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"prod\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"backup-123\"\n\n\n# [/DEF:test_backup_dashboards_success:Function]\n\n\n# [DEF:test_backup_dashboards_pre_checks:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_pre_checks(mock_deps):\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"prod\", \"dashboard_ids\": []}\n )\n assert response.status_code == 400\n\n\n# [/DEF:test_backup_dashboards_pre_checks:Function]\n\n\n# [DEF:test_backup_dashboards_env_not_found:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_env_not_found(mock_deps):\n \"\"\"@PRE: env_id is a valid environment ID.\"\"\"\n mock_deps[\"config\"].get_environments.return_value = []\n response = client.post(\n \"/api/dashboards/backup\", json={\"env_id\": \"ghost\", \"dashboard_ids\": [1]}\n )\n assert response.status_code == 404\n assert \"Environment not found\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_backup_dashboards_env_not_found:Function]\n\n\n# [DEF:test_backup_dashboards_with_schedule:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_backup_dashboards_with_schedule(mock_deps):\n \"\"\"@POST: If schedule is provided, a scheduled task is created.\"\"\"\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_deps[\"config\"].get_environments.return_value = [mock_env]\n mock_deps[\"task\"].create_task = AsyncMock(return_value=MagicMock(id=\"sched-456\"))\n\n response = client.post(\n \"/api/dashboards/backup\",\n json={\"env_id\": \"prod\", \"dashboard_ids\": [1], \"schedule\": \"0 0 * * *\"},\n )\n assert response.status_code == 200\n assert response.json()[\"task_id\"] == \"sched-456\"\n\n # Verify schedule was propagated to create_task\n call_kwargs = mock_deps[\"task\"].create_task.call_args\n task_params = call_kwargs.kwargs.get(\"params\") or call_kwargs[1].get(\"params\", {})\n assert task_params[\"schedule\"] == \"0 0 * * *\"\n\n\n# --- 8. Internal logic: _task_matches_dashboard ---\n# [/DEF:test_backup_dashboards_with_schedule:Function]\n\nfrom src.api.routes.dashboards._projection import _task_matches_dashboard\n\n\n# [DEF:test_task_matches_dashboard_logic:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_task_matches_dashboard_logic():\n task = MagicMock(\n plugin_id=\"superset-backup\", params={\"dashboards\": [42], \"env\": \"prod\"}\n )\n assert _task_matches_dashboard(task, 42, \"prod\") is True\n assert _task_matches_dashboard(task, 43, \"prod\") is False\n assert _task_matches_dashboard(task, 42, \"dev\") is False\n\n llm_task = MagicMock(\n plugin_id=\"llm_dashboard_validation\",\n params={\"dashboard_id\": 42, \"environment_id\": \"prod\"},\n )\n assert _task_matches_dashboard(llm_task, 42, \"prod\") is True\n assert _task_matches_dashboard(llm_task, 42, None) is True\n\n\n# [/DEF:test_task_matches_dashboard_logic:Function]\n# [/DEF:TestDashboardsApi:Module]\n" }, { "contract_id": "test_get_dashboard_tasks_history_success", "contract_type": "Function", "file_path": "backend/tests/test_dashboards_api.py", - "start_line": 300, - "end_line": 323, + "start_line": 301, + "end_line": 324, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -103438,14 +71825,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboard_tasks_history_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_success(mock_deps):\n now = datetime.now(timezone.utc)\n task1 = MagicMock(\n id=\"t1\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=now,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n mock_deps[\"task\"].get_all_tasks.return_value = [task1]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 1\n DashboardTaskHistoryResponse(**data)\n\n\n# [/DEF:test_get_dashboard_tasks_history_success:Function]\n" + "body": "# [DEF:test_get_dashboard_tasks_history_success:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_success(mock_deps):\n now = datetime.now(UTC)\n task1 = MagicMock(\n id=\"t1\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=now,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n mock_deps[\"task\"].get_all_tasks.return_value = [task1]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"dashboard_id\"] == 42\n assert len(data[\"items\"]) == 1\n DashboardTaskHistoryResponse(**data)\n\n\n# [/DEF:test_get_dashboard_tasks_history_success:Function]\n" }, { "contract_id": "test_get_dashboard_tasks_history_sorting", "contract_type": "Function", "file_path": "backend/tests/test_dashboards_api.py", - "start_line": 326, - "end_line": 369, + "start_line": 327, + "end_line": 370, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -103478,14 +71865,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_get_dashboard_tasks_history_sorting:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_sorting(mock_deps):\n \"\"\"@POST: Response contains sorted task history (newest first).\"\"\"\n from datetime import timedelta\n\n now = datetime.now(timezone.utc)\n older = now - timedelta(hours=2)\n newest = now\n\n task_old = MagicMock(\n id=\"t-old\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=older,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n task_new = MagicMock(\n id=\"t-new\",\n plugin_id=\"superset-backup\",\n status=\"RUNNING\",\n started_at=newest,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n\n # Provide in wrong order to verify the endpoint sorts\n mock_deps[\"task\"].get_all_tasks.return_value = [task_old, task_new]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"items\"]) == 2\n # Newest first\n assert data[\"items\"][0][\"id\"] == \"t-new\"\n assert data[\"items\"][1][\"id\"] == \"t-old\"\n\n\n# --- 5. get_dashboard_thumbnail tests ---\n\n# [/DEF:test_get_dashboard_tasks_history_sorting:Function]\n" + "body": "# [DEF:test_get_dashboard_tasks_history_sorting:Function]\n# @RELATION: BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_sorting(mock_deps):\n \"\"\"@POST: Response contains sorted task history (newest first).\"\"\"\n from datetime import timedelta\n\n now = datetime.now(UTC)\n older = now - timedelta(hours=2)\n newest = now\n\n task_old = MagicMock(\n id=\"t-old\",\n plugin_id=\"superset-backup\",\n status=\"SUCCESS\",\n started_at=older,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n task_new = MagicMock(\n id=\"t-new\",\n plugin_id=\"superset-backup\",\n status=\"RUNNING\",\n started_at=newest,\n finished_at=None,\n params={\"env\": \"prod\", \"dashboards\": [42]},\n result={},\n )\n\n # Provide in wrong order to verify the endpoint sorts\n mock_deps[\"task\"].get_all_tasks.return_value = [task_old, task_new]\n\n response = client.get(\"/api/dashboards/42/tasks?env_id=prod\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"items\"]) == 2\n # Newest first\n assert data[\"items\"][0][\"id\"] == \"t-new\"\n assert data[\"items\"][1][\"id\"] == \"t-old\"\n\n\n# --- 5. get_dashboard_thumbnail tests ---\n\n# [/DEF:test_get_dashboard_tasks_history_sorting:Function]\n" }, { "contract_id": "test_get_dashboard_thumbnail_env_not_found", "contract_type": "Function", "file_path": "backend/tests/test_dashboards_api.py", - "start_line": 398, - "end_line": 406, + "start_line": 399, + "end_line": 407, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -103524,8 +71911,8 @@ "contract_id": "test_get_dashboard_thumbnail_202", "contract_type": "Function", "file_path": "backend/tests/test_dashboards_api.py", - "start_line": 409, - "end_line": 437, + "start_line": 410, + "end_line": 438, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -103564,8 +71951,8 @@ "contract_id": "test_migrate_dashboards_pre_checks", "contract_type": "Function", "file_path": "backend/tests/test_dashboards_api.py", - "start_line": 461, - "end_line": 473, + "start_line": 462, + "end_line": 474, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -103604,8 +71991,8 @@ "contract_id": "test_backup_dashboards_pre_checks", "contract_type": "Function", "file_path": "backend/tests/test_dashboards_api.py", - "start_line": 512, - "end_line": 521, + "start_line": 513, + "end_line": 522, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -103644,8 +72031,8 @@ "contract_id": "test_backup_dashboards_with_schedule", "contract_type": "Function", "file_path": "backend/tests/test_dashboards_api.py", - "start_line": 539, - "end_line": 562, + "start_line": 540, + "end_line": 563, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -103684,8 +72071,8 @@ "contract_id": "test_task_matches_dashboard_logic", "contract_type": "Function", "file_path": "backend/tests/test_dashboards_api.py", - "start_line": 567, - "end_line": 585, + "start_line": 568, + "end_line": 586, "tier": "TIER_1", "complexity": 2, "metadata": {}, @@ -103725,11 +72112,10 @@ "contract_type": "Module", "file_path": "backend/tests/test_log_persistence.py", "start_line": 1, - "end_line": 339, - "tier": "TIER_3", - "complexity": 5, + "end_line": 338, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": "5", "LAYER": "Test", "PURPOSE": "Unit tests for TaskLogPersistenceService.", "SEMANTICS": [ @@ -103749,65 +72135,11 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 2, "contract_type": "Module" } }, @@ -103831,18 +72163,17 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_log_persistence:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: test, log, persistence, unit_test\n# @PURPOSE: Unit tests for TaskLogPersistenceService.\n# @LAYER: Test\n# @COMPLEXITY: 5\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom unittest.mock import patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.models.mapping import Base\nfrom src.core.task_manager.persistence import TaskLogPersistenceService\nfrom src.core.task_manager.models import LogEntry, LogFilter\n# [/SECTION]\n\n# [DEF:TestLogPersistence:Class]\n# @RELATION: BINDS_TO -> test_log_persistence\n# @PURPOSE: Test suite for TaskLogPersistenceService.\n# @COMPLEXITY: 5\n# @TEST_DATA: log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}\nclass TestLogPersistence:\n\n # [DEF:setup_class:Function]\n # @PURPOSE: Setup test database and service instance.\n # @PRE: None.\n # @POST: In-memory database and service instance created.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskLogPersistenceService()\n # [/DEF:setup_class:Function]\n\n # [DEF:teardown_class:Function]\n # @PURPOSE: Clean up test database.\n # @PRE: None.\n # @POST: Database disposed.\n @classmethod\n def teardown_class(cls):\n \"\"\"Dispose of the database engine.\"\"\"\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n\n # [DEF:setup_method:Function]\n # @PURPOSE: Setup for each test method — clean task_logs table.\n # @PRE: None.\n # @POST: task_logs table is empty.\n def setup_method(self):\n \"\"\"Clean task_logs table before each test.\"\"\"\n session = self.TestSessionLocal()\n from src.models.task import TaskLogRecord\n session.query(TaskLogRecord).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n\n def _patched(self, method_name):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n # [DEF:test_add_logs_single:Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.utcnow(),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # [/DEF:test_add_logs_single:Function]\n\n # [DEF:test_add_logs_batch:Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # [/DEF:test_add_logs_batch:Function]\n\n # [DEF:test_add_logs_empty:Function]\n # @PURPOSE: Test adding empty log list (should be no-op).\n # @PRE: Service initialized.\n # @POST: No logs added.\n def test_add_logs_empty(self):\n \"\"\"Test adding empty log list is a no-op.\"\"\"\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-X\", [])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-X\").all()\n session.close()\n assert len(results) == 0\n # [/DEF:test_add_logs_empty:Function]\n\n # [DEF:test_get_logs_by_task_id:Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # [/DEF:test_get_logs_by_task_id:Function]\n\n # [DEF:test_get_logs_with_filters:Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # [/DEF:test_get_logs_with_filters:Function]\n\n # [DEF:test_get_logs_with_pagination:Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # [/DEF:test_get_logs_with_pagination:Function]\n\n # [DEF:test_get_logs_with_search:Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # [/DEF:test_get_logs_with_search:Function]\n\n # [DEF:test_get_log_stats:Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # [/DEF:test_get_log_stats:Function]\n\n # [DEF:test_get_sources:Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # [/DEF:test_get_sources:Function]\n\n # [DEF:test_delete_logs_for_task:Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # [/DEF:test_delete_logs_for_task:Function]\n\n # [DEF:test_delete_logs_for_tasks:Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # [/DEF:test_delete_logs_for_tasks:Function]\n\n # [DEF:test_delete_logs_for_tasks_empty:Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: Service initialized.\n # @POST: No error, no deletion.\n def test_delete_logs_for_tasks_empty(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([]) # Should not raise\n # [/DEF:test_delete_logs_for_tasks_empty:Function]\n\n# [/DEF:TestLogPersistence:Class]\n# [/DEF:test_log_persistence:Module]\n" + "body": "# [DEF:test_log_persistence:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: test, log, persistence, unit_test\n# @PURPOSE: Unit tests for TaskLogPersistenceService.\n# @LAYER: Test\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom unittest.mock import patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.task_manager.models import LogEntry, LogFilter\nfrom src.core.task_manager.persistence import TaskLogPersistenceService\nfrom src.models.mapping import Base\n\n# [/SECTION]\n\n# [DEF:TestLogPersistence:Class]\n# @RELATION: BINDS_TO -> test_log_persistence\n# @PURPOSE: Test suite for TaskLogPersistenceService.\n# @TEST_DATA: log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}\nclass TestLogPersistence:\n\n # [DEF:setup_class:Function]\n # @PURPOSE: Setup test database and service instance.\n # @PRE: None.\n # @POST: In-memory database and service instance created.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskLogPersistenceService()\n # [/DEF:setup_class:Function]\n\n # [DEF:teardown_class:Function]\n # @PURPOSE: Clean up test database.\n # @PRE: None.\n # @POST: Database disposed.\n @classmethod\n def teardown_class(cls):\n \"\"\"Dispose of the database engine.\"\"\"\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n\n # [DEF:setup_method:Function]\n # @PURPOSE: Setup for each test method — clean task_logs table.\n # @PRE: None.\n # @POST: task_logs table is empty.\n def setup_method(self):\n \"\"\"Clean task_logs table before each test.\"\"\"\n session = self.TestSessionLocal()\n from src.models.task import TaskLogRecord\n session.query(TaskLogRecord).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n\n def _patched(self, method_name):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n # [DEF:test_add_logs_single:Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.utcnow(),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # [/DEF:test_add_logs_single:Function]\n\n # [DEF:test_add_logs_batch:Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # [/DEF:test_add_logs_batch:Function]\n\n # [DEF:test_add_logs_empty:Function]\n # @PURPOSE: Test adding empty log list (should be no-op).\n # @PRE: Service initialized.\n # @POST: No logs added.\n def test_add_logs_empty(self):\n \"\"\"Test adding empty log list is a no-op.\"\"\"\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-X\", [])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-X\").all()\n session.close()\n assert len(results) == 0\n # [/DEF:test_add_logs_empty:Function]\n\n # [DEF:test_get_logs_by_task_id:Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # [/DEF:test_get_logs_by_task_id:Function]\n\n # [DEF:test_get_logs_with_filters:Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # [/DEF:test_get_logs_with_filters:Function]\n\n # [DEF:test_get_logs_with_pagination:Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # [/DEF:test_get_logs_with_pagination:Function]\n\n # [DEF:test_get_logs_with_search:Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # [/DEF:test_get_logs_with_search:Function]\n\n # [DEF:test_get_log_stats:Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # [/DEF:test_get_log_stats:Function]\n\n # [DEF:test_get_sources:Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # [/DEF:test_get_sources:Function]\n\n # [DEF:test_delete_logs_for_task:Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # [/DEF:test_delete_logs_for_task:Function]\n\n # [DEF:test_delete_logs_for_tasks:Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # [/DEF:test_delete_logs_for_tasks:Function]\n\n # [DEF:test_delete_logs_for_tasks_empty:Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: Service initialized.\n # @POST: No error, no deletion.\n def test_delete_logs_for_tasks_empty(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([]) # Should not raise\n # [/DEF:test_delete_logs_for_tasks_empty:Function]\n\n# [/DEF:TestLogPersistence:Class]\n# [/DEF:test_log_persistence:Module]\n" }, { "contract_id": "TestLogPersistence", "contract_type": "Class", "file_path": "backend/tests/test_log_persistence.py", "start_line": 20, - "end_line": 338, - "tier": "TIER_3", - "complexity": 5, + "end_line": 337, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "PURPOSE": "Test suite for TaskLogPersistenceService.", "TEST_DATA": "log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}" }, @@ -103855,6 +72186,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "unknown_tag", "tag": "TEST_DATA", @@ -103862,78 +72202,24 @@ "detail": null }, { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Class' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Class' at C1", "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Class" } } ], "anchor_syntax": "def", - "body": "# [DEF:TestLogPersistence:Class]\n# @RELATION: BINDS_TO -> test_log_persistence\n# @PURPOSE: Test suite for TaskLogPersistenceService.\n# @COMPLEXITY: 5\n# @TEST_DATA: log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}\nclass TestLogPersistence:\n\n # [DEF:setup_class:Function]\n # @PURPOSE: Setup test database and service instance.\n # @PRE: None.\n # @POST: In-memory database and service instance created.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskLogPersistenceService()\n # [/DEF:setup_class:Function]\n\n # [DEF:teardown_class:Function]\n # @PURPOSE: Clean up test database.\n # @PRE: None.\n # @POST: Database disposed.\n @classmethod\n def teardown_class(cls):\n \"\"\"Dispose of the database engine.\"\"\"\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n\n # [DEF:setup_method:Function]\n # @PURPOSE: Setup for each test method — clean task_logs table.\n # @PRE: None.\n # @POST: task_logs table is empty.\n def setup_method(self):\n \"\"\"Clean task_logs table before each test.\"\"\"\n session = self.TestSessionLocal()\n from src.models.task import TaskLogRecord\n session.query(TaskLogRecord).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n\n def _patched(self, method_name):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n # [DEF:test_add_logs_single:Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.utcnow(),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # [/DEF:test_add_logs_single:Function]\n\n # [DEF:test_add_logs_batch:Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # [/DEF:test_add_logs_batch:Function]\n\n # [DEF:test_add_logs_empty:Function]\n # @PURPOSE: Test adding empty log list (should be no-op).\n # @PRE: Service initialized.\n # @POST: No logs added.\n def test_add_logs_empty(self):\n \"\"\"Test adding empty log list is a no-op.\"\"\"\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-X\", [])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-X\").all()\n session.close()\n assert len(results) == 0\n # [/DEF:test_add_logs_empty:Function]\n\n # [DEF:test_get_logs_by_task_id:Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # [/DEF:test_get_logs_by_task_id:Function]\n\n # [DEF:test_get_logs_with_filters:Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # [/DEF:test_get_logs_with_filters:Function]\n\n # [DEF:test_get_logs_with_pagination:Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # [/DEF:test_get_logs_with_pagination:Function]\n\n # [DEF:test_get_logs_with_search:Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # [/DEF:test_get_logs_with_search:Function]\n\n # [DEF:test_get_log_stats:Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # [/DEF:test_get_log_stats:Function]\n\n # [DEF:test_get_sources:Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # [/DEF:test_get_sources:Function]\n\n # [DEF:test_delete_logs_for_task:Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # [/DEF:test_delete_logs_for_task:Function]\n\n # [DEF:test_delete_logs_for_tasks:Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # [/DEF:test_delete_logs_for_tasks:Function]\n\n # [DEF:test_delete_logs_for_tasks_empty:Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: Service initialized.\n # @POST: No error, no deletion.\n def test_delete_logs_for_tasks_empty(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([]) # Should not raise\n # [/DEF:test_delete_logs_for_tasks_empty:Function]\n\n# [/DEF:TestLogPersistence:Class]\n" + "body": "# [DEF:TestLogPersistence:Class]\n# @RELATION: BINDS_TO -> test_log_persistence\n# @PURPOSE: Test suite for TaskLogPersistenceService.\n# @TEST_DATA: log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}\nclass TestLogPersistence:\n\n # [DEF:setup_class:Function]\n # @PURPOSE: Setup test database and service instance.\n # @PRE: None.\n # @POST: In-memory database and service instance created.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskLogPersistenceService()\n # [/DEF:setup_class:Function]\n\n # [DEF:teardown_class:Function]\n # @PURPOSE: Clean up test database.\n # @PRE: None.\n # @POST: Database disposed.\n @classmethod\n def teardown_class(cls):\n \"\"\"Dispose of the database engine.\"\"\"\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n\n # [DEF:setup_method:Function]\n # @PURPOSE: Setup for each test method — clean task_logs table.\n # @PRE: None.\n # @POST: task_logs table is empty.\n def setup_method(self):\n \"\"\"Clean task_logs table before each test.\"\"\"\n session = self.TestSessionLocal()\n from src.models.task import TaskLogRecord\n session.query(TaskLogRecord).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n\n def _patched(self, method_name):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n # [DEF:test_add_logs_single:Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.utcnow(),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # [/DEF:test_add_logs_single:Function]\n\n # [DEF:test_add_logs_batch:Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # [/DEF:test_add_logs_batch:Function]\n\n # [DEF:test_add_logs_empty:Function]\n # @PURPOSE: Test adding empty log list (should be no-op).\n # @PRE: Service initialized.\n # @POST: No logs added.\n def test_add_logs_empty(self):\n \"\"\"Test adding empty log list is a no-op.\"\"\"\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-X\", [])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-X\").all()\n session.close()\n assert len(results) == 0\n # [/DEF:test_add_logs_empty:Function]\n\n # [DEF:test_get_logs_by_task_id:Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # [/DEF:test_get_logs_by_task_id:Function]\n\n # [DEF:test_get_logs_with_filters:Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # [/DEF:test_get_logs_with_filters:Function]\n\n # [DEF:test_get_logs_with_pagination:Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # [/DEF:test_get_logs_with_pagination:Function]\n\n # [DEF:test_get_logs_with_search:Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # [/DEF:test_get_logs_with_search:Function]\n\n # [DEF:test_get_log_stats:Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # [/DEF:test_get_log_stats:Function]\n\n # [DEF:test_get_sources:Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # [/DEF:test_get_sources:Function]\n\n # [DEF:test_delete_logs_for_task:Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # [/DEF:test_delete_logs_for_task:Function]\n\n # [DEF:test_delete_logs_for_tasks:Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # [/DEF:test_delete_logs_for_tasks:Function]\n\n # [DEF:test_delete_logs_for_tasks_empty:Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: Service initialized.\n # @POST: No error, no deletion.\n def test_delete_logs_for_tasks_empty(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([]) # Should not raise\n # [/DEF:test_delete_logs_for_tasks_empty:Function]\n\n# [/DEF:TestLogPersistence:Class]\n" }, { "contract_id": "test_add_logs_single", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 70, - "end_line": 96, + "start_line": 69, + "end_line": 95, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -103969,8 +72255,8 @@ "contract_id": "test_add_logs_batch", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 98, - "end_line": 119, + "start_line": 97, + "end_line": 118, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104006,8 +72292,8 @@ "contract_id": "test_add_logs_empty", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 121, - "end_line": 135, + "start_line": 120, + "end_line": 134, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104043,8 +72329,8 @@ "contract_id": "test_get_logs_by_task_id", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 137, - "end_line": 155, + "start_line": 136, + "end_line": 154, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104080,8 +72366,8 @@ "contract_id": "test_get_logs_with_filters", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 157, - "end_line": 182, + "start_line": 156, + "end_line": 181, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104117,8 +72403,8 @@ "contract_id": "test_get_logs_with_pagination", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 184, - "end_line": 204, + "start_line": 183, + "end_line": 203, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104154,8 +72440,8 @@ "contract_id": "test_get_logs_with_search", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 206, - "end_line": 224, + "start_line": 205, + "end_line": 223, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104191,8 +72477,8 @@ "contract_id": "test_get_log_stats", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 226, - "end_line": 251, + "start_line": 225, + "end_line": 250, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104228,8 +72514,8 @@ "contract_id": "test_get_sources", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 253, - "end_line": 274, + "start_line": 252, + "end_line": 273, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104265,8 +72551,8 @@ "contract_id": "test_delete_logs_for_task", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 276, - "end_line": 302, + "start_line": 275, + "end_line": 301, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104302,8 +72588,8 @@ "contract_id": "test_delete_logs_for_tasks", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 304, - "end_line": 326, + "start_line": 303, + "end_line": 325, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104339,8 +72625,8 @@ "contract_id": "test_delete_logs_for_tasks_empty", "contract_type": "Function", "file_path": "backend/tests/test_log_persistence.py", - "start_line": 328, - "end_line": 336, + "start_line": 327, + "end_line": 335, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104377,11 +72663,10 @@ "contract_type": "Module", "file_path": "backend/tests/test_logger.py", "start_line": 1, - "end_line": 360, - "tier": "TIER_2", - "complexity": 3, + "end_line": 358, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "INVARIANT": "All required log statements must correctly check the threshold.", "LAYER": "Logging (Tests)", "PURPOSE": "Unit tests for the custom logger CoT JSON formatters and configuration context manager.", @@ -104405,26 +72690,215 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "message": "@INVARIANT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 3, + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Module" } } ], "anchor_syntax": "def", - "body": "# [DEF:TestLogger:Module]\n# @COMPLEXITY: 3\n# @SEMANTICS: logging, tests, belief_state, cot, json\n# @PURPOSE: Unit tests for the custom logger CoT JSON formatters and configuration context manager.\n# @LAYER: Logging (Tests)\n# @RELATION: VERIFIES -> src/core/logger.py\n# @INVARIANT: All required log statements must correctly check the threshold.\n\nimport pytest\nimport logging\n\nfrom src.core.logger import (\n belief_scope,\n logger,\n configure_logger,\n get_task_log_level,\n should_log_task_level,\n CotJsonFormatter,\n)\nfrom src.core.config_models import LoggingConfig\n\n\n@pytest.fixture(autouse=True)\ndef reset_logger_state():\n \"\"\"Reset logger state before each test to avoid cross-test contamination.\"\"\"\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n # Also reset the logger level for caplog to work correctly\n logging.getLogger(\"superset_tools_app\").setLevel(logging.DEBUG)\n yield\n # Reset after test too\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n\n# [DEF:test_belief_scope_logs_reason_reflect_at_debug:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REASON (entry) and REFLECT (coherence) markers.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n\n # Check that the records contain the expected markers\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'TestFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'TestFunction'\n ]\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n\n assert len(reason_records) >= 1, \"REASON marker not found for TestFunction\"\n assert len(reflect_records) >= 1, \"REFLECT marker not found for TestFunction\"\n assert len(info_records) >= 1, \"INFO log 'Doing something important' not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# [/DEF:test_belief_scope_logs_reason_reflect_at_debug:Function]\n\n\n# [DEF:test_belief_scope_error_handling:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs EXPLORE marker on exception.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain EXPLORE marker with error context.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE marker on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with pytest.raises(ValueError):\n with belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n\n # Check that an EXPLORE marker was emitted\n explore_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'EXPLORE'\n and getattr(r, 'src', None) == 'FailingFunction'\n ]\n\n assert len(explore_records) >= 1, f\"EXPLORE marker not found for FailingFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Something went wrong' in explore_records[0].getMessage() or \\\n 'Something went wrong' in str(getattr(explore_records[0], 'error', ''))\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# [/DEF:test_belief_scope_error_handling:Function]\n\n\n# [DEF:test_belief_scope_success_coherence:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs REFLECT marker on success.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_coherence(caplog):\n \"\"\"Test that belief_scope logs REFLECT marker on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"SuccessFunction\"):\n pass\n\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT'\n and getattr(r, 'src', None) == 'SuccessFunction'\n ]\n\n assert len(reflect_records) >= 1, f\"REFLECT marker not found for SuccessFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Coherence OK' in reflect_records[0].getMessage() or \\\n getattr(reflect_records[0], 'intent', '') == 'Coherence OK'\n\n\n# [/DEF:test_belief_scope_success_coherence:Function]\n\n\n# [DEF:test_belief_scope_reason_not_visible_at_info:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\n# @PRE: belief_scope is available. caplog fixture is used.\n# @POST: REASON/REFLECT markers are not captured at INFO level.\ndef test_belief_scope_reason_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n\n # The REASON and REFLECT markers are debug-level, so they should NOT appear at INFO\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n\n assert len(reason_records) == 0, \"REASON marker should not be visible at INFO\"\n assert len(reflect_records) == 0, \"REFLECT marker should not be visible at INFO\"\n\n # But the INFO-level message should be visible\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n assert len(info_records) >= 1, \"INFO log 'Doing something important' should be visible\"\n# [/DEF:test_belief_scope_reason_not_visible_at_info:Function]\n\n\n# [DEF:test_task_log_level_default:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that default task log level is INFO.\n# @PRE: None.\n# @POST: Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO.\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# [/DEF:test_task_log_level_default:Function]\n\n\n# [DEF:test_should_log_task_level:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that should_log_task_level correctly filters log levels.\n# @PRE: None.\n# @POST: Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# [/DEF:test_should_log_task_level:Function]\n\n\n# [DEF:test_configure_logger_task_log_level:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that configure_logger updates task_log_level.\n# @PRE: LoggingConfig is available.\n# @POST: task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n\n # Reset to INFO\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n assert get_task_log_level() == \"INFO\", \"task_log_level should be reset to INFO\"\n# [/DEF:test_configure_logger_task_log_level:Function]\n\n\n# [DEF:test_enable_belief_state_flag:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that enable_belief_state flag controls belief_scope entry logging.\n# @PRE: LoggingConfig is available. caplog fixture is used.\n# @POST: REASON entry marker suppressed when disabled; REFLECT coherence still logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n # REASON entry marker should NOT be logged when disabled\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reason_records) == 0, \"REASON entry should not be logged when disabled\"\n\n # REFLECT coherence marker should still be logged (internal tracking)\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reflect_records) >= 1, \"REFLECT coherence should still be logged\"\n\n # Re-enable for other tests\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n# [/DEF:test_enable_belief_state_flag:Function]\n\n# [DEF:test_cot_json_formatter_output:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter produces valid JSON with expected fields.\ndef test_cot_json_formatter_output():\n \"\"\"Test that CotJsonFormatter produces valid JSON with expected fields.\"\"\"\n import json\n from datetime import datetime, timezone\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord with structured extra data\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Test action\",\n args=(),\n exc_info=None,\n )\n record.marker = \"REASON\"\n record.intent = \"Test action\"\n record.src = \"TestModule\"\n record.payload = {\"key\": \"value\"}\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\"\n assert parsed[\"intent\"] == \"Test action\"\n assert parsed[\"src\"] == \"TestModule\"\n assert parsed[\"payload\"] == {\"key\": \"value\"}\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_output:Function]\n\n# [DEF:test_cot_json_formatter_plain_message:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter wraps plain messages (no extra) with default marker.\ndef test_cot_json_formatter_plain_message():\n \"\"\"Test that CotJsonFormatter wraps plain messages with default marker.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord WITHOUT extra data (plain message)\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Plain info message\",\n args=(),\n exc_info=None,\n )\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\" # default for plain messages\n assert parsed[\"intent\"] == \"Plain info message\"\n assert parsed[\"src\"] == \"test.module\"\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_plain_message:Function]\n\n# [/DEF:TestLogger:Module]\n" + "body": "# [DEF:TestLogger:Module]\n# @SEMANTICS: logging, tests, belief_state, cot, json\n# @PURPOSE: Unit tests for the custom logger CoT JSON formatters and configuration context manager.\n# @LAYER: Logging (Tests)\n# @RELATION: VERIFIES -> src/core/logger.py\n# @INVARIANT: All required log statements must correctly check the threshold.\n\nimport logging\n\nimport pytest\n\nfrom src.core.config_models import LoggingConfig\nfrom src.core.logger import (\n CotJsonFormatter,\n belief_scope,\n configure_logger,\n get_task_log_level,\n logger,\n should_log_task_level,\n)\n\n\n@pytest.fixture(autouse=True)\ndef reset_logger_state():\n \"\"\"Reset logger state before each test to avoid cross-test contamination.\"\"\"\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n # Also reset the logger level for caplog to work correctly\n logging.getLogger(\"superset_tools_app\").setLevel(logging.DEBUG)\n yield\n # Reset after test too\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n\n# [DEF:test_belief_scope_logs_reason_reflect_at_debug:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REASON (entry) and REFLECT (coherence) markers.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n\n # Check that the records contain the expected markers\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'TestFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'TestFunction'\n ]\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n\n assert len(reason_records) >= 1, \"REASON marker not found for TestFunction\"\n assert len(reflect_records) >= 1, \"REFLECT marker not found for TestFunction\"\n assert len(info_records) >= 1, \"INFO log 'Doing something important' not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# [/DEF:test_belief_scope_logs_reason_reflect_at_debug:Function]\n\n\n# [DEF:test_belief_scope_error_handling:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs EXPLORE marker on exception.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain EXPLORE marker with error context.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE marker on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with pytest.raises(ValueError), belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n\n # Check that an EXPLORE marker was emitted\n explore_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'EXPLORE'\n and getattr(r, 'src', None) == 'FailingFunction'\n ]\n\n assert len(explore_records) >= 1, f\"EXPLORE marker not found for FailingFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Something went wrong' in explore_records[0].getMessage() or \\\n 'Something went wrong' in str(getattr(explore_records[0], 'error', ''))\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# [/DEF:test_belief_scope_error_handling:Function]\n\n\n# [DEF:test_belief_scope_success_coherence:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs REFLECT marker on success.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_coherence(caplog):\n \"\"\"Test that belief_scope logs REFLECT marker on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"SuccessFunction\"):\n pass\n\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT'\n and getattr(r, 'src', None) == 'SuccessFunction'\n ]\n\n assert len(reflect_records) >= 1, f\"REFLECT marker not found for SuccessFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Coherence OK' in reflect_records[0].getMessage() or \\\n getattr(reflect_records[0], 'intent', '') == 'Coherence OK'\n\n\n# [/DEF:test_belief_scope_success_coherence:Function]\n\n\n# [DEF:test_belief_scope_reason_not_visible_at_info:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\n# @PRE: belief_scope is available. caplog fixture is used.\n# @POST: REASON/REFLECT markers are not captured at INFO level.\ndef test_belief_scope_reason_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n\n # The REASON and REFLECT markers are debug-level, so they should NOT appear at INFO\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n\n assert len(reason_records) == 0, \"REASON marker should not be visible at INFO\"\n assert len(reflect_records) == 0, \"REFLECT marker should not be visible at INFO\"\n\n # But the INFO-level message should be visible\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n assert len(info_records) >= 1, \"INFO log 'Doing something important' should be visible\"\n# [/DEF:test_belief_scope_reason_not_visible_at_info:Function]\n\n\n# [DEF:test_task_log_level_default:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that default task log level is INFO.\n# @PRE: None.\n# @POST: Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO.\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# [/DEF:test_task_log_level_default:Function]\n\n\n# [DEF:test_should_log_task_level:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that should_log_task_level correctly filters log levels.\n# @PRE: None.\n# @POST: Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# [/DEF:test_should_log_task_level:Function]\n\n\n# [DEF:test_configure_logger_task_log_level:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that configure_logger updates task_log_level.\n# @PRE: LoggingConfig is available.\n# @POST: task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n\n # Reset to INFO\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n assert get_task_log_level() == \"INFO\", \"task_log_level should be reset to INFO\"\n# [/DEF:test_configure_logger_task_log_level:Function]\n\n\n# [DEF:test_enable_belief_state_flag:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that enable_belief_state flag controls belief_scope entry logging.\n# @PRE: LoggingConfig is available. caplog fixture is used.\n# @POST: REASON entry marker suppressed when disabled; REFLECT coherence still logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n # REASON entry marker should NOT be logged when disabled\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reason_records) == 0, \"REASON entry should not be logged when disabled\"\n\n # REFLECT coherence marker should still be logged (internal tracking)\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reflect_records) >= 1, \"REFLECT coherence should still be logged\"\n\n # Re-enable for other tests\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n# [/DEF:test_enable_belief_state_flag:Function]\n\n# [DEF:test_cot_json_formatter_output:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter produces valid JSON with expected fields.\ndef test_cot_json_formatter_output():\n \"\"\"Test that CotJsonFormatter produces valid JSON with expected fields.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord with structured extra data\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Test action\",\n args=(),\n exc_info=None,\n )\n record.marker = \"REASON\"\n record.intent = \"Test action\"\n record.src = \"TestModule\"\n record.payload = {\"key\": \"value\"}\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\"\n assert parsed[\"intent\"] == \"Test action\"\n assert parsed[\"src\"] == \"TestModule\"\n assert parsed[\"payload\"] == {\"key\": \"value\"}\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_output:Function]\n\n# [DEF:test_cot_json_formatter_plain_message:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter wraps plain messages (no extra) with default marker.\ndef test_cot_json_formatter_plain_message():\n \"\"\"Test that CotJsonFormatter wraps plain messages with default marker.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord WITHOUT extra data (plain message)\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Plain info message\",\n args=(),\n exc_info=None,\n )\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\" # default for plain messages\n assert parsed[\"intent\"] == \"Plain info message\"\n assert parsed[\"src\"] == \"test.module\"\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_plain_message:Function]\n\n# [/DEF:TestLogger:Module]\n" + }, + { + "contract_id": "test_belief_scope_success_coherence", + "contract_type": "Function", + "file_path": "backend/tests/test_logger.py", + "start_line": 125, + "end_line": 156, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "POST": "Logs are verified to contain REFLECT marker.", + "PRE": "belief_scope is available. caplog fixture is used. Logger configured to DEBUG.", + "PURPOSE": "Test that belief_scope logs REFLECT marker on success." + }, + "relations": [ + { + "source_id": "test_belief_scope_success_coherence", + "relation_type": "BINDS_TO", + "target_id": "TestLogger", + "target_ref": "TestLogger" + } + ], + "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": "def", + "body": "# [DEF:test_belief_scope_success_coherence:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs REFLECT marker on success.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_coherence(caplog):\n \"\"\"Test that belief_scope logs REFLECT marker on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"SuccessFunction\"):\n pass\n\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT'\n and getattr(r, 'src', None) == 'SuccessFunction'\n ]\n\n assert len(reflect_records) >= 1, f\"REFLECT marker not found for SuccessFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Coherence OK' in reflect_records[0].getMessage() or \\\n getattr(reflect_records[0], 'intent', '') == 'Coherence OK'\n\n\n# [/DEF:test_belief_scope_success_coherence:Function]\n" + }, + { + "contract_id": "test_belief_scope_reason_not_visible_at_info", + "contract_type": "Function", + "file_path": "backend/tests/test_logger.py", + "start_line": 159, + "end_line": 190, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "POST": "REASON/REFLECT markers are not captured at INFO level.", + "PRE": "belief_scope is available. caplog fixture is used.", + "PURPOSE": "Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level." + }, + "relations": [ + { + "source_id": "test_belief_scope_reason_not_visible_at_info", + "relation_type": "BINDS_TO", + "target_id": "TestLogger", + "target_ref": "TestLogger" + } + ], + "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": "def", + "body": "# [DEF:test_belief_scope_reason_not_visible_at_info:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\n# @PRE: belief_scope is available. caplog fixture is used.\n# @POST: REASON/REFLECT markers are not captured at INFO level.\ndef test_belief_scope_reason_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n\n # The REASON and REFLECT markers are debug-level, so they should NOT appear at INFO\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n\n assert len(reason_records) == 0, \"REASON marker should not be visible at INFO\"\n assert len(reflect_records) == 0, \"REFLECT marker should not be visible at INFO\"\n\n # But the INFO-level message should be visible\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n assert len(info_records) >= 1, \"INFO log 'Doing something important' should be visible\"\n# [/DEF:test_belief_scope_reason_not_visible_at_info:Function]\n" + }, + { + "contract_id": "test_cot_json_formatter_output", + "contract_type": "Function", + "file_path": "backend/tests/test_logger.py", + "start_line": 291, + "end_line": 325, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Test that CotJsonFormatter produces valid JSON with expected fields." + }, + "relations": [ + { + "source_id": "test_cot_json_formatter_output", + "relation_type": "BINDS_TO", + "target_id": "TestLogger", + "target_ref": "TestLogger" + } + ], + "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": "def", + "body": "# [DEF:test_cot_json_formatter_output:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter produces valid JSON with expected fields.\ndef test_cot_json_formatter_output():\n \"\"\"Test that CotJsonFormatter produces valid JSON with expected fields.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord with structured extra data\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Test action\",\n args=(),\n exc_info=None,\n )\n record.marker = \"REASON\"\n record.intent = \"Test action\"\n record.src = \"TestModule\"\n record.payload = {\"key\": \"value\"}\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\"\n assert parsed[\"intent\"] == \"Test action\"\n assert parsed[\"src\"] == \"TestModule\"\n assert parsed[\"payload\"] == {\"key\": \"value\"}\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_output:Function]\n" + }, + { + "contract_id": "test_cot_json_formatter_plain_message", + "contract_type": "Function", + "file_path": "backend/tests/test_logger.py", + "start_line": 327, + "end_line": 356, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "PURPOSE": "Test that CotJsonFormatter wraps plain messages (no extra) with default marker." + }, + "relations": [ + { + "source_id": "test_cot_json_formatter_plain_message", + "relation_type": "BINDS_TO", + "target_id": "TestLogger", + "target_ref": "TestLogger" + } + ], + "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": "def", + "body": "# [DEF:test_cot_json_formatter_plain_message:Function]\n# @RELATION: BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter wraps plain messages (no extra) with default marker.\ndef test_cot_json_formatter_plain_message():\n \"\"\"Test that CotJsonFormatter wraps plain messages with default marker.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord WITHOUT extra data (plain message)\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Plain info message\",\n args=(),\n exc_info=None,\n )\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\" # default for plain messages\n assert parsed[\"intent\"] == \"Plain info message\"\n assert parsed[\"src\"] == \"test.module\"\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# [/DEF:test_cot_json_formatter_plain_message:Function]\n" }, { "contract_id": "TestResourceHubs", "contract_type": "Module", "file_path": "backend/tests/test_resource_hubs.py", "start_line": 1, - "end_line": 310, - "tier": "TIER_2", - "complexity": 3, + "end_line": 311, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "Domain (Tests)", "PURPOSE": "Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.", "SEMANTICS": [ @@ -104450,16 +72924,35 @@ "target_ref": "[DatasetsApi]" } ], - "schema_warnings": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + } + ], "anchor_syntax": "def", - "body": "# [DEF:TestResourceHubs:Module]\n# @RELATION: DEPENDS_ON -> [DashboardsApi]\n# @RELATION: DEPENDS_ON -> [DatasetsApi]\n# @COMPLEXITY: 3\n# @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api\n# @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.\n# @LAYER: Domain (Tests)\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom unittest.mock import MagicMock, AsyncMock\nfrom src.app import app\nfrom src.dependencies import (\n get_config_manager,\n get_task_manager,\n get_resource_service,\n has_permission,\n)\n\nclient = TestClient(app)\n\n# [DEF:test_dashboards_api:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify GET /api/dashboards contract compliance\n# @TEST_CONTRACT: dashboards_query -> dashboards payload or not_found response\n# @TEST_SCENARIO: dashboards_env_found_returns_payload -> HTTP 200 returns normalized dashboards list.\n# @TEST_SCENARIO: dashboards_unknown_env_returns_not_found -> HTTP 404 is returned for unknown env_id.\n# @TEST_SCENARIO: dashboards_search_filters_results -> Search narrows payload to matching dashboard title.\n# @TEST_INVARIANT: dashboards_route_contract_stays_observable -> VERIFIED_BY: [dashboards_env_found_returns_payload, dashboards_unknown_env_returns_not_found, dashboards_search_filters_results]\n\n\n# [DEF:mock_deps:Function]\n# @PURPOSE: Provide dependency override fixture for resource hub route tests.\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @TEST_FIXTURE: resource_hub_overrides -> INLINE_JSON\n@pytest.fixture\ndef mock_deps():\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n config_manager = MagicMock()\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n task_manager = MagicMock()\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n resource_service = MagicMock()\n\n # Mock environment\n env = MagicMock()\n env.id = \"env1\"\n config_manager.get_environments.return_value = [env]\n\n # Mock tasks\n task_manager.get_all_tasks.return_value = []\n\n # Mock dashboards\n resource_service.get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Sales\",\n \"slug\": \"sales\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"title\": \"Marketing\",\n \"slug\": \"mkt\",\n \"git_status\": None,\n \"last_task\": {\"task_id\": \"t1\", \"status\": \"SUCCESS\"},\n },\n ]\n )\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n\n # Bypass permission check\n mock_user = MagicMock()\n mock_user.username = \"testadmin\"\n mock_user.roles = []\n admin_role = MagicMock()\n admin_role.name = \"Admin\"\n mock_user.roles.append(admin_role)\n\n # Override both get_current_user and has_permission\n from src.dependencies import get_current_user\n\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n # We need to override the specific instance returned by has_permission\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n\n yield {\"config\": config_manager, \"task\": task_manager, \"resource\": resource_service}\n\n app.dependency_overrides.clear()\n\n\n# [/DEF:mock_deps:Function]\n\n\n# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint returns 200 with expected dashboard payload fields.\ndef test_get_dashboards_success(mock_deps):\n response = client.get(\"/api/dashboards?env_id=env1\")\n assert response.status_code == 200\n data = response.json()\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 2\n assert data[\"dashboards\"][0][\"title\"] == \"Sales\"\n assert data[\"dashboards\"][0][\"git_status\"][\"sync_status\"] == \"OK\"\n\n\n# [/DEF:test_get_dashboards_success:Function]\n\n\n# [DEF:test_get_dashboards_not_found:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint returns 404 for unknown environment identifier.\ndef test_get_dashboards_not_found(mock_deps):\n response = client.get(\"/api/dashboards?env_id=invalid\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_dashboards_not_found:Function]\n\n\n# [DEF:test_get_dashboards_search:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint search filter returns matching subset.\ndef test_get_dashboards_search(mock_deps):\n response = client.get(\"/api/dashboards?env_id=env1&search=Sales\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales\"\n\n\n# [/DEF:test_get_dashboards_search:Function]\n# [/DEF:test_dashboards_api:Block]\n\n\n# [DEF:test_datasets_api:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify GET /api/datasets contract compliance\n# @TEST_CONTRACT: datasets_query -> datasets payload or error response\n# @TEST_SCENARIO: datasets_env_found_returns_payload -> HTTP 200 returns normalized datasets list.\n# @TEST_SCENARIO: datasets_unknown_env_returns_not_found -> HTTP 404 is returned for unknown env_id.\n# @TEST_SCENARIO: datasets_search_filters_results -> Search narrows payload to matching dataset table.\n# @TEST_SCENARIO: datasets_service_failure_returns_503 -> Backend fetch failure surfaces as HTTP 503.\n# @TEST_INVARIANT: datasets_route_contract_stays_observable -> VERIFIED_BY: [datasets_env_found_returns_payload, datasets_unknown_env_returns_not_found, datasets_search_filters_results, datasets_service_failure_returns_503]\n\n\n# [DEF:test_get_datasets_success:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint returns 200 with mapped fields payload.\ndef test_get_datasets_success(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"orders\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": None,\n }\n ]\n )\n\n response = client.get(\"/api/datasets?env_id=env1\")\n assert response.status_code == 200\n data = response.json()\n assert \"datasets\" in data\n assert len(data[\"datasets\"]) == 1\n assert data[\"datasets\"][0][\"table_name\"] == \"orders\"\n assert data[\"datasets\"][0][\"mapped_fields\"][\"mapped\"] == 5\n\n\n# [/DEF:test_get_datasets_success:Function]\n\n\n# [DEF:test_get_datasets_not_found:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint returns 404 for unknown environment identifier.\ndef test_get_datasets_not_found(mock_deps):\n response = client.get(\"/api/datasets?env_id=invalid\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_datasets_not_found:Function]\n\n\n# [DEF:test_get_datasets_search:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint search filter returns matching dataset subset.\ndef test_get_datasets_search(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"orders\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"table_name\": \"users\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 5, \"mapped\": 5},\n \"last_task\": None,\n },\n ]\n )\n\n response = client.get(\"/api/datasets?env_id=env1&search=orders\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"datasets\"]) == 1\n assert data[\"datasets\"][0][\"table_name\"] == \"orders\"\n\n\n# [/DEF:test_get_datasets_search:Function]\n\n\n# [DEF:test_get_datasets_service_failure:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint surfaces backend fetch failure as HTTP 503.\ndef test_get_datasets_service_failure(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n side_effect=Exception(\"Superset down\")\n )\n\n response = client.get(\"/api/datasets?env_id=env1\")\n assert response.status_code == 503\n assert \"Failed to fetch datasets\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_service_failure:Function]\n# [/DEF:test_datasets_api:Block]\n\n\n# [DEF:test_pagination_boundaries:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify pagination validation for GET endpoints\n# @TEST_CONTRACT: pagination_query -> validation error response\n# @TEST_SCENARIO: dashboards_zero_page_rejected -> page=0 returns HTTP 400.\n# @TEST_SCENARIO: dashboards_oversize_page_rejected -> page_size=101 returns HTTP 400.\n# @TEST_SCENARIO: datasets_zero_page_rejected -> page=0 returns HTTP 400.\n# @TEST_SCENARIO: datasets_oversize_page_rejected -> page_size=101 returns HTTP 400.\n# @TEST_EDGE: missing_field -> Missing env_id prevents route contract completion.\n# @TEST_EDGE: invalid_type -> Invalid pagination values are rejected at route validation layer.\n# @TEST_EDGE: external_fail -> Validation failure returns HTTP 400 without partial payload.\n# @TEST_INVARIANT: pagination_limits_apply_to_both_routes -> VERIFIED_BY: [dashboards_zero_page_rejected, dashboards_oversize_page_rejected, datasets_zero_page_rejected, datasets_oversize_page_rejected]\n\n\n# [DEF:test_get_dashboards_pagination_zero_page:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify dashboards endpoint rejects page=0 with HTTP 400 validation error.\ndef test_get_dashboards_pagination_zero_page(mock_deps):\n # @TEST_EDGE: pagination_zero_page -> {page: 0, status: 400}\n response = client.get(\"/api/dashboards?env_id=env1&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_pagination_zero_page:Function]\n\n\n# [DEF:test_get_dashboards_pagination_oversize:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify dashboards endpoint rejects oversized page_size with HTTP 400.\ndef test_get_dashboards_pagination_oversize(mock_deps):\n # @TEST_EDGE: pagination_oversize -> {page_size: 101, status: 400}\n response = client.get(\"/api/dashboards?env_id=env1&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_pagination_oversize:Function]\n\n\n# [DEF:test_get_datasets_pagination_zero_page:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify datasets endpoint rejects page=0 with HTTP 400.\ndef test_get_datasets_pagination_zero_page(mock_deps):\n # @TEST_EDGE: pagination_zero_page_datasets -> {page: 0, status: 400}\n response = client.get(\"/api/datasets?env_id=env1&page=0\")\n assert response.status_code == 400\n\n\n# [/DEF:test_get_datasets_pagination_zero_page:Function]\n\n\n# [DEF:test_get_datasets_pagination_oversize:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify datasets endpoint rejects oversized page_size with HTTP 400.\ndef test_get_datasets_pagination_oversize(mock_deps):\n # @TEST_EDGE: pagination_oversize_datasets -> {page_size: 101, status: 400}\n response = client.get(\"/api/datasets?env_id=env1&page_size=101\")\n assert response.status_code == 400\n\n\n# [/DEF:test_get_datasets_pagination_oversize:Function]\n# [/DEF:test_pagination_boundaries:Block]\n# [/DEF:TestResourceHubs:Module]\n" + "body": "# [DEF:TestResourceHubs:Module]\n# @RELATION: DEPENDS_ON -> [DashboardsApi]\n# @RELATION: DEPENDS_ON -> [DatasetsApi]\n# @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api\n# @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.\n# @LAYER: Domain (Tests)\nfrom unittest.mock import AsyncMock, MagicMock\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.dependencies import (\n get_config_manager,\n get_resource_service,\n get_task_manager,\n has_permission,\n)\n\nclient = TestClient(app)\n\n# [DEF:test_dashboards_api:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify GET /api/dashboards contract compliance\n# @TEST_CONTRACT: dashboards_query -> dashboards payload or not_found response\n# @TEST_SCENARIO: dashboards_env_found_returns_payload -> HTTP 200 returns normalized dashboards list.\n# @TEST_SCENARIO: dashboards_unknown_env_returns_not_found -> HTTP 404 is returned for unknown env_id.\n# @TEST_SCENARIO: dashboards_search_filters_results -> Search narrows payload to matching dashboard title.\n# @TEST_INVARIANT: dashboards_route_contract_stays_observable -> VERIFIED_BY: [dashboards_env_found_returns_payload, dashboards_unknown_env_returns_not_found, dashboards_search_filters_results]\n\n\n# [DEF:mock_deps:Function]\n# @PURPOSE: Provide dependency override fixture for resource hub route tests.\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @TEST_FIXTURE: resource_hub_overrides -> INLINE_JSON\n@pytest.fixture\ndef mock_deps():\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n config_manager = MagicMock()\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n task_manager = MagicMock()\n # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass\n resource_service = MagicMock()\n\n # Mock environment\n env = MagicMock()\n env.id = \"env1\"\n config_manager.get_environments.return_value = [env]\n\n # Mock tasks\n task_manager.get_all_tasks.return_value = []\n\n # Mock dashboards\n resource_service.get_dashboards_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"title\": \"Sales\",\n \"slug\": \"sales\",\n \"git_status\": {\"branch\": \"main\", \"sync_status\": \"OK\"},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"title\": \"Marketing\",\n \"slug\": \"mkt\",\n \"git_status\": None,\n \"last_task\": {\"task_id\": \"t1\", \"status\": \"SUCCESS\"},\n },\n ]\n )\n\n app.dependency_overrides[get_config_manager] = lambda: config_manager\n app.dependency_overrides[get_task_manager] = lambda: task_manager\n app.dependency_overrides[get_resource_service] = lambda: resource_service\n\n # Bypass permission check\n mock_user = MagicMock()\n mock_user.username = \"testadmin\"\n mock_user.roles = []\n admin_role = MagicMock()\n admin_role.name = \"Admin\"\n mock_user.roles.append(admin_role)\n\n # Override both get_current_user and has_permission\n from src.dependencies import get_current_user\n\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n # We need to override the specific instance returned by has_permission\n app.dependency_overrides[has_permission(\"plugin:migration\", \"READ\")] = (\n lambda: mock_user\n )\n\n yield {\"config\": config_manager, \"task\": task_manager, \"resource\": resource_service}\n\n app.dependency_overrides.clear()\n\n\n# [/DEF:mock_deps:Function]\n\n\n# [DEF:test_get_dashboards_success:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint returns 200 with expected dashboard payload fields.\ndef test_get_dashboards_success(mock_deps):\n response = client.get(\"/api/dashboards?env_id=env1\")\n assert response.status_code == 200\n data = response.json()\n assert \"dashboards\" in data\n assert len(data[\"dashboards\"]) == 2\n assert data[\"dashboards\"][0][\"title\"] == \"Sales\"\n assert data[\"dashboards\"][0][\"git_status\"][\"sync_status\"] == \"OK\"\n\n\n# [/DEF:test_get_dashboards_success:Function]\n\n\n# [DEF:test_get_dashboards_not_found:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint returns 404 for unknown environment identifier.\ndef test_get_dashboards_not_found(mock_deps):\n response = client.get(\"/api/dashboards?env_id=invalid\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_dashboards_not_found:Function]\n\n\n# [DEF:test_get_dashboards_search:Function]\n# @RELATION: BINDS_TO -> [test_dashboards_api]\n# @PURPOSE: Verify dashboards endpoint search filter returns matching subset.\ndef test_get_dashboards_search(mock_deps):\n response = client.get(\"/api/dashboards?env_id=env1&search=Sales\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"dashboards\"]) == 1\n assert data[\"dashboards\"][0][\"title\"] == \"Sales\"\n\n\n# [/DEF:test_get_dashboards_search:Function]\n# [/DEF:test_dashboards_api:Block]\n\n\n# [DEF:test_datasets_api:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify GET /api/datasets contract compliance\n# @TEST_CONTRACT: datasets_query -> datasets payload or error response\n# @TEST_SCENARIO: datasets_env_found_returns_payload -> HTTP 200 returns normalized datasets list.\n# @TEST_SCENARIO: datasets_unknown_env_returns_not_found -> HTTP 404 is returned for unknown env_id.\n# @TEST_SCENARIO: datasets_search_filters_results -> Search narrows payload to matching dataset table.\n# @TEST_SCENARIO: datasets_service_failure_returns_503 -> Backend fetch failure surfaces as HTTP 503.\n# @TEST_INVARIANT: datasets_route_contract_stays_observable -> VERIFIED_BY: [datasets_env_found_returns_payload, datasets_unknown_env_returns_not_found, datasets_search_filters_results, datasets_service_failure_returns_503]\n\n\n# [DEF:test_get_datasets_success:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint returns 200 with mapped fields payload.\ndef test_get_datasets_success(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"orders\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": None,\n }\n ]\n )\n\n response = client.get(\"/api/datasets?env_id=env1\")\n assert response.status_code == 200\n data = response.json()\n assert \"datasets\" in data\n assert len(data[\"datasets\"]) == 1\n assert data[\"datasets\"][0][\"table_name\"] == \"orders\"\n assert data[\"datasets\"][0][\"mapped_fields\"][\"mapped\"] == 5\n\n\n# [/DEF:test_get_datasets_success:Function]\n\n\n# [DEF:test_get_datasets_not_found:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint returns 404 for unknown environment identifier.\ndef test_get_datasets_not_found(mock_deps):\n response = client.get(\"/api/datasets?env_id=invalid\")\n assert response.status_code == 404\n\n\n# [/DEF:test_get_datasets_not_found:Function]\n\n\n# [DEF:test_get_datasets_search:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint search filter returns matching dataset subset.\ndef test_get_datasets_search(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n return_value=[\n {\n \"id\": 1,\n \"table_name\": \"orders\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 10, \"mapped\": 5},\n \"last_task\": None,\n },\n {\n \"id\": 2,\n \"table_name\": \"users\",\n \"schema\": \"public\",\n \"database\": \"db1\",\n \"mapped_fields\": {\"total\": 5, \"mapped\": 5},\n \"last_task\": None,\n },\n ]\n )\n\n response = client.get(\"/api/datasets?env_id=env1&search=orders\")\n assert response.status_code == 200\n data = response.json()\n assert len(data[\"datasets\"]) == 1\n assert data[\"datasets\"][0][\"table_name\"] == \"orders\"\n\n\n# [/DEF:test_get_datasets_search:Function]\n\n\n# [DEF:test_get_datasets_service_failure:Function]\n# @RELATION: BINDS_TO -> [test_datasets_api]\n# @PURPOSE: Verify datasets endpoint surfaces backend fetch failure as HTTP 503.\ndef test_get_datasets_service_failure(mock_deps):\n mock_deps[\"resource\"].get_datasets_with_status = AsyncMock(\n side_effect=Exception(\"Superset down\")\n )\n\n response = client.get(\"/api/datasets?env_id=env1\")\n assert response.status_code == 503\n assert \"Failed to fetch datasets\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_datasets_service_failure:Function]\n# [/DEF:test_datasets_api:Block]\n\n\n# [DEF:test_pagination_boundaries:Block]\n# @RELATION: BINDS_TO -> [TestResourceHubs]\n# @PURPOSE: Verify pagination validation for GET endpoints\n# @TEST_CONTRACT: pagination_query -> validation error response\n# @TEST_SCENARIO: dashboards_zero_page_rejected -> page=0 returns HTTP 400.\n# @TEST_SCENARIO: dashboards_oversize_page_rejected -> page_size=101 returns HTTP 400.\n# @TEST_SCENARIO: datasets_zero_page_rejected -> page=0 returns HTTP 400.\n# @TEST_SCENARIO: datasets_oversize_page_rejected -> page_size=101 returns HTTP 400.\n# @TEST_EDGE: missing_field -> Missing env_id prevents route contract completion.\n# @TEST_EDGE: invalid_type -> Invalid pagination values are rejected at route validation layer.\n# @TEST_EDGE: external_fail -> Validation failure returns HTTP 400 without partial payload.\n# @TEST_INVARIANT: pagination_limits_apply_to_both_routes -> VERIFIED_BY: [dashboards_zero_page_rejected, dashboards_oversize_page_rejected, datasets_zero_page_rejected, datasets_oversize_page_rejected]\n\n\n# [DEF:test_get_dashboards_pagination_zero_page:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify dashboards endpoint rejects page=0 with HTTP 400 validation error.\ndef test_get_dashboards_pagination_zero_page(mock_deps):\n # @TEST_EDGE: pagination_zero_page -> {page: 0, status: 400}\n response = client.get(\"/api/dashboards?env_id=env1&page=0\")\n assert response.status_code == 400\n assert \"Page must be >= 1\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_pagination_zero_page:Function]\n\n\n# [DEF:test_get_dashboards_pagination_oversize:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify dashboards endpoint rejects oversized page_size with HTTP 400.\ndef test_get_dashboards_pagination_oversize(mock_deps):\n # @TEST_EDGE: pagination_oversize -> {page_size: 101, status: 400}\n response = client.get(\"/api/dashboards?env_id=env1&page_size=101\")\n assert response.status_code == 400\n assert \"Page size must be between 1 and 100\" in response.json()[\"detail\"]\n\n\n# [/DEF:test_get_dashboards_pagination_oversize:Function]\n\n\n# [DEF:test_get_datasets_pagination_zero_page:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify datasets endpoint rejects page=0 with HTTP 400.\ndef test_get_datasets_pagination_zero_page(mock_deps):\n # @TEST_EDGE: pagination_zero_page_datasets -> {page: 0, status: 400}\n response = client.get(\"/api/datasets?env_id=env1&page=0\")\n assert response.status_code == 400\n\n\n# [/DEF:test_get_datasets_pagination_zero_page:Function]\n\n\n# [DEF:test_get_datasets_pagination_oversize:Function]\n# @RELATION: BINDS_TO -> [test_pagination_boundaries]\n# @PURPOSE: Verify datasets endpoint rejects oversized page_size with HTTP 400.\ndef test_get_datasets_pagination_oversize(mock_deps):\n # @TEST_EDGE: pagination_oversize_datasets -> {page_size: 101, status: 400}\n response = client.get(\"/api/datasets?env_id=env1&page_size=101\")\n assert response.status_code == 400\n\n\n# [/DEF:test_get_datasets_pagination_oversize:Function]\n# [/DEF:test_pagination_boundaries:Block]\n# [/DEF:TestResourceHubs:Module]\n" }, { "contract_id": "test_dashboards_api", "contract_type": "Block", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 21, - "end_line": 141, + "start_line": 22, + "end_line": 142, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104515,8 +73008,8 @@ "contract_id": "mock_deps", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 31, - "end_line": 99, + "start_line": 32, + "end_line": 100, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -104588,8 +73081,8 @@ "contract_id": "test_get_dashboards_not_found", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 118, - "end_line": 126, + "start_line": 119, + "end_line": 127, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104621,8 +73114,8 @@ "contract_id": "test_get_dashboards_search", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 129, - "end_line": 140, + "start_line": 130, + "end_line": 141, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104654,8 +73147,8 @@ "contract_id": "test_datasets_api", "contract_type": "Block", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 144, - "end_line": 244, + "start_line": 145, + "end_line": 245, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104711,8 +73204,8 @@ "contract_id": "test_get_datasets_not_found", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 184, - "end_line": 192, + "start_line": 185, + "end_line": 193, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104744,8 +73237,8 @@ "contract_id": "test_get_datasets_search", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 195, - "end_line": 227, + "start_line": 196, + "end_line": 228, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104777,8 +73270,8 @@ "contract_id": "test_get_datasets_service_failure", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 230, - "end_line": 243, + "start_line": 231, + "end_line": 244, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104810,8 +73303,8 @@ "contract_id": "test_pagination_boundaries", "contract_type": "Block", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 247, - "end_line": 309, + "start_line": 248, + "end_line": 310, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104868,8 +73361,8 @@ "contract_id": "test_get_dashboards_pagination_zero_page", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 261, - "end_line": 271, + "start_line": 262, + "end_line": 272, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104902,8 +73395,8 @@ "contract_id": "test_get_dashboards_pagination_oversize", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 274, - "end_line": 284, + "start_line": 275, + "end_line": 285, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104936,8 +73429,8 @@ "contract_id": "test_get_datasets_pagination_zero_page", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 287, - "end_line": 296, + "start_line": 288, + "end_line": 297, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -104970,8 +73463,8 @@ "contract_id": "test_get_datasets_pagination_oversize", "contract_type": "Function", "file_path": "backend/tests/test_resource_hubs.py", - "start_line": 299, - "end_line": 308, + "start_line": 300, + "end_line": 309, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105005,11 +73498,10 @@ "contract_type": "Module", "file_path": "backend/tests/test_task_manager.py", "start_line": 1, - "end_line": 503, - "tier": "TIER_3", - "complexity": 5, + "end_line": 504, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": "5", "INVARIANT": "TaskManager state changes are deterministic and testable with mocked dependencies.", "LAYER": "Core", "PURPOSE": "Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering.", @@ -105032,56 +73524,20 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Module' at C2", "detail": { - "actual_complexity": 5, + "actual_complexity": 2, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 2, "contract_type": "Module" } }, @@ -105105,14 +73561,14 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_task_manager:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @COMPLEXITY: 5\n# @SEMANTICS: task-manager, lifecycle, CRUD, log-buffer, filtering, tests\n# @PURPOSE: Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering.\n# @LAYER: Core\n# @INVARIANT: TaskManager state changes are deterministic and testable with mocked dependencies.\n\nimport sys\nfrom pathlib import Path\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nimport asyncio\nfrom unittest.mock import MagicMock, patch, AsyncMock\nfrom datetime import datetime\n\n\n# Helper to create a TaskManager with mocked dependencies\n# [DEF:_make_manager:Function]\n# @RELATION: BINDS_TO -> test_task_manager\ndef _make_manager():\n \"\"\"Create TaskManager with mocked plugin_loader and persistence services.\"\"\"\n mock_plugin_loader = MagicMock()\n mock_plugin_loader.has_plugin.return_value = True\n\n mock_plugin = MagicMock()\n mock_plugin.name = \"test_plugin\"\n mock_plugin.execute = MagicMock(return_value={\"status\": \"ok\"})\n mock_plugin_loader.get_plugin.return_value = mock_plugin\n\n with patch(\"src.core.task_manager.manager.TaskPersistenceService\") as MockPersistence, \\\n patch(\"src.core.task_manager.manager.TaskLogPersistenceService\") as MockLogPersistence:\n MockPersistence.return_value.load_tasks.return_value = []\n MockLogPersistence.return_value.add_logs = MagicMock()\n MockLogPersistence.return_value.get_logs = MagicMock(return_value=[])\n MockLogPersistence.return_value.get_log_stats = MagicMock()\n MockLogPersistence.return_value.get_sources = MagicMock(return_value=[])\n MockLogPersistence.return_value.delete_logs_for_tasks = MagicMock()\n\n manager = None\n try:\n from src.core.task_manager.manager import TaskManager\n manager = TaskManager(mock_plugin_loader)\n except RuntimeError:\n # No event loop — create one\n loop = asyncio.new_event_loop()\n asyncio.set_event_loop(loop)\n from src.core.task_manager.manager import TaskManager\n manager = TaskManager(mock_plugin_loader)\n\n return manager, mock_plugin_loader, MockPersistence.return_value, MockLogPersistence.return_value\n\n\n# [/DEF:_make_manager:Function]\n\n# [DEF:_cleanup_manager:Function]\n# @RELATION: BINDS_TO -> test_task_manager\ndef _cleanup_manager(manager):\n \"\"\"Stop the flusher thread.\"\"\"\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# [/DEF:_cleanup_manager:Function]\n\nclass TestTaskManagerInit:\n \"\"\"Tests for TaskManager initialization.\"\"\"\n\n def test_init_creates_empty_tasks(self):\n mgr, _, _, _ = _make_manager()\n try:\n assert isinstance(mgr.tasks, dict)\n finally:\n _cleanup_manager(mgr)\n\n def test_init_loads_persisted_tasks(self):\n mgr, _, persist_svc, _ = _make_manager()\n try:\n persist_svc.load_tasks.assert_called_once_with(limit=100)\n finally:\n _cleanup_manager(mgr)\n\n def test_init_starts_flusher_thread(self):\n mgr, _, _, _ = _make_manager()\n try:\n assert mgr._flusher_thread.is_alive()\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerCRUD:\n \"\"\"Tests for TaskManager task retrieval methods.\"\"\"\n\n def test_get_task_returns_none_for_missing(self):\n mgr, _, _, _ = _make_manager()\n try:\n assert mgr.get_task(\"nonexistent\") is None\n finally:\n _cleanup_manager(mgr)\n\n def test_get_task_returns_existing(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"test\", params={})\n mgr.tasks[task.id] = task\n assert mgr.get_task(task.id) is task\n finally:\n _cleanup_manager(mgr)\n\n def test_get_all_tasks(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n t1 = Task(plugin_id=\"p1\", params={})\n t2 = Task(plugin_id=\"p2\", params={})\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n assert len(mgr.get_all_tasks()) == 2\n finally:\n _cleanup_manager(mgr)\n\n def test_get_tasks_with_status_filter(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.SUCCESS\n t1.started_at = datetime(2024, 1, 1, 12, 0, 0)\n t2 = Task(plugin_id=\"p2\", params={})\n t2.status = TaskStatus.FAILED\n t2.started_at = datetime(2024, 1, 1, 13, 0, 0)\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n\n result = mgr.get_tasks(status=TaskStatus.SUCCESS)\n assert len(result) == 1\n assert result[0].status == TaskStatus.SUCCESS\n finally:\n _cleanup_manager(mgr)\n\n def test_get_tasks_with_plugin_filter(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n t1 = Task(plugin_id=\"backup\", params={})\n t1.started_at = datetime(2024, 1, 1, 12, 0, 0)\n t2 = Task(plugin_id=\"migrate\", params={})\n t2.started_at = datetime(2024, 1, 1, 13, 0, 0)\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n\n result = mgr.get_tasks(plugin_ids=[\"backup\"])\n assert len(result) == 1\n assert result[0].plugin_id == \"backup\"\n finally:\n _cleanup_manager(mgr)\n\n def test_get_tasks_with_pagination(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n for i in range(5):\n t = Task(plugin_id=f\"p{i}\", params={})\n t.started_at = datetime(2024, 1, 1, i, 0, 0)\n mgr.tasks[t.id] = t\n\n result = mgr.get_tasks(limit=2, offset=0)\n assert len(result) == 2\n\n result2 = mgr.get_tasks(limit=2, offset=4)\n assert len(result2) == 1\n finally:\n _cleanup_manager(mgr)\n\n def test_get_tasks_completed_only(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.SUCCESS\n t1.started_at = datetime(2024, 1, 1)\n t2 = Task(plugin_id=\"p2\", params={})\n t2.status = TaskStatus.RUNNING\n t2.started_at = datetime(2024, 1, 2)\n t3 = Task(plugin_id=\"p3\", params={})\n t3.status = TaskStatus.FAILED\n t3.started_at = datetime(2024, 1, 3)\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n mgr.tasks[t3.id] = t3\n\n result = mgr.get_tasks(completed_only=True)\n assert len(result) == 2 # SUCCESS + FAILED\n statuses = {t.status for t in result}\n assert TaskStatus.RUNNING not in statuses\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerCreateTask:\n \"\"\"Tests for TaskManager.create_task.\"\"\"\n\n @pytest.mark.asyncio\n async def test_create_task_success(self):\n mgr, loader, persist_svc, _ = _make_manager()\n try:\n task = await mgr.create_task(\"test_plugin\", {\"key\": \"value\"})\n assert task.plugin_id == \"test_plugin\"\n assert task.params == {\"key\": \"value\"}\n assert task.id in mgr.tasks\n persist_svc.persist_task.assert_called()\n finally:\n _cleanup_manager(mgr)\n\n @pytest.mark.asyncio\n async def test_create_task_unknown_plugin_raises(self):\n mgr, loader, _, _ = _make_manager()\n try:\n loader.has_plugin.return_value = False\n with pytest.raises(ValueError, match=\"not found\"):\n await mgr.create_task(\"unknown_plugin\", {})\n finally:\n _cleanup_manager(mgr)\n\n @pytest.mark.asyncio\n async def test_create_task_invalid_params_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n with pytest.raises(ValueError, match=\"dictionary\"):\n await mgr.create_task(\"test_plugin\", \"not-a-dict\")\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerLogBuffer:\n \"\"\"Tests for log buffering and flushing.\"\"\"\n\n def test_add_log_appends_to_task_and_buffer(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"p1\", params={})\n mgr.tasks[task.id] = task\n\n mgr._add_log(task.id, \"INFO\", \"Test log message\", source=\"test\")\n\n assert len(task.logs) == 1\n assert task.logs[0].message == \"Test log message\"\n assert task.id in mgr._log_buffer\n assert len(mgr._log_buffer[task.id]) == 1\n finally:\n _cleanup_manager(mgr)\n\n def test_add_log_skips_nonexistent_task(self):\n mgr, _, _, _ = _make_manager()\n try:\n mgr._add_log(\"nonexistent\", \"INFO\", \"Should not crash\")\n # No error raised\n finally:\n _cleanup_manager(mgr)\n\n def test_flush_logs_writes_to_persistence(self):\n mgr, _, _, log_persist = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"p1\", params={})\n mgr.tasks[task.id] = task\n\n mgr._add_log(task.id, \"INFO\", \"Log 1\", source=\"test\")\n mgr._add_log(task.id, \"INFO\", \"Log 2\", source=\"test\")\n mgr._flush_logs()\n\n log_persist.add_logs.assert_called_once()\n args = log_persist.add_logs.call_args\n assert args[0][0] == task.id # task_id\n assert len(args[0][1]) == 2 # 2 log entries\n finally:\n _cleanup_manager(mgr)\n\n def test_flush_task_logs_writes_single_task(self):\n mgr, _, _, log_persist = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"p1\", params={})\n mgr.tasks[task.id] = task\n\n mgr._add_log(task.id, \"INFO\", \"Log 1\", source=\"test\")\n mgr._flush_task_logs(task.id)\n\n log_persist.add_logs.assert_called_once()\n # Buffer should be empty now\n assert task.id not in mgr._log_buffer\n finally:\n _cleanup_manager(mgr)\n\n def test_flush_logs_requeues_on_failure(self):\n mgr, _, _, log_persist = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"p1\", params={})\n mgr.tasks[task.id] = task\n\n mgr._add_log(task.id, \"INFO\", \"Log 1\", source=\"test\")\n log_persist.add_logs.side_effect = Exception(\"DB error\")\n mgr._flush_logs()\n\n # Logs should be re-added to buffer\n assert task.id in mgr._log_buffer\n assert len(mgr._log_buffer[task.id]) == 1\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerClearTasks:\n \"\"\"Tests for TaskManager.clear_tasks.\"\"\"\n\n def test_clear_all_non_active(self):\n mgr, _, persist_svc, log_persist = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.SUCCESS\n t2 = Task(plugin_id=\"p2\", params={})\n t2.status = TaskStatus.RUNNING\n t3 = Task(plugin_id=\"p3\", params={})\n t3.status = TaskStatus.FAILED\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n mgr.tasks[t3.id] = t3\n\n removed = mgr.clear_tasks()\n assert removed == 2 # SUCCESS + FAILED\n assert t2.id in mgr.tasks # RUNNING kept\n assert t1.id not in mgr.tasks\n persist_svc.delete_tasks.assert_called_once()\n log_persist.delete_logs_for_tasks.assert_called_once()\n finally:\n _cleanup_manager(mgr)\n\n def test_clear_by_status(self):\n mgr, _, persist_svc, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.SUCCESS\n t2 = Task(plugin_id=\"p2\", params={})\n t2.status = TaskStatus.FAILED\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n\n removed = mgr.clear_tasks(status=TaskStatus.FAILED)\n assert removed == 1\n assert t1.id in mgr.tasks\n assert t2.id not in mgr.tasks\n finally:\n _cleanup_manager(mgr)\n\n def test_clear_preserves_awaiting_input(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.AWAITING_INPUT\n mgr.tasks[t1.id] = t1\n\n removed = mgr.clear_tasks()\n assert removed == 0\n assert t1.id in mgr.tasks\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerSubscriptions:\n \"\"\"Tests for log subscription management.\"\"\"\n\n @pytest.mark.asyncio\n async def test_subscribe_creates_queue(self):\n mgr, _, _, _ = _make_manager()\n try:\n queue = await mgr.subscribe_logs(\"task-1\")\n assert isinstance(queue, asyncio.Queue)\n assert \"task-1\" in mgr.subscribers\n assert queue in mgr.subscribers[\"task-1\"]\n finally:\n _cleanup_manager(mgr)\n\n @pytest.mark.asyncio\n async def test_unsubscribe_removes_queue(self):\n mgr, _, _, _ = _make_manager()\n try:\n queue = await mgr.subscribe_logs(\"task-1\")\n mgr.unsubscribe_logs(\"task-1\", queue)\n assert \"task-1\" not in mgr.subscribers\n finally:\n _cleanup_manager(mgr)\n\n @pytest.mark.asyncio\n async def test_multiple_subscribers(self):\n mgr, _, _, _ = _make_manager()\n try:\n q1 = await mgr.subscribe_logs(\"task-1\")\n q2 = await mgr.subscribe_logs(\"task-1\")\n assert len(mgr.subscribers[\"task-1\"]) == 2\n\n mgr.unsubscribe_logs(\"task-1\", q1)\n assert len(mgr.subscribers[\"task-1\"]) == 1\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerInput:\n \"\"\"Tests for await_input and resume_task_with_password.\"\"\"\n\n def test_await_input_sets_status(self):\n mgr, _, persist_svc, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.RUNNING\n mgr.tasks[task.id] = task\n\n # NOTE: source code has a bug where await_input calls _add_log\n # with a dict as 4th positional arg (source), causing Pydantic\n # ValidationError. We patch _add_log to test the state transition.\n mgr._add_log = MagicMock()\n mgr.await_input(task.id, {\"prompt\": \"Enter password\"})\n\n assert task.status == TaskStatus.AWAITING_INPUT\n assert task.input_required is True\n assert task.input_request == {\"prompt\": \"Enter password\"}\n persist_svc.persist_task.assert_called()\n finally:\n _cleanup_manager(mgr)\n\n def test_await_input_not_running_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.PENDING\n mgr.tasks[task.id] = task\n\n with pytest.raises(ValueError, match=\"not RUNNING\"):\n mgr.await_input(task.id, {})\n finally:\n _cleanup_manager(mgr)\n\n def test_await_input_nonexistent_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n with pytest.raises(ValueError, match=\"not found\"):\n mgr.await_input(\"nonexistent\", {})\n finally:\n _cleanup_manager(mgr)\n\n def test_resume_with_password(self):\n mgr, _, persist_svc, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.AWAITING_INPUT\n mgr.tasks[task.id] = task\n\n # NOTE: source code has same _add_log positional-arg bug in resume too.\n mgr._add_log = MagicMock()\n mgr.resume_task_with_password(task.id, {\"db1\": \"pass123\"})\n\n assert task.status == TaskStatus.RUNNING\n assert task.params[\"passwords\"] == {\"db1\": \"pass123\"}\n assert task.input_required is False\n assert task.input_request is None\n finally:\n _cleanup_manager(mgr)\n\n def test_resume_not_awaiting_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.RUNNING\n mgr.tasks[task.id] = task\n\n with pytest.raises(ValueError, match=\"not AWAITING_INPUT\"):\n mgr.resume_task_with_password(task.id, {\"db\": \"pass\"})\n finally:\n _cleanup_manager(mgr)\n\n def test_resume_empty_passwords_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.AWAITING_INPUT\n mgr.tasks[task.id] = task\n\n with pytest.raises(ValueError, match=\"non-empty\"):\n mgr.resume_task_with_password(task.id, {})\n finally:\n _cleanup_manager(mgr)\n\n# [/DEF:test_task_manager:Module]\n" + "body": "# [DEF:test_task_manager:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: task-manager, lifecycle, CRUD, log-buffer, filtering, tests\n# @PURPOSE: Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering.\n# @LAYER: Core\n# @INVARIANT: TaskManager state changes are deterministic and testable with mocked dependencies.\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport asyncio\nfrom datetime import datetime\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\n\n# Helper to create a TaskManager with mocked dependencies\n# [DEF:_make_manager:Function]\n# @RELATION: BINDS_TO -> test_task_manager\ndef _make_manager():\n \"\"\"Create TaskManager with mocked plugin_loader and persistence services.\"\"\"\n mock_plugin_loader = MagicMock()\n mock_plugin_loader.has_plugin.return_value = True\n\n mock_plugin = MagicMock()\n mock_plugin.name = \"test_plugin\"\n mock_plugin.execute = MagicMock(return_value={\"status\": \"ok\"})\n mock_plugin_loader.get_plugin.return_value = mock_plugin\n\n with patch(\"src.core.task_manager.manager.TaskPersistenceService\") as MockPersistence, \\\n patch(\"src.core.task_manager.manager.TaskLogPersistenceService\") as MockLogPersistence:\n MockPersistence.return_value.load_tasks.return_value = []\n MockLogPersistence.return_value.add_logs = MagicMock()\n MockLogPersistence.return_value.get_logs = MagicMock(return_value=[])\n MockLogPersistence.return_value.get_log_stats = MagicMock()\n MockLogPersistence.return_value.get_sources = MagicMock(return_value=[])\n MockLogPersistence.return_value.delete_logs_for_tasks = MagicMock()\n\n manager = None\n try:\n from src.core.task_manager.manager import TaskManager\n manager = TaskManager(mock_plugin_loader)\n except RuntimeError:\n # No event loop — create one\n loop = asyncio.new_event_loop()\n asyncio.set_event_loop(loop)\n from src.core.task_manager.manager import TaskManager\n manager = TaskManager(mock_plugin_loader)\n\n return manager, mock_plugin_loader, MockPersistence.return_value, MockLogPersistence.return_value\n\n\n# [/DEF:_make_manager:Function]\n\n# [DEF:_cleanup_manager:Function]\n# @RELATION: BINDS_TO -> test_task_manager\ndef _cleanup_manager(manager):\n \"\"\"Stop the flusher thread.\"\"\"\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# [/DEF:_cleanup_manager:Function]\n\nclass TestTaskManagerInit:\n \"\"\"Tests for TaskManager initialization.\"\"\"\n\n def test_init_creates_empty_tasks(self):\n mgr, _, _, _ = _make_manager()\n try:\n assert isinstance(mgr.tasks, dict)\n finally:\n _cleanup_manager(mgr)\n\n def test_init_loads_persisted_tasks(self):\n mgr, _, persist_svc, _ = _make_manager()\n try:\n persist_svc.load_tasks.assert_called_once_with(limit=100)\n finally:\n _cleanup_manager(mgr)\n\n def test_init_starts_flusher_thread(self):\n mgr, _, _, _ = _make_manager()\n try:\n assert mgr._flusher_thread.is_alive()\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerCRUD:\n \"\"\"Tests for TaskManager task retrieval methods.\"\"\"\n\n def test_get_task_returns_none_for_missing(self):\n mgr, _, _, _ = _make_manager()\n try:\n assert mgr.get_task(\"nonexistent\") is None\n finally:\n _cleanup_manager(mgr)\n\n def test_get_task_returns_existing(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"test\", params={})\n mgr.tasks[task.id] = task\n assert mgr.get_task(task.id) is task\n finally:\n _cleanup_manager(mgr)\n\n def test_get_all_tasks(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n t1 = Task(plugin_id=\"p1\", params={})\n t2 = Task(plugin_id=\"p2\", params={})\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n assert len(mgr.get_all_tasks()) == 2\n finally:\n _cleanup_manager(mgr)\n\n def test_get_tasks_with_status_filter(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.SUCCESS\n t1.started_at = datetime(2024, 1, 1, 12, 0, 0)\n t2 = Task(plugin_id=\"p2\", params={})\n t2.status = TaskStatus.FAILED\n t2.started_at = datetime(2024, 1, 1, 13, 0, 0)\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n\n result = mgr.get_tasks(status=TaskStatus.SUCCESS)\n assert len(result) == 1\n assert result[0].status == TaskStatus.SUCCESS\n finally:\n _cleanup_manager(mgr)\n\n def test_get_tasks_with_plugin_filter(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n t1 = Task(plugin_id=\"backup\", params={})\n t1.started_at = datetime(2024, 1, 1, 12, 0, 0)\n t2 = Task(plugin_id=\"migrate\", params={})\n t2.started_at = datetime(2024, 1, 1, 13, 0, 0)\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n\n result = mgr.get_tasks(plugin_ids=[\"backup\"])\n assert len(result) == 1\n assert result[0].plugin_id == \"backup\"\n finally:\n _cleanup_manager(mgr)\n\n def test_get_tasks_with_pagination(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n for i in range(5):\n t = Task(plugin_id=f\"p{i}\", params={})\n t.started_at = datetime(2024, 1, 1, i, 0, 0)\n mgr.tasks[t.id] = t\n\n result = mgr.get_tasks(limit=2, offset=0)\n assert len(result) == 2\n\n result2 = mgr.get_tasks(limit=2, offset=4)\n assert len(result2) == 1\n finally:\n _cleanup_manager(mgr)\n\n def test_get_tasks_completed_only(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.SUCCESS\n t1.started_at = datetime(2024, 1, 1)\n t2 = Task(plugin_id=\"p2\", params={})\n t2.status = TaskStatus.RUNNING\n t2.started_at = datetime(2024, 1, 2)\n t3 = Task(plugin_id=\"p3\", params={})\n t3.status = TaskStatus.FAILED\n t3.started_at = datetime(2024, 1, 3)\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n mgr.tasks[t3.id] = t3\n\n result = mgr.get_tasks(completed_only=True)\n assert len(result) == 2 # SUCCESS + FAILED\n statuses = {t.status for t in result}\n assert TaskStatus.RUNNING not in statuses\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerCreateTask:\n \"\"\"Tests for TaskManager.create_task.\"\"\"\n\n @pytest.mark.asyncio\n async def test_create_task_success(self):\n mgr, loader, persist_svc, _ = _make_manager()\n try:\n task = await mgr.create_task(\"test_plugin\", {\"key\": \"value\"})\n assert task.plugin_id == \"test_plugin\"\n assert task.params == {\"key\": \"value\"}\n assert task.id in mgr.tasks\n persist_svc.persist_task.assert_called()\n finally:\n _cleanup_manager(mgr)\n\n @pytest.mark.asyncio\n async def test_create_task_unknown_plugin_raises(self):\n mgr, loader, _, _ = _make_manager()\n try:\n loader.has_plugin.return_value = False\n with pytest.raises(ValueError, match=\"not found\"):\n await mgr.create_task(\"unknown_plugin\", {})\n finally:\n _cleanup_manager(mgr)\n\n @pytest.mark.asyncio\n async def test_create_task_invalid_params_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n with pytest.raises(ValueError, match=\"dictionary\"):\n await mgr.create_task(\"test_plugin\", \"not-a-dict\")\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerLogBuffer:\n \"\"\"Tests for log buffering and flushing.\"\"\"\n\n def test_add_log_appends_to_task_and_buffer(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"p1\", params={})\n mgr.tasks[task.id] = task\n\n mgr._add_log(task.id, \"INFO\", \"Test log message\", source=\"test\")\n\n assert len(task.logs) == 1\n assert task.logs[0].message == \"Test log message\"\n assert task.id in mgr._log_buffer\n assert len(mgr._log_buffer[task.id]) == 1\n finally:\n _cleanup_manager(mgr)\n\n def test_add_log_skips_nonexistent_task(self):\n mgr, _, _, _ = _make_manager()\n try:\n mgr._add_log(\"nonexistent\", \"INFO\", \"Should not crash\")\n # No error raised\n finally:\n _cleanup_manager(mgr)\n\n def test_flush_logs_writes_to_persistence(self):\n mgr, _, _, log_persist = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"p1\", params={})\n mgr.tasks[task.id] = task\n\n mgr._add_log(task.id, \"INFO\", \"Log 1\", source=\"test\")\n mgr._add_log(task.id, \"INFO\", \"Log 2\", source=\"test\")\n mgr._flush_logs()\n\n log_persist.add_logs.assert_called_once()\n args = log_persist.add_logs.call_args\n assert args[0][0] == task.id # task_id\n assert len(args[0][1]) == 2 # 2 log entries\n finally:\n _cleanup_manager(mgr)\n\n def test_flush_task_logs_writes_single_task(self):\n mgr, _, _, log_persist = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"p1\", params={})\n mgr.tasks[task.id] = task\n\n mgr._add_log(task.id, \"INFO\", \"Log 1\", source=\"test\")\n mgr._flush_task_logs(task.id)\n\n log_persist.add_logs.assert_called_once()\n # Buffer should be empty now\n assert task.id not in mgr._log_buffer\n finally:\n _cleanup_manager(mgr)\n\n def test_flush_logs_requeues_on_failure(self):\n mgr, _, _, log_persist = _make_manager()\n try:\n from src.core.task_manager.models import Task\n task = Task(plugin_id=\"p1\", params={})\n mgr.tasks[task.id] = task\n\n mgr._add_log(task.id, \"INFO\", \"Log 1\", source=\"test\")\n log_persist.add_logs.side_effect = Exception(\"DB error\")\n mgr._flush_logs()\n\n # Logs should be re-added to buffer\n assert task.id in mgr._log_buffer\n assert len(mgr._log_buffer[task.id]) == 1\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerClearTasks:\n \"\"\"Tests for TaskManager.clear_tasks.\"\"\"\n\n def test_clear_all_non_active(self):\n mgr, _, persist_svc, log_persist = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.SUCCESS\n t2 = Task(plugin_id=\"p2\", params={})\n t2.status = TaskStatus.RUNNING\n t3 = Task(plugin_id=\"p3\", params={})\n t3.status = TaskStatus.FAILED\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n mgr.tasks[t3.id] = t3\n\n removed = mgr.clear_tasks()\n assert removed == 2 # SUCCESS + FAILED\n assert t2.id in mgr.tasks # RUNNING kept\n assert t1.id not in mgr.tasks\n persist_svc.delete_tasks.assert_called_once()\n log_persist.delete_logs_for_tasks.assert_called_once()\n finally:\n _cleanup_manager(mgr)\n\n def test_clear_by_status(self):\n mgr, _, persist_svc, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.SUCCESS\n t2 = Task(plugin_id=\"p2\", params={})\n t2.status = TaskStatus.FAILED\n mgr.tasks[t1.id] = t1\n mgr.tasks[t2.id] = t2\n\n removed = mgr.clear_tasks(status=TaskStatus.FAILED)\n assert removed == 1\n assert t1.id in mgr.tasks\n assert t2.id not in mgr.tasks\n finally:\n _cleanup_manager(mgr)\n\n def test_clear_preserves_awaiting_input(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n t1 = Task(plugin_id=\"p1\", params={})\n t1.status = TaskStatus.AWAITING_INPUT\n mgr.tasks[t1.id] = t1\n\n removed = mgr.clear_tasks()\n assert removed == 0\n assert t1.id in mgr.tasks\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerSubscriptions:\n \"\"\"Tests for log subscription management.\"\"\"\n\n @pytest.mark.asyncio\n async def test_subscribe_creates_queue(self):\n mgr, _, _, _ = _make_manager()\n try:\n queue = await mgr.subscribe_logs(\"task-1\")\n assert isinstance(queue, asyncio.Queue)\n assert \"task-1\" in mgr.subscribers\n assert queue in mgr.subscribers[\"task-1\"]\n finally:\n _cleanup_manager(mgr)\n\n @pytest.mark.asyncio\n async def test_unsubscribe_removes_queue(self):\n mgr, _, _, _ = _make_manager()\n try:\n queue = await mgr.subscribe_logs(\"task-1\")\n mgr.unsubscribe_logs(\"task-1\", queue)\n assert \"task-1\" not in mgr.subscribers\n finally:\n _cleanup_manager(mgr)\n\n @pytest.mark.asyncio\n async def test_multiple_subscribers(self):\n mgr, _, _, _ = _make_manager()\n try:\n q1 = await mgr.subscribe_logs(\"task-1\")\n q2 = await mgr.subscribe_logs(\"task-1\")\n assert len(mgr.subscribers[\"task-1\"]) == 2\n\n mgr.unsubscribe_logs(\"task-1\", q1)\n assert len(mgr.subscribers[\"task-1\"]) == 1\n finally:\n _cleanup_manager(mgr)\n\n\nclass TestTaskManagerInput:\n \"\"\"Tests for await_input and resume_task_with_password.\"\"\"\n\n def test_await_input_sets_status(self):\n mgr, _, persist_svc, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.RUNNING\n mgr.tasks[task.id] = task\n\n # NOTE: source code has a bug where await_input calls _add_log\n # with a dict as 4th positional arg (source), causing Pydantic\n # ValidationError. We patch _add_log to test the state transition.\n mgr._add_log = MagicMock()\n mgr.await_input(task.id, {\"prompt\": \"Enter password\"})\n\n assert task.status == TaskStatus.AWAITING_INPUT\n assert task.input_required is True\n assert task.input_request == {\"prompt\": \"Enter password\"}\n persist_svc.persist_task.assert_called()\n finally:\n _cleanup_manager(mgr)\n\n def test_await_input_not_running_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.PENDING\n mgr.tasks[task.id] = task\n\n with pytest.raises(ValueError, match=\"not RUNNING\"):\n mgr.await_input(task.id, {})\n finally:\n _cleanup_manager(mgr)\n\n def test_await_input_nonexistent_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n with pytest.raises(ValueError, match=\"not found\"):\n mgr.await_input(\"nonexistent\", {})\n finally:\n _cleanup_manager(mgr)\n\n def test_resume_with_password(self):\n mgr, _, persist_svc, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.AWAITING_INPUT\n mgr.tasks[task.id] = task\n\n # NOTE: source code has same _add_log positional-arg bug in resume too.\n mgr._add_log = MagicMock()\n mgr.resume_task_with_password(task.id, {\"db1\": \"pass123\"})\n\n assert task.status == TaskStatus.RUNNING\n assert task.params[\"passwords\"] == {\"db1\": \"pass123\"}\n assert task.input_required is False\n assert task.input_request is None\n finally:\n _cleanup_manager(mgr)\n\n def test_resume_not_awaiting_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.RUNNING\n mgr.tasks[task.id] = task\n\n with pytest.raises(ValueError, match=\"not AWAITING_INPUT\"):\n mgr.resume_task_with_password(task.id, {\"db\": \"pass\"})\n finally:\n _cleanup_manager(mgr)\n\n def test_resume_empty_passwords_raises(self):\n mgr, _, _, _ = _make_manager()\n try:\n from src.core.task_manager.models import Task, TaskStatus\n task = Task(plugin_id=\"p1\", params={})\n task.status = TaskStatus.AWAITING_INPUT\n mgr.tasks[task.id] = task\n\n with pytest.raises(ValueError, match=\"non-empty\"):\n mgr.resume_task_with_password(task.id, {})\n finally:\n _cleanup_manager(mgr)\n\n# [/DEF:test_task_manager:Module]\n" }, { "contract_id": "_make_manager", "contract_type": "Function", "file_path": "backend/tests/test_task_manager.py", - "start_line": 20, - "end_line": 55, + "start_line": 21, + "end_line": 56, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -105142,8 +73598,8 @@ "contract_id": "_cleanup_manager", "contract_type": "Function", "file_path": "backend/tests/test_task_manager.py", - "start_line": 57, - "end_line": 65, + "start_line": 58, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -105174,11 +73630,10 @@ "contract_type": "Module", "file_path": "backend/tests/test_task_persistence.py", "start_line": 1, - "end_line": 433, - "tier": "TIER_3", - "complexity": 5, + "end_line": 431, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "COMPLEXITY": "5", "LAYER": "Test", "PURPOSE": "Unit tests for TaskPersistenceService.", "SEMANTICS": [ @@ -105205,65 +73660,11 @@ "detail": null }, { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Module' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C2", "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Module" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Module' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 2, "contract_type": "Module" } }, @@ -105287,18 +73688,17 @@ } ], "anchor_syntax": "def", - "body": "# [DEF:test_task_persistence:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: test, task, persistence, unit_test\n# @PURPOSE: Unit tests for TaskPersistenceService.\n# @LAYER: Test\n# @COMPLEXITY: 5\n# @TEST_DATA: valid_task -> {\"id\": \"test-uuid-1\", \"plugin_id\": \"backup\", \"status\": \"PENDING\"}\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime, timedelta\nfrom unittest.mock import patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.models.mapping import Base, Environment\nfrom src.models.task import TaskRecord\nfrom src.core.task_manager.persistence import TaskPersistenceService\nfrom src.core.task_manager.models import Task, TaskStatus, LogEntry\n# [/SECTION]\n\n\n# [DEF:TestTaskPersistenceHelpers:Class]\n# @RELATION: BINDS_TO -> test_task_persistence\n# @PURPOSE: Test suite for TaskPersistenceService static helper methods.\n# @COMPLEXITY: 5\nclass TestTaskPersistenceHelpers:\n\n # [DEF:test_json_load_if_needed_none:Function]\n # @PURPOSE: Test _json_load_if_needed with None input.\n def test_json_load_if_needed_none(self):\n assert TaskPersistenceService._json_load_if_needed(None) is None\n # [/DEF:test_json_load_if_needed_none:Function]\n\n # [DEF:test_json_load_if_needed_dict:Function]\n # @PURPOSE: Test _json_load_if_needed with dict input.\n def test_json_load_if_needed_dict(self):\n data = {\"key\": \"value\"}\n assert TaskPersistenceService._json_load_if_needed(data) == data\n # [/DEF:test_json_load_if_needed_dict:Function]\n\n # [DEF:test_json_load_if_needed_list:Function]\n # @PURPOSE: Test _json_load_if_needed with list input.\n def test_json_load_if_needed_list(self):\n data = [1, 2, 3]\n assert TaskPersistenceService._json_load_if_needed(data) == data\n # [/DEF:test_json_load_if_needed_list:Function]\n\n # [DEF:test_json_load_if_needed_json_string:Function]\n # @PURPOSE: Test _json_load_if_needed with JSON string.\n def test_json_load_if_needed_json_string(self):\n result = TaskPersistenceService._json_load_if_needed('{\"key\": \"value\"}')\n assert result == {\"key\": \"value\"}\n # [/DEF:test_json_load_if_needed_json_string:Function]\n\n # [DEF:test_json_load_if_needed_empty_string:Function]\n # @PURPOSE: Test _json_load_if_needed with empty/null strings.\n def test_json_load_if_needed_empty_string(self):\n assert TaskPersistenceService._json_load_if_needed(\"\") is None\n assert TaskPersistenceService._json_load_if_needed(\"null\") is None\n assert TaskPersistenceService._json_load_if_needed(\" null \") is None\n # [/DEF:test_json_load_if_needed_empty_string:Function]\n\n # [DEF:test_json_load_if_needed_plain_string:Function]\n # @PURPOSE: Test _json_load_if_needed with non-JSON string.\n def test_json_load_if_needed_plain_string(self):\n result = TaskPersistenceService._json_load_if_needed(\"not json\")\n assert result == \"not json\"\n # [/DEF:test_json_load_if_needed_plain_string:Function]\n\n # [DEF:test_json_load_if_needed_integer:Function]\n # @PURPOSE: Test _json_load_if_needed with integer.\n def test_json_load_if_needed_integer(self):\n assert TaskPersistenceService._json_load_if_needed(42) == 42\n # [/DEF:test_json_load_if_needed_integer:Function]\n\n # [DEF:test_parse_datetime_none:Function]\n # @PURPOSE: Test _parse_datetime with None.\n def test_parse_datetime_none(self):\n assert TaskPersistenceService._parse_datetime(None) is None\n # [/DEF:test_parse_datetime_none:Function]\n\n # [DEF:test_parse_datetime_datetime_object:Function]\n # @PURPOSE: Test _parse_datetime with datetime object.\n def test_parse_datetime_datetime_object(self):\n dt = datetime(2024, 1, 1, 12, 0, 0)\n assert TaskPersistenceService._parse_datetime(dt) == dt\n # [/DEF:test_parse_datetime_datetime_object:Function]\n\n # [DEF:test_parse_datetime_iso_string:Function]\n # @PURPOSE: Test _parse_datetime with ISO string.\n def test_parse_datetime_iso_string(self):\n result = TaskPersistenceService._parse_datetime(\"2024-01-01T12:00:00\")\n assert isinstance(result, datetime)\n assert result.year == 2024\n # [/DEF:test_parse_datetime_iso_string:Function]\n\n # [DEF:test_parse_datetime_invalid_string:Function]\n # @PURPOSE: Test _parse_datetime with invalid string.\n def test_parse_datetime_invalid_string(self):\n assert TaskPersistenceService._parse_datetime(\"not-a-date\") is None\n # [/DEF:test_parse_datetime_invalid_string:Function]\n\n # [DEF:test_parse_datetime_integer:Function]\n # @PURPOSE: Test _parse_datetime with non-string, non-datetime.\n def test_parse_datetime_integer(self):\n assert TaskPersistenceService._parse_datetime(12345) is None\n # [/DEF:test_parse_datetime_integer:Function]\n\n# [/DEF:TestTaskPersistenceHelpers:Class]\n\n\n# [DEF:TestTaskPersistenceService:Class]\n# @RELATION: BINDS_TO -> test_task_persistence\n# @PURPOSE: Test suite for TaskPersistenceService CRUD operations.\n# @COMPLEXITY: 5\n# @TEST_DATA: valid_task -> {\"id\": \"test-uuid-1\", \"plugin_id\": \"backup\", \"status\": \"PENDING\"}\nclass TestTaskPersistenceService:\n\n # [DEF:setup_class:Function]\n # @PURPOSE: Setup in-memory test database.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory SQLite database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskPersistenceService()\n # [/DEF:setup_class:Function]\n\n # [DEF:teardown_class:Function]\n # @PURPOSE: Dispose of test database.\n @classmethod\n def teardown_class(cls):\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n\n # [DEF:setup_method:Function]\n # @PURPOSE: Clean task_records table before each test.\n def setup_method(self):\n session = self.TestSessionLocal()\n session.query(TaskRecord).delete()\n session.query(Environment).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n\n def _patched(self):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n def _make_task(self, **kwargs):\n \"\"\"Helper: create a Task with test defaults.\"\"\"\n defaults = {\n \"id\": \"test-uuid-1\",\n \"plugin_id\": \"backup\",\n \"status\": TaskStatus.PENDING,\n \"params\": {\"source_env_id\": \"env-1\"},\n }\n defaults.update(kwargs)\n return Task(**defaults)\n\n # [DEF:test_persist_task_new:Function]\n # @PURPOSE: Test persisting a new task creates a record.\n # @PRE: Empty database.\n # @POST: TaskRecord exists in database.\n def test_persist_task_new(self):\n \"\"\"Test persisting a new task creates a record.\"\"\"\n task = self._make_task()\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record is not None\n assert record.type == \"backup\"\n assert record.status == \"PENDING\"\n # [/DEF:test_persist_task_new:Function]\n\n # [DEF:test_persist_task_update:Function]\n # @PURPOSE: Test updating an existing task.\n # @PRE: Task already persisted.\n # @POST: Task record updated with new status.\n def test_persist_task_update(self):\n \"\"\"Test persisting an existing task updates the record.\"\"\"\n task = self._make_task(status=TaskStatus.PENDING)\n\n with self._patched():\n self.service.persist_task(task)\n\n # Update status\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.status == \"RUNNING\"\n assert record.started_at is not None\n # [/DEF:test_persist_task_update:Function]\n\n # [DEF:test_persist_task_with_logs:Function]\n # @PURPOSE: Test persisting a task with log entries.\n # @PRE: Task has logs attached.\n # @POST: Logs serialized as JSON in task record.\n def test_persist_task_with_logs(self):\n \"\"\"Test persisting a task with log entries.\"\"\"\n task = self._make_task()\n task.logs = [\n LogEntry(message=\"Step 1\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Step 2\", level=\"INFO\", source=\"plugin\"),\n ]\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.logs is not None\n assert len(record.logs) == 2\n # [/DEF:test_persist_task_with_logs:Function]\n\n # [DEF:test_persist_task_failed_extracts_error:Function]\n # @PURPOSE: Test that FAILED task extracts last error message.\n # @PRE: Task has FAILED status with ERROR logs.\n # @POST: record.error contains last error message.\n def test_persist_task_failed_extracts_error(self):\n \"\"\"Test that FAILED tasks extract the last error message.\"\"\"\n task = self._make_task(status=TaskStatus.FAILED)\n task.logs = [\n LogEntry(message=\"Started OK\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Connection failed\", level=\"ERROR\", source=\"plugin\"),\n LogEntry(message=\"Retrying...\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Fatal: timeout\", level=\"ERROR\", source=\"plugin\"),\n ]\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.error == \"Fatal: timeout\"\n # [/DEF:test_persist_task_failed_extracts_error:Function]\n\n # [DEF:test_persist_tasks_batch:Function]\n # @PURPOSE: Test persisting multiple tasks.\n # @PRE: Empty database.\n # @POST: All task records created.\n def test_persist_tasks_batch(self):\n \"\"\"Test persisting multiple tasks at once.\"\"\"\n tasks = [\n self._make_task(id=f\"batch-{i}\", plugin_id=\"migration\")\n for i in range(3)\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n session = self.TestSessionLocal()\n count = session.query(TaskRecord).count()\n session.close()\n\n assert count == 3\n # [/DEF:test_persist_tasks_batch:Function]\n\n # [DEF:test_load_tasks:Function]\n # @PURPOSE: Test loading tasks from database.\n # @PRE: Tasks persisted.\n # @POST: Returns list of Task objects with correct data.\n def test_load_tasks(self):\n \"\"\"Test loading tasks from database (round-trip).\"\"\"\n task = self._make_task(\n status=TaskStatus.SUCCESS,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n )\n task.params = {\"key\": \"value\"}\n task.result = {\"output\": \"done\"}\n task.logs = [LogEntry(message=\"Done\", level=\"INFO\", source=\"plugin\")]\n\n with self._patched():\n self.service.persist_task(task)\n\n with self._patched():\n loaded = self.service.load_tasks(limit=10)\n\n assert len(loaded) == 1\n assert loaded[0].id == \"test-uuid-1\"\n assert loaded[0].plugin_id == \"backup\"\n assert loaded[0].status == TaskStatus.SUCCESS\n assert loaded[0].params == {\"key\": \"value\"}\n # [/DEF:test_load_tasks:Function]\n\n # [DEF:test_load_tasks_with_status_filter:Function]\n # @PURPOSE: Test loading tasks filtered by status.\n # @PRE: Tasks with different statuses persisted.\n # @POST: Returns only tasks matching status filter.\n def test_load_tasks_with_status_filter(self):\n \"\"\"Test loading tasks filtered by status.\"\"\"\n tasks = [\n self._make_task(id=\"s1\", status=TaskStatus.SUCCESS),\n self._make_task(id=\"s2\", status=TaskStatus.FAILED),\n self._make_task(id=\"s3\", status=TaskStatus.SUCCESS),\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n failed_tasks = self.service.load_tasks(status=TaskStatus.FAILED)\n\n assert len(failed_tasks) == 1\n assert failed_tasks[0].id == \"s2\"\n assert failed_tasks[0].status == TaskStatus.FAILED\n # [/DEF:test_load_tasks_with_status_filter:Function]\n\n # [DEF:test_load_tasks_with_limit:Function]\n # @PURPOSE: Test loading tasks with limit.\n # @PRE: Multiple tasks persisted.\n # @POST: Returns at most `limit` tasks.\n def test_load_tasks_with_limit(self):\n \"\"\"Test loading tasks respects limit parameter.\"\"\"\n tasks = [\n self._make_task(id=f\"lim-{i}\")\n for i in range(10)\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n loaded = self.service.load_tasks(limit=3)\n\n assert len(loaded) == 3\n # [/DEF:test_load_tasks_with_limit:Function]\n\n # [DEF:test_delete_tasks:Function]\n # @PURPOSE: Test deleting tasks by ID list.\n # @PRE: Tasks persisted.\n # @POST: Specified tasks deleted, others remain.\n def test_delete_tasks(self):\n \"\"\"Test deleting tasks by ID list.\"\"\"\n tasks = [\n self._make_task(id=\"del-1\"),\n self._make_task(id=\"del-2\"),\n self._make_task(id=\"keep-1\"),\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n self.service.delete_tasks([\"del-1\", \"del-2\"])\n\n session = self.TestSessionLocal()\n remaining = session.query(TaskRecord).all()\n session.close()\n\n assert len(remaining) == 1\n assert remaining[0].id == \"keep-1\"\n # [/DEF:test_delete_tasks:Function]\n\n # [DEF:test_delete_tasks_empty_list:Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: None.\n # @POST: No error, no changes.\n def test_delete_tasks_empty_list(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched():\n self.service.delete_tasks([]) # Should not raise\n # [/DEF:test_delete_tasks_empty_list:Function]\n\n # [DEF:test_persist_task_with_datetime_in_params:Function]\n # @PURPOSE: Test json_serializable handles datetime in params.\n # @PRE: Task params contain datetime values.\n # @POST: Params serialized correctly.\n def test_persist_task_with_datetime_in_params(self):\n \"\"\"Test that datetime values in params are serialized to ISO format.\"\"\"\n dt = datetime(2024, 6, 15, 10, 30, 0)\n task = self._make_task(params={\"timestamp\": dt, \"name\": \"test\"})\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.params is not None\n assert record.params[\"timestamp\"] == \"2024-06-15T10:30:00\"\n assert record.params[\"name\"] == \"test\"\n # [/DEF:test_persist_task_with_datetime_in_params:Function]\n\n # [DEF:test_persist_task_resolves_environment_slug_to_existing_id:Function]\n # @PURPOSE: Ensure slug-like environment token resolves to environments.id before persisting task.\n # @PRE: environments table contains env with name convertible to provided slug token.\n # @POST: task_records.environment_id stores actual environments.id and does not violate FK.\n def test_persist_task_resolves_environment_slug_to_existing_id(self):\n session = self.TestSessionLocal()\n env = Environment(id=\"env-uuid-1\", name=\"SS DEV\", url=\"https://example.local\", credentials_id=\"cred-1\")\n session.add(env)\n session.commit()\n session.close()\n\n task = self._make_task(params={\"environment_id\": \"ss-dev\"})\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record is not None\n assert record.environment_id == \"env-uuid-1\"\n # [/DEF:test_persist_task_resolves_environment_slug_to_existing_id:Function]\n\n# [/DEF:TestTaskPersistenceService:Class]\n# [/DEF:test_task_persistence:Module]\n" + "body": "# [DEF:test_task_persistence:Module]\n# @RELATION: BELONGS_TO -> SrcRoot\n# @SEMANTICS: test, task, persistence, unit_test\n# @PURPOSE: Unit tests for TaskPersistenceService.\n# @LAYER: Test\n# @TEST_DATA: valid_task -> {\"id\": \"test-uuid-1\", \"plugin_id\": \"backup\", \"status\": \"PENDING\"}\n\n# [SECTION: IMPORTS]\nfrom datetime import datetime\nfrom unittest.mock import patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.task_manager.models import LogEntry, Task, TaskStatus\nfrom src.core.task_manager.persistence import TaskPersistenceService\nfrom src.models.mapping import Base, Environment\nfrom src.models.task import TaskRecord\n\n# [/SECTION]\n\n\n# [DEF:TestTaskPersistenceHelpers:Class]\n# @RELATION: BINDS_TO -> test_task_persistence\n# @PURPOSE: Test suite for TaskPersistenceService static helper methods.\nclass TestTaskPersistenceHelpers:\n\n # [DEF:test_json_load_if_needed_none:Function]\n # @PURPOSE: Test _json_load_if_needed with None input.\n def test_json_load_if_needed_none(self):\n assert TaskPersistenceService._json_load_if_needed(None) is None\n # [/DEF:test_json_load_if_needed_none:Function]\n\n # [DEF:test_json_load_if_needed_dict:Function]\n # @PURPOSE: Test _json_load_if_needed with dict input.\n def test_json_load_if_needed_dict(self):\n data = {\"key\": \"value\"}\n assert TaskPersistenceService._json_load_if_needed(data) == data\n # [/DEF:test_json_load_if_needed_dict:Function]\n\n # [DEF:test_json_load_if_needed_list:Function]\n # @PURPOSE: Test _json_load_if_needed with list input.\n def test_json_load_if_needed_list(self):\n data = [1, 2, 3]\n assert TaskPersistenceService._json_load_if_needed(data) == data\n # [/DEF:test_json_load_if_needed_list:Function]\n\n # [DEF:test_json_load_if_needed_json_string:Function]\n # @PURPOSE: Test _json_load_if_needed with JSON string.\n def test_json_load_if_needed_json_string(self):\n result = TaskPersistenceService._json_load_if_needed('{\"key\": \"value\"}')\n assert result == {\"key\": \"value\"}\n # [/DEF:test_json_load_if_needed_json_string:Function]\n\n # [DEF:test_json_load_if_needed_empty_string:Function]\n # @PURPOSE: Test _json_load_if_needed with empty/null strings.\n def test_json_load_if_needed_empty_string(self):\n assert TaskPersistenceService._json_load_if_needed(\"\") is None\n assert TaskPersistenceService._json_load_if_needed(\"null\") is None\n assert TaskPersistenceService._json_load_if_needed(\" null \") is None\n # [/DEF:test_json_load_if_needed_empty_string:Function]\n\n # [DEF:test_json_load_if_needed_plain_string:Function]\n # @PURPOSE: Test _json_load_if_needed with non-JSON string.\n def test_json_load_if_needed_plain_string(self):\n result = TaskPersistenceService._json_load_if_needed(\"not json\")\n assert result == \"not json\"\n # [/DEF:test_json_load_if_needed_plain_string:Function]\n\n # [DEF:test_json_load_if_needed_integer:Function]\n # @PURPOSE: Test _json_load_if_needed with integer.\n def test_json_load_if_needed_integer(self):\n assert TaskPersistenceService._json_load_if_needed(42) == 42\n # [/DEF:test_json_load_if_needed_integer:Function]\n\n # [DEF:test_parse_datetime_none:Function]\n # @PURPOSE: Test _parse_datetime with None.\n def test_parse_datetime_none(self):\n assert TaskPersistenceService._parse_datetime(None) is None\n # [/DEF:test_parse_datetime_none:Function]\n\n # [DEF:test_parse_datetime_datetime_object:Function]\n # @PURPOSE: Test _parse_datetime with datetime object.\n def test_parse_datetime_datetime_object(self):\n dt = datetime(2024, 1, 1, 12, 0, 0)\n assert TaskPersistenceService._parse_datetime(dt) == dt\n # [/DEF:test_parse_datetime_datetime_object:Function]\n\n # [DEF:test_parse_datetime_iso_string:Function]\n # @PURPOSE: Test _parse_datetime with ISO string.\n def test_parse_datetime_iso_string(self):\n result = TaskPersistenceService._parse_datetime(\"2024-01-01T12:00:00\")\n assert isinstance(result, datetime)\n assert result.year == 2024\n # [/DEF:test_parse_datetime_iso_string:Function]\n\n # [DEF:test_parse_datetime_invalid_string:Function]\n # @PURPOSE: Test _parse_datetime with invalid string.\n def test_parse_datetime_invalid_string(self):\n assert TaskPersistenceService._parse_datetime(\"not-a-date\") is None\n # [/DEF:test_parse_datetime_invalid_string:Function]\n\n # [DEF:test_parse_datetime_integer:Function]\n # @PURPOSE: Test _parse_datetime with non-string, non-datetime.\n def test_parse_datetime_integer(self):\n assert TaskPersistenceService._parse_datetime(12345) is None\n # [/DEF:test_parse_datetime_integer:Function]\n\n# [/DEF:TestTaskPersistenceHelpers:Class]\n\n\n# [DEF:TestTaskPersistenceService:Class]\n# @RELATION: BINDS_TO -> test_task_persistence\n# @PURPOSE: Test suite for TaskPersistenceService CRUD operations.\n# @TEST_DATA: valid_task -> {\"id\": \"test-uuid-1\", \"plugin_id\": \"backup\", \"status\": \"PENDING\"}\nclass TestTaskPersistenceService:\n\n # [DEF:setup_class:Function]\n # @PURPOSE: Setup in-memory test database.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory SQLite database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskPersistenceService()\n # [/DEF:setup_class:Function]\n\n # [DEF:teardown_class:Function]\n # @PURPOSE: Dispose of test database.\n @classmethod\n def teardown_class(cls):\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n\n # [DEF:setup_method:Function]\n # @PURPOSE: Clean task_records table before each test.\n def setup_method(self):\n session = self.TestSessionLocal()\n session.query(TaskRecord).delete()\n session.query(Environment).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n\n def _patched(self):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n def _make_task(self, **kwargs):\n \"\"\"Helper: create a Task with test defaults.\"\"\"\n defaults = {\n \"id\": \"test-uuid-1\",\n \"plugin_id\": \"backup\",\n \"status\": TaskStatus.PENDING,\n \"params\": {\"source_env_id\": \"env-1\"},\n }\n defaults.update(kwargs)\n return Task(**defaults)\n\n # [DEF:test_persist_task_new:Function]\n # @PURPOSE: Test persisting a new task creates a record.\n # @PRE: Empty database.\n # @POST: TaskRecord exists in database.\n def test_persist_task_new(self):\n \"\"\"Test persisting a new task creates a record.\"\"\"\n task = self._make_task()\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record is not None\n assert record.type == \"backup\"\n assert record.status == \"PENDING\"\n # [/DEF:test_persist_task_new:Function]\n\n # [DEF:test_persist_task_update:Function]\n # @PURPOSE: Test updating an existing task.\n # @PRE: Task already persisted.\n # @POST: Task record updated with new status.\n def test_persist_task_update(self):\n \"\"\"Test persisting an existing task updates the record.\"\"\"\n task = self._make_task(status=TaskStatus.PENDING)\n\n with self._patched():\n self.service.persist_task(task)\n\n # Update status\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.status == \"RUNNING\"\n assert record.started_at is not None\n # [/DEF:test_persist_task_update:Function]\n\n # [DEF:test_persist_task_with_logs:Function]\n # @PURPOSE: Test persisting a task with log entries.\n # @PRE: Task has logs attached.\n # @POST: Logs serialized as JSON in task record.\n def test_persist_task_with_logs(self):\n \"\"\"Test persisting a task with log entries.\"\"\"\n task = self._make_task()\n task.logs = [\n LogEntry(message=\"Step 1\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Step 2\", level=\"INFO\", source=\"plugin\"),\n ]\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.logs is not None\n assert len(record.logs) == 2\n # [/DEF:test_persist_task_with_logs:Function]\n\n # [DEF:test_persist_task_failed_extracts_error:Function]\n # @PURPOSE: Test that FAILED task extracts last error message.\n # @PRE: Task has FAILED status with ERROR logs.\n # @POST: record.error contains last error message.\n def test_persist_task_failed_extracts_error(self):\n \"\"\"Test that FAILED tasks extract the last error message.\"\"\"\n task = self._make_task(status=TaskStatus.FAILED)\n task.logs = [\n LogEntry(message=\"Started OK\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Connection failed\", level=\"ERROR\", source=\"plugin\"),\n LogEntry(message=\"Retrying...\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Fatal: timeout\", level=\"ERROR\", source=\"plugin\"),\n ]\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.error == \"Fatal: timeout\"\n # [/DEF:test_persist_task_failed_extracts_error:Function]\n\n # [DEF:test_persist_tasks_batch:Function]\n # @PURPOSE: Test persisting multiple tasks.\n # @PRE: Empty database.\n # @POST: All task records created.\n def test_persist_tasks_batch(self):\n \"\"\"Test persisting multiple tasks at once.\"\"\"\n tasks = [\n self._make_task(id=f\"batch-{i}\", plugin_id=\"migration\")\n for i in range(3)\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n session = self.TestSessionLocal()\n count = session.query(TaskRecord).count()\n session.close()\n\n assert count == 3\n # [/DEF:test_persist_tasks_batch:Function]\n\n # [DEF:test_load_tasks:Function]\n # @PURPOSE: Test loading tasks from database.\n # @PRE: Tasks persisted.\n # @POST: Returns list of Task objects with correct data.\n def test_load_tasks(self):\n \"\"\"Test loading tasks from database (round-trip).\"\"\"\n task = self._make_task(\n status=TaskStatus.SUCCESS,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n )\n task.params = {\"key\": \"value\"}\n task.result = {\"output\": \"done\"}\n task.logs = [LogEntry(message=\"Done\", level=\"INFO\", source=\"plugin\")]\n\n with self._patched():\n self.service.persist_task(task)\n\n with self._patched():\n loaded = self.service.load_tasks(limit=10)\n\n assert len(loaded) == 1\n assert loaded[0].id == \"test-uuid-1\"\n assert loaded[0].plugin_id == \"backup\"\n assert loaded[0].status == TaskStatus.SUCCESS\n assert loaded[0].params == {\"key\": \"value\"}\n # [/DEF:test_load_tasks:Function]\n\n # [DEF:test_load_tasks_with_status_filter:Function]\n # @PURPOSE: Test loading tasks filtered by status.\n # @PRE: Tasks with different statuses persisted.\n # @POST: Returns only tasks matching status filter.\n def test_load_tasks_with_status_filter(self):\n \"\"\"Test loading tasks filtered by status.\"\"\"\n tasks = [\n self._make_task(id=\"s1\", status=TaskStatus.SUCCESS),\n self._make_task(id=\"s2\", status=TaskStatus.FAILED),\n self._make_task(id=\"s3\", status=TaskStatus.SUCCESS),\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n failed_tasks = self.service.load_tasks(status=TaskStatus.FAILED)\n\n assert len(failed_tasks) == 1\n assert failed_tasks[0].id == \"s2\"\n assert failed_tasks[0].status == TaskStatus.FAILED\n # [/DEF:test_load_tasks_with_status_filter:Function]\n\n # [DEF:test_load_tasks_with_limit:Function]\n # @PURPOSE: Test loading tasks with limit.\n # @PRE: Multiple tasks persisted.\n # @POST: Returns at most `limit` tasks.\n def test_load_tasks_with_limit(self):\n \"\"\"Test loading tasks respects limit parameter.\"\"\"\n tasks = [\n self._make_task(id=f\"lim-{i}\")\n for i in range(10)\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n loaded = self.service.load_tasks(limit=3)\n\n assert len(loaded) == 3\n # [/DEF:test_load_tasks_with_limit:Function]\n\n # [DEF:test_delete_tasks:Function]\n # @PURPOSE: Test deleting tasks by ID list.\n # @PRE: Tasks persisted.\n # @POST: Specified tasks deleted, others remain.\n def test_delete_tasks(self):\n \"\"\"Test deleting tasks by ID list.\"\"\"\n tasks = [\n self._make_task(id=\"del-1\"),\n self._make_task(id=\"del-2\"),\n self._make_task(id=\"keep-1\"),\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n self.service.delete_tasks([\"del-1\", \"del-2\"])\n\n session = self.TestSessionLocal()\n remaining = session.query(TaskRecord).all()\n session.close()\n\n assert len(remaining) == 1\n assert remaining[0].id == \"keep-1\"\n # [/DEF:test_delete_tasks:Function]\n\n # [DEF:test_delete_tasks_empty_list:Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: None.\n # @POST: No error, no changes.\n def test_delete_tasks_empty_list(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched():\n self.service.delete_tasks([]) # Should not raise\n # [/DEF:test_delete_tasks_empty_list:Function]\n\n # [DEF:test_persist_task_with_datetime_in_params:Function]\n # @PURPOSE: Test json_serializable handles datetime in params.\n # @PRE: Task params contain datetime values.\n # @POST: Params serialized correctly.\n def test_persist_task_with_datetime_in_params(self):\n \"\"\"Test that datetime values in params are serialized to ISO format.\"\"\"\n dt = datetime(2024, 6, 15, 10, 30, 0)\n task = self._make_task(params={\"timestamp\": dt, \"name\": \"test\"})\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.params is not None\n assert record.params[\"timestamp\"] == \"2024-06-15T10:30:00\"\n assert record.params[\"name\"] == \"test\"\n # [/DEF:test_persist_task_with_datetime_in_params:Function]\n\n # [DEF:test_persist_task_resolves_environment_slug_to_existing_id:Function]\n # @PURPOSE: Ensure slug-like environment token resolves to environments.id before persisting task.\n # @PRE: environments table contains env with name convertible to provided slug token.\n # @POST: task_records.environment_id stores actual environments.id and does not violate FK.\n def test_persist_task_resolves_environment_slug_to_existing_id(self):\n session = self.TestSessionLocal()\n env = Environment(id=\"env-uuid-1\", name=\"SS DEV\", url=\"https://example.local\", credentials_id=\"cred-1\")\n session.add(env)\n session.commit()\n session.close()\n\n task = self._make_task(params={\"environment_id\": \"ss-dev\"})\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record is not None\n assert record.environment_id == \"env-uuid-1\"\n # [/DEF:test_persist_task_resolves_environment_slug_to_existing_id:Function]\n\n# [/DEF:TestTaskPersistenceService:Class]\n# [/DEF:test_task_persistence:Module]\n" }, { "contract_id": "TestTaskPersistenceHelpers", "contract_type": "Class", "file_path": "backend/tests/test_task_persistence.py", "start_line": 23, - "end_line": 110, - "tier": "TIER_3", - "complexity": 5, + "end_line": 109, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "PURPOSE": "Test suite for TaskPersistenceService static helper methods." }, "relations": [ @@ -105311,78 +73711,33 @@ ], "schema_warnings": [ { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Class' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Class" } }, { - "code": "missing_required_tag", - "tag": "INVARIANT", - "message": "@INVARIANT is required for contract type 'Class' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Class' at C1", "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Class" } } ], "anchor_syntax": "def", - "body": "# [DEF:TestTaskPersistenceHelpers:Class]\n# @RELATION: BINDS_TO -> test_task_persistence\n# @PURPOSE: Test suite for TaskPersistenceService static helper methods.\n# @COMPLEXITY: 5\nclass TestTaskPersistenceHelpers:\n\n # [DEF:test_json_load_if_needed_none:Function]\n # @PURPOSE: Test _json_load_if_needed with None input.\n def test_json_load_if_needed_none(self):\n assert TaskPersistenceService._json_load_if_needed(None) is None\n # [/DEF:test_json_load_if_needed_none:Function]\n\n # [DEF:test_json_load_if_needed_dict:Function]\n # @PURPOSE: Test _json_load_if_needed with dict input.\n def test_json_load_if_needed_dict(self):\n data = {\"key\": \"value\"}\n assert TaskPersistenceService._json_load_if_needed(data) == data\n # [/DEF:test_json_load_if_needed_dict:Function]\n\n # [DEF:test_json_load_if_needed_list:Function]\n # @PURPOSE: Test _json_load_if_needed with list input.\n def test_json_load_if_needed_list(self):\n data = [1, 2, 3]\n assert TaskPersistenceService._json_load_if_needed(data) == data\n # [/DEF:test_json_load_if_needed_list:Function]\n\n # [DEF:test_json_load_if_needed_json_string:Function]\n # @PURPOSE: Test _json_load_if_needed with JSON string.\n def test_json_load_if_needed_json_string(self):\n result = TaskPersistenceService._json_load_if_needed('{\"key\": \"value\"}')\n assert result == {\"key\": \"value\"}\n # [/DEF:test_json_load_if_needed_json_string:Function]\n\n # [DEF:test_json_load_if_needed_empty_string:Function]\n # @PURPOSE: Test _json_load_if_needed with empty/null strings.\n def test_json_load_if_needed_empty_string(self):\n assert TaskPersistenceService._json_load_if_needed(\"\") is None\n assert TaskPersistenceService._json_load_if_needed(\"null\") is None\n assert TaskPersistenceService._json_load_if_needed(\" null \") is None\n # [/DEF:test_json_load_if_needed_empty_string:Function]\n\n # [DEF:test_json_load_if_needed_plain_string:Function]\n # @PURPOSE: Test _json_load_if_needed with non-JSON string.\n def test_json_load_if_needed_plain_string(self):\n result = TaskPersistenceService._json_load_if_needed(\"not json\")\n assert result == \"not json\"\n # [/DEF:test_json_load_if_needed_plain_string:Function]\n\n # [DEF:test_json_load_if_needed_integer:Function]\n # @PURPOSE: Test _json_load_if_needed with integer.\n def test_json_load_if_needed_integer(self):\n assert TaskPersistenceService._json_load_if_needed(42) == 42\n # [/DEF:test_json_load_if_needed_integer:Function]\n\n # [DEF:test_parse_datetime_none:Function]\n # @PURPOSE: Test _parse_datetime with None.\n def test_parse_datetime_none(self):\n assert TaskPersistenceService._parse_datetime(None) is None\n # [/DEF:test_parse_datetime_none:Function]\n\n # [DEF:test_parse_datetime_datetime_object:Function]\n # @PURPOSE: Test _parse_datetime with datetime object.\n def test_parse_datetime_datetime_object(self):\n dt = datetime(2024, 1, 1, 12, 0, 0)\n assert TaskPersistenceService._parse_datetime(dt) == dt\n # [/DEF:test_parse_datetime_datetime_object:Function]\n\n # [DEF:test_parse_datetime_iso_string:Function]\n # @PURPOSE: Test _parse_datetime with ISO string.\n def test_parse_datetime_iso_string(self):\n result = TaskPersistenceService._parse_datetime(\"2024-01-01T12:00:00\")\n assert isinstance(result, datetime)\n assert result.year == 2024\n # [/DEF:test_parse_datetime_iso_string:Function]\n\n # [DEF:test_parse_datetime_invalid_string:Function]\n # @PURPOSE: Test _parse_datetime with invalid string.\n def test_parse_datetime_invalid_string(self):\n assert TaskPersistenceService._parse_datetime(\"not-a-date\") is None\n # [/DEF:test_parse_datetime_invalid_string:Function]\n\n # [DEF:test_parse_datetime_integer:Function]\n # @PURPOSE: Test _parse_datetime with non-string, non-datetime.\n def test_parse_datetime_integer(self):\n assert TaskPersistenceService._parse_datetime(12345) is None\n # [/DEF:test_parse_datetime_integer:Function]\n\n# [/DEF:TestTaskPersistenceHelpers:Class]\n" + "body": "# [DEF:TestTaskPersistenceHelpers:Class]\n# @RELATION: BINDS_TO -> test_task_persistence\n# @PURPOSE: Test suite for TaskPersistenceService static helper methods.\nclass TestTaskPersistenceHelpers:\n\n # [DEF:test_json_load_if_needed_none:Function]\n # @PURPOSE: Test _json_load_if_needed with None input.\n def test_json_load_if_needed_none(self):\n assert TaskPersistenceService._json_load_if_needed(None) is None\n # [/DEF:test_json_load_if_needed_none:Function]\n\n # [DEF:test_json_load_if_needed_dict:Function]\n # @PURPOSE: Test _json_load_if_needed with dict input.\n def test_json_load_if_needed_dict(self):\n data = {\"key\": \"value\"}\n assert TaskPersistenceService._json_load_if_needed(data) == data\n # [/DEF:test_json_load_if_needed_dict:Function]\n\n # [DEF:test_json_load_if_needed_list:Function]\n # @PURPOSE: Test _json_load_if_needed with list input.\n def test_json_load_if_needed_list(self):\n data = [1, 2, 3]\n assert TaskPersistenceService._json_load_if_needed(data) == data\n # [/DEF:test_json_load_if_needed_list:Function]\n\n # [DEF:test_json_load_if_needed_json_string:Function]\n # @PURPOSE: Test _json_load_if_needed with JSON string.\n def test_json_load_if_needed_json_string(self):\n result = TaskPersistenceService._json_load_if_needed('{\"key\": \"value\"}')\n assert result == {\"key\": \"value\"}\n # [/DEF:test_json_load_if_needed_json_string:Function]\n\n # [DEF:test_json_load_if_needed_empty_string:Function]\n # @PURPOSE: Test _json_load_if_needed with empty/null strings.\n def test_json_load_if_needed_empty_string(self):\n assert TaskPersistenceService._json_load_if_needed(\"\") is None\n assert TaskPersistenceService._json_load_if_needed(\"null\") is None\n assert TaskPersistenceService._json_load_if_needed(\" null \") is None\n # [/DEF:test_json_load_if_needed_empty_string:Function]\n\n # [DEF:test_json_load_if_needed_plain_string:Function]\n # @PURPOSE: Test _json_load_if_needed with non-JSON string.\n def test_json_load_if_needed_plain_string(self):\n result = TaskPersistenceService._json_load_if_needed(\"not json\")\n assert result == \"not json\"\n # [/DEF:test_json_load_if_needed_plain_string:Function]\n\n # [DEF:test_json_load_if_needed_integer:Function]\n # @PURPOSE: Test _json_load_if_needed with integer.\n def test_json_load_if_needed_integer(self):\n assert TaskPersistenceService._json_load_if_needed(42) == 42\n # [/DEF:test_json_load_if_needed_integer:Function]\n\n # [DEF:test_parse_datetime_none:Function]\n # @PURPOSE: Test _parse_datetime with None.\n def test_parse_datetime_none(self):\n assert TaskPersistenceService._parse_datetime(None) is None\n # [/DEF:test_parse_datetime_none:Function]\n\n # [DEF:test_parse_datetime_datetime_object:Function]\n # @PURPOSE: Test _parse_datetime with datetime object.\n def test_parse_datetime_datetime_object(self):\n dt = datetime(2024, 1, 1, 12, 0, 0)\n assert TaskPersistenceService._parse_datetime(dt) == dt\n # [/DEF:test_parse_datetime_datetime_object:Function]\n\n # [DEF:test_parse_datetime_iso_string:Function]\n # @PURPOSE: Test _parse_datetime with ISO string.\n def test_parse_datetime_iso_string(self):\n result = TaskPersistenceService._parse_datetime(\"2024-01-01T12:00:00\")\n assert isinstance(result, datetime)\n assert result.year == 2024\n # [/DEF:test_parse_datetime_iso_string:Function]\n\n # [DEF:test_parse_datetime_invalid_string:Function]\n # @PURPOSE: Test _parse_datetime with invalid string.\n def test_parse_datetime_invalid_string(self):\n assert TaskPersistenceService._parse_datetime(\"not-a-date\") is None\n # [/DEF:test_parse_datetime_invalid_string:Function]\n\n # [DEF:test_parse_datetime_integer:Function]\n # @PURPOSE: Test _parse_datetime with non-string, non-datetime.\n def test_parse_datetime_integer(self):\n assert TaskPersistenceService._parse_datetime(12345) is None\n # [/DEF:test_parse_datetime_integer:Function]\n\n# [/DEF:TestTaskPersistenceHelpers:Class]\n" }, { "contract_id": "test_json_load_if_needed_none", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 29, - "end_line": 33, + "start_line": 28, + "end_line": 32, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105397,8 +73752,8 @@ "contract_id": "test_json_load_if_needed_dict", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 35, - "end_line": 40, + "start_line": 34, + "end_line": 39, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105413,8 +73768,8 @@ "contract_id": "test_json_load_if_needed_list", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 42, - "end_line": 47, + "start_line": 41, + "end_line": 46, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105429,8 +73784,8 @@ "contract_id": "test_json_load_if_needed_json_string", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 49, - "end_line": 54, + "start_line": 48, + "end_line": 53, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105445,8 +73800,8 @@ "contract_id": "test_json_load_if_needed_empty_string", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 56, - "end_line": 62, + "start_line": 55, + "end_line": 61, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105461,8 +73816,8 @@ "contract_id": "test_json_load_if_needed_plain_string", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 64, - "end_line": 69, + "start_line": 63, + "end_line": 68, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105477,8 +73832,8 @@ "contract_id": "test_json_load_if_needed_integer", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 71, - "end_line": 75, + "start_line": 70, + "end_line": 74, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105493,8 +73848,8 @@ "contract_id": "test_parse_datetime_none", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 77, - "end_line": 81, + "start_line": 76, + "end_line": 80, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105509,8 +73864,8 @@ "contract_id": "test_parse_datetime_datetime_object", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 83, - "end_line": 88, + "start_line": 82, + "end_line": 87, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105525,8 +73880,8 @@ "contract_id": "test_parse_datetime_iso_string", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 90, - "end_line": 96, + "start_line": 89, + "end_line": 95, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105541,8 +73896,8 @@ "contract_id": "test_parse_datetime_invalid_string", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 98, - "end_line": 102, + "start_line": 97, + "end_line": 101, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105557,8 +73912,8 @@ "contract_id": "test_parse_datetime_integer", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 104, - "end_line": 108, + "start_line": 103, + "end_line": 107, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105573,12 +73928,11 @@ "contract_id": "TestTaskPersistenceService", "contract_type": "Class", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 113, - "end_line": 432, - "tier": "TIER_3", - "complexity": 5, + "start_line": 112, + "end_line": 430, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "5", "PURPOSE": "Test suite for TaskPersistenceService CRUD operations.", "TEST_DATA": "valid_task -> {\"id\": \"test-uuid-1\", \"plugin_id\": \"backup\", \"status\": \"PENDING\"}" }, @@ -105591,6 +73945,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Class" + } + }, { "code": "unknown_tag", "tag": "TEST_DATA", @@ -105598,78 +73961,24 @@ "detail": null }, { - "code": "missing_required_tag", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is required for contract type 'Class' at C5", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Class' at C1", "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": "RATIONALE", - "message": "@RATIONALE is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, - { - "code": "missing_required_tag", - "tag": "REJECTED", - "message": "@REJECTED is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Class" - } - }, - { - "code": "missing_required_tag", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is required for contract type 'Class' at C5", - "detail": { - "actual_complexity": 5, + "actual_complexity": 1, "contract_type": "Class" } } ], "anchor_syntax": "def", - "body": "# [DEF:TestTaskPersistenceService:Class]\n# @RELATION: BINDS_TO -> test_task_persistence\n# @PURPOSE: Test suite for TaskPersistenceService CRUD operations.\n# @COMPLEXITY: 5\n# @TEST_DATA: valid_task -> {\"id\": \"test-uuid-1\", \"plugin_id\": \"backup\", \"status\": \"PENDING\"}\nclass TestTaskPersistenceService:\n\n # [DEF:setup_class:Function]\n # @PURPOSE: Setup in-memory test database.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory SQLite database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskPersistenceService()\n # [/DEF:setup_class:Function]\n\n # [DEF:teardown_class:Function]\n # @PURPOSE: Dispose of test database.\n @classmethod\n def teardown_class(cls):\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n\n # [DEF:setup_method:Function]\n # @PURPOSE: Clean task_records table before each test.\n def setup_method(self):\n session = self.TestSessionLocal()\n session.query(TaskRecord).delete()\n session.query(Environment).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n\n def _patched(self):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n def _make_task(self, **kwargs):\n \"\"\"Helper: create a Task with test defaults.\"\"\"\n defaults = {\n \"id\": \"test-uuid-1\",\n \"plugin_id\": \"backup\",\n \"status\": TaskStatus.PENDING,\n \"params\": {\"source_env_id\": \"env-1\"},\n }\n defaults.update(kwargs)\n return Task(**defaults)\n\n # [DEF:test_persist_task_new:Function]\n # @PURPOSE: Test persisting a new task creates a record.\n # @PRE: Empty database.\n # @POST: TaskRecord exists in database.\n def test_persist_task_new(self):\n \"\"\"Test persisting a new task creates a record.\"\"\"\n task = self._make_task()\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record is not None\n assert record.type == \"backup\"\n assert record.status == \"PENDING\"\n # [/DEF:test_persist_task_new:Function]\n\n # [DEF:test_persist_task_update:Function]\n # @PURPOSE: Test updating an existing task.\n # @PRE: Task already persisted.\n # @POST: Task record updated with new status.\n def test_persist_task_update(self):\n \"\"\"Test persisting an existing task updates the record.\"\"\"\n task = self._make_task(status=TaskStatus.PENDING)\n\n with self._patched():\n self.service.persist_task(task)\n\n # Update status\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.status == \"RUNNING\"\n assert record.started_at is not None\n # [/DEF:test_persist_task_update:Function]\n\n # [DEF:test_persist_task_with_logs:Function]\n # @PURPOSE: Test persisting a task with log entries.\n # @PRE: Task has logs attached.\n # @POST: Logs serialized as JSON in task record.\n def test_persist_task_with_logs(self):\n \"\"\"Test persisting a task with log entries.\"\"\"\n task = self._make_task()\n task.logs = [\n LogEntry(message=\"Step 1\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Step 2\", level=\"INFO\", source=\"plugin\"),\n ]\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.logs is not None\n assert len(record.logs) == 2\n # [/DEF:test_persist_task_with_logs:Function]\n\n # [DEF:test_persist_task_failed_extracts_error:Function]\n # @PURPOSE: Test that FAILED task extracts last error message.\n # @PRE: Task has FAILED status with ERROR logs.\n # @POST: record.error contains last error message.\n def test_persist_task_failed_extracts_error(self):\n \"\"\"Test that FAILED tasks extract the last error message.\"\"\"\n task = self._make_task(status=TaskStatus.FAILED)\n task.logs = [\n LogEntry(message=\"Started OK\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Connection failed\", level=\"ERROR\", source=\"plugin\"),\n LogEntry(message=\"Retrying...\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Fatal: timeout\", level=\"ERROR\", source=\"plugin\"),\n ]\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.error == \"Fatal: timeout\"\n # [/DEF:test_persist_task_failed_extracts_error:Function]\n\n # [DEF:test_persist_tasks_batch:Function]\n # @PURPOSE: Test persisting multiple tasks.\n # @PRE: Empty database.\n # @POST: All task records created.\n def test_persist_tasks_batch(self):\n \"\"\"Test persisting multiple tasks at once.\"\"\"\n tasks = [\n self._make_task(id=f\"batch-{i}\", plugin_id=\"migration\")\n for i in range(3)\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n session = self.TestSessionLocal()\n count = session.query(TaskRecord).count()\n session.close()\n\n assert count == 3\n # [/DEF:test_persist_tasks_batch:Function]\n\n # [DEF:test_load_tasks:Function]\n # @PURPOSE: Test loading tasks from database.\n # @PRE: Tasks persisted.\n # @POST: Returns list of Task objects with correct data.\n def test_load_tasks(self):\n \"\"\"Test loading tasks from database (round-trip).\"\"\"\n task = self._make_task(\n status=TaskStatus.SUCCESS,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n )\n task.params = {\"key\": \"value\"}\n task.result = {\"output\": \"done\"}\n task.logs = [LogEntry(message=\"Done\", level=\"INFO\", source=\"plugin\")]\n\n with self._patched():\n self.service.persist_task(task)\n\n with self._patched():\n loaded = self.service.load_tasks(limit=10)\n\n assert len(loaded) == 1\n assert loaded[0].id == \"test-uuid-1\"\n assert loaded[0].plugin_id == \"backup\"\n assert loaded[0].status == TaskStatus.SUCCESS\n assert loaded[0].params == {\"key\": \"value\"}\n # [/DEF:test_load_tasks:Function]\n\n # [DEF:test_load_tasks_with_status_filter:Function]\n # @PURPOSE: Test loading tasks filtered by status.\n # @PRE: Tasks with different statuses persisted.\n # @POST: Returns only tasks matching status filter.\n def test_load_tasks_with_status_filter(self):\n \"\"\"Test loading tasks filtered by status.\"\"\"\n tasks = [\n self._make_task(id=\"s1\", status=TaskStatus.SUCCESS),\n self._make_task(id=\"s2\", status=TaskStatus.FAILED),\n self._make_task(id=\"s3\", status=TaskStatus.SUCCESS),\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n failed_tasks = self.service.load_tasks(status=TaskStatus.FAILED)\n\n assert len(failed_tasks) == 1\n assert failed_tasks[0].id == \"s2\"\n assert failed_tasks[0].status == TaskStatus.FAILED\n # [/DEF:test_load_tasks_with_status_filter:Function]\n\n # [DEF:test_load_tasks_with_limit:Function]\n # @PURPOSE: Test loading tasks with limit.\n # @PRE: Multiple tasks persisted.\n # @POST: Returns at most `limit` tasks.\n def test_load_tasks_with_limit(self):\n \"\"\"Test loading tasks respects limit parameter.\"\"\"\n tasks = [\n self._make_task(id=f\"lim-{i}\")\n for i in range(10)\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n loaded = self.service.load_tasks(limit=3)\n\n assert len(loaded) == 3\n # [/DEF:test_load_tasks_with_limit:Function]\n\n # [DEF:test_delete_tasks:Function]\n # @PURPOSE: Test deleting tasks by ID list.\n # @PRE: Tasks persisted.\n # @POST: Specified tasks deleted, others remain.\n def test_delete_tasks(self):\n \"\"\"Test deleting tasks by ID list.\"\"\"\n tasks = [\n self._make_task(id=\"del-1\"),\n self._make_task(id=\"del-2\"),\n self._make_task(id=\"keep-1\"),\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n self.service.delete_tasks([\"del-1\", \"del-2\"])\n\n session = self.TestSessionLocal()\n remaining = session.query(TaskRecord).all()\n session.close()\n\n assert len(remaining) == 1\n assert remaining[0].id == \"keep-1\"\n # [/DEF:test_delete_tasks:Function]\n\n # [DEF:test_delete_tasks_empty_list:Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: None.\n # @POST: No error, no changes.\n def test_delete_tasks_empty_list(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched():\n self.service.delete_tasks([]) # Should not raise\n # [/DEF:test_delete_tasks_empty_list:Function]\n\n # [DEF:test_persist_task_with_datetime_in_params:Function]\n # @PURPOSE: Test json_serializable handles datetime in params.\n # @PRE: Task params contain datetime values.\n # @POST: Params serialized correctly.\n def test_persist_task_with_datetime_in_params(self):\n \"\"\"Test that datetime values in params are serialized to ISO format.\"\"\"\n dt = datetime(2024, 6, 15, 10, 30, 0)\n task = self._make_task(params={\"timestamp\": dt, \"name\": \"test\"})\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.params is not None\n assert record.params[\"timestamp\"] == \"2024-06-15T10:30:00\"\n assert record.params[\"name\"] == \"test\"\n # [/DEF:test_persist_task_with_datetime_in_params:Function]\n\n # [DEF:test_persist_task_resolves_environment_slug_to_existing_id:Function]\n # @PURPOSE: Ensure slug-like environment token resolves to environments.id before persisting task.\n # @PRE: environments table contains env with name convertible to provided slug token.\n # @POST: task_records.environment_id stores actual environments.id and does not violate FK.\n def test_persist_task_resolves_environment_slug_to_existing_id(self):\n session = self.TestSessionLocal()\n env = Environment(id=\"env-uuid-1\", name=\"SS DEV\", url=\"https://example.local\", credentials_id=\"cred-1\")\n session.add(env)\n session.commit()\n session.close()\n\n task = self._make_task(params={\"environment_id\": \"ss-dev\"})\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record is not None\n assert record.environment_id == \"env-uuid-1\"\n # [/DEF:test_persist_task_resolves_environment_slug_to_existing_id:Function]\n\n# [/DEF:TestTaskPersistenceService:Class]\n" + "body": "# [DEF:TestTaskPersistenceService:Class]\n# @RELATION: BINDS_TO -> test_task_persistence\n# @PURPOSE: Test suite for TaskPersistenceService CRUD operations.\n# @TEST_DATA: valid_task -> {\"id\": \"test-uuid-1\", \"plugin_id\": \"backup\", \"status\": \"PENDING\"}\nclass TestTaskPersistenceService:\n\n # [DEF:setup_class:Function]\n # @PURPOSE: Setup in-memory test database.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory SQLite database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskPersistenceService()\n # [/DEF:setup_class:Function]\n\n # [DEF:teardown_class:Function]\n # @PURPOSE: Dispose of test database.\n @classmethod\n def teardown_class(cls):\n cls.engine.dispose()\n # [/DEF:teardown_class:Function]\n\n # [DEF:setup_method:Function]\n # @PURPOSE: Clean task_records table before each test.\n def setup_method(self):\n session = self.TestSessionLocal()\n session.query(TaskRecord).delete()\n session.query(Environment).delete()\n session.commit()\n session.close()\n # [/DEF:setup_method:Function]\n\n def _patched(self):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n def _make_task(self, **kwargs):\n \"\"\"Helper: create a Task with test defaults.\"\"\"\n defaults = {\n \"id\": \"test-uuid-1\",\n \"plugin_id\": \"backup\",\n \"status\": TaskStatus.PENDING,\n \"params\": {\"source_env_id\": \"env-1\"},\n }\n defaults.update(kwargs)\n return Task(**defaults)\n\n # [DEF:test_persist_task_new:Function]\n # @PURPOSE: Test persisting a new task creates a record.\n # @PRE: Empty database.\n # @POST: TaskRecord exists in database.\n def test_persist_task_new(self):\n \"\"\"Test persisting a new task creates a record.\"\"\"\n task = self._make_task()\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record is not None\n assert record.type == \"backup\"\n assert record.status == \"PENDING\"\n # [/DEF:test_persist_task_new:Function]\n\n # [DEF:test_persist_task_update:Function]\n # @PURPOSE: Test updating an existing task.\n # @PRE: Task already persisted.\n # @POST: Task record updated with new status.\n def test_persist_task_update(self):\n \"\"\"Test persisting an existing task updates the record.\"\"\"\n task = self._make_task(status=TaskStatus.PENDING)\n\n with self._patched():\n self.service.persist_task(task)\n\n # Update status\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.status == \"RUNNING\"\n assert record.started_at is not None\n # [/DEF:test_persist_task_update:Function]\n\n # [DEF:test_persist_task_with_logs:Function]\n # @PURPOSE: Test persisting a task with log entries.\n # @PRE: Task has logs attached.\n # @POST: Logs serialized as JSON in task record.\n def test_persist_task_with_logs(self):\n \"\"\"Test persisting a task with log entries.\"\"\"\n task = self._make_task()\n task.logs = [\n LogEntry(message=\"Step 1\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Step 2\", level=\"INFO\", source=\"plugin\"),\n ]\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.logs is not None\n assert len(record.logs) == 2\n # [/DEF:test_persist_task_with_logs:Function]\n\n # [DEF:test_persist_task_failed_extracts_error:Function]\n # @PURPOSE: Test that FAILED task extracts last error message.\n # @PRE: Task has FAILED status with ERROR logs.\n # @POST: record.error contains last error message.\n def test_persist_task_failed_extracts_error(self):\n \"\"\"Test that FAILED tasks extract the last error message.\"\"\"\n task = self._make_task(status=TaskStatus.FAILED)\n task.logs = [\n LogEntry(message=\"Started OK\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Connection failed\", level=\"ERROR\", source=\"plugin\"),\n LogEntry(message=\"Retrying...\", level=\"INFO\", source=\"plugin\"),\n LogEntry(message=\"Fatal: timeout\", level=\"ERROR\", source=\"plugin\"),\n ]\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.error == \"Fatal: timeout\"\n # [/DEF:test_persist_task_failed_extracts_error:Function]\n\n # [DEF:test_persist_tasks_batch:Function]\n # @PURPOSE: Test persisting multiple tasks.\n # @PRE: Empty database.\n # @POST: All task records created.\n def test_persist_tasks_batch(self):\n \"\"\"Test persisting multiple tasks at once.\"\"\"\n tasks = [\n self._make_task(id=f\"batch-{i}\", plugin_id=\"migration\")\n for i in range(3)\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n session = self.TestSessionLocal()\n count = session.query(TaskRecord).count()\n session.close()\n\n assert count == 3\n # [/DEF:test_persist_tasks_batch:Function]\n\n # [DEF:test_load_tasks:Function]\n # @PURPOSE: Test loading tasks from database.\n # @PRE: Tasks persisted.\n # @POST: Returns list of Task objects with correct data.\n def test_load_tasks(self):\n \"\"\"Test loading tasks from database (round-trip).\"\"\"\n task = self._make_task(\n status=TaskStatus.SUCCESS,\n started_at=datetime.utcnow(),\n finished_at=datetime.utcnow(),\n )\n task.params = {\"key\": \"value\"}\n task.result = {\"output\": \"done\"}\n task.logs = [LogEntry(message=\"Done\", level=\"INFO\", source=\"plugin\")]\n\n with self._patched():\n self.service.persist_task(task)\n\n with self._patched():\n loaded = self.service.load_tasks(limit=10)\n\n assert len(loaded) == 1\n assert loaded[0].id == \"test-uuid-1\"\n assert loaded[0].plugin_id == \"backup\"\n assert loaded[0].status == TaskStatus.SUCCESS\n assert loaded[0].params == {\"key\": \"value\"}\n # [/DEF:test_load_tasks:Function]\n\n # [DEF:test_load_tasks_with_status_filter:Function]\n # @PURPOSE: Test loading tasks filtered by status.\n # @PRE: Tasks with different statuses persisted.\n # @POST: Returns only tasks matching status filter.\n def test_load_tasks_with_status_filter(self):\n \"\"\"Test loading tasks filtered by status.\"\"\"\n tasks = [\n self._make_task(id=\"s1\", status=TaskStatus.SUCCESS),\n self._make_task(id=\"s2\", status=TaskStatus.FAILED),\n self._make_task(id=\"s3\", status=TaskStatus.SUCCESS),\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n failed_tasks = self.service.load_tasks(status=TaskStatus.FAILED)\n\n assert len(failed_tasks) == 1\n assert failed_tasks[0].id == \"s2\"\n assert failed_tasks[0].status == TaskStatus.FAILED\n # [/DEF:test_load_tasks_with_status_filter:Function]\n\n # [DEF:test_load_tasks_with_limit:Function]\n # @PURPOSE: Test loading tasks with limit.\n # @PRE: Multiple tasks persisted.\n # @POST: Returns at most `limit` tasks.\n def test_load_tasks_with_limit(self):\n \"\"\"Test loading tasks respects limit parameter.\"\"\"\n tasks = [\n self._make_task(id=f\"lim-{i}\")\n for i in range(10)\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n loaded = self.service.load_tasks(limit=3)\n\n assert len(loaded) == 3\n # [/DEF:test_load_tasks_with_limit:Function]\n\n # [DEF:test_delete_tasks:Function]\n # @PURPOSE: Test deleting tasks by ID list.\n # @PRE: Tasks persisted.\n # @POST: Specified tasks deleted, others remain.\n def test_delete_tasks(self):\n \"\"\"Test deleting tasks by ID list.\"\"\"\n tasks = [\n self._make_task(id=\"del-1\"),\n self._make_task(id=\"del-2\"),\n self._make_task(id=\"keep-1\"),\n ]\n\n with self._patched():\n self.service.persist_tasks(tasks)\n\n with self._patched():\n self.service.delete_tasks([\"del-1\", \"del-2\"])\n\n session = self.TestSessionLocal()\n remaining = session.query(TaskRecord).all()\n session.close()\n\n assert len(remaining) == 1\n assert remaining[0].id == \"keep-1\"\n # [/DEF:test_delete_tasks:Function]\n\n # [DEF:test_delete_tasks_empty_list:Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: None.\n # @POST: No error, no changes.\n def test_delete_tasks_empty_list(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched():\n self.service.delete_tasks([]) # Should not raise\n # [/DEF:test_delete_tasks_empty_list:Function]\n\n # [DEF:test_persist_task_with_datetime_in_params:Function]\n # @PURPOSE: Test json_serializable handles datetime in params.\n # @PRE: Task params contain datetime values.\n # @POST: Params serialized correctly.\n def test_persist_task_with_datetime_in_params(self):\n \"\"\"Test that datetime values in params are serialized to ISO format.\"\"\"\n dt = datetime(2024, 6, 15, 10, 30, 0)\n task = self._make_task(params={\"timestamp\": dt, \"name\": \"test\"})\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record.params is not None\n assert record.params[\"timestamp\"] == \"2024-06-15T10:30:00\"\n assert record.params[\"name\"] == \"test\"\n # [/DEF:test_persist_task_with_datetime_in_params:Function]\n\n # [DEF:test_persist_task_resolves_environment_slug_to_existing_id:Function]\n # @PURPOSE: Ensure slug-like environment token resolves to environments.id before persisting task.\n # @PRE: environments table contains env with name convertible to provided slug token.\n # @POST: task_records.environment_id stores actual environments.id and does not violate FK.\n def test_persist_task_resolves_environment_slug_to_existing_id(self):\n session = self.TestSessionLocal()\n env = Environment(id=\"env-uuid-1\", name=\"SS DEV\", url=\"https://example.local\", credentials_id=\"cred-1\")\n session.add(env)\n session.commit()\n session.close()\n\n task = self._make_task(params={\"environment_id\": \"ss-dev\"})\n\n with self._patched():\n self.service.persist_task(task)\n\n session = self.TestSessionLocal()\n record = session.query(TaskRecord).filter_by(id=\"test-uuid-1\").first()\n session.close()\n\n assert record is not None\n assert record.environment_id == \"env-uuid-1\"\n # [/DEF:test_persist_task_resolves_environment_slug_to_existing_id:Function]\n\n# [/DEF:TestTaskPersistenceService:Class]\n" }, { "contract_id": "setup_class", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 120, - "end_line": 129, + "start_line": 118, + "end_line": 127, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -105694,8 +74003,8 @@ "contract_id": "teardown_class", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 131, - "end_line": 136, + "start_line": 129, + "end_line": 134, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -105720,8 +74029,8 @@ "contract_id": "setup_method", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 138, - "end_line": 146, + "start_line": 136, + "end_line": 144, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -105746,8 +74055,8 @@ "contract_id": "test_persist_task_new", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 166, - "end_line": 184, + "start_line": 164, + "end_line": 182, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105783,8 +74092,8 @@ "contract_id": "test_persist_task_update", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 186, - "end_line": 210, + "start_line": 184, + "end_line": 208, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105820,8 +74129,8 @@ "contract_id": "test_persist_task_with_logs", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 212, - "end_line": 233, + "start_line": 210, + "end_line": 231, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105857,8 +74166,8 @@ "contract_id": "test_persist_task_failed_extracts_error", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 235, - "end_line": 257, + "start_line": 233, + "end_line": 255, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105894,8 +74203,8 @@ "contract_id": "test_persist_tasks_batch", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 259, - "end_line": 278, + "start_line": 257, + "end_line": 276, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105931,8 +74240,8 @@ "contract_id": "test_load_tasks", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 280, - "end_line": 306, + "start_line": 278, + "end_line": 304, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -105968,8 +74277,8 @@ "contract_id": "test_load_tasks_with_status_filter", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 308, - "end_line": 329, + "start_line": 306, + "end_line": 327, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106005,8 +74314,8 @@ "contract_id": "test_load_tasks_with_limit", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 331, - "end_line": 349, + "start_line": 329, + "end_line": 347, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106042,8 +74351,8 @@ "contract_id": "test_delete_tasks", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 351, - "end_line": 375, + "start_line": 349, + "end_line": 373, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106079,8 +74388,8 @@ "contract_id": "test_delete_tasks_empty_list", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 377, - "end_line": 385, + "start_line": 375, + "end_line": 383, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106116,8 +74425,8 @@ "contract_id": "test_persist_task_with_datetime_in_params", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 387, - "end_line": 406, + "start_line": 385, + "end_line": 404, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106153,8 +74462,8 @@ "contract_id": "test_persist_task_resolves_environment_slug_to_existing_id", "contract_type": "Function", "file_path": "backend/tests/test_task_persistence.py", - "start_line": 408, - "end_line": 430, + "start_line": 406, + "end_line": 428, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106191,11 +74500,10 @@ "contract_type": "Module", "file_path": "backend/tests/test_translate_corrections.py", "start_line": 1, - "end_line": 252, - "tier": "TIER_2", - "complexity": 4, + "end_line": 249, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "4", "LAYER": "Test", "PURPOSE": "Tests for term correction API endpoints and DictionaryManager correction methods.", "SEMANTICS": [ @@ -106221,6 +74529,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -106236,49 +74553,31 @@ { "code": "tag_forbidden_by_complexity", "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C4", + "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 4, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "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, + "actual_complexity": 1, "contract_type": "Module" } } ], "anchor_syntax": "def", - "body": "# [DEF:TranslateCorrectionTests:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: tests, translate, corrections, dictionary\n# @PURPOSE: Tests for term correction API endpoints and DictionaryManager correction methods.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [DictionaryManagerModule:Module]\n# @RELATION: BINDS_TO -> [TranslateRoutes:Module]\n#\n# @TEST_CONTRACT: CorrectionFlow ->\n# {\n# required_fields: {source_term, incorrect_target_term, corrected_target_term, dictionary_id},\n# invariants: [\n# \"POST /api/translate/corrections creates or updates entry\",\n# \"Conflict detection works for existing entries\",\n# \"POST /api/translate/corrections/bulk atomic all-or-nothing\",\n# \"Origin tracking fields are populated\"\n# ]\n# }\n# @TEST_FIXTURE: valid_dictionary -> created via DictionaryManager\n# @TEST_EDGE: missing_dictionary_id -> 422\n# @TEST_EDGE: conflict_detected -> conflict response returned\n# @TEST_EDGE: bulk_correction_all_succeed -> atomic commit\n# @TEST_EDGE: bulk_correction_with_conflicts -> conflicts listed\n# @TEST_INVARIANT: correction_flow -> VERIFIED_BY: [valid_dictionary, conflict_detected]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom unittest.mock import MagicMock\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom datetime import datetime, timezone\nimport uuid\n\nfrom src.core.database import Base\nfrom src.app import app\nfrom src.dependencies import get_current_user, get_config_manager, has_permission, get_db\nfrom src.plugins.translate.dictionary import DictionaryManager\nfrom src.models.translate import TerminologyDictionary, DictionaryEntry\n\n# In-memory SQLite\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase.metadata.create_all(bind=engine)\n\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n yield session\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef mock_api_deps():\n config_manager = MagicMock()\n config_manager.get_environments.return_value = []\n\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n def override_get_db():\n try:\n yield session\n finally:\n pass\n\n overrides = {\n get_current_user: lambda: mock_user,\n get_config_manager: lambda: config_manager,\n get_db: override_get_db,\n }\n for dep, override in overrides.items():\n app.dependency_overrides[dep] = override\n\n yield {\"config_manager\": config_manager, \"session\": session}\n\n app.dependency_overrides.clear()\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef client(mock_api_deps):\n return TestClient(app)\n\n\n# [DEF:test_submit_correction_creates_entry:Function]\n# @PURPOSE: Verify that a correction creates a new dictionary entry.\ndef test_submit_correction_creates_entry(db_session):\n \"\"\"Test that submitting a correction creates a new entry.\"\"\"\n dict_obj = DictionaryManager.create_dictionary(\n db_session, name=\"Test Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n created_by=\"testuser\",\n )\n\n result = DictionaryManager.submit_correction(\n db_session,\n dict_id=dict_obj.id,\n source_term=\"hello\",\n incorrect_target_term=\"privet\",\n corrected_target_term=\"zdravstvuyte\",\n origin_run_id=\"run-123\",\n origin_row_key=\"row-1\",\n origin_user_id=\"testuser\",\n )\n\n assert result[\"action\"] == \"created\"\n assert result[\"entry_id\"] is not None\n\n entries, total = DictionaryManager.list_entries(db_session, dict_obj.id)\n assert total == 1\n assert entries[0].source_term == \"hello\"\n assert entries[0].target_term == \"zdravstvuyte\"\n assert entries[0].origin_run_id == \"run-123\"\n assert entries[0].origin_row_key == \"row-1\"\n assert entries[0].origin_user_id == \"testuser\"\n# [/DEF:test_submit_correction_creates_entry:Function]\n\n\n# [DEF:test_submit_correction_conflict_detected:Function]\n# @PURPOSE: Verify conflict detection when entry already exists.\ndef test_submit_correction_conflict_detected(db_session):\n \"\"\"Test that conflict is detected when entry already exists.\"\"\"\n dict_obj = DictionaryManager.create_dictionary(\n db_session, name=\"Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n # Create initial entry\n DictionaryManager.add_entry(db_session, dict_obj.id, \"hello\", \"privet\")\n\n # Submit correction with conflict\n result = DictionaryManager.submit_correction(\n db_session,\n dict_id=dict_obj.id,\n source_term=\"hello\",\n incorrect_target_term=\"privet\",\n corrected_target_term=\"zdravstvuyte\",\n on_conflict=\"keep_existing\",\n )\n\n assert result[\"action\"] == \"conflict_detected\"\n assert result[\"conflict\"] is not None\n assert result[\"conflict\"][\"existing_target_term\"] == \"privet\"\n# [/DEF:test_submit_correction_conflict_detected:Function]\n\n\n# [DEF:test_submit_correction_overwrite:Function]\n# @PURPOSE: Verify that correction overwrites existing entry.\ndef test_submit_correction_overwrite(db_session):\n \"\"\"Test that correction overwrites existing entry.\"\"\"\n dict_obj = DictionaryManager.create_dictionary(\n db_session, name=\"Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n DictionaryManager.add_entry(db_session, dict_obj.id, \"hello\", \"privet\")\n\n result = DictionaryManager.submit_correction(\n db_session,\n dict_id=dict_obj.id,\n source_term=\"hello\",\n incorrect_target_term=\"privet\",\n corrected_target_term=\"hi_there\",\n on_conflict=\"overwrite\",\n )\n\n assert result[\"action\"] == \"updated\"\n entries, _ = DictionaryManager.list_entries(db_session, dict_obj.id)\n assert entries[0].target_term == \"hi_there\"\n# [/DEF:test_submit_correction_overwrite:Function]\n\n\n# [DEF:test_bulk_corrections_atomic:Function]\n# @PURPOSE: Verify bulk corrections are applied atomically.\ndef test_bulk_corrections_atomic(db_session):\n \"\"\"Test that bulk corrections are applied atomically.\"\"\"\n dict_obj = DictionaryManager.create_dictionary(\n db_session, name=\"Bulk Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n corrections = [\n {\"source_term\": \"cat\", \"incorrect_target_term\": \"kot\", \"corrected_target_term\": \"koshka\"},\n {\"source_term\": \"dog\", \"incorrect_target_term\": \"sobaka\", \"corrected_target_term\": \"pyos\"},\n ]\n\n result = DictionaryManager.submit_bulk_corrections(\n db_session,\n dict_id=dict_obj.id,\n corrections=corrections,\n origin_user_id=\"testuser\",\n )\n\n assert result[\"status\"] == \"completed\"\n assert result[\"applied\"] == 2\n assert len(result[\"conflicts\"]) == 0\n\n entries, total = DictionaryManager.list_entries(db_session, dict_obj.id)\n assert total == 2\n# [/DEF:test_bulk_corrections_atomic:Function]\n\n\n# [DEF:test_api_correction_missing_dict:Function]\n# @PURPOSE: Verify POST corrections without dictionary_id returns 422.\ndef test_api_correction_missing_dict(client):\n \"\"\"Test correction without dict_id returns 422.\"\"\"\n response = client.post(\"/api/translate/corrections\", json={\n \"source_term\": \"hello\",\n \"incorrect_target_term\": \"privet\",\n \"corrected_target_term\": \"zdravstvuyte\",\n })\n assert response.status_code == 422\n# [/DEF:test_api_correction_missing_dict:Function]\n\n\n# [DEF:test_api_bulk_corrections:Function]\n# @PURPOSE: Verify POST /corrections/bulk works.\ndef test_api_bulk_corrections(client, mock_api_deps):\n \"\"\"Test bulk corrections endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n dict_obj = DictionaryManager.create_dictionary(\n session, name=\"API Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n response = client.post(\"/api/translate/corrections/bulk\", json={\n \"dictionary_id\": dict_obj.id,\n \"corrections\": [\n {\"source_term\": \"hello\", \"incorrect_target_term\": \"privet\", \"corrected_target_term\": \"hi\"},\n ],\n })\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"completed\"\n# [/DEF:test_api_bulk_corrections:Function]\n# [/DEF:TranslateCorrectionTests:Module]\n" + "body": "# [DEF:TranslateCorrectionTests:Module]\n# @SEMANTICS: tests, translate, corrections, dictionary\n# @PURPOSE: Tests for term correction API endpoints and DictionaryManager correction methods.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [DictionaryManagerModule:Module]\n# @RELATION: BINDS_TO -> [TranslateRoutes:Module]\n#\n# @TEST_CONTRACT: CorrectionFlow ->\n# {\n# required_fields: {source_term, incorrect_target_term, corrected_target_term, dictionary_id},\n# invariants: [\n# \"POST /api/translate/corrections creates or updates entry\",\n# \"Conflict detection works for existing entries\",\n# \"POST /api/translate/corrections/bulk atomic all-or-nothing\",\n# \"Origin tracking fields are populated\"\n# ]\n# }\n# @TEST_FIXTURE: valid_dictionary -> created via DictionaryManager\n# @TEST_EDGE: missing_dictionary_id -> 422\n# @TEST_EDGE: conflict_detected -> conflict response returned\n# @TEST_EDGE: bulk_correction_all_succeed -> atomic commit\n# @TEST_EDGE: bulk_correction_with_conflicts -> conflicts listed\n# @TEST_INVARIANT: correction_flow -> VERIFIED_BY: [valid_dictionary, conflict_detected]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.app import app\nfrom src.core.database import Base\nfrom src.dependencies import get_config_manager, get_current_user, get_db\nfrom src.plugins.translate.dictionary import DictionaryManager\n\n# In-memory SQLite\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase.metadata.create_all(bind=engine)\n\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n yield session\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef mock_api_deps():\n config_manager = MagicMock()\n config_manager.get_environments.return_value = []\n\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n def override_get_db():\n try:\n yield session\n finally:\n pass\n\n overrides = {\n get_current_user: lambda: mock_user,\n get_config_manager: lambda: config_manager,\n get_db: override_get_db,\n }\n for dep, override in overrides.items():\n app.dependency_overrides[dep] = override\n\n yield {\"config_manager\": config_manager, \"session\": session}\n\n app.dependency_overrides.clear()\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef client(mock_api_deps):\n return TestClient(app)\n\n\n# [DEF:test_submit_correction_creates_entry:Function]\n# @PURPOSE: Verify that a correction creates a new dictionary entry.\ndef test_submit_correction_creates_entry(db_session):\n \"\"\"Test that submitting a correction creates a new entry.\"\"\"\n dict_obj = DictionaryManager.create_dictionary(\n db_session, name=\"Test Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n created_by=\"testuser\",\n )\n\n result = DictionaryManager.submit_correction(\n db_session,\n dict_id=dict_obj.id,\n source_term=\"hello\",\n incorrect_target_term=\"privet\",\n corrected_target_term=\"zdravstvuyte\",\n origin_run_id=\"run-123\",\n origin_row_key=\"row-1\",\n origin_user_id=\"testuser\",\n )\n\n assert result[\"action\"] == \"created\"\n assert result[\"entry_id\"] is not None\n\n entries, total = DictionaryManager.list_entries(db_session, dict_obj.id)\n assert total == 1\n assert entries[0].source_term == \"hello\"\n assert entries[0].target_term == \"zdravstvuyte\"\n assert entries[0].origin_run_id == \"run-123\"\n assert entries[0].origin_row_key == \"row-1\"\n assert entries[0].origin_user_id == \"testuser\"\n# [/DEF:test_submit_correction_creates_entry:Function]\n\n\n# [DEF:test_submit_correction_conflict_detected:Function]\n# @PURPOSE: Verify conflict detection when entry already exists.\ndef test_submit_correction_conflict_detected(db_session):\n \"\"\"Test that conflict is detected when entry already exists.\"\"\"\n dict_obj = DictionaryManager.create_dictionary(\n db_session, name=\"Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n # Create initial entry\n DictionaryManager.add_entry(db_session, dict_obj.id, \"hello\", \"privet\")\n\n # Submit correction with conflict\n result = DictionaryManager.submit_correction(\n db_session,\n dict_id=dict_obj.id,\n source_term=\"hello\",\n incorrect_target_term=\"privet\",\n corrected_target_term=\"zdravstvuyte\",\n on_conflict=\"keep_existing\",\n )\n\n assert result[\"action\"] == \"conflict_detected\"\n assert result[\"conflict\"] is not None\n assert result[\"conflict\"][\"existing_target_term\"] == \"privet\"\n# [/DEF:test_submit_correction_conflict_detected:Function]\n\n\n# [DEF:test_submit_correction_overwrite:Function]\n# @PURPOSE: Verify that correction overwrites existing entry.\ndef test_submit_correction_overwrite(db_session):\n \"\"\"Test that correction overwrites existing entry.\"\"\"\n dict_obj = DictionaryManager.create_dictionary(\n db_session, name=\"Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n DictionaryManager.add_entry(db_session, dict_obj.id, \"hello\", \"privet\")\n\n result = DictionaryManager.submit_correction(\n db_session,\n dict_id=dict_obj.id,\n source_term=\"hello\",\n incorrect_target_term=\"privet\",\n corrected_target_term=\"hi_there\",\n on_conflict=\"overwrite\",\n )\n\n assert result[\"action\"] == \"updated\"\n entries, _ = DictionaryManager.list_entries(db_session, dict_obj.id)\n assert entries[0].target_term == \"hi_there\"\n# [/DEF:test_submit_correction_overwrite:Function]\n\n\n# [DEF:test_bulk_corrections_atomic:Function]\n# @PURPOSE: Verify bulk corrections are applied atomically.\ndef test_bulk_corrections_atomic(db_session):\n \"\"\"Test that bulk corrections are applied atomically.\"\"\"\n dict_obj = DictionaryManager.create_dictionary(\n db_session, name=\"Bulk Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n corrections = [\n {\"source_term\": \"cat\", \"incorrect_target_term\": \"kot\", \"corrected_target_term\": \"koshka\"},\n {\"source_term\": \"dog\", \"incorrect_target_term\": \"sobaka\", \"corrected_target_term\": \"pyos\"},\n ]\n\n result = DictionaryManager.submit_bulk_corrections(\n db_session,\n dict_id=dict_obj.id,\n corrections=corrections,\n origin_user_id=\"testuser\",\n )\n\n assert result[\"status\"] == \"completed\"\n assert result[\"applied\"] == 2\n assert len(result[\"conflicts\"]) == 0\n\n entries, total = DictionaryManager.list_entries(db_session, dict_obj.id)\n assert total == 2\n# [/DEF:test_bulk_corrections_atomic:Function]\n\n\n# [DEF:test_api_correction_missing_dict:Function]\n# @PURPOSE: Verify POST corrections without dictionary_id returns 422.\ndef test_api_correction_missing_dict(client):\n \"\"\"Test correction without dict_id returns 422.\"\"\"\n response = client.post(\"/api/translate/corrections\", json={\n \"source_term\": \"hello\",\n \"incorrect_target_term\": \"privet\",\n \"corrected_target_term\": \"zdravstvuyte\",\n })\n assert response.status_code == 422\n# [/DEF:test_api_correction_missing_dict:Function]\n\n\n# [DEF:test_api_bulk_corrections:Function]\n# @PURPOSE: Verify POST /corrections/bulk works.\ndef test_api_bulk_corrections(client, mock_api_deps):\n \"\"\"Test bulk corrections endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n dict_obj = DictionaryManager.create_dictionary(\n session, name=\"API Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n response = client.post(\"/api/translate/corrections/bulk\", json={\n \"dictionary_id\": dict_obj.id,\n \"corrections\": [\n {\"source_term\": \"hello\", \"incorrect_target_term\": \"privet\", \"corrected_target_term\": \"hi\"},\n ],\n })\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"completed\"\n# [/DEF:test_api_bulk_corrections:Function]\n# [/DEF:TranslateCorrectionTests:Module]\n" }, { "contract_id": "test_submit_correction_creates_entry", "contract_type": "Function", "file_path": "backend/tests/test_translate_corrections.py", - "start_line": 106, - "end_line": 136, + "start_line": 103, + "end_line": 133, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106293,8 +74592,8 @@ "contract_id": "test_submit_correction_conflict_detected", "contract_type": "Function", "file_path": "backend/tests/test_translate_corrections.py", - "start_line": 139, - "end_line": 163, + "start_line": 136, + "end_line": 160, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106309,8 +74608,8 @@ "contract_id": "test_submit_correction_overwrite", "contract_type": "Function", "file_path": "backend/tests/test_translate_corrections.py", - "start_line": 166, - "end_line": 188, + "start_line": 163, + "end_line": 185, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106325,8 +74624,8 @@ "contract_id": "test_bulk_corrections_atomic", "contract_type": "Function", "file_path": "backend/tests/test_translate_corrections.py", - "start_line": 191, - "end_line": 217, + "start_line": 188, + "end_line": 214, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106341,8 +74640,8 @@ "contract_id": "test_api_correction_missing_dict", "contract_type": "Function", "file_path": "backend/tests/test_translate_corrections.py", - "start_line": 220, - "end_line": 230, + "start_line": 217, + "end_line": 227, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106357,8 +74656,8 @@ "contract_id": "test_api_bulk_corrections", "contract_type": "Function", "file_path": "backend/tests/test_translate_corrections.py", - "start_line": 233, - "end_line": 251, + "start_line": 230, + "end_line": 248, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106369,16 +74668,191 @@ "anchor_syntax": "def", "body": "# [DEF:test_api_bulk_corrections:Function]\n# @PURPOSE: Verify POST /corrections/bulk works.\ndef test_api_bulk_corrections(client, mock_api_deps):\n \"\"\"Test bulk corrections endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n dict_obj = DictionaryManager.create_dictionary(\n session, name=\"API Dict\", source_dialect=\"en\", target_dialect=\"ru\",\n )\n\n response = client.post(\"/api/translate/corrections/bulk\", json={\n \"dictionary_id\": dict_obj.id,\n \"corrections\": [\n {\"source_term\": \"hello\", \"incorrect_target_term\": \"privet\", \"corrected_target_term\": \"hi\"},\n ],\n })\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"completed\"\n# [/DEF:test_api_bulk_corrections:Function]\n" }, + { + "contract_id": "TestTranslateExecutorFilter", + "contract_type": "Module", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 1, + "end_line": 289, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Verify TranslationExecutor._filter_new_keys contracts — key dedup, empty-set, guard triggers.", + "TEST_EDGE": "null_source_data — None source_data is safely handled", + "TEST_INVARIANT": "new_key_only_filter -> VERIFIED_BY: [test_no_prior_run_returns_all_rows, test_filters_existing_keys, test_composite_keys]" + }, + "relations": [ + { + "source_id": "TestTranslateExecutorFilter", + "relation_type": "BINDS_TO", + "target_id": "TranslationExecutor", + "target_ref": "[TranslationExecutor]" + } + ], + "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", + "body": "# #region TestTranslateExecutorFilter [C:3] [TYPE Module] [SEMANTICS test, translate, executor, filter, new-key-only]\n# @BRIEF Verify TranslationExecutor._filter_new_keys contracts — key dedup, empty-set, guard triggers.\n# @RELATION BINDS_TO -> [TranslationExecutor]\n# @TEST_EDGE: no_prior_run — all rows pass through when no previous run exists\n# @TEST_EDGE: filter_existing_keys — rows with keys from prior run are skipped\n# @TEST_EDGE: composite_keys — multiple target_key_cols produce correct dedup\n# @TEST_EDGE: all_existing — zero new rows returns empty list\n# @TEST_EDGE: empty_key_cols — guard triggers, all rows returned with warning\n# @TEST_EDGE: null_source_data — None source_data is safely handled\n# @TEST_INVARIANT: new_key_only_filter -> VERIFIED_BY: [test_no_prior_run_returns_all_rows, test_filters_existing_keys, test_composite_keys]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nfrom unittest.mock import MagicMock\n\nfrom src.plugins.translate.executor import TranslationExecutor\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _make_executor(mock_db=None):\n \"\"\"Create a TranslationExecutor with a mocked DB session and config.\"\"\"\n db = mock_db or MagicMock()\n cfg = MagicMock()\n return TranslationExecutor(db, cfg, current_user=\"test_user\")\n\n\ndef _make_job(source_key_cols=None, target_key_cols=None):\n \"\"\"Build a MagicMock TranslationJob with key columns configured.\"\"\"\n job = MagicMock()\n job.id = \"job-test-1\"\n job.source_key_cols = source_key_cols or []\n job.target_key_cols = target_key_cols or []\n return job\n\n\ndef _make_rows(*source_data_dicts):\n \"\"\"Build source_rows list from source_data dicts.\"\"\"\n rows = []\n for sd in source_data_dicts:\n rows.append({\n \"row_index\": sd.get(\"_idx\", \"0\"),\n \"source_text\": sd.get(\"text\", \"\"),\n \"source_data\": sd,\n })\n return rows\n\n\ndef _make_prev_records(run_id, source_data_dicts):\n \"\"\"Build MagicMock TranslationRecord list with source_data and SUCCESS status.\"\"\"\n recs = []\n for sd in source_data_dicts:\n rec = MagicMock()\n rec.run_id = run_id\n rec.status = \"SUCCESS\"\n rec.source_data = sd\n recs.append(rec)\n return recs\n\n\ndef _wire_query_chain(mock_db, prev_run, prev_records):\n \"\"\"Wire mock DB for _filter_new_keys: two sequential db.query() calls.\n\n Query 1: db.query(TranslationRun).filter(a,b,c,d).order_by(desc).first() → prev_run\n Query 2: db.query(TranslationRecord).filter(a,b).all() → prev_records\n \"\"\"\n q1 = MagicMock()\n q1.filter.return_value.order_by.return_value.first.return_value = prev_run\n q2 = MagicMock()\n q2.filter.return_value.all.return_value = prev_records\n mock_db.query.side_effect = [q1, q2]\n\n\n# #region test_no_prior_run_returns_all_rows [C:1] [TYPE Function]\ndef test_no_prior_run_returns_all_rows():\n \"\"\"When no prior TranslationRun exists, all source_rows pass through unchanged.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"], target_key_cols=[\"id\"])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"hello\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"world\", \"_idx\": \"1\"},\n {\"id\": 3, \"text\": \"test\", \"_idx\": \"2\"},\n )\n\n # Mock: no prior run (query returns None)\n _wire_query_chain(executor.db, prev_run=None, prev_records=[])\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 3\n assert result == rows # identity — all pass through\n# #endregion test_no_prior_run_returns_all_rows\n\n\n# #region test_filters_existing_keys [C:1] [TYPE Function]\ndef test_filters_existing_keys():\n \"\"\"Rows whose key exists in a prior run's records are excluded.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"], target_key_cols=[\"id\"])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"existing\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"new\", \"_idx\": \"1\"},\n {\"id\": 3, \"text\": \"existing\", \"_idx\": \"2\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n # Only id=1 and id=3 were previously translated successfully\n prev_recs = _make_prev_records(\"prev-run-1\", [\n {\"id\": 1, \"text\": \"existing\"},\n {\"id\": 3, \"text\": \"existing\"},\n ])\n\n _wire_query_chain(executor.db, mock_prev_run, prev_recs)\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 1, f\"Expected 1 new row, got {len(result)}\"\n assert result[0][\"source_data\"][\"id\"] == 2\n# #endregion test_filters_existing_keys\n\n\n# #region test_composite_keys [C:1] [TYPE Function]\ndef test_composite_keys():\n \"\"\"Multiple target_key_cols produce correct composite key dedup.\"\"\"\n executor = _make_executor()\n job = _make_job(target_key_cols=[\"schema\", \"table\"])\n\n rows = _make_rows(\n {\"schema\": \"public\", \"table\": \"users\", \"text\": \"a\", \"_idx\": \"0\"},\n {\"schema\": \"public\", \"table\": \"orders\", \"text\": \"b\", \"_idx\": \"1\"},\n {\"schema\": \"admin\", \"table\": \"users\", \"text\": \"c\", \"_idx\": \"2\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n # Only (public, users) was already translated\n prev_recs = _make_prev_records(\"prev-run-1\", [\n {\"schema\": \"public\", \"table\": \"users\"},\n ])\n\n _wire_query_chain(executor.db, mock_prev_run, prev_recs)\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 2, f\"Expected 2 new rows, got {len(result)}\"\n\n result_keys = {\n tuple(str(r[\"source_data\"].get(k, \"\")) for k in [\"schema\", \"table\"])\n for r in result\n }\n assert (\"public\", \"orders\") in result_keys\n assert (\"admin\", \"users\") in result_keys\n assert (\"public\", \"users\") not in result_keys\n# #endregion test_composite_keys\n\n\n# #region test_all_keys_existing_returns_empty [C:1] [TYPE Function]\ndef test_all_keys_existing_returns_empty():\n \"\"\"When every source row's key already exists, the filtered list is empty.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"a\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"b\", \"_idx\": \"1\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n prev_recs = _make_prev_records(\"prev-run-1\", [\n {\"id\": 1}, {\"id\": 2},\n ])\n\n _wire_query_chain(executor.db, mock_prev_run, prev_recs)\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert result == [], \"All keys existing should produce empty result\"\n# #endregion test_all_keys_existing_returns_empty\n\n\n# #region test_empty_key_cols_returns_all_rows [C:1] [TYPE Function]\ndef test_empty_key_cols_returns_all_rows():\n \"\"\"When no key columns are configured, guard triggers — all rows returned.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[], target_key_cols=[])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"a\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"b\", \"_idx\": \"1\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n _wire_query_chain(executor.db, mock_prev_run, prev_records=[])\n\n # Even with a prior run and records, no key cols means all rows pass through\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 2\n assert result == rows\n# #endregion test_empty_key_cols_returns_all_rows\n\n\n# #region test_null_source_data_handled [C:1] [TYPE Function]\ndef test_null_source_data_handled():\n \"\"\"None source_data is safely handled with or {} fallback — no crash.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"])\n\n rows = [\n {\"row_index\": \"0\", \"source_text\": \"a\", \"source_data\": None},\n {\"row_index\": \"1\", \"source_text\": \"b\", \"source_data\": {\"id\": 2}},\n ]\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n # Previous run only had id=2\n prev_recs = _make_prev_records(\"prev-run-1\", [{\"id\": 2}])\n\n _wire_query_chain(executor.db, mock_prev_run, prev_recs)\n\n # Should NOT crash on None source_data\n result = executor._filter_new_keys(job, \"run-1\", rows)\n\n # The row with None source_data has key_tuple (\"\",) — NOT in existing_keys\n # (existing has (\"2\",)). So it should pass through.\n assert len(result) == 1, (\n f\"Expected 1 new row (None source_data treated as ''), got {len(result)}\"\n )\n assert result[0][\"source_data\"] is None\n# #endregion test_null_source_data_handled\n\n\n# #region test_prior_run_no_records_returns_all [C:1] [TYPE Function]\ndef test_prior_run_no_records_returns_all():\n \"\"\"Prior run exists but has no successful records — all rows treated as new.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"a\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"b\", \"_idx\": \"1\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n _wire_query_chain(executor.db, mock_prev_run, prev_records=[])\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 2\n assert result == rows\n# #endregion test_prior_run_no_records_returns_all\n\n\n# #region test_key_cols_none_returns_all [C:1] [TYPE Function]\ndef test_key_cols_none_returns_all():\n \"\"\"When source_key_cols and target_key_cols are both None, all rows pass through.\"\"\"\n executor = _make_executor()\n # Job with no key_cols at all (None, not empty list)\n job = MagicMock()\n job.id = \"job-test-1\"\n job.source_key_cols = None\n job.target_key_cols = None\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"a\", \"_idx\": \"0\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n _wire_query_chain(executor.db, mock_prev_run, prev_records=[])\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 1\n assert result == rows\n# #endregion test_key_cols_none_returns_all\n\n\n# #endregion TestTranslateExecutorFilter\n" + }, + { + "contract_id": "test_no_prior_run_returns_all_rows", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 78, + "end_line": 96, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_no_prior_run_returns_all_rows [C:1] [TYPE Function]\ndef test_no_prior_run_returns_all_rows():\n \"\"\"When no prior TranslationRun exists, all source_rows pass through unchanged.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"], target_key_cols=[\"id\"])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"hello\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"world\", \"_idx\": \"1\"},\n {\"id\": 3, \"text\": \"test\", \"_idx\": \"2\"},\n )\n\n # Mock: no prior run (query returns None)\n _wire_query_chain(executor.db, prev_run=None, prev_records=[])\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 3\n assert result == rows # identity — all pass through\n# #endregion test_no_prior_run_returns_all_rows\n" + }, + { + "contract_id": "test_filters_existing_keys", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 99, + "end_line": 125, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_filters_existing_keys [C:1] [TYPE Function]\ndef test_filters_existing_keys():\n \"\"\"Rows whose key exists in a prior run's records are excluded.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"], target_key_cols=[\"id\"])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"existing\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"new\", \"_idx\": \"1\"},\n {\"id\": 3, \"text\": \"existing\", \"_idx\": \"2\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n # Only id=1 and id=3 were previously translated successfully\n prev_recs = _make_prev_records(\"prev-run-1\", [\n {\"id\": 1, \"text\": \"existing\"},\n {\"id\": 3, \"text\": \"existing\"},\n ])\n\n _wire_query_chain(executor.db, mock_prev_run, prev_recs)\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 1, f\"Expected 1 new row, got {len(result)}\"\n assert result[0][\"source_data\"][\"id\"] == 2\n# #endregion test_filters_existing_keys\n" + }, + { + "contract_id": "test_composite_keys", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 128, + "end_line": 160, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_composite_keys [C:1] [TYPE Function]\ndef test_composite_keys():\n \"\"\"Multiple target_key_cols produce correct composite key dedup.\"\"\"\n executor = _make_executor()\n job = _make_job(target_key_cols=[\"schema\", \"table\"])\n\n rows = _make_rows(\n {\"schema\": \"public\", \"table\": \"users\", \"text\": \"a\", \"_idx\": \"0\"},\n {\"schema\": \"public\", \"table\": \"orders\", \"text\": \"b\", \"_idx\": \"1\"},\n {\"schema\": \"admin\", \"table\": \"users\", \"text\": \"c\", \"_idx\": \"2\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n # Only (public, users) was already translated\n prev_recs = _make_prev_records(\"prev-run-1\", [\n {\"schema\": \"public\", \"table\": \"users\"},\n ])\n\n _wire_query_chain(executor.db, mock_prev_run, prev_recs)\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 2, f\"Expected 2 new rows, got {len(result)}\"\n\n result_keys = {\n tuple(str(r[\"source_data\"].get(k, \"\")) for k in [\"schema\", \"table\"])\n for r in result\n }\n assert (\"public\", \"orders\") in result_keys\n assert (\"admin\", \"users\") in result_keys\n assert (\"public\", \"users\") not in result_keys\n# #endregion test_composite_keys\n" + }, + { + "contract_id": "test_all_keys_existing_returns_empty", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 163, + "end_line": 185, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_all_keys_existing_returns_empty [C:1] [TYPE Function]\ndef test_all_keys_existing_returns_empty():\n \"\"\"When every source row's key already exists, the filtered list is empty.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"a\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"b\", \"_idx\": \"1\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n prev_recs = _make_prev_records(\"prev-run-1\", [\n {\"id\": 1}, {\"id\": 2},\n ])\n\n _wire_query_chain(executor.db, mock_prev_run, prev_recs)\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert result == [], \"All keys existing should produce empty result\"\n# #endregion test_all_keys_existing_returns_empty\n" + }, + { + "contract_id": "test_empty_key_cols_returns_all_rows", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 188, + "end_line": 208, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_empty_key_cols_returns_all_rows [C:1] [TYPE Function]\ndef test_empty_key_cols_returns_all_rows():\n \"\"\"When no key columns are configured, guard triggers — all rows returned.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[], target_key_cols=[])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"a\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"b\", \"_idx\": \"1\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n _wire_query_chain(executor.db, mock_prev_run, prev_records=[])\n\n # Even with a prior run and records, no key cols means all rows pass through\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 2\n assert result == rows\n# #endregion test_empty_key_cols_returns_all_rows\n" + }, + { + "contract_id": "test_null_source_data_handled", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 211, + "end_line": 239, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_null_source_data_handled [C:1] [TYPE Function]\ndef test_null_source_data_handled():\n \"\"\"None source_data is safely handled with or {} fallback — no crash.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"])\n\n rows = [\n {\"row_index\": \"0\", \"source_text\": \"a\", \"source_data\": None},\n {\"row_index\": \"1\", \"source_text\": \"b\", \"source_data\": {\"id\": 2}},\n ]\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n # Previous run only had id=2\n prev_recs = _make_prev_records(\"prev-run-1\", [{\"id\": 2}])\n\n _wire_query_chain(executor.db, mock_prev_run, prev_recs)\n\n # Should NOT crash on None source_data\n result = executor._filter_new_keys(job, \"run-1\", rows)\n\n # The row with None source_data has key_tuple (\"\",) — NOT in existing_keys\n # (existing has (\"2\",)). So it should pass through.\n assert len(result) == 1, (\n f\"Expected 1 new row (None source_data treated as ''), got {len(result)}\"\n )\n assert result[0][\"source_data\"] is None\n# #endregion test_null_source_data_handled\n" + }, + { + "contract_id": "test_prior_run_no_records_returns_all", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 242, + "end_line": 261, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_prior_run_no_records_returns_all [C:1] [TYPE Function]\ndef test_prior_run_no_records_returns_all():\n \"\"\"Prior run exists but has no successful records — all rows treated as new.\"\"\"\n executor = _make_executor()\n job = _make_job(source_key_cols=[\"id\"])\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"a\", \"_idx\": \"0\"},\n {\"id\": 2, \"text\": \"b\", \"_idx\": \"1\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n _wire_query_chain(executor.db, mock_prev_run, prev_records=[])\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 2\n assert result == rows\n# #endregion test_prior_run_no_records_returns_all\n" + }, + { + "contract_id": "test_key_cols_none_returns_all", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_executor_filter.py", + "start_line": 264, + "end_line": 286, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_key_cols_none_returns_all [C:1] [TYPE Function]\ndef test_key_cols_none_returns_all():\n \"\"\"When source_key_cols and target_key_cols are both None, all rows pass through.\"\"\"\n executor = _make_executor()\n # Job with no key_cols at all (None, not empty list)\n job = MagicMock()\n job.id = \"job-test-1\"\n job.source_key_cols = None\n job.target_key_cols = None\n\n rows = _make_rows(\n {\"id\": 1, \"text\": \"a\", \"_idx\": \"0\"},\n )\n\n mock_prev_run = MagicMock()\n mock_prev_run.id = \"prev-run-1\"\n\n _wire_query_chain(executor.db, mock_prev_run, prev_records=[])\n\n result = executor._filter_new_keys(job, \"run-1\", rows)\n assert len(result) == 1\n assert result == rows\n# #endregion test_key_cols_none_returns_all\n" + }, { "contract_id": "TranslateHistoryTests", "contract_type": "Module", "file_path": "backend/tests/test_translate_history.py", "start_line": 1, - "end_line": 300, - "tier": "TIER_2", - "complexity": 4, + "end_line": 298, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "4", "LAYER": "Test", "PURPOSE": "Tests for run history list/detail endpoints and metrics aggregation.", "SEMANTICS": [ @@ -106404,6 +74878,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -106419,49 +74902,31 @@ { "code": "tag_forbidden_by_complexity", "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C4", + "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 4, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "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, + "actual_complexity": 1, "contract_type": "Module" } } ], "anchor_syntax": "def", - "body": "# [DEF:TranslateHistoryTests:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: tests, translate, history, metrics\n# @PURPOSE: Tests for run history list/detail endpoints and metrics aggregation.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [TranslateRoutes:Module]\n# @RELATION: BINDS_TO -> [TranslationMetrics:Module]\n#\n# @TEST_CONTRACT: HistoryFlow ->\n# {\n# invariants: [\n# \"GET /api/translate/runs returns paginated list\",\n# \"GET /api/translate/runs supports filters (job_id, status, trigger_type)\",\n# \"GET /api/translate/runs/{id}/detail returns config_snapshot, records, events\",\n# \"GET /api/translate/jobs/{id}/metrics returns aggregated data\",\n# \"Metrics include run counts, record counts, duration\"\n# ]\n# }\n# @TEST_FIXTURE: completed_run -> run with COMPLETED status\n# @TEST_EDGE: filter_by_job_id -> returns only that job's runs\n# @TEST_EDGE: filter_by_status -> returns only matching status\n# @TEST_EDGE: metrics_empty_job -> returns zero counts\n# @TEST_INVARIANT: history_list_and_detail -> VERIFIED_BY: [completed_run, filter_by_status]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom unittest.mock import MagicMock, patch\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom datetime import datetime, timezone\nimport uuid\nimport json\n\nfrom src.core.database import Base\nfrom src.app import app\nfrom src.dependencies import get_current_user, get_config_manager, has_permission, get_db\nfrom src.plugins.translate.metrics import TranslationMetrics\nfrom src.models.translate import TranslationJob, TranslationRun, TranslationEvent, MetricSnapshot\n\n# In-memory SQLite\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase.metadata.create_all(bind=engine)\n\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n yield session\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef mock_api_deps():\n config_manager = MagicMock()\n config_manager.get_environments.return_value = []\n\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n def override_get_db():\n try:\n yield session\n finally:\n pass\n\n overrides = {\n get_current_user: lambda: mock_user,\n get_config_manager: lambda: config_manager,\n get_db: override_get_db,\n }\n for dep, override in overrides.items():\n app.dependency_overrides[dep] = override\n\n yield {\"config_manager\": config_manager, \"session\": session}\n\n app.dependency_overrides.clear()\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef client(mock_api_deps):\n return TestClient(app)\n\n\ndef _create_test_job(db_session) -> TranslationJob:\n job = TranslationJob(\n id=str(uuid.uuid4()),\n name=\"Test Job\",\n source_dialect=\"postgresql\",\n target_dialect=\"clickhouse\",\n status=\"ACTIVE\",\n )\n db_session.add(job)\n db_session.flush()\n return job\n\n\ndef _create_test_run(db_session, job_id: str, status: str = \"COMPLETED\", trigger: str = \"manual\") -> TranslationRun:\n run = TranslationRun(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=status,\n trigger_type=trigger,\n total_records=100,\n successful_records=80,\n failed_records=10,\n skipped_records=10,\n config_snapshot={\"test\": True},\n config_hash=\"abc123\",\n dict_snapshot_hash=\"def456\",\n key_hash=\"ghi789\",\n created_by=\"testuser\",\n created_at=datetime.now(timezone.utc),\n )\n db_session.add(run)\n db_session.flush()\n return run\n\n\n# [DEF:test_list_runs_empty:Function]\n# @PURPOSE: Verify runs list returns empty result initially.\ndef test_list_runs_empty(client):\n \"\"\"Test runs list initially returns empty.\"\"\"\n response = client.get(\"/api/translate/runs\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert data[\"items\"] == []\n# [/DEF:test_list_runs_empty:Function]\n\n\n# [DEF:test_list_runs_with_data:Function]\n# @PURPOSE: Verify runs list returns data when runs exist.\ndef test_list_runs_with_data(client, mock_api_deps):\n \"\"\"Test runs list with existing runs.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id)\n session.commit()\n\n response = client.get(\"/api/translate/runs\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] >= 1\n assert len(data[\"items\"]) >= 1\n assert data[\"items\"][0][\"job_id\"] == job.id\n# [/DEF:test_list_runs_with_data:Function]\n\n\n# [DEF:test_list_runs_filter_job_id:Function]\n# @PURPOSE: Verify filtering runs by job_id works.\ndef test_list_runs_filter_job_id(client, mock_api_deps):\n \"\"\"Test filtering runs by job_id.\"\"\"\n session = mock_api_deps[\"session\"]\n job1 = _create_test_job(session)\n job2 = _create_test_job(session)\n _create_test_run(session, job1.id)\n _create_test_run(session, job2.id)\n session.commit()\n\n response = client.get(f\"/api/translate/runs?job_id={job1.id}\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] >= 1\n for item in data[\"items\"]:\n assert item[\"job_id\"] == job1.id\n# [/DEF:test_list_runs_filter_job_id:Function]\n\n\n# [DEF:test_list_runs_filter_status:Function]\n# @PURPOSE: Verify filtering runs by status works.\ndef test_list_runs_filter_status(client, mock_api_deps):\n \"\"\"Test filtering runs by status.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id, status=\"COMPLETED\")\n _create_test_run(session, job.id, status=\"FAILED\")\n session.commit()\n\n response = client.get(f\"/api/translate/runs?status=FAILED\")\n assert response.status_code == 200\n data = response.json()\n for item in data[\"items\"]:\n assert item[\"status\"] == \"FAILED\"\n# [/DEF:test_list_runs_filter_status:Function]\n\n\n# [DEF:test_get_run_detail:Function]\n# @PURPOSE: Verify run detail returns config_snapshot, records, events.\ndef test_get_run_detail(client, mock_api_deps):\n \"\"\"Test run detail endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n run = _create_test_run(session, job.id)\n\n # Add an event\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n event_data={},\n created_by=\"testuser\",\n created_at=datetime.now(timezone.utc),\n )\n session.add(event)\n session.commit()\n\n response = client.get(f\"/api/translate/runs/{run.id}/detail\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == run.id\n assert data[\"config_snapshot\"] == {\"test\": True}\n assert data[\"config_hash\"] == \"abc123\"\n assert len(data[\"events\"]) >= 1\n assert data[\"events\"][0][\"event_type\"] == \"RUN_STARTED\"\n# [/DEF:test_get_run_detail:Function]\n\n\n# [DEF:test_get_job_metrics:Function]\n# @PURPOSE: Verify metrics endpoint returns aggregated data.\ndef test_get_job_metrics(client, mock_api_deps):\n \"\"\"Test job metrics endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id, status=\"COMPLETED\")\n _create_test_run(session, job.id, status=\"FAILED\")\n session.commit()\n\n response = client.get(f\"/api/translate/jobs/{job.id}/metrics\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"job_id\"] == job.id\n assert data[\"total_runs\"] >= 2\n assert data[\"successful_runs\"] >= 1\n assert data[\"failed_runs\"] >= 1\n# [/DEF:test_get_job_metrics:Function]\n\n\n# [DEF:test_get_all_metrics:Function]\n# @PURPOSE: Verify global metrics endpoint returns data.\ndef test_get_all_metrics(client, mock_api_deps):\n \"\"\"Test global metrics endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id)\n session.commit()\n\n response = client.get(\"/api/translate/metrics\")\n assert response.status_code == 200\n data = response.json()\n assert isinstance(data, list)\n assert len(data) >= 1\n# [/DEF:test_get_all_metrics:Function]\n\n\n# [DEF:test_metrics_empty_job:Function]\n# @PURPOSE: Verify metrics for job with no runs returns zeros.\ndef test_metrics_empty_job(client, mock_api_deps):\n \"\"\"Test metrics for job with no runs.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n session.commit()\n\n response = client.get(f\"/api/translate/jobs/{job.id}/metrics\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total_runs\"] == 0\n# [/DEF:test_metrics_empty_job:Function]\n\n\n# [DEF:test_run_detail_not_found:Function]\n# @PURPOSE: Verify 404 on non-existent run detail.\ndef test_run_detail_not_found(client):\n \"\"\"Test run detail returns 404 for non-existent run.\"\"\"\n response = client.get(\"/api/translate/runs/non-existent/detail\")\n assert response.status_code == 404\n# [/DEF:test_run_detail_not_found:Function]\n# [/DEF:TranslateHistoryTests:Module]\n" + "body": "# [DEF:TranslateHistoryTests:Module]\n# @SEMANTICS: tests, translate, history, metrics\n# @PURPOSE: Tests for run history list/detail endpoints and metrics aggregation.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [TranslateRoutes:Module]\n# @RELATION: BINDS_TO -> [TranslationMetrics:Module]\n#\n# @TEST_CONTRACT: HistoryFlow ->\n# {\n# invariants: [\n# \"GET /api/translate/runs returns paginated list\",\n# \"GET /api/translate/runs supports filters (job_id, status, trigger_type)\",\n# \"GET /api/translate/runs/{id}/detail returns config_snapshot, records, events\",\n# \"GET /api/translate/jobs/{id}/metrics returns aggregated data\",\n# \"Metrics include run counts, record counts, duration\"\n# ]\n# }\n# @TEST_FIXTURE: completed_run -> run with COMPLETED status\n# @TEST_EDGE: filter_by_job_id -> returns only that job's runs\n# @TEST_EDGE: filter_by_status -> returns only matching status\n# @TEST_EDGE: metrics_empty_job -> returns zero counts\n# @TEST_INVARIANT: history_list_and_detail -> VERIFIED_BY: [completed_run, filter_by_status]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport uuid\nfrom datetime import UTC, datetime\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.app import app\nfrom src.core.database import Base\nfrom src.dependencies import get_config_manager, get_current_user, get_db\nfrom src.models.translate import TranslationEvent, TranslationJob, TranslationRun\n\n# In-memory SQLite\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase.metadata.create_all(bind=engine)\n\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n yield session\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef mock_api_deps():\n config_manager = MagicMock()\n config_manager.get_environments.return_value = []\n\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n def override_get_db():\n try:\n yield session\n finally:\n pass\n\n overrides = {\n get_current_user: lambda: mock_user,\n get_config_manager: lambda: config_manager,\n get_db: override_get_db,\n }\n for dep, override in overrides.items():\n app.dependency_overrides[dep] = override\n\n yield {\"config_manager\": config_manager, \"session\": session}\n\n app.dependency_overrides.clear()\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef client(mock_api_deps):\n return TestClient(app)\n\n\ndef _create_test_job(db_session) -> TranslationJob:\n job = TranslationJob(\n id=str(uuid.uuid4()),\n name=\"Test Job\",\n source_dialect=\"postgresql\",\n target_dialect=\"clickhouse\",\n status=\"ACTIVE\",\n )\n db_session.add(job)\n db_session.flush()\n return job\n\n\ndef _create_test_run(db_session, job_id: str, status: str = \"COMPLETED\", trigger: str = \"manual\") -> TranslationRun:\n run = TranslationRun(\n id=str(uuid.uuid4()),\n job_id=job_id,\n status=status,\n trigger_type=trigger,\n total_records=100,\n successful_records=80,\n failed_records=10,\n skipped_records=10,\n config_snapshot={\"test\": True},\n config_hash=\"abc123\",\n dict_snapshot_hash=\"def456\",\n key_hash=\"ghi789\",\n created_by=\"testuser\",\n created_at=datetime.now(UTC),\n )\n db_session.add(run)\n db_session.flush()\n return run\n\n\n# [DEF:test_list_runs_empty:Function]\n# @PURPOSE: Verify runs list returns empty result initially.\ndef test_list_runs_empty(client):\n \"\"\"Test runs list initially returns empty.\"\"\"\n response = client.get(\"/api/translate/runs\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 0\n assert data[\"items\"] == []\n# [/DEF:test_list_runs_empty:Function]\n\n\n# [DEF:test_list_runs_with_data:Function]\n# @PURPOSE: Verify runs list returns data when runs exist.\ndef test_list_runs_with_data(client, mock_api_deps):\n \"\"\"Test runs list with existing runs.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id)\n session.commit()\n\n response = client.get(\"/api/translate/runs\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] >= 1\n assert len(data[\"items\"]) >= 1\n assert data[\"items\"][0][\"job_id\"] == job.id\n# [/DEF:test_list_runs_with_data:Function]\n\n\n# [DEF:test_list_runs_filter_job_id:Function]\n# @PURPOSE: Verify filtering runs by job_id works.\ndef test_list_runs_filter_job_id(client, mock_api_deps):\n \"\"\"Test filtering runs by job_id.\"\"\"\n session = mock_api_deps[\"session\"]\n job1 = _create_test_job(session)\n job2 = _create_test_job(session)\n _create_test_run(session, job1.id)\n _create_test_run(session, job2.id)\n session.commit()\n\n response = client.get(f\"/api/translate/runs?job_id={job1.id}\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] >= 1\n for item in data[\"items\"]:\n assert item[\"job_id\"] == job1.id\n# [/DEF:test_list_runs_filter_job_id:Function]\n\n\n# [DEF:test_list_runs_filter_status:Function]\n# @PURPOSE: Verify filtering runs by status works.\ndef test_list_runs_filter_status(client, mock_api_deps):\n \"\"\"Test filtering runs by status.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id, status=\"COMPLETED\")\n _create_test_run(session, job.id, status=\"FAILED\")\n session.commit()\n\n response = client.get(\"/api/translate/runs?run_status=FAILED\")\n assert response.status_code == 200\n data = response.json()\n for item in data[\"items\"]:\n assert item[\"status\"] == \"FAILED\"\n# [/DEF:test_list_runs_filter_status:Function]\n\n\n# [DEF:test_get_run_detail:Function]\n# @PURPOSE: Verify run detail returns config_snapshot, records, events.\ndef test_get_run_detail(client, mock_api_deps):\n \"\"\"Test run detail endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n run = _create_test_run(session, job.id)\n\n # Add an event\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n event_data={},\n created_by=\"testuser\",\n created_at=datetime.now(UTC),\n )\n session.add(event)\n session.commit()\n\n response = client.get(f\"/api/translate/runs/{run.id}/detail\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == run.id\n assert data[\"config_snapshot\"] == {\"test\": True}\n assert data[\"config_hash\"] == \"abc123\"\n assert len(data[\"events\"]) >= 1\n assert data[\"events\"][0][\"event_type\"] == \"RUN_STARTED\"\n# [/DEF:test_get_run_detail:Function]\n\n\n# [DEF:test_get_job_metrics:Function]\n# @PURPOSE: Verify metrics endpoint returns aggregated data.\ndef test_get_job_metrics(client, mock_api_deps):\n \"\"\"Test job metrics endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id, status=\"COMPLETED\")\n _create_test_run(session, job.id, status=\"FAILED\")\n session.commit()\n\n response = client.get(f\"/api/translate/jobs/{job.id}/metrics\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"job_id\"] == job.id\n assert data[\"total_runs\"] >= 2\n assert data[\"successful_runs\"] >= 1\n assert data[\"failed_runs\"] >= 1\n# [/DEF:test_get_job_metrics:Function]\n\n\n# [DEF:test_get_all_metrics:Function]\n# @PURPOSE: Verify global metrics endpoint returns data.\ndef test_get_all_metrics(client, mock_api_deps):\n \"\"\"Test global metrics endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id)\n session.commit()\n\n response = client.get(\"/api/translate/metrics\")\n assert response.status_code == 200\n data = response.json()\n assert isinstance(data, list)\n assert len(data) >= 1\n# [/DEF:test_get_all_metrics:Function]\n\n\n# [DEF:test_metrics_empty_job:Function]\n# @PURPOSE: Verify metrics for job with no runs returns zeros.\ndef test_metrics_empty_job(client, mock_api_deps):\n \"\"\"Test metrics for job with no runs.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n session.commit()\n\n response = client.get(f\"/api/translate/jobs/{job.id}/metrics\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total_runs\"] == 0\n# [/DEF:test_metrics_empty_job:Function]\n\n\n# [DEF:test_run_detail_not_found:Function]\n# @PURPOSE: Verify 404 on non-existent run detail.\ndef test_run_detail_not_found(client):\n \"\"\"Test run detail returns 404 for non-existent run.\"\"\"\n response = client.get(\"/api/translate/runs/non-existent/detail\")\n assert response.status_code == 404\n# [/DEF:test_run_detail_not_found:Function]\n# [/DEF:TranslateHistoryTests:Module]\n" }, { "contract_id": "test_list_runs_empty", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 141, - "end_line": 150, + "start_line": 139, + "end_line": 148, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106476,8 +74941,8 @@ "contract_id": "test_list_runs_with_data", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 153, - "end_line": 168, + "start_line": 151, + "end_line": 166, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106492,8 +74957,8 @@ "contract_id": "test_list_runs_filter_job_id", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 171, - "end_line": 188, + "start_line": 169, + "end_line": 186, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106508,8 +74973,8 @@ "contract_id": "test_list_runs_filter_status", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 191, - "end_line": 206, + "start_line": 189, + "end_line": 204, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106518,14 +74983,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:test_list_runs_filter_status:Function]\n# @PURPOSE: Verify filtering runs by status works.\ndef test_list_runs_filter_status(client, mock_api_deps):\n \"\"\"Test filtering runs by status.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id, status=\"COMPLETED\")\n _create_test_run(session, job.id, status=\"FAILED\")\n session.commit()\n\n response = client.get(f\"/api/translate/runs?status=FAILED\")\n assert response.status_code == 200\n data = response.json()\n for item in data[\"items\"]:\n assert item[\"status\"] == \"FAILED\"\n# [/DEF:test_list_runs_filter_status:Function]\n" + "body": "# [DEF:test_list_runs_filter_status:Function]\n# @PURPOSE: Verify filtering runs by status works.\ndef test_list_runs_filter_status(client, mock_api_deps):\n \"\"\"Test filtering runs by status.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n _create_test_run(session, job.id, status=\"COMPLETED\")\n _create_test_run(session, job.id, status=\"FAILED\")\n session.commit()\n\n response = client.get(\"/api/translate/runs?run_status=FAILED\")\n assert response.status_code == 200\n data = response.json()\n for item in data[\"items\"]:\n assert item[\"status\"] == \"FAILED\"\n# [/DEF:test_list_runs_filter_status:Function]\n" }, { "contract_id": "test_get_run_detail", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 209, - "end_line": 238, + "start_line": 207, + "end_line": 236, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106534,14 +74999,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:test_get_run_detail:Function]\n# @PURPOSE: Verify run detail returns config_snapshot, records, events.\ndef test_get_run_detail(client, mock_api_deps):\n \"\"\"Test run detail endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n run = _create_test_run(session, job.id)\n\n # Add an event\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n event_data={},\n created_by=\"testuser\",\n created_at=datetime.now(timezone.utc),\n )\n session.add(event)\n session.commit()\n\n response = client.get(f\"/api/translate/runs/{run.id}/detail\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == run.id\n assert data[\"config_snapshot\"] == {\"test\": True}\n assert data[\"config_hash\"] == \"abc123\"\n assert len(data[\"events\"]) >= 1\n assert data[\"events\"][0][\"event_type\"] == \"RUN_STARTED\"\n# [/DEF:test_get_run_detail:Function]\n" + "body": "# [DEF:test_get_run_detail:Function]\n# @PURPOSE: Verify run detail returns config_snapshot, records, events.\ndef test_get_run_detail(client, mock_api_deps):\n \"\"\"Test run detail endpoint.\"\"\"\n session = mock_api_deps[\"session\"]\n job = _create_test_job(session)\n run = _create_test_run(session, job.id)\n\n # Add an event\n event = TranslationEvent(\n id=str(uuid.uuid4()),\n job_id=job.id,\n run_id=run.id,\n event_type=\"RUN_STARTED\",\n event_data={},\n created_by=\"testuser\",\n created_at=datetime.now(UTC),\n )\n session.add(event)\n session.commit()\n\n response = client.get(f\"/api/translate/runs/{run.id}/detail\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == run.id\n assert data[\"config_snapshot\"] == {\"test\": True}\n assert data[\"config_hash\"] == \"abc123\"\n assert len(data[\"events\"]) >= 1\n assert data[\"events\"][0][\"event_type\"] == \"RUN_STARTED\"\n# [/DEF:test_get_run_detail:Function]\n" }, { "contract_id": "test_get_job_metrics", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 241, - "end_line": 258, + "start_line": 239, + "end_line": 256, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106556,8 +75021,8 @@ "contract_id": "test_get_all_metrics", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 261, - "end_line": 275, + "start_line": 259, + "end_line": 273, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106572,8 +75037,8 @@ "contract_id": "test_metrics_empty_job", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 278, - "end_line": 290, + "start_line": 276, + "end_line": 288, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106588,8 +75053,8 @@ "contract_id": "test_run_detail_not_found", "contract_type": "Function", "file_path": "backend/tests/test_translate_history.py", - "start_line": 293, - "end_line": 299, + "start_line": 291, + "end_line": 297, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106605,11 +75070,10 @@ "contract_type": "Module", "file_path": "backend/tests/test_translate_jobs.py", "start_line": 1, - "end_line": 569, - "tier": "TIER_2", - "complexity": 4, + "end_line": 562, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "4", "LAYER": "Test", "PURPOSE": "Tests for translation job CRUD endpoints and service layer with column validation.", "SEMANTICS": [ @@ -106636,6 +75100,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -106651,49 +75124,31 @@ { "code": "tag_forbidden_by_complexity", "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C4", + "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 4, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "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, + "actual_complexity": 1, "contract_type": "Module" } } ], "anchor_syntax": "def", - "body": "# [DEF:TranslateJobTests:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: tests, translate, jobs, crud, validation\n# @PURPOSE: Tests for translation job CRUD endpoints and service layer with column validation.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [TranslateRoutes:Module]\n# @RELATION: BINDS_TO -> [TranslateJobService:Module]\n#\n# @TEST_CONTRACT: TranslateJobCRUD ->\n# {\n# required_fields: {name: str, source_dialect: str, target_dialect: str},\n# optional_fields: {description, source_datasource_id, translation_column, ...},\n# invariants: [\n# \"POST /api/translate/jobs returns 201 with valid config\",\n# \"GET /api/translate/jobs returns job list\",\n# \"GET /api/translate/jobs/{id} returns single job\",\n# \"PUT /api/translate/jobs/{id} updates and returns job\",\n# \"DELETE /api/translate/jobs/{id} returns 204\",\n# \"POST /api/translate/jobs/{id}/duplicate returns new job copy\"\n# ]\n# }\n# @TEST_FIXTURE: valid_job_payload -> {name: \"Test Job\", source_dialect: \"postgresql\", target_dialect: \"clickhouse\"}\n# @TEST_EDGE: missing_translation_column -> 422 when datasource set but no translation column\n# @TEST_EDGE: virtual_key_column_warning -> warning emitted when virtual column used as key\n# @TEST_EDGE: invalid_dialect_rejected -> error on unsupported dialect\n# @TEST_INVARIANT: valid_CRUD_flow -> VERIFIED_BY: [valid_job_payload, missing_translation_column]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom unittest.mock import MagicMock, patch\nfrom datetime import datetime, timezone\nfrom fastapi import status\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom typing import Any, Dict, List, Optional, Tuple\nimport json\nimport uuid\n\nfrom src.core.database import Base\nfrom src.app import app\nfrom src.dependencies import (\n get_current_user,\n get_config_manager,\n has_permission,\n)\nfrom src.core.config_models import Environment\n\n\n# [DEF:valid_job_payload:Variable]\n# @PURPOSE: Standard valid payload for creating a translation job.\nvalid_job_payload = {\n \"name\": \"Test Translation Job\",\n \"description\": \"A test job for unit tests\",\n \"source_dialect\": \"postgresql\",\n \"target_dialect\": \"clickhouse\",\n \"source_key_cols\": [\"id\"],\n \"target_key_cols\": [\"id\"],\n \"translation_column\": \"name\",\n \"context_columns\": [\"description\"],\n \"target_language\": \"ru\",\n \"batch_size\": 100,\n \"upsert_strategy\": \"MERGE\",\n \"dictionary_ids\": [],\n}\n# [/DEF:valid_job_payload:Variable]\n\n\n# Mock user\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n# In-memory SQLite database for service tests\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n\n# Create DB tables once\nBase.metadata.create_all(bind=engine)\n\n\n# [DEF:MockConfigManager:Class]\n# @PURPOSE: Mock ConfigManager for service tests that returns a test environment.\nclass MockConfigManager:\n def get_environments(self):\n return [\n Environment(\n id=\"test_env\",\n name=\"Test Environment\",\n url=\"http://superset:8088\",\n username=\"admin\",\n password=\"admin\",\n )\n ]\n\n def get_config(self):\n return MagicMock()\n# [/DEF:MockConfigManager:Class]\n\n\n# [DEF:db_session:Function]\n# @PURPOSE: Create a fresh DB session with transaction rollback for each test.\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n yield session\n\n session.close()\n transaction.rollback()\n connection.close()\n# [/DEF:db_session:Function]\n\n\n# [DEF:mock_api_deps:Function]\n# @PURPOSE: Override FastAPI dependencies (including get_db with in-memory SQLite) for API route tests.\n# @RATIONALE: Uses the same in-memory SQLite engine as service tests to avoid real PostgreSQL dependency.\n# @REJECTED: Mocking get_db with MagicMock — the route handlers need a real session for ORM queries.\n@pytest.fixture\ndef mock_api_deps():\n from src.core.database import get_db\n\n config_manager = MagicMock()\n config_manager.get_environments.return_value = []\n\n # Create in-memory SQLite session for API tests (same engine as service tests)\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n def override_get_db():\n try:\n yield session\n finally:\n pass # Session lifecycle managed by fixture teardown\n\n overrides = {\n get_current_user: lambda: mock_user,\n get_config_manager: lambda: config_manager,\n get_db: override_get_db,\n }\n\n # Apply all overrides\n for dep, override in overrides.items():\n app.dependency_overrides[dep] = override\n\n yield {\"config_manager\": config_manager}\n\n app.dependency_overrides.clear()\n session.close()\n transaction.rollback()\n connection.close()\n# [/DEF:mock_api_deps:Function]\n\n\n# [DEF:client:Function]\n# @PURPOSE: FastAPI TestClient for API route tests.\n@pytest.fixture\ndef client(mock_api_deps):\n return TestClient(app)\n# [/DEF:client:Function]\n\n\n# ============================================================\n# Service Layer Tests\n# ============================================================\n\n\n# [DEF:test_create_job_valid:Function]\n# @PURPOSE: Verify that a valid job payload creates a job successfully.\ndef test_create_job_valid(db_session):\n \"\"\"Test creating a valid translation job.\"\"\"\n from src.plugins.translate.service import TranslateJobService, job_to_response\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n job = service.create_job(payload)\n\n assert job.id is not None\n assert job.name == \"Test Translation Job\"\n assert job.source_dialect == \"postgresql\"\n assert job.target_dialect == \"clickhouse\"\n assert job.translation_column == \"name\"\n assert job.source_key_cols == [\"id\"]\n assert job.target_key_cols == [\"id\"]\n assert job.context_columns == [\"description\"]\n assert job.batch_size == 100\n assert job.upsert_strategy == \"MERGE\"\n assert job.target_language == \"ru\"\n assert job.status == \"DRAFT\"\n assert job.created_by == \"test_user\"\n\n # Verify response serialization\n response = job_to_response(job, [])\n assert response.name == \"Test Translation Job\"\n assert response.source_key_cols == [\"id\"]\n# [/DEF:test_create_job_valid:Function]\n\n\n# [DEF:test_create_job_missing_translation_column:Function]\n# @PURPOSE: Verify that creating a job with datasource but no translation column raises ValueError.\ndef test_create_job_missing_translation_column(db_session):\n \"\"\"Test that a datasource without a translation column is rejected.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(\n name=\"Bad Job\",\n source_dialect=\"postgresql\",\n target_dialect=\"clickhouse\",\n source_datasource_id=\"42\",\n translation_column=None,\n )\n\n with pytest.raises(ValueError, match=\"translation column is required\"):\n service.create_job(payload)\n# [/DEF:test_create_job_missing_translation_column:Function]\n\n\n# [DEF:test_create_job_invalid_upsert_strategy:Function]\n# @PURPOSE: Verify that an invalid upsert strategy is rejected.\ndef test_create_job_invalid_upsert_strategy(db_session):\n \"\"\"Test that an invalid upsert strategy raises ValueError.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(\n name=\"Bad Strategy Job\",\n source_dialect=\"postgresql\",\n target_dialect=\"clickhouse\",\n upsert_strategy=\"INVALID\",\n )\n\n with pytest.raises(ValueError, match=\"Invalid upsert_strategy\"):\n service.create_job(payload)\n# [/DEF:test_create_job_invalid_upsert_strategy:Function]\n\n\n# [DEF:test_get_job:Function]\n# @PURPOSE: Verify that a job can be retrieved by ID.\ndef test_get_job(db_session):\n \"\"\"Test retrieving a translation job by ID.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n created = service.create_job(payload)\n\n fetched = service.get_job(created.id)\n assert fetched.id == created.id\n assert fetched.name == \"Test Translation Job\"\n# [/DEF:test_get_job:Function]\n\n\n# [DEF:test_get_job_not_found:Function]\n# @PURPOSE: Verify that getting a non-existent job raises ValueError.\ndef test_get_job_not_found(db_session):\n \"\"\"Test that a non-existent job raises ValueError.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n with pytest.raises(ValueError, match=\"not found\"):\n service.get_job(\"non-existent-id\")\n# [/DEF:test_get_job_not_found:Function]\n\n\n# [DEF:test_list_jobs:Function]\n# @PURPOSE: Verify that listing jobs returns all created jobs.\ndef test_list_jobs(db_session):\n \"\"\"Test listing translation jobs.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n p1 = TranslateJobCreate(name=\"Job 1\", source_dialect=\"postgresql\", target_dialect=\"clickhouse\")\n p2 = TranslateJobCreate(name=\"Job 2\", source_dialect=\"mysql\", target_dialect=\"postgresql\")\n service.create_job(p1)\n service.create_job(p2)\n\n total, jobs = service.list_jobs()\n assert total == 2\n assert len(jobs) == 2\n# [/DEF:test_list_jobs:Function]\n\n\n# [DEF:test_list_jobs_with_status_filter:Function]\n# @PURPOSE: Verify that listing jobs with a status filter works.\ndef test_list_jobs_with_status_filter(db_session):\n \"\"\"Test listing jobs filtered by status.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate, TranslateJobUpdate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n p1 = TranslateJobCreate(name=\"Draft Job\", source_dialect=\"pg\", target_dialect=\"ch\")\n p2 = TranslateJobCreate(name=\"Ready Job\", source_dialect=\"pg\", target_dialect=\"ch\")\n service.create_job(p1)\n job2 = service.create_job(p2)\n\n service.update_job(job2.id, TranslateJobUpdate(status=\"READY\"))\n\n total, jobs = service.list_jobs(status_filter=\"READY\")\n assert total == 1\n assert jobs[0].name == \"Ready Job\"\n# [/DEF:test_list_jobs_with_status_filter:Function]\n\n\n# [DEF:test_update_job:Function]\n# @PURPOSE: Verify that a job can be updated.\ndef test_update_job(db_session):\n \"\"\"Test updating a translation job.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate, TranslateJobUpdate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n job = service.create_job(payload)\n\n update = TranslateJobUpdate(\n name=\"Updated Job\",\n description=\"Updated description\",\n batch_size=200,\n )\n updated = service.update_job(job.id, update)\n\n assert updated.name == \"Updated Job\"\n assert updated.description == \"Updated description\"\n assert updated.batch_size == 200\n# [/DEF:test_update_job:Function]\n\n\n# [DEF:test_delete_job:Function]\n# @PURPOSE: Verify that a job can be deleted.\ndef test_delete_job(db_session):\n \"\"\"Test deleting a translation job.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n job = service.create_job(payload)\n\n service.delete_job(job.id)\n\n with pytest.raises(ValueError, match=\"not found\"):\n service.get_job(job.id)\n# [/DEF:test_delete_job:Function]\n\n\n# [DEF:test_duplicate_job:Function]\n# @PURPOSE: Verify that a job can be duplicated.\ndef test_duplicate_job(db_session):\n \"\"\"Test duplicating a translation job.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n original = service.create_job(payload)\n\n duplicate = service.duplicate_job(original.id)\n\n assert duplicate.id != original.id\n assert duplicate.name == f\"{original.name} (Copy)\"\n assert duplicate.source_dialect == original.source_dialect\n assert duplicate.target_dialect == original.target_dialect\n assert duplicate.translation_column == original.translation_column\n assert duplicate.source_key_cols == original.source_key_cols\n assert duplicate.status == \"DRAFT\"\n# [/DEF:test_duplicate_job:Function]\n\n\n# [DEF:test_duplicate_job_custom_name:Function]\n# @PURPOSE: Verify that a job can be duplicated with a custom name.\ndef test_duplicate_job_custom_name(db_session):\n \"\"\"Test duplicating a job with a custom name.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n original = service.create_job(payload)\n\n duplicate = service.duplicate_job(original.id, new_name=\"Custom Copy Name\")\n assert duplicate.name == \"Custom Copy Name\"\n# [/DEF:test_duplicate_job_custom_name:Function]\n\n\n# [DEF:test_detect_virtual_columns:Function]\n# @PURPOSE: Verify virtual column detection from column metadata.\ndef test_detect_virtual_columns():\n \"\"\"Test that virtual columns are correctly identified.\"\"\"\n from src.plugins.translate.service import detect_virtual_columns\n\n columns = [\n {\"name\": \"id\", \"is_physical\": True},\n {\"name\": \"name\", \"is_physical\": True},\n {\"name\": \"virtual_col\", \"is_physical\": False},\n ]\n\n virtuals = detect_virtual_columns(columns)\n assert \"virtual_col\" in virtuals\n assert \"id\" not in virtuals\n assert \"name\" not in virtuals\n# [/DEF:test_detect_virtual_columns:Function]\n\n\n# [DEF:test_get_dialect_from_database:Function]\n# @PURPOSE: Verify dialect extraction from Superset database records.\ndef test_get_dialect_from_database():\n \"\"\"Test dialect extraction from Superset database records.\"\"\"\n from src.plugins.translate.service import get_dialect_from_database\n\n dialect = get_dialect_from_database({\"backend\": \"postgresql\"})\n assert dialect == \"postgresql\"\n\n dialect = get_dialect_from_database({\"engine\": \"mysql\"})\n assert dialect == \"mysql\"\n\n dialect = get_dialect_from_database({\"backend\": \"clickhouse\", \"engine\": \"mysql\"})\n assert dialect == \"clickhouse\"\n# [/DEF:test_get_dialect_from_database:Function]\n\n\n# [DEF:test_get_dialect_from_database_unsupported:Function]\n# @PURPOSE: Verify that unsupported dialects raise ValueError.\ndef test_get_dialect_from_database_unsupported():\n \"\"\"Test that unsupported dialects raise ValueError.\"\"\"\n from src.plugins.translate.service import get_dialect_from_database\n\n with pytest.raises(ValueError, match=\"Unsupported database dialect\"):\n get_dialect_from_database({\"backend\": \"mongodb\"})\n\n with pytest.raises(ValueError, match=\"Could not determine\"):\n get_dialect_from_database({})\n# [/DEF:test_get_dialect_from_database_unsupported:Function]\n\n\n# ============================================================\n# API Route Tests\n# ============================================================\n\n\n# [DEF:test_api_create_job:Function]\n# @PURPOSE: Verify POST /api/translate/jobs returns 201 with valid payload.\ndef test_api_create_job(client):\n \"\"\"Test POST /api/translate/jobs returns 201.\"\"\"\n response = client.post(\"/api/translate/jobs\", json=valid_job_payload)\n\n # Note: This may return 422 because the mock ConfigManager returns no real environments\n # but the route requires get_config_manager. The service is tested directly above.\n assert response.status_code in (201, 422, 500)\n if response.status_code == 201:\n data = response.json()\n assert data[\"name\"] == \"Test Translation Job\"\n assert \"id\" in data\n# [/DEF:test_api_create_job:Function]\n\n\n# [DEF:test_api_list_jobs:Function]\n# @PURPOSE: Verify GET /api/translate/jobs returns 200.\ndef test_api_list_jobs(client):\n \"\"\"Test GET /api/translate/jobs returns list.\"\"\"\n response = client.get(\"/api/translate/jobs\")\n assert response.status_code == 200\n assert isinstance(response.json(), list)\n# [/DEF:test_api_list_jobs:Function]\n\n\n# [DEF:test_api_get_job_not_found:Function]\n# @PURPOSE: Verify GET non-existent job returns 404.\ndef test_api_get_job_not_found(client):\n \"\"\"Test GET non-existent job returns 404.\"\"\"\n response = client.get(\"/api/translate/jobs/non-existent-id\")\n assert response.status_code == 404\n# [/DEF:test_api_get_job_not_found:Function]\n\n\n# [DEF:test_api_delete_job_not_found:Function]\n# @PURPOSE: Verify DELETE non-existent job returns 404.\ndef test_api_delete_job_not_found(client):\n \"\"\"Test DELETE non-existent job returns 404.\"\"\"\n response = client.delete(\"/api/translate/jobs/non-existent-id\")\n assert response.status_code == 404\n# [/DEF:test_api_delete_job_not_found:Function]\n\n\n# [DEF:test_api_duplicate_job_not_found:Function]\n# @PURPOSE: Verify duplicating a non-existent job returns 404.\ndef test_api_duplicate_job_not_found(client):\n \"\"\"Test duplicating a non-existent job returns 404.\"\"\"\n response = client.post(\"/api/translate/jobs/non-existent-id/duplicate\")\n assert response.status_code == 404\n# [/DEF:test_api_duplicate_job_not_found:Function]\n\n\n# [DEF:test_api_create_job_422_missing_name:Function]\n# @PURPOSE: Verify POST with missing required fields returns 422.\ndef test_api_create_job_422_missing_name(client):\n \"\"\"Test POST with missing required fields returns 422.\"\"\"\n response = client.post(\"/api/translate/jobs\", json={\"source_dialect\": \"pg\"})\n assert response.status_code == 422\n# [/DEF:test_api_create_job_422_missing_name:Function]\n\n\n# [DEF:test_api_datasource_columns_missing_env:Function]\n# @PURPOSE: Verify datasource columns endpoint without env_id returns 422.\ndef test_api_datasource_columns_missing_env(client):\n \"\"\"Test datasource columns endpoint without env_id returns 422.\"\"\"\n response = client.get(\"/api/translate/datasources/42/columns\")\n assert response.status_code == 422\n# [/DEF:test_api_datasource_columns_missing_env:Function]\n\n\n# [DEF:test_api_datasource_columns_bad_env:Function]\n# @PURPOSE: Verify datasource columns with unknown env returns 400.\ndef test_api_datasource_columns_bad_env(client):\n \"\"\"Test datasource columns with unknown env returns 400.\"\"\"\n response = client.get(\"/api/translate/datasources/42/columns?env_id=unknown\")\n assert response.status_code in (400, 502, 422)\n# [/DEF:test_api_datasource_columns_bad_env:Function]\n\n\n# [DEF:test_api_update_job_not_found:Function]\n# @PURPOSE: Verify PUT non-existent job returns 404.\ndef test_api_update_job_not_found(client):\n \"\"\"Test PUT non-existent job returns 404.\"\"\"\n response = client.put(\"/api/translate/jobs/non-existent-id\", json={\"name\": \"Updated\"})\n assert response.status_code == 404\n# [/DEF:test_api_update_job_not_found:Function]\n\n# [/DEF:TranslateJobTests:Module]\n" + "body": "# [DEF:TranslateJobTests:Module]\n# @SEMANTICS: tests, translate, jobs, crud, validation\n# @PURPOSE: Tests for translation job CRUD endpoints and service layer with column validation.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [TranslateRoutes:Module]\n# @RELATION: BINDS_TO -> [TranslateJobService:Module]\n#\n# @TEST_CONTRACT: TranslateJobCRUD ->\n# {\n# required_fields: {name: str, source_dialect: str, target_dialect: str},\n# optional_fields: {description, source_datasource_id, translation_column, ...},\n# invariants: [\n# \"POST /api/translate/jobs returns 201 with valid config\",\n# \"GET /api/translate/jobs returns job list\",\n# \"GET /api/translate/jobs/{id} returns single job\",\n# \"PUT /api/translate/jobs/{id} updates and returns job\",\n# \"DELETE /api/translate/jobs/{id} returns 204\",\n# \"POST /api/translate/jobs/{id}/duplicate returns new job copy\"\n# ]\n# }\n# @TEST_FIXTURE: valid_job_payload -> {name: \"Test Job\", source_dialect: \"postgresql\", target_dialect: \"clickhouse\"}\n# @TEST_EDGE: missing_translation_column -> 422 when datasource set but no translation column\n# @TEST_EDGE: virtual_key_column_warning -> warning emitted when virtual column used as key\n# @TEST_EDGE: invalid_dialect_rejected -> error on unsupported dialect\n# @TEST_INVARIANT: valid_CRUD_flow -> VERIFIED_BY: [valid_job_payload, missing_translation_column]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom fastapi.testclient import TestClient\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.app import app\nfrom src.core.config_models import Environment\nfrom src.core.database import Base\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n)\n\n# [DEF:valid_job_payload:Variable]\n# @PURPOSE: Standard valid payload for creating a translation job.\nvalid_job_payload = {\n \"name\": \"Test Translation Job\",\n \"description\": \"A test job for unit tests\",\n \"source_dialect\": \"postgresql\",\n \"target_dialect\": \"clickhouse\",\n \"source_key_cols\": [\"id\"],\n \"target_key_cols\": [\"id\"],\n \"translation_column\": \"name\",\n \"context_columns\": [\"description\"],\n \"target_language\": \"ru\",\n \"batch_size\": 100,\n \"upsert_strategy\": \"MERGE\",\n \"dictionary_ids\": [],\n}\n# [/DEF:valid_job_payload:Variable]\n\n\n# Mock user\nmock_user = MagicMock()\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nmock_user.roles.append(admin_role)\n\n\n# In-memory SQLite database for service tests\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n\n# Create DB tables once\nBase.metadata.create_all(bind=engine)\n\n\n# [DEF:MockConfigManager:Class]\n# @PURPOSE: Mock ConfigManager for service tests that returns a test environment.\nclass MockConfigManager:\n def get_environments(self):\n return [\n Environment(\n id=\"test_env\",\n name=\"Test Environment\",\n url=\"http://superset:8088\",\n username=\"admin\",\n password=\"admin\",\n )\n ]\n\n def get_config(self):\n return MagicMock()\n# [/DEF:MockConfigManager:Class]\n\n\n# [DEF:db_session:Function]\n# @PURPOSE: Create a fresh DB session with transaction rollback for each test.\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n yield session\n\n session.close()\n transaction.rollback()\n connection.close()\n# [/DEF:db_session:Function]\n\n\n# [DEF:mock_api_deps:Function]\n# @PURPOSE: Override FastAPI dependencies (including get_db with in-memory SQLite) for API route tests.\n# @RATIONALE: Uses the same in-memory SQLite engine as service tests to avoid real PostgreSQL dependency.\n# @REJECTED: Mocking get_db with MagicMock — the route handlers need a real session for ORM queries.\n@pytest.fixture\ndef mock_api_deps():\n from src.core.database import get_db\n\n config_manager = MagicMock()\n config_manager.get_environments.return_value = []\n\n # Create in-memory SQLite session for API tests (same engine as service tests)\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n def override_get_db():\n try:\n yield session\n finally:\n pass # Session lifecycle managed by fixture teardown\n\n overrides = {\n get_current_user: lambda: mock_user,\n get_config_manager: lambda: config_manager,\n get_db: override_get_db,\n }\n\n # Apply all overrides\n for dep, override in overrides.items():\n app.dependency_overrides[dep] = override\n\n yield {\"config_manager\": config_manager}\n\n app.dependency_overrides.clear()\n session.close()\n transaction.rollback()\n connection.close()\n# [/DEF:mock_api_deps:Function]\n\n\n# [DEF:client:Function]\n# @PURPOSE: FastAPI TestClient for API route tests.\n@pytest.fixture\ndef client(mock_api_deps):\n return TestClient(app)\n# [/DEF:client:Function]\n\n\n# ============================================================\n# Service Layer Tests\n# ============================================================\n\n\n# [DEF:test_create_job_valid:Function]\n# @PURPOSE: Verify that a valid job payload creates a job successfully.\ndef test_create_job_valid(db_session):\n \"\"\"Test creating a valid translation job.\"\"\"\n from src.plugins.translate.service import TranslateJobService, job_to_response\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n job = service.create_job(payload)\n\n assert job.id is not None\n assert job.name == \"Test Translation Job\"\n assert job.source_dialect == \"postgresql\"\n assert job.target_dialect == \"clickhouse\"\n assert job.translation_column == \"name\"\n assert job.source_key_cols == [\"id\"]\n assert job.target_key_cols == [\"id\"]\n assert job.context_columns == [\"description\"]\n assert job.batch_size == 100\n assert job.upsert_strategy == \"MERGE\"\n assert job.target_language == \"ru\"\n assert job.status == \"DRAFT\"\n assert job.created_by == \"test_user\"\n\n # Verify response serialization\n response = job_to_response(job, [])\n assert response.name == \"Test Translation Job\"\n assert response.source_key_cols == [\"id\"]\n# [/DEF:test_create_job_valid:Function]\n\n\n# [DEF:test_create_job_missing_translation_column:Function]\n# @PURPOSE: Verify that creating a job with datasource but no translation column raises ValueError.\ndef test_create_job_missing_translation_column(db_session):\n \"\"\"Test that a datasource without a translation column is rejected.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(\n name=\"Bad Job\",\n source_dialect=\"postgresql\",\n target_dialect=\"clickhouse\",\n source_datasource_id=\"42\",\n translation_column=None,\n )\n\n with pytest.raises(ValueError, match=\"translation column is required\"):\n service.create_job(payload)\n# [/DEF:test_create_job_missing_translation_column:Function]\n\n\n# [DEF:test_create_job_invalid_upsert_strategy:Function]\n# @PURPOSE: Verify that an invalid upsert strategy is rejected.\ndef test_create_job_invalid_upsert_strategy(db_session):\n \"\"\"Test that an invalid upsert strategy raises ValueError.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(\n name=\"Bad Strategy Job\",\n source_dialect=\"postgresql\",\n target_dialect=\"clickhouse\",\n upsert_strategy=\"INVALID\",\n )\n\n with pytest.raises(ValueError, match=\"Invalid upsert_strategy\"):\n service.create_job(payload)\n# [/DEF:test_create_job_invalid_upsert_strategy:Function]\n\n\n# [DEF:test_get_job:Function]\n# @PURPOSE: Verify that a job can be retrieved by ID.\ndef test_get_job(db_session):\n \"\"\"Test retrieving a translation job by ID.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n created = service.create_job(payload)\n\n fetched = service.get_job(created.id)\n assert fetched.id == created.id\n assert fetched.name == \"Test Translation Job\"\n# [/DEF:test_get_job:Function]\n\n\n# [DEF:test_get_job_not_found:Function]\n# @PURPOSE: Verify that getting a non-existent job raises ValueError.\ndef test_get_job_not_found(db_session):\n \"\"\"Test that a non-existent job raises ValueError.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n with pytest.raises(ValueError, match=\"not found\"):\n service.get_job(\"non-existent-id\")\n# [/DEF:test_get_job_not_found:Function]\n\n\n# [DEF:test_list_jobs:Function]\n# @PURPOSE: Verify that listing jobs returns all created jobs.\ndef test_list_jobs(db_session):\n \"\"\"Test listing translation jobs.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n p1 = TranslateJobCreate(name=\"Job 1\", source_dialect=\"postgresql\", target_dialect=\"clickhouse\")\n p2 = TranslateJobCreate(name=\"Job 2\", source_dialect=\"mysql\", target_dialect=\"postgresql\")\n service.create_job(p1)\n service.create_job(p2)\n\n total, jobs = service.list_jobs()\n assert total == 2\n assert len(jobs) == 2\n# [/DEF:test_list_jobs:Function]\n\n\n# [DEF:test_list_jobs_with_status_filter:Function]\n# @PURPOSE: Verify that listing jobs with a status filter works.\ndef test_list_jobs_with_status_filter(db_session):\n \"\"\"Test listing jobs filtered by status.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate, TranslateJobUpdate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n p1 = TranslateJobCreate(name=\"Draft Job\", source_dialect=\"pg\", target_dialect=\"ch\")\n p2 = TranslateJobCreate(name=\"Ready Job\", source_dialect=\"pg\", target_dialect=\"ch\")\n service.create_job(p1)\n job2 = service.create_job(p2)\n\n service.update_job(job2.id, TranslateJobUpdate(status=\"READY\"))\n\n total, jobs = service.list_jobs(status_filter=\"READY\")\n assert total == 1\n assert jobs[0].name == \"Ready Job\"\n# [/DEF:test_list_jobs_with_status_filter:Function]\n\n\n# [DEF:test_update_job:Function]\n# @PURPOSE: Verify that a job can be updated.\ndef test_update_job(db_session):\n \"\"\"Test updating a translation job.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate, TranslateJobUpdate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n job = service.create_job(payload)\n\n update = TranslateJobUpdate(\n name=\"Updated Job\",\n description=\"Updated description\",\n batch_size=200,\n )\n updated = service.update_job(job.id, update)\n\n assert updated.name == \"Updated Job\"\n assert updated.description == \"Updated description\"\n assert updated.batch_size == 200\n# [/DEF:test_update_job:Function]\n\n\n# [DEF:test_delete_job:Function]\n# @PURPOSE: Verify that a job can be deleted.\ndef test_delete_job(db_session):\n \"\"\"Test deleting a translation job.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n job = service.create_job(payload)\n\n service.delete_job(job.id)\n\n with pytest.raises(ValueError, match=\"not found\"):\n service.get_job(job.id)\n# [/DEF:test_delete_job:Function]\n\n\n# [DEF:test_duplicate_job:Function]\n# @PURPOSE: Verify that a job can be duplicated.\ndef test_duplicate_job(db_session):\n \"\"\"Test duplicating a translation job.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n original = service.create_job(payload)\n\n duplicate = service.duplicate_job(original.id)\n\n assert duplicate.id != original.id\n assert duplicate.name == f\"{original.name} (Copy)\"\n assert duplicate.source_dialect == original.source_dialect\n assert duplicate.target_dialect == original.target_dialect\n assert duplicate.translation_column == original.translation_column\n assert duplicate.source_key_cols == original.source_key_cols\n assert duplicate.status == \"DRAFT\"\n# [/DEF:test_duplicate_job:Function]\n\n\n# [DEF:test_duplicate_job_custom_name:Function]\n# @PURPOSE: Verify that a job can be duplicated with a custom name.\ndef test_duplicate_job_custom_name(db_session):\n \"\"\"Test duplicating a job with a custom name.\"\"\"\n from src.plugins.translate.service import TranslateJobService\n from src.schemas.translate import TranslateJobCreate\n\n config_mgr = MockConfigManager()\n service = TranslateJobService(db_session, config_mgr, \"test_user\")\n\n payload = TranslateJobCreate(**valid_job_payload)\n original = service.create_job(payload)\n\n duplicate = service.duplicate_job(original.id, new_name=\"Custom Copy Name\")\n assert duplicate.name == \"Custom Copy Name\"\n# [/DEF:test_duplicate_job_custom_name:Function]\n\n\n# [DEF:test_detect_virtual_columns:Function]\n# @PURPOSE: Verify virtual column detection from column metadata.\ndef test_detect_virtual_columns():\n \"\"\"Test that virtual columns are correctly identified.\"\"\"\n from src.plugins.translate.service import detect_virtual_columns\n\n columns = [\n {\"name\": \"id\", \"is_physical\": True},\n {\"name\": \"name\", \"is_physical\": True},\n {\"name\": \"virtual_col\", \"is_physical\": False},\n ]\n\n virtuals = detect_virtual_columns(columns)\n assert \"virtual_col\" in virtuals\n assert \"id\" not in virtuals\n assert \"name\" not in virtuals\n# [/DEF:test_detect_virtual_columns:Function]\n\n\n# [DEF:test_get_dialect_from_database:Function]\n# @PURPOSE: Verify dialect extraction from Superset database records.\ndef test_get_dialect_from_database():\n \"\"\"Test dialect extraction from Superset database records.\"\"\"\n from src.plugins.translate.service import get_dialect_from_database\n\n dialect = get_dialect_from_database({\"backend\": \"postgresql\"})\n assert dialect == \"postgresql\"\n\n dialect = get_dialect_from_database({\"engine\": \"mysql\"})\n assert dialect == \"mysql\"\n\n dialect = get_dialect_from_database({\"backend\": \"clickhouse\", \"engine\": \"mysql\"})\n assert dialect == \"clickhouse\"\n# [/DEF:test_get_dialect_from_database:Function]\n\n\n# [DEF:test_get_dialect_from_database_unsupported:Function]\n# @PURPOSE: Verify that unsupported dialects raise ValueError.\ndef test_get_dialect_from_database_unsupported():\n \"\"\"Test that unsupported dialects raise ValueError.\"\"\"\n from src.plugins.translate.service import get_dialect_from_database\n\n with pytest.raises(ValueError, match=\"Unsupported database dialect\"):\n get_dialect_from_database({\"backend\": \"mongodb\"})\n\n with pytest.raises(ValueError, match=\"Could not determine\"):\n get_dialect_from_database({})\n# [/DEF:test_get_dialect_from_database_unsupported:Function]\n\n\n# ============================================================\n# API Route Tests\n# ============================================================\n\n\n# [DEF:test_api_create_job:Function]\n# @PURPOSE: Verify POST /api/translate/jobs returns 201 with valid payload.\ndef test_api_create_job(client):\n \"\"\"Test POST /api/translate/jobs returns 201.\"\"\"\n response = client.post(\"/api/translate/jobs\", json=valid_job_payload)\n\n # Note: This may return 422 because the mock ConfigManager returns no real environments\n # but the route requires get_config_manager. The service is tested directly above.\n assert response.status_code in (201, 422, 500)\n if response.status_code == 201:\n data = response.json()\n assert data[\"name\"] == \"Test Translation Job\"\n assert \"id\" in data\n# [/DEF:test_api_create_job:Function]\n\n\n# [DEF:test_api_list_jobs:Function]\n# @PURPOSE: Verify GET /api/translate/jobs returns 200.\ndef test_api_list_jobs(client):\n \"\"\"Test GET /api/translate/jobs returns list.\"\"\"\n response = client.get(\"/api/translate/jobs\")\n assert response.status_code == 200\n assert isinstance(response.json(), list)\n# [/DEF:test_api_list_jobs:Function]\n\n\n# [DEF:test_api_get_job_not_found:Function]\n# @PURPOSE: Verify GET non-existent job returns 404.\ndef test_api_get_job_not_found(client):\n \"\"\"Test GET non-existent job returns 404.\"\"\"\n response = client.get(\"/api/translate/jobs/non-existent-id\")\n assert response.status_code == 404\n# [/DEF:test_api_get_job_not_found:Function]\n\n\n# [DEF:test_api_delete_job_not_found:Function]\n# @PURPOSE: Verify DELETE non-existent job returns 404.\ndef test_api_delete_job_not_found(client):\n \"\"\"Test DELETE non-existent job returns 404.\"\"\"\n response = client.delete(\"/api/translate/jobs/non-existent-id\")\n assert response.status_code == 404\n# [/DEF:test_api_delete_job_not_found:Function]\n\n\n# [DEF:test_api_duplicate_job_not_found:Function]\n# @PURPOSE: Verify duplicating a non-existent job returns 404.\ndef test_api_duplicate_job_not_found(client):\n \"\"\"Test duplicating a non-existent job returns 404.\"\"\"\n response = client.post(\"/api/translate/jobs/non-existent-id/duplicate\")\n assert response.status_code == 404\n# [/DEF:test_api_duplicate_job_not_found:Function]\n\n\n# [DEF:test_api_create_job_422_missing_name:Function]\n# @PURPOSE: Verify POST with missing required fields returns 422.\ndef test_api_create_job_422_missing_name(client):\n \"\"\"Test POST with missing required fields returns 422.\"\"\"\n response = client.post(\"/api/translate/jobs\", json={\"source_dialect\": \"pg\"})\n assert response.status_code == 422\n# [/DEF:test_api_create_job_422_missing_name:Function]\n\n\n# [DEF:test_api_datasource_columns_missing_env:Function]\n# @PURPOSE: Verify datasource columns endpoint without env_id returns 422.\ndef test_api_datasource_columns_missing_env(client):\n \"\"\"Test datasource columns endpoint without env_id returns 422.\"\"\"\n response = client.get(\"/api/translate/datasources/42/columns\")\n assert response.status_code == 422\n# [/DEF:test_api_datasource_columns_missing_env:Function]\n\n\n# [DEF:test_api_datasource_columns_bad_env:Function]\n# @PURPOSE: Verify datasource columns with unknown env returns 400.\ndef test_api_datasource_columns_bad_env(client):\n \"\"\"Test datasource columns with unknown env returns 400.\"\"\"\n response = client.get(\"/api/translate/datasources/42/columns?env_id=unknown\")\n assert response.status_code in (400, 502, 422)\n# [/DEF:test_api_datasource_columns_bad_env:Function]\n\n\n# [DEF:test_api_update_job_not_found:Function]\n# @PURPOSE: Verify PUT non-existent job returns 404.\ndef test_api_update_job_not_found(client):\n \"\"\"Test PUT non-existent job returns 404.\"\"\"\n response = client.put(\"/api/translate/jobs/non-existent-id\", json={\"name\": \"Updated\"})\n assert response.status_code == 404\n# [/DEF:test_api_update_job_not_found:Function]\n\n# [/DEF:TranslateJobTests:Module]\n" }, { "contract_id": "valid_job_payload", "contract_type": "Variable", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 54, - "end_line": 70, + "start_line": 47, + "end_line": 63, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -106736,8 +75191,8 @@ "contract_id": "MockConfigManager", "contract_type": "Class", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 92, - "end_line": 108, + "start_line": 85, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -106758,12 +75213,38 @@ "anchor_syntax": "def", "body": "# [DEF:MockConfigManager:Class]\n# @PURPOSE: Mock ConfigManager for service tests that returns a test environment.\nclass MockConfigManager:\n def get_environments(self):\n return [\n Environment(\n id=\"test_env\",\n name=\"Test Environment\",\n url=\"http://superset:8088\",\n username=\"admin\",\n password=\"admin\",\n )\n ]\n\n def get_config(self):\n return MagicMock()\n# [/DEF:MockConfigManager:Class]\n" }, + { + "contract_id": "db_session", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_jobs.py", + "start_line": 104, + "end_line": 117, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "PURPOSE": "Create a fresh DB session with transaction rollback for each test." + }, + "relations": [], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "def", + "body": "# [DEF:db_session:Function]\n# @PURPOSE: Create a fresh DB session with transaction rollback for each test.\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n yield session\n\n session.close()\n transaction.rollback()\n connection.close()\n# [/DEF:db_session:Function]\n" + }, { "contract_id": "mock_api_deps", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 127, - "end_line": 165, + "start_line": 120, + "end_line": 158, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -106808,8 +75289,8 @@ "contract_id": "client", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 168, - "end_line": 173, + "start_line": 161, + "end_line": 166, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -106834,8 +75315,8 @@ "contract_id": "test_create_job_valid", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 181, - "end_line": 212, + "start_line": 174, + "end_line": 205, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106850,8 +75331,8 @@ "contract_id": "test_create_job_missing_translation_column", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 215, - "end_line": 235, + "start_line": 208, + "end_line": 228, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106866,8 +75347,8 @@ "contract_id": "test_create_job_invalid_upsert_strategy", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 238, - "end_line": 257, + "start_line": 231, + "end_line": 250, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106882,8 +75363,8 @@ "contract_id": "test_get_job", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 260, - "end_line": 276, + "start_line": 253, + "end_line": 269, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106898,8 +75379,8 @@ "contract_id": "test_get_job_not_found", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 279, - "end_line": 290, + "start_line": 272, + "end_line": 283, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106914,8 +75395,8 @@ "contract_id": "test_list_jobs", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 293, - "end_line": 311, + "start_line": 286, + "end_line": 304, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106930,8 +75411,8 @@ "contract_id": "test_list_jobs_with_status_filter", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 314, - "end_line": 334, + "start_line": 307, + "end_line": 327, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106946,8 +75427,8 @@ "contract_id": "test_update_job", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 337, - "end_line": 360, + "start_line": 330, + "end_line": 353, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106962,8 +75443,8 @@ "contract_id": "test_delete_job", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 363, - "end_line": 380, + "start_line": 356, + "end_line": 373, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106978,8 +75459,8 @@ "contract_id": "test_duplicate_job", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 383, - "end_line": 405, + "start_line": 376, + "end_line": 398, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -106994,8 +75475,8 @@ "contract_id": "test_duplicate_job_custom_name", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 408, - "end_line": 423, + "start_line": 401, + "end_line": 416, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107010,8 +75491,8 @@ "contract_id": "test_detect_virtual_columns", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 426, - "end_line": 442, + "start_line": 419, + "end_line": 435, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107026,8 +75507,8 @@ "contract_id": "test_get_dialect_from_database", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 445, - "end_line": 459, + "start_line": 438, + "end_line": 452, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107042,8 +75523,8 @@ "contract_id": "test_get_dialect_from_database_unsupported", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 462, - "end_line": 473, + "start_line": 455, + "end_line": 466, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107058,8 +75539,8 @@ "contract_id": "test_api_create_job", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 481, - "end_line": 494, + "start_line": 474, + "end_line": 487, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107074,8 +75555,8 @@ "contract_id": "test_api_list_jobs", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 497, - "end_line": 504, + "start_line": 490, + "end_line": 497, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107090,8 +75571,8 @@ "contract_id": "test_api_get_job_not_found", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 507, - "end_line": 513, + "start_line": 500, + "end_line": 506, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107106,8 +75587,8 @@ "contract_id": "test_api_delete_job_not_found", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 516, - "end_line": 522, + "start_line": 509, + "end_line": 515, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107122,8 +75603,8 @@ "contract_id": "test_api_duplicate_job_not_found", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 525, - "end_line": 531, + "start_line": 518, + "end_line": 524, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107138,8 +75619,8 @@ "contract_id": "test_api_create_job_422_missing_name", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 534, - "end_line": 540, + "start_line": 527, + "end_line": 533, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107154,8 +75635,8 @@ "contract_id": "test_api_datasource_columns_missing_env", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 543, - "end_line": 549, + "start_line": 536, + "end_line": 542, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107170,8 +75651,8 @@ "contract_id": "test_api_datasource_columns_bad_env", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 552, - "end_line": 558, + "start_line": 545, + "end_line": 551, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107186,8 +75667,8 @@ "contract_id": "test_api_update_job_not_found", "contract_type": "Function", "file_path": "backend/tests/test_translate_jobs.py", - "start_line": 561, - "end_line": 567, + "start_line": 554, + "end_line": 560, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107203,11 +75684,10 @@ "contract_type": "Module", "file_path": "backend/tests/test_translate_scheduler.py", "start_line": 1, - "end_line": 206, - "tier": "TIER_2", - "complexity": 4, + "end_line": 195, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "4", "LAYER": "Test", "PURPOSE": "Tests for TranslationScheduler CRUD and APScheduler integration.", "SEMANTICS": [ @@ -107226,6 +75706,15 @@ } ], "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Module' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Module" + } + }, { "code": "tag_not_for_contract_type", "tag": "TEST_CONTRACT", @@ -107241,49 +75730,31 @@ { "code": "tag_forbidden_by_complexity", "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C4", + "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C1", "detail": { - "actual_complexity": 4, + "actual_complexity": 1, "contract_type": "Module" } }, { - "code": "missing_required_tag", - "tag": "POST", - "message": "@POST is required for contract type 'Module' at C4", + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Module' at C1", "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, + "actual_complexity": 1, "contract_type": "Module" } } ], "anchor_syntax": "def", - "body": "# [DEF:TranslateSchedulerTests:Module]\n# @COMPLEXITY: 4\n# @SEMANTICS: tests, translate, scheduler\n# @PURPOSE: Tests for TranslationScheduler CRUD and APScheduler integration.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [TranslationScheduler:Module]\n#\n# @TEST_CONTRACT: ScheduleFlow ->\n# {\n# invariants: [\n# \"Create schedule returns TranslationSchedule with valid fields\",\n# \"Update schedule modifies cron/timezone/active\",\n# \"Delete schedule removes the row\",\n# \"Enable/disable toggles is_active\",\n# \"get_next_executions returns ISO datetime strings\",\n# \"List active schedules returns only active rows\"\n# ]\n# }\n# @TEST_FIXTURE: valid_job -> created via TranslateJobService\n# @TEST_FIXTURE: schedule_data -> valid cron expression\n# @TEST_EDGE: schedule_not_found -> ValueError on get/delete non-existent\n# @TEST_EDGE: next_executions_invalid_cron -> empty list\n# @TEST_INVARIANT: schedule_crud_flow -> VERIFIED_BY: [valid_job, schedule_data]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom unittest.mock import MagicMock\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom datetime import datetime, timezone\nimport uuid\n\nfrom src.core.database import Base\nfrom src.app import app\nfrom src.dependencies import get_current_user, get_config_manager, get_db\nfrom src.plugins.translate.scheduler import TranslationScheduler, execute_scheduled_translation\nfrom src.plugins.translate.service import TranslateJobService\nfrom src.schemas.translate import TranslateJobCreate\nfrom src.models.translate import TranslationJob, TranslationSchedule, TranslationRun\n\n# In-memory SQLite\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase.metadata.create_all(bind=engine)\n\n\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n yield session\n session.close()\n transaction.rollback()\n connection.close()\n\n\n# [DEF:test_create_schedule:Function]\n# @PURPOSE: Verify schedule creation with valid params.\ndef test_create_schedule(db_session):\n \"\"\"Test creating a schedule for a job.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n payload = TranslateJobCreate(name=\"Sched Job\", source_dialect=\"pg\", target_dialect=\"ch\")\n job = svc.create_job(payload)\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n schedule = scheduler.create_schedule(\n job_id=job.id,\n cron_expression=\"0 2 * * *\",\n timezone=\"UTC\",\n )\n\n assert schedule.job_id == job.id\n assert schedule.cron_expression == \"0 2 * * *\"\n assert schedule.timezone == \"UTC\"\n assert schedule.is_active is True\n assert schedule.id is not None\n# [/DEF:test_create_schedule:Function]\n\n\n# [DEF:test_update_schedule:Function]\n# @PURPOSE: Verify schedule update.\ndef test_update_schedule(db_session):\n \"\"\"Test updating a schedule.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n\n updated = scheduler.update_schedule(job.id, cron_expression=\"30 3 * * *\", timezone_str=\"US/Eastern\", is_active=False)\n assert updated.cron_expression == \"30 3 * * *\"\n assert updated.timezone == \"US/Eastern\"\n assert updated.is_active is False\n# [/DEF:test_update_schedule:Function]\n\n\n# [DEF:test_delete_schedule:Function]\n# @PURPOSE: Verify schedule deletion.\ndef test_delete_schedule(db_session):\n \"\"\"Test deleting a schedule.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n scheduler.delete_schedule(job.id)\n\n with pytest.raises(ValueError, match=\"No schedule found\"):\n scheduler.get_schedule(job.id)\n# [/DEF:test_delete_schedule:Function]\n\n\n# [DEF:test_enable_disable_schedule:Function]\n# @PURPOSE: Verify enable/disable toggle.\ndef test_enable_disable_schedule(db_session):\n \"\"\"Test enabling and disabling a schedule.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n\n sched = scheduler.set_schedule_active(job.id, False)\n assert sched.is_active is False\n\n sched = scheduler.set_schedule_active(job.id, True)\n assert sched.is_active is True\n# [/DEF:test_enable_disable_schedule:Function]\n\n\n# [DEF:test_get_schedule_not_found:Function]\n# @PURPOSE: Verify ValueError on getting non-existent schedule.\ndef test_get_schedule_not_found(db_session):\n \"\"\"Test that getting a non-existent schedule raises ValueError.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n with pytest.raises(ValueError, match=\"No schedule found\"):\n scheduler.get_schedule(\"non-existent\")\n# [/DEF:test_get_schedule_not_found:Function]\n\n\n# [DEF:test_list_active_schedules:Function]\n# @PURPOSE: Verify listing only active schedules.\ndef test_list_active_schedules(db_session):\n \"\"\"Test listing active schedules.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job1 = svc.create_job(TranslateJobCreate(name=\"Job1\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job1.id, \"0 2 * * *\")\n scheduler.set_schedule_active(job1.id, True)\n\n active = TranslationScheduler.list_active_schedules(db_session)\n assert len(active) >= 1\n assert active[0].job_id == job1.id\n# [/DEF:test_list_active_schedules:Function]\n\n\n# [DEF:test_get_next_executions:Function]\n# @PURPOSE: Verify next execution time computation.\ndef test_get_next_executions():\n \"\"\"Test computing next execution times.\"\"\"\n times = TranslationScheduler.get_next_executions(\"0 2 * * *\", \"UTC\", n=3)\n assert len(times) == 3\n for t in times:\n assert \"T\" in t # ISO format check\n# [/DEF:test_get_next_executions:Function]\n\n\n# [DEF:test_get_next_executions_invalid:Function]\n# @PURPOSE: Verify invalid cron returns empty list.\ndef test_get_next_executions_invalid():\n \"\"\"Test invalid cron returns empty list.\"\"\"\n times = TranslationScheduler.get_next_executions(\"invalid cron\", \"UTC\", n=3)\n assert times == []\n# [/DEF:test_get_next_executions_invalid:Function]\n# [/DEF:TranslateSchedulerTests:Module]\n" + "body": "# [DEF:TranslateSchedulerTests:Module]\n# @SEMANTICS: tests, translate, scheduler\n# @PURPOSE: Tests for TranslationScheduler CRUD and APScheduler integration.\n# @LAYER: Test\n# @RELATION: BINDS_TO -> [TranslationScheduler:Module]\n#\n# @TEST_CONTRACT: ScheduleFlow ->\n# {\n# invariants: [\n# \"Create schedule returns TranslationSchedule with valid fields\",\n# \"Update schedule modifies cron/timezone/active\",\n# \"Delete schedule removes the row\",\n# \"Enable/disable toggles is_active\",\n# \"get_next_executions returns ISO datetime strings\",\n# \"List active schedules returns only active rows\"\n# ]\n# }\n# @TEST_FIXTURE: valid_job -> created via TranslateJobService\n# @TEST_FIXTURE: schedule_data -> valid cron expression\n# @TEST_EDGE: schedule_not_found -> ValueError on get/delete non-existent\n# @TEST_EDGE: next_executions_invalid_cron -> empty list\n# @TEST_INVARIANT: schedule_crud_flow -> VERIFIED_BY: [valid_job, schedule_data]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nfrom unittest.mock import MagicMock\n\nimport pytest\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.database import Base\nfrom src.plugins.translate.scheduler import TranslationScheduler\nfrom src.plugins.translate.service import TranslateJobService\nfrom src.schemas.translate import TranslateJobCreate\n\n# In-memory SQLite\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\nengine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False})\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase.metadata.create_all(bind=engine)\n\n\n@pytest.fixture\ndef db_session():\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n yield session\n session.close()\n transaction.rollback()\n connection.close()\n\n\n# [DEF:test_create_schedule:Function]\n# @PURPOSE: Verify schedule creation with valid params.\ndef test_create_schedule(db_session):\n \"\"\"Test creating a schedule for a job.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n payload = TranslateJobCreate(name=\"Sched Job\", source_dialect=\"pg\", target_dialect=\"ch\")\n job = svc.create_job(payload)\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n schedule = scheduler.create_schedule(\n job_id=job.id,\n cron_expression=\"0 2 * * *\",\n timezone=\"UTC\",\n )\n\n assert schedule.job_id == job.id\n assert schedule.cron_expression == \"0 2 * * *\"\n assert schedule.timezone == \"UTC\"\n assert schedule.is_active is True\n assert schedule.id is not None\n# [/DEF:test_create_schedule:Function]\n\n\n# [DEF:test_update_schedule:Function]\n# @PURPOSE: Verify schedule update.\ndef test_update_schedule(db_session):\n \"\"\"Test updating a schedule.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n\n updated = scheduler.update_schedule(job.id, cron_expression=\"30 3 * * *\", timezone_str=\"US/Eastern\", is_active=False)\n assert updated.cron_expression == \"30 3 * * *\"\n assert updated.timezone == \"US/Eastern\"\n assert updated.is_active is False\n# [/DEF:test_update_schedule:Function]\n\n\n# [DEF:test_delete_schedule:Function]\n# @PURPOSE: Verify schedule deletion.\ndef test_delete_schedule(db_session):\n \"\"\"Test deleting a schedule.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n scheduler.delete_schedule(job.id)\n\n with pytest.raises(ValueError, match=\"No schedule found\"):\n scheduler.get_schedule(job.id)\n# [/DEF:test_delete_schedule:Function]\n\n\n# [DEF:test_enable_disable_schedule:Function]\n# @PURPOSE: Verify enable/disable toggle.\ndef test_enable_disable_schedule(db_session):\n \"\"\"Test enabling and disabling a schedule.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n\n sched = scheduler.set_schedule_active(job.id, False)\n assert sched.is_active is False\n\n sched = scheduler.set_schedule_active(job.id, True)\n assert sched.is_active is True\n# [/DEF:test_enable_disable_schedule:Function]\n\n\n# [DEF:test_get_schedule_not_found:Function]\n# @PURPOSE: Verify ValueError on getting non-existent schedule.\ndef test_get_schedule_not_found(db_session):\n \"\"\"Test that getting a non-existent schedule raises ValueError.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n with pytest.raises(ValueError, match=\"No schedule found\"):\n scheduler.get_schedule(\"non-existent\")\n# [/DEF:test_get_schedule_not_found:Function]\n\n\n# [DEF:test_list_active_schedules:Function]\n# @PURPOSE: Verify listing only active schedules.\ndef test_list_active_schedules(db_session):\n \"\"\"Test listing active schedules.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job1 = svc.create_job(TranslateJobCreate(name=\"Job1\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job1.id, \"0 2 * * *\")\n scheduler.set_schedule_active(job1.id, True)\n\n active = TranslationScheduler.list_active_schedules(db_session)\n assert len(active) >= 1\n assert active[0].job_id == job1.id\n# [/DEF:test_list_active_schedules:Function]\n\n\n# [DEF:test_get_next_executions:Function]\n# @PURPOSE: Verify next execution time computation.\ndef test_get_next_executions():\n \"\"\"Test computing next execution times.\"\"\"\n times = TranslationScheduler.get_next_executions(\"0 2 * * *\", \"UTC\", n=3)\n assert len(times) == 3\n for t in times:\n assert \"T\" in t # ISO format check\n# [/DEF:test_get_next_executions:Function]\n\n\n# [DEF:test_get_next_executions_invalid:Function]\n# @PURPOSE: Verify invalid cron returns empty list.\ndef test_get_next_executions_invalid():\n \"\"\"Test invalid cron returns empty list.\"\"\"\n times = TranslationScheduler.get_next_executions(\"invalid cron\", \"UTC\", n=3)\n assert times == []\n# [/DEF:test_get_next_executions_invalid:Function]\n# [/DEF:TranslateSchedulerTests:Module]\n" }, { "contract_id": "test_create_schedule", "contract_type": "Function", "file_path": "backend/tests/test_translate_scheduler.py", - "start_line": 63, - "end_line": 87, + "start_line": 58, + "end_line": 81, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107292,14 +75763,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:test_create_schedule:Function]\n# @PURPOSE: Verify schedule creation with valid params.\ndef test_create_schedule(db_session):\n \"\"\"Test creating a schedule for a job.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n payload = TranslateJobCreate(name=\"Sched Job\", source_dialect=\"pg\", target_dialect=\"ch\")\n job = svc.create_job(payload)\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n schedule = scheduler.create_schedule(\n job_id=job.id,\n cron_expression=\"0 2 * * *\",\n timezone=\"UTC\",\n )\n\n assert schedule.job_id == job.id\n assert schedule.cron_expression == \"0 2 * * *\"\n assert schedule.timezone == \"UTC\"\n assert schedule.is_active is True\n assert schedule.id is not None\n# [/DEF:test_create_schedule:Function]\n" + "body": "# [DEF:test_create_schedule:Function]\n# @PURPOSE: Verify schedule creation with valid params.\ndef test_create_schedule(db_session):\n \"\"\"Test creating a schedule for a job.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n payload = TranslateJobCreate(name=\"Sched Job\", source_dialect=\"pg\", target_dialect=\"ch\")\n job = svc.create_job(payload)\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n schedule = scheduler.create_schedule(\n job_id=job.id,\n cron_expression=\"0 2 * * *\",\n timezone=\"UTC\",\n )\n\n assert schedule.job_id == job.id\n assert schedule.cron_expression == \"0 2 * * *\"\n assert schedule.timezone == \"UTC\"\n assert schedule.is_active is True\n assert schedule.id is not None\n# [/DEF:test_create_schedule:Function]\n" }, { "contract_id": "test_update_schedule", "contract_type": "Function", "file_path": "backend/tests/test_translate_scheduler.py", - "start_line": 90, - "end_line": 108, + "start_line": 84, + "end_line": 101, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107308,14 +75779,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:test_update_schedule:Function]\n# @PURPOSE: Verify schedule update.\ndef test_update_schedule(db_session):\n \"\"\"Test updating a schedule.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n\n updated = scheduler.update_schedule(job.id, cron_expression=\"30 3 * * *\", timezone_str=\"US/Eastern\", is_active=False)\n assert updated.cron_expression == \"30 3 * * *\"\n assert updated.timezone == \"US/Eastern\"\n assert updated.is_active is False\n# [/DEF:test_update_schedule:Function]\n" + "body": "# [DEF:test_update_schedule:Function]\n# @PURPOSE: Verify schedule update.\ndef test_update_schedule(db_session):\n \"\"\"Test updating a schedule.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n\n updated = scheduler.update_schedule(job.id, cron_expression=\"30 3 * * *\", timezone_str=\"US/Eastern\", is_active=False)\n assert updated.cron_expression == \"30 3 * * *\"\n assert updated.timezone == \"US/Eastern\"\n assert updated.is_active is False\n# [/DEF:test_update_schedule:Function]\n" }, { "contract_id": "test_delete_schedule", "contract_type": "Function", "file_path": "backend/tests/test_translate_scheduler.py", - "start_line": 111, - "end_line": 128, + "start_line": 104, + "end_line": 120, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107324,14 +75795,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:test_delete_schedule:Function]\n# @PURPOSE: Verify schedule deletion.\ndef test_delete_schedule(db_session):\n \"\"\"Test deleting a schedule.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n scheduler.delete_schedule(job.id)\n\n with pytest.raises(ValueError, match=\"No schedule found\"):\n scheduler.get_schedule(job.id)\n# [/DEF:test_delete_schedule:Function]\n" + "body": "# [DEF:test_delete_schedule:Function]\n# @PURPOSE: Verify schedule deletion.\ndef test_delete_schedule(db_session):\n \"\"\"Test deleting a schedule.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n scheduler.delete_schedule(job.id)\n\n with pytest.raises(ValueError, match=\"No schedule found\"):\n scheduler.get_schedule(job.id)\n# [/DEF:test_delete_schedule:Function]\n" }, { "contract_id": "test_enable_disable_schedule", "contract_type": "Function", "file_path": "backend/tests/test_translate_scheduler.py", - "start_line": 131, - "end_line": 150, + "start_line": 123, + "end_line": 141, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107340,14 +75811,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:test_enable_disable_schedule:Function]\n# @PURPOSE: Verify enable/disable toggle.\ndef test_enable_disable_schedule(db_session):\n \"\"\"Test enabling and disabling a schedule.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n\n sched = scheduler.set_schedule_active(job.id, False)\n assert sched.is_active is False\n\n sched = scheduler.set_schedule_active(job.id, True)\n assert sched.is_active is True\n# [/DEF:test_enable_disable_schedule:Function]\n" + "body": "# [DEF:test_enable_disable_schedule:Function]\n# @PURPOSE: Verify enable/disable toggle.\ndef test_enable_disable_schedule(db_session):\n \"\"\"Test enabling and disabling a schedule.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job = svc.create_job(TranslateJobCreate(name=\"Job\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job.id, \"0 2 * * *\")\n\n sched = scheduler.set_schedule_active(job.id, False)\n assert sched.is_active is False\n\n sched = scheduler.set_schedule_active(job.id, True)\n assert sched.is_active is True\n# [/DEF:test_enable_disable_schedule:Function]\n" }, { "contract_id": "test_get_schedule_not_found", "contract_type": "Function", "file_path": "backend/tests/test_translate_scheduler.py", - "start_line": 153, - "end_line": 164, + "start_line": 144, + "end_line": 154, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107356,14 +75827,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:test_get_schedule_not_found:Function]\n# @PURPOSE: Verify ValueError on getting non-existent schedule.\ndef test_get_schedule_not_found(db_session):\n \"\"\"Test that getting a non-existent schedule raises ValueError.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n with pytest.raises(ValueError, match=\"No schedule found\"):\n scheduler.get_schedule(\"non-existent\")\n# [/DEF:test_get_schedule_not_found:Function]\n" + "body": "# [DEF:test_get_schedule_not_found:Function]\n# @PURPOSE: Verify ValueError on getting non-existent schedule.\ndef test_get_schedule_not_found(db_session):\n \"\"\"Test that getting a non-existent schedule raises ValueError.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n with pytest.raises(ValueError, match=\"No schedule found\"):\n scheduler.get_schedule(\"non-existent\")\n# [/DEF:test_get_schedule_not_found:Function]\n" }, { "contract_id": "test_list_active_schedules", "contract_type": "Function", "file_path": "backend/tests/test_translate_scheduler.py", - "start_line": 167, - "end_line": 185, + "start_line": 157, + "end_line": 174, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107372,14 +75843,14 @@ "relations": [], "schema_warnings": [], "anchor_syntax": "def", - "body": "# [DEF:test_list_active_schedules:Function]\n# @PURPOSE: Verify listing only active schedules.\ndef test_list_active_schedules(db_session):\n \"\"\"Test listing active schedules.\"\"\"\n from src.core.config_models import Environment\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job1 = svc.create_job(TranslateJobCreate(name=\"Job1\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job1.id, \"0 2 * * *\")\n scheduler.set_schedule_active(job1.id, True)\n\n active = TranslationScheduler.list_active_schedules(db_session)\n assert len(active) >= 1\n assert active[0].job_id == job1.id\n# [/DEF:test_list_active_schedules:Function]\n" + "body": "# [DEF:test_list_active_schedules:Function]\n# @PURPOSE: Verify listing only active schedules.\ndef test_list_active_schedules(db_session):\n \"\"\"Test listing active schedules.\"\"\"\n config_mgr = MagicMock()\n config_mgr.get_environments.return_value = []\n\n svc = TranslateJobService(db_session, config_mgr, \"test_user\")\n job1 = svc.create_job(TranslateJobCreate(name=\"Job1\", source_dialect=\"pg\", target_dialect=\"ch\"))\n\n scheduler = TranslationScheduler(db_session, config_mgr, \"test_user\")\n scheduler.create_schedule(job1.id, \"0 2 * * *\")\n scheduler.set_schedule_active(job1.id, True)\n\n active = TranslationScheduler.list_active_schedules(db_session)\n assert len(active) >= 1\n assert active[0].job_id == job1.id\n# [/DEF:test_list_active_schedules:Function]\n" }, { "contract_id": "test_get_next_executions", "contract_type": "Function", "file_path": "backend/tests/test_translate_scheduler.py", - "start_line": 188, - "end_line": 196, + "start_line": 177, + "end_line": 185, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107394,8 +75865,8 @@ "contract_id": "test_get_next_executions_invalid", "contract_type": "Function", "file_path": "backend/tests/test_translate_scheduler.py", - "start_line": 199, - "end_line": 205, + "start_line": 188, + "end_line": 194, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -107406,6 +75877,278 @@ "anchor_syntax": "def", "body": "# [DEF:test_get_next_executions_invalid:Function]\n# @PURPOSE: Verify invalid cron returns empty list.\ndef test_get_next_executions_invalid():\n \"\"\"Test invalid cron returns empty list.\"\"\"\n times = TranslationScheduler.get_next_executions(\"invalid cron\", \"UTC\", n=3)\n assert times == []\n# [/DEF:test_get_next_executions_invalid:Function]\n" }, + { + "contract_id": "TestTranslateSchedulerExecution", + "contract_type": "Module", + "file_path": "backend/tests/test_translate_scheduler_execution.py", + "start_line": 1, + "end_line": 399, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Verify execute_scheduled_translation contracts — trigger_type dispatch, baseline expiry fallback.", + "TEST_EDGE": "execution_error_handled — run marked FAILED, schedule tracking updated", + "TEST_INVARIANT": "trigger_type_dispatch -> VERIFIED_BY: [test_new_key_only_mode, test_baseline_expired_fallback, test_full_mode_default]" + }, + "relations": [ + { + "source_id": "TestTranslateSchedulerExecution", + "relation_type": "BINDS_TO", + "target_id": "execute_scheduled_translation", + "target_ref": "[execute_scheduled_translation]" + } + ], + "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", + "body": "# #region TestTranslateSchedulerExecution [C:3] [TYPE Module] [SEMANTICS test, translate, scheduler, execution, trigger-type, baseline]\n# @BRIEF Verify execute_scheduled_translation contracts — trigger_type dispatch, baseline expiry fallback.\n# @RELATION BINDS_TO -> [execute_scheduled_translation]\n# @TEST_EDGE: new_key_only_mode — trigger_type=\"new_key_only\" when baseline not expired\n# @TEST_EDGE: baseline_expired_fallback — trigger_type=\"scheduled\" when baseline > 90 days old\n# @TEST_EDGE: full_mode_default — trigger_type=\"scheduled\" for execution_mode=\"full\"\n# @TEST_EDGE: inactive_schedule — function returns early without executing\n# @TEST_EDGE: execution_error_handled — run marked FAILED, schedule tracking updated\n# @TEST_INVARIANT: trigger_type_dispatch -> VERIFIED_BY: [test_new_key_only_mode, test_baseline_expired_fallback, test_full_mode_default]\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\nfrom datetime import UTC, datetime, timedelta\nfrom unittest.mock import MagicMock, patch\n\n# -- Helpers -----------------------------------------------------------------\n\ndef _make_most_recent_run(created_at_delta_days=60):\n \"\"\"Build a MagicMock TranslationRun with given age.\"\"\"\n run = MagicMock()\n run.id = \"prev-run-1\"\n run.job_id = \"job-1\"\n run.status = \"COMPLETED\"\n run.insert_status = \"succeeded\"\n run.created_at = datetime.now(UTC) - timedelta(days=created_at_delta_days)\n return run\n\n\ndef _make_mock_run(run_id=\"run-1\", job_id=\"job-1\"):\n \"\"\"Build a mock TranslationRun returned by start_run.\"\"\"\n run = MagicMock()\n run.id = run_id\n run.job_id = job_id\n run.status = \"PENDING\"\n run.trigger_type = None\n run.total_records = 0\n run.successful_records = 0\n run.failed_records = 0\n run.skipped_records = 0\n run.insert_status = None\n run.superset_execution_id = None\n run.superset_execution_log = None\n run.error_message = None\n return run\n\n\n# -- Tests: trigger_type dispatch and execution modes -----------------------\n\n# #region test_new_key_only_mode [C:1] [TYPE Function]\ndef test_new_key_only_mode():\n \"\"\"execution_mode=\"new_key_only\" with 60-day-old prev run → trigger_type=\"new_key_only\".\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"new_key_only\"\n\n most_recent = _make_most_recent_run(created_at_delta_days=60) # 60 days < 90\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None # no concurrent\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"new_key_only\",\n )\n\n assert mock_run.trigger_type == \"new_key_only\", (\n f\"Expected trigger_type='new_key_only', got '{mock_run.trigger_type}'\"\n )\n mock_orch.start_run.assert_called_once_with(job_id=\"job-1\", is_scheduled=True)\n mock_orch.execute_run.assert_called_once()\n mock_db.close.assert_called_once()\n# #endregion test_new_key_only_mode\n\n\n# #region test_baseline_expired_fallback [C:1] [TYPE Function]\ndef test_baseline_expired_fallback():\n \"\"\"execution_mode=\"new_key_only\" with >90-day-old prev run → trigger_type=\"scheduled\" baseline fallback.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"new_key_only\"\n\n most_recent = _make_most_recent_run(created_at_delta_days=95) # 95 days > 90\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n with patch(\n \"src.plugins.translate.scheduler.TranslationEventLog\"\n ) as mock_event_log_cls:\n mock_event_log = MagicMock()\n mock_event_log_cls.return_value = mock_event_log\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"new_key_only\",\n )\n\n assert mock_run.trigger_type == \"scheduled\", (\n f\"Expected trigger_type='scheduled' (baseline expired), got '{mock_run.trigger_type}'\"\n )\n\n mock_event_log.log_event.assert_called_once()\n event_call = mock_event_log.log_event.call_args\n assert event_call.kwargs[\"job_id\"] == \"job-1\"\n assert event_call.kwargs[\"event_type\"] == \"RUN_STARTED\"\n assert event_call.kwargs[\"payload\"][\"reason\"] == \"baseline_expired\"\n assert event_call.kwargs[\"payload\"][\"age_days\"] == 95\n\n mock_db.close.assert_called_once()\n# #endregion test_baseline_expired_fallback\n\n\n# #region test_full_mode_default [C:1] [TYPE Function]\ndef test_full_mode_default():\n \"\"\"execution_mode=\"full\" → trigger_type=\"scheduled\" regardless of baseline age.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n most_recent = _make_most_recent_run(created_at_delta_days=30)\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n assert mock_run.trigger_type == \"scheduled\", (\n f\"Expected trigger_type='scheduled' for full mode, got '{mock_run.trigger_type}'\"\n )\n mock_orch.execute_run.assert_called_once()\n mock_db.close.assert_called_once()\n# #endregion test_full_mode_default\n\n\n# #region test_inactive_schedule_skips [C:1] [TYPE Function]\ndef test_inactive_schedule_skips():\n \"\"\"Inactive schedule → function returns early without executing anything.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = False\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n mock_db.query.return_value = q1\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n mock_orch_cls.assert_not_called()\n mock_db.close.assert_called_once()\n# #endregion test_inactive_schedule_skips\n\n\n# #region test_schedule_not_found_skips [C:1] [TYPE Function]\ndef test_schedule_not_found_skips():\n \"\"\"Schedule not found → function returns early.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = None\n\n mock_db.query.return_value = q1\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n mock_orch_cls.assert_not_called()\n mock_db.close.assert_called_once()\n# #endregion test_schedule_not_found_skips\n\n\n# #region test_baseline_expired_full_mode [C:1] [TYPE Function]\ndef test_baseline_expired_full_mode():\n \"\"\"execution_mode=\"full\" with expired baseline → trigger_type stays 'scheduled'.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n most_recent = _make_most_recent_run(created_at_delta_days=100) # expired\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n with patch(\n \"src.plugins.translate.scheduler.TranslationEventLog\"\n ) as mock_event_log_cls:\n mock_event_log = MagicMock()\n mock_event_log_cls.return_value = mock_event_log\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n assert mock_run.trigger_type == \"scheduled\"\n mock_event_log.log_event.assert_called_once()\n event_call = mock_event_log.log_event.call_args\n assert event_call.kwargs[\"payload\"][\"reason\"] == \"baseline_expired\"\n mock_db.close.assert_called_once()\n# #endregion test_baseline_expired_full_mode\n\n\n# #region test_execution_error_handled [C:1] [TYPE Function]\ndef test_execution_error_handled():\n \"\"\"execute_run raises → run is marked FAILED, schedule tracking updated, no crash.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = None\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch.execute_run.side_effect = RuntimeError(\"LLM timeout\")\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n assert mock_run.status == \"FAILED\"\n assert \"LLM timeout\" in mock_run.error_message\n assert mock_run.completed_at is not None\n assert schedule.last_run_at is not None\n mock_db.commit.assert_called()\n mock_db.close.assert_called_once()\n# #endregion test_execution_error_handled\n\n# #endregion TestTranslateSchedulerExecution\n" + }, + { + "contract_id": "test_new_key_only_mode", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_execution.py", + "start_line": 50, + "end_line": 101, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_new_key_only_mode [C:1] [TYPE Function]\ndef test_new_key_only_mode():\n \"\"\"execution_mode=\"new_key_only\" with 60-day-old prev run → trigger_type=\"new_key_only\".\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"new_key_only\"\n\n most_recent = _make_most_recent_run(created_at_delta_days=60) # 60 days < 90\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None # no concurrent\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"new_key_only\",\n )\n\n assert mock_run.trigger_type == \"new_key_only\", (\n f\"Expected trigger_type='new_key_only', got '{mock_run.trigger_type}'\"\n )\n mock_orch.start_run.assert_called_once_with(job_id=\"job-1\", is_scheduled=True)\n mock_orch.execute_run.assert_called_once()\n mock_db.close.assert_called_once()\n# #endregion test_new_key_only_mode\n" + }, + { + "contract_id": "test_baseline_expired_fallback", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_execution.py", + "start_line": 104, + "end_line": 167, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_baseline_expired_fallback [C:1] [TYPE Function]\ndef test_baseline_expired_fallback():\n \"\"\"execution_mode=\"new_key_only\" with >90-day-old prev run → trigger_type=\"scheduled\" baseline fallback.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"new_key_only\"\n\n most_recent = _make_most_recent_run(created_at_delta_days=95) # 95 days > 90\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n with patch(\n \"src.plugins.translate.scheduler.TranslationEventLog\"\n ) as mock_event_log_cls:\n mock_event_log = MagicMock()\n mock_event_log_cls.return_value = mock_event_log\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"new_key_only\",\n )\n\n assert mock_run.trigger_type == \"scheduled\", (\n f\"Expected trigger_type='scheduled' (baseline expired), got '{mock_run.trigger_type}'\"\n )\n\n mock_event_log.log_event.assert_called_once()\n event_call = mock_event_log.log_event.call_args\n assert event_call.kwargs[\"job_id\"] == \"job-1\"\n assert event_call.kwargs[\"event_type\"] == \"RUN_STARTED\"\n assert event_call.kwargs[\"payload\"][\"reason\"] == \"baseline_expired\"\n assert event_call.kwargs[\"payload\"][\"age_days\"] == 95\n\n mock_db.close.assert_called_once()\n# #endregion test_baseline_expired_fallback\n" + }, + { + "contract_id": "test_full_mode_default", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_execution.py", + "start_line": 170, + "end_line": 220, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_full_mode_default [C:1] [TYPE Function]\ndef test_full_mode_default():\n \"\"\"execution_mode=\"full\" → trigger_type=\"scheduled\" regardless of baseline age.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n most_recent = _make_most_recent_run(created_at_delta_days=30)\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n assert mock_run.trigger_type == \"scheduled\", (\n f\"Expected trigger_type='scheduled' for full mode, got '{mock_run.trigger_type}'\"\n )\n mock_orch.execute_run.assert_called_once()\n mock_db.close.assert_called_once()\n# #endregion test_full_mode_default\n" + }, + { + "contract_id": "test_inactive_schedule_skips", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_execution.py", + "start_line": 223, + "end_line": 255, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_inactive_schedule_skips [C:1] [TYPE Function]\ndef test_inactive_schedule_skips():\n \"\"\"Inactive schedule → function returns early without executing anything.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = False\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n mock_db.query.return_value = q1\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n mock_orch_cls.assert_not_called()\n mock_db.close.assert_called_once()\n# #endregion test_inactive_schedule_skips\n" + }, + { + "contract_id": "test_schedule_not_found_skips", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_execution.py", + "start_line": 258, + "end_line": 285, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_schedule_not_found_skips [C:1] [TYPE Function]\ndef test_schedule_not_found_skips():\n \"\"\"Schedule not found → function returns early.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = None\n\n mock_db.query.return_value = q1\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n mock_orch_cls.assert_not_called()\n mock_db.close.assert_called_once()\n# #endregion test_schedule_not_found_skips\n" + }, + { + "contract_id": "test_baseline_expired_full_mode", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_execution.py", + "start_line": 288, + "end_line": 344, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_baseline_expired_full_mode [C:1] [TYPE Function]\ndef test_baseline_expired_full_mode():\n \"\"\"execution_mode=\"full\" with expired baseline → trigger_type stays 'scheduled'.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n most_recent = _make_most_recent_run(created_at_delta_days=100) # expired\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n with patch(\n \"src.plugins.translate.scheduler.TranslationEventLog\"\n ) as mock_event_log_cls:\n mock_event_log = MagicMock()\n mock_event_log_cls.return_value = mock_event_log\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n assert mock_run.trigger_type == \"scheduled\"\n mock_event_log.log_event.assert_called_once()\n event_call = mock_event_log.log_event.call_args\n assert event_call.kwargs[\"payload\"][\"reason\"] == \"baseline_expired\"\n mock_db.close.assert_called_once()\n# #endregion test_baseline_expired_full_mode\n" + }, + { + "contract_id": "test_execution_error_handled", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_execution.py", + "start_line": 347, + "end_line": 397, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_execution_error_handled [C:1] [TYPE Function]\ndef test_execution_error_handled():\n \"\"\"execute_run raises → run is marked FAILED, schedule tracking updated, no crash.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = None\n\n q3 = MagicMock()\n q3.filter.return_value.order_by.return_value.first.return_value = None\n\n mock_db.query.side_effect = [q1, q2, q3]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch.execute_run.side_effect = RuntimeError(\"LLM timeout\")\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n assert mock_run.status == \"FAILED\"\n assert \"LLM timeout\" in mock_run.error_message\n assert mock_run.completed_at is not None\n assert schedule.last_run_at is not None\n mock_db.commit.assert_called()\n mock_db.close.assert_called_once()\n# #endregion test_execution_error_handled\n" + }, + { + "contract_id": "TestTranslateSchedulerGuard", + "contract_type": "Module", + "file_path": "backend/tests/test_translate_scheduler_guard.py", + "start_line": 1, + "end_line": 298, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3, + "PURPOSE": "Verify execute_scheduled_translation concurrency guard and stale-PENDING clearance contracts.", + "TEST_EDGE": "multiple_stale_pending_all_cleared — multiple stale PENDING runs → all cleared as FAILED, execution proceeds", + "TEST_INVARIANT": "stale_pending_clearance -> VERIFIED_BY: [test_stale_pending_cleared_and_proceeds, test_multiple_stale_pending_all_cleared]" + }, + "relations": [ + { + "source_id": "TestTranslateSchedulerGuard", + "relation_type": "BINDS_TO", + "target_id": "execute_scheduled_translation", + "target_ref": "[execute_scheduled_translation]" + } + ], + "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", + "body": "# #region TestTranslateSchedulerGuard [C:3] [TYPE Module] [SEMANTICS test, translate, scheduler, guard, concurrency, stale, pending]\n# @BRIEF Verify execute_scheduled_translation concurrency guard and stale-PENDING clearance contracts.\n# @RELATION BINDS_TO -> [execute_scheduled_translation]\n# @TEST_EDGE: concurrent_run_skips — active RUNNING concurrent run → function skips and logs skipped_concurrent event\n# @TEST_EDGE: recent_pending_run_not_stale — recent PENDING run (30 min old) → function skips, not stale\n# @TEST_EDGE: stale_pending_cleared_and_proceeds — stale PENDING run (>1h old) → cleared as FAILED, execution proceeds\n# @TEST_EDGE: multiple_stale_pending_all_cleared — multiple stale PENDING runs → all cleared as FAILED, execution proceeds\n# @TEST_INVARIANT: concurrency_guard -> VERIFIED_BY: [test_concurrent_run_skips, test_recent_pending_run_not_stale]\n# @TEST_INVARIANT: stale_pending_clearance -> VERIFIED_BY: [test_stale_pending_cleared_and_proceeds, test_multiple_stale_pending_all_cleared]\n\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nfrom datetime import UTC, datetime, timedelta\nfrom unittest.mock import MagicMock, patch\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _make_most_recent_run(created_at_delta_days=60):\n \"\"\"Build a MagicMock TranslationRun with given age.\"\"\"\n run = MagicMock()\n run.id = \"prev-run-1\"\n run.job_id = \"job-1\"\n run.status = \"COMPLETED\"\n run.insert_status = \"succeeded\"\n run.created_at = datetime.now(UTC) - timedelta(days=created_at_delta_days)\n return run\n\n\ndef _make_mock_run(run_id=\"run-1\", job_id=\"job-1\"):\n \"\"\"Build a mock TranslationRun returned by start_run.\"\"\"\n run = MagicMock()\n run.id = run_id\n run.job_id = job_id\n run.status = \"PENDING\"\n run.trigger_type = None\n run.total_records = 0\n run.successful_records = 0\n run.failed_records = 0\n run.skipped_records = 0\n run.insert_status = None\n run.superset_execution_id = None\n run.superset_execution_log = None\n run.error_message = None\n return run\n\n\n# ---------------------------------------------------------------------------\n# Tests — concurrency guard and stale PENDING clearance\n# ---------------------------------------------------------------------------\n\n# #region test_concurrent_run_skips [C:1] [TYPE Function]\ndef test_concurrent_run_skips():\n \"\"\"Active RUNNING concurrent run → function skips and logs skipped_concurrent event.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n active_run = MagicMock()\n active_run.id = \"active-run-1\"\n active_run.job_id = \"job-1\"\n active_run.status = \"RUNNING\"\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = active_run\n\n mock_db.query.side_effect = [q1, q2]\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls, patch(\n \"src.plugins.translate.scheduler.TranslationEventLog\"\n ) as mock_event_log_cls:\n mock_event_log = MagicMock()\n mock_event_log_cls.return_value = mock_event_log\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n mock_orch_cls.assert_not_called()\n mock_event_log.log_event.assert_called_once()\n event_call = mock_event_log.log_event.call_args\n assert event_call.kwargs[\"event_type\"] == \"RUN_STARTED\"\n assert event_call.kwargs[\"payload\"][\"reason\"] == \"skipped_concurrent\"\n mock_db.close.assert_called_once()\n# #endregion test_concurrent_run_skips\n\n\n# #region test_recent_pending_run_not_stale [C:1] [TYPE Function]\ndef test_recent_pending_run_not_stale():\n \"\"\"Recent PENDING run (30 min old) → function skips, not stale.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n pending_run = MagicMock()\n pending_run.id = \"pending-run-1\"\n pending_run.job_id = \"job-1\"\n pending_run.status = \"PENDING\"\n pending_run.created_at = datetime.now(UTC) - timedelta(minutes=30)\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = pending_run\n\n mock_db.query.side_effect = [q1, q2]\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls, patch(\n \"src.plugins.translate.scheduler.TranslationEventLog\"\n ) as mock_event_log_cls:\n mock_event_log = MagicMock()\n mock_event_log_cls.return_value = mock_event_log\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n mock_orch_cls.assert_not_called()\n mock_event_log.log_event.assert_called_once()\n event_call = mock_event_log.log_event.call_args\n assert event_call.kwargs[\"event_type\"] == \"RUN_STARTED\"\n assert event_call.kwargs[\"payload\"][\"reason\"] == \"skipped_concurrent\"\n mock_db.close.assert_called_once()\n# #endregion test_recent_pending_run_not_stale\n\n\n# #region test_stale_pending_cleared_and_proceeds [C:1] [TYPE Function]\ndef test_stale_pending_cleared_and_proceeds():\n \"\"\"Stale PENDING run (>1h old) → cleared as FAILED, execution proceeds.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n stale_run = MagicMock()\n stale_run.id = \"stale-run-1\"\n stale_run.job_id = \"job-1\"\n stale_run.status = \"PENDING\"\n stale_run.created_at = datetime.now(UTC) - timedelta(hours=2)\n\n most_recent = _make_most_recent_run(created_at_delta_days=30)\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = stale_run\n\n q3 = MagicMock()\n q3.filter.return_value.all.return_value = [stale_run]\n\n q4 = MagicMock()\n q4.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3, q4]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n assert stale_run.status == \"FAILED\"\n assert \"Stale: concurrency deadlock\" in stale_run.error_message\n assert stale_run.completed_at is not None\n\n mock_orch.start_run.assert_called_once_with(job_id=\"job-1\", is_scheduled=True)\n mock_orch.execute_run.assert_called_once()\n assert mock_db.commit.call_count >= 1\n mock_db.close.assert_called_once()\n# #endregion test_stale_pending_cleared_and_proceeds\n\n\n# #region test_multiple_stale_pending_all_cleared [C:1] [TYPE Function]\ndef test_multiple_stale_pending_all_cleared():\n \"\"\"Multiple stale PENDING runs → all cleared as FAILED, execution proceeds.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n stale_run_a = MagicMock()\n stale_run_a.id = \"stale-run-a\"\n stale_run_a.job_id = \"job-1\"\n stale_run_a.status = \"PENDING\"\n stale_run_a.created_at = datetime.now(UTC) - timedelta(hours=5)\n\n stale_run_b = MagicMock()\n stale_run_b.id = \"stale-run-b\"\n stale_run_b.job_id = \"job-1\"\n stale_run_b.status = \"PENDING\"\n stale_run_b.created_at = datetime.now(UTC) - timedelta(hours=3)\n\n most_recent = _make_most_recent_run(created_at_delta_days=30)\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = stale_run_a\n\n q3 = MagicMock()\n q3.filter.return_value.all.return_value = [stale_run_a, stale_run_b]\n\n q4 = MagicMock()\n q4.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3, q4]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n for sr in [stale_run_a, stale_run_b]:\n assert sr.status == \"FAILED\"\n assert \"Stale: concurrency deadlock\" in sr.error_message\n assert sr.completed_at is not None\n\n mock_orch.start_run.assert_called_once_with(job_id=\"job-1\", is_scheduled=True)\n mock_orch.execute_run.assert_called_once()\n mock_db.close.assert_called_once()\n# #endregion test_multiple_stale_pending_all_cleared\n\n# #endregion TestTranslateSchedulerGuard\n" + }, + { + "contract_id": "test_concurrent_run_skips", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_guard.py", + "start_line": 56, + "end_line": 106, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_concurrent_run_skips [C:1] [TYPE Function]\ndef test_concurrent_run_skips():\n \"\"\"Active RUNNING concurrent run → function skips and logs skipped_concurrent event.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n active_run = MagicMock()\n active_run.id = \"active-run-1\"\n active_run.job_id = \"job-1\"\n active_run.status = \"RUNNING\"\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = active_run\n\n mock_db.query.side_effect = [q1, q2]\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls, patch(\n \"src.plugins.translate.scheduler.TranslationEventLog\"\n ) as mock_event_log_cls:\n mock_event_log = MagicMock()\n mock_event_log_cls.return_value = mock_event_log\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n mock_orch_cls.assert_not_called()\n mock_event_log.log_event.assert_called_once()\n event_call = mock_event_log.log_event.call_args\n assert event_call.kwargs[\"event_type\"] == \"RUN_STARTED\"\n assert event_call.kwargs[\"payload\"][\"reason\"] == \"skipped_concurrent\"\n mock_db.close.assert_called_once()\n# #endregion test_concurrent_run_skips\n" + }, + { + "contract_id": "test_recent_pending_run_not_stale", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_guard.py", + "start_line": 109, + "end_line": 160, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_recent_pending_run_not_stale [C:1] [TYPE Function]\ndef test_recent_pending_run_not_stale():\n \"\"\"Recent PENDING run (30 min old) → function skips, not stale.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n pending_run = MagicMock()\n pending_run.id = \"pending-run-1\"\n pending_run.job_id = \"job-1\"\n pending_run.status = \"PENDING\"\n pending_run.created_at = datetime.now(UTC) - timedelta(minutes=30)\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = pending_run\n\n mock_db.query.side_effect = [q1, q2]\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls, patch(\n \"src.plugins.translate.scheduler.TranslationEventLog\"\n ) as mock_event_log_cls:\n mock_event_log = MagicMock()\n mock_event_log_cls.return_value = mock_event_log\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n mock_orch_cls.assert_not_called()\n mock_event_log.log_event.assert_called_once()\n event_call = mock_event_log.log_event.call_args\n assert event_call.kwargs[\"event_type\"] == \"RUN_STARTED\"\n assert event_call.kwargs[\"payload\"][\"reason\"] == \"skipped_concurrent\"\n mock_db.close.assert_called_once()\n# #endregion test_recent_pending_run_not_stale\n" + }, + { + "contract_id": "test_stale_pending_cleared_and_proceeds", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_guard.py", + "start_line": 163, + "end_line": 225, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_stale_pending_cleared_and_proceeds [C:1] [TYPE Function]\ndef test_stale_pending_cleared_and_proceeds():\n \"\"\"Stale PENDING run (>1h old) → cleared as FAILED, execution proceeds.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n stale_run = MagicMock()\n stale_run.id = \"stale-run-1\"\n stale_run.job_id = \"job-1\"\n stale_run.status = \"PENDING\"\n stale_run.created_at = datetime.now(UTC) - timedelta(hours=2)\n\n most_recent = _make_most_recent_run(created_at_delta_days=30)\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = stale_run\n\n q3 = MagicMock()\n q3.filter.return_value.all.return_value = [stale_run]\n\n q4 = MagicMock()\n q4.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3, q4]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n assert stale_run.status == \"FAILED\"\n assert \"Stale: concurrency deadlock\" in stale_run.error_message\n assert stale_run.completed_at is not None\n\n mock_orch.start_run.assert_called_once_with(job_id=\"job-1\", is_scheduled=True)\n mock_orch.execute_run.assert_called_once()\n assert mock_db.commit.call_count >= 1\n mock_db.close.assert_called_once()\n# #endregion test_stale_pending_cleared_and_proceeds\n" + }, + { + "contract_id": "test_multiple_stale_pending_all_cleared", + "contract_type": "Function", + "file_path": "backend/tests/test_translate_scheduler_guard.py", + "start_line": 228, + "end_line": 296, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "# #region test_multiple_stale_pending_all_cleared [C:1] [TYPE Function]\ndef test_multiple_stale_pending_all_cleared():\n \"\"\"Multiple stale PENDING runs → all cleared as FAILED, execution proceeds.\"\"\"\n from src.plugins.translate.scheduler import execute_scheduled_translation\n\n mock_db = MagicMock()\n mock_session_maker = MagicMock(return_value=mock_db)\n mock_config = MagicMock()\n\n schedule = MagicMock()\n schedule.id = \"sched-1\"\n schedule.job_id = \"job-1\"\n schedule.is_active = True\n schedule.execution_mode = \"full\"\n\n stale_run_a = MagicMock()\n stale_run_a.id = \"stale-run-a\"\n stale_run_a.job_id = \"job-1\"\n stale_run_a.status = \"PENDING\"\n stale_run_a.created_at = datetime.now(UTC) - timedelta(hours=5)\n\n stale_run_b = MagicMock()\n stale_run_b.id = \"stale-run-b\"\n stale_run_b.job_id = \"job-1\"\n stale_run_b.status = \"PENDING\"\n stale_run_b.created_at = datetime.now(UTC) - timedelta(hours=3)\n\n most_recent = _make_most_recent_run(created_at_delta_days=30)\n\n q1 = MagicMock()\n q1.filter.return_value.first.return_value = schedule\n\n q2 = MagicMock()\n q2.filter.return_value.order_by.return_value.first.return_value = stale_run_a\n\n q3 = MagicMock()\n q3.filter.return_value.all.return_value = [stale_run_a, stale_run_b]\n\n q4 = MagicMock()\n q4.filter.return_value.order_by.return_value.first.return_value = most_recent\n\n mock_db.query.side_effect = [q1, q2, q3, q4]\n\n mock_run = _make_mock_run()\n\n with patch(\n \"src.plugins.translate.orchestrator.TranslationOrchestrator\"\n ) as mock_orch_cls:\n mock_orch = MagicMock()\n mock_orch.start_run.return_value = mock_run\n mock_orch_cls.return_value = mock_orch\n\n execute_scheduled_translation(\n schedule_id=\"sched-1\",\n job_id=\"job-1\",\n db_session_maker=mock_session_maker,\n config_manager=mock_config,\n execution_mode=\"full\",\n )\n\n for sr in [stale_run_a, stale_run_b]:\n assert sr.status == \"FAILED\"\n assert \"Stale: concurrency deadlock\" in sr.error_message\n assert sr.completed_at is not None\n\n mock_orch.start_run.assert_called_once_with(job_id=\"job-1\", is_scheduled=True)\n mock_orch.execute_run.assert_called_once()\n mock_db.close.assert_called_once()\n# #endregion test_multiple_stale_pending_all_cleared\n" + }, { "contract_id": "build", "contract_type": "Module", @@ -107731,11 +76474,11 @@ "body": "# [DEF:ADR-0006:ADR]\n# @STATUS ACTIVE\n# @PURPOSE Define the frontend architecture for ss-tools: Svelte 5 runes reactivity model, SvelteKit routing, component composition rules, state management patterns, API client conventions, and UX contract expectations for C4/C5 components.\n# @RELATION DEPENDS_ON -> [ADR-0001:ADR]\n# @RELATION DEPENDS_ON -> [ADR-0002:ADR]\n# @RATIONALE Svelte 5 introduces a fundamentally different reactivity model (runes: `$state`, `$derived`, `$effect`, `$props`) compared to Svelte 4's `$:` reactive statements. This ADR locks in the Svelte 5 runes approach and prevents backsliding into legacy patterns when new developers or AI agents contribute code.\n# @RATIONALE SvelteKit with static adapter (`@sveltejs/adapter-static`) was chosen over SvelteKit SSR because: (a) ss-tools is a Docker‑deployed SPA behind nginx — server‑side rendering provides no latency benefit, (b) static SPA simplifies Docker deployment (no Node.js server needed, just nginx serving static files), (c) all data is fetched via REST API, not server‑side load functions.\n# @RATIONALE Svelte 5 runes (`$state`, `$derived`, `$effect`) are mandated over Svelte 4 `$:` reactivity and `writable` stores because: (a) runes are the forward path — Svelte 4 patterns are deprecated, (b) runes provide fine‑grained reactivity without subscription boilerplate, (c) `$state` can be used outside `.svelte` files in `.svelte.js` modules, enabling reusable reactive logic.\n# @REJECTED React/Next.js — rejected by ADR-0003 (technology stack independence from Superset). Svelte 5 was chosen for its compile‑time approach, smaller bundle size, and superior DX for this project's scale.\n# @REJECTED Svelte 4 legacy patterns (`$:`, `writable`/`readable` stores) — rejected because Svelte 5 runes are the actively maintained reactivity model. Mixing two models creates confusion and potential reactivity bugs.\n# @REJECTED Server‑Side Rendering (SSR) with SvelteKit — rejected because the data is entirely API‑driven (no server‑side page data loading), and SSR adds deployment complexity (Node.js process) without latency benefit for authenticated SPA users.\n\n## Decision\n\n### Technology Stack\n\n| Layer | Choice | Version |\n|-------|--------|---------|\n| Framework | SvelteKit | 2.x |\n| UI Library | Svelte | 5.x (runes mode) |\n| Build | Vite | 7.x |\n| Styling | Tailwind CSS | 3.x |\n| Adapter | @sveltejs/adapter-static | 3.x (SPA mode) |\n| Testing | vitest + @testing-library/svelte | 4.x / 5.x |\n\n### Reactivity Model (Svelte 5 Runes)\n\n```\nLegacy (FORBIDDEN) Svelte 5 Runes (REQUIRED)\n──────────────────────── ──────────────────────────\nlet count = 0; let count = $state(0);\n$: doubled = count * 2; let doubled = $derived(count * 2);\n$: { /* side effect */ } $effect(() => { /* side effect */ });\nexport let name; let { name } = $props();\nimport { writable } from ... ❌ stores are .svelte.js modules with $state\n```\n\n### State Management Pattern\n\nThree tiers of state, strictly separated:\n\n1. **Component‑local state**: `$state()` inside `.svelte` files. Never exported.\n2. **Shared reactive state**: `.svelte.js` modules exporting `$state` objects. Imported by components.\n ```js\n // frontend/src/lib/stores/dashboard.svelte.js\n export const dashboardState = $state({\n dashboards: [],\n selectedId: null,\n isLoading: false\n });\n ```\n3. **Server state**: Fetched via API client, held in `$state` variables locally. No global cache — each route fetches what it needs.\n\n### API Client Convention\n\nAPI client modules live under `frontend/src/lib/api/`. Each module wraps `fetch` calls to the FastAPI backend, attaches the JWT access token from the auth store, and normalises errors into a typed `ApiError`. Contract annotations follow the rules defined in the `semantics-core` and `semantics-contracts` skills.\n\n### Component Complexity & UX Contracts\n\nSvelte components with side effects (API calls, WebSocket subscriptions, file operations) MUST carry UX contract tags as defined by the `semantics-frontend` skill (`.opencode/skills/semantics-frontend/SKILL.md`). The skill is the canonical source for tag inventory, syntax, and per‑complexity requirements.\n\n### Component File Structure\n\n```\nfrontend/src/lib/components/\n├── DashboardGrid.svelte # C4: complex data grid with WebSocket updates\n├── MappingTable.svelte # C3: migration mapping table\n├── PluginExecutionCard.svelte # C4: plugin runner with progress feedback\n├── RoleBadge.svelte # C1: simple role display\n└── ...\n```\n\n### Routing\n\n- SvelteKit file‑based routing under `frontend/src/routes/`\n- Protected routes check auth in `+layout.svelte` (redirect to `/login` if no token)\n- Route structure: `/` (dashboard list), `/envs/[id]` (environment detail), `/migration` (migration wizard), `/plugins` (plugin management), `/admin` (user/role management)\n\n# [/DEF:ADR-0006:ADR]\n" }, { - "contract_id": "handleValidate", + "contract_id": "handleValidate:Function", "contract_type": "Function", "file_path": "frontend/src/components/DashboardGrid.svelte", - "start_line": 44, - "end_line": 71, + "start_line": 45, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -107753,15 +76496,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleValidate:Function]\n /**\n * @purpose Triggers dashboard validation task.\n */\n async function handleValidate(dashboard: DashboardMetadata) {\n if (validatingIds.has(dashboard.id)) return;\n\n validatingIds.add(dashboard.id);\n validatingIds = new Set(validatingIds);\n\n try {\n await api.postApi(\"/tasks\", {\n plugin_id: \"llm_dashboard_validation\",\n params: {\n dashboard_id: dashboard.id.toString(),\n environment_id: environmentId,\n },\n });\n\n toast(\"Validation task started\", \"success\");\n } catch (e: any) {\n toast(e.message || \"Validation failed to start\", \"error\");\n } finally {\n validatingIds.delete(dashboard.id);\n validatingIds = new Set(validatingIds);\n }\n }\n // [/DEF:handleValidate:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleValidate:Function [TYPE Function]\n /**\n * @purpose Triggers dashboard validation task.\n */\n async function handleValidate(dashboard: DashboardMetadata) {\n if (validatingIds.has(dashboard.id)) return;\n\n validatingIds.add(dashboard.id);\n validatingIds = new Set(validatingIds);\n\n try {\n await api.postApi(\"/tasks\", {\n plugin_id: \"llm_dashboard_validation\",\n params: {\n dashboard_id: dashboard.id.toString(),\n environment_id: environmentId,\n },\n });\n\n toast(\"Validation task started\", \"success\");\n } catch (e: any) {\n toast(e.message || \"Validation failed to start\", \"error\");\n } finally {\n validatingIds.delete(dashboard.id);\n validatingIds = new Set(validatingIds);\n }\n }\n // #endregion handleValidate:Function\n" }, { - "contract_id": "openGit", + "contract_id": "openGit:Function", "contract_type": "Function", "file_path": "frontend/src/components/DashboardGrid.svelte", - "start_line": 171, - "end_line": 180, + "start_line": 172, + "end_line": 181, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -107779,116 +76522,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:openGit:Function]\n /**\n * @purpose Opens the Git management modal for a dashboard.\n */\n function openGit(dashboard: DashboardMetadata) {\n gitDashboardId = String(dashboard.slug || dashboard.id);\n gitDashboardTitle = dashboard.title;\n showGitManager = true;\n }\n // [/DEF:openGit:Function]\n" + "anchor_syntax": "region", + "body": " // #region openGit:Function [TYPE Function]\n /**\n * @purpose Opens the Git management modal for a dashboard.\n */\n function openGit(dashboard: DashboardMetadata) {\n gitDashboardId = String(dashboard.slug || dashboard.id);\n gitDashboardTitle = dashboard.title;\n showGitManager = true;\n }\n // #endregion openGit:Function\n" }, { - "contract_id": "DynamicForm", - "contract_type": "Component", - "file_path": "frontend/src/components/DynamicForm.svelte", - "start_line": 1, - "end_line": 90, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "LAYER": "UI", - "PROPS": "", - "PURPOSE": "Generates a form dynamically based on a JSON schema.", - "SEMANTICS": [ - "form", - "schema", - "dynamic", - "json-schema" - ] - }, - "relations": [ - { - "source_id": "DynamicForm", - "relation_type": "BINDS_TO", - "target_id": "onsubmit callback", - "target_ref": "onsubmit callback" - } - ], - "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "unknown_tag", - "tag": "PROPS", - "message": "@PROPS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n
{ event.preventDefault(); handleSubmit(); }} class=\"space-y-4\">\n {#if schema && schema.properties}\n {#each Object.entries(schema.properties) as [key, prop]}\n
\n \n {#if prop.type === 'string'}\n \n {:else if prop.type === 'number' || prop.type === 'integer'}\n \n {:else if prop.type === 'boolean'}\n \n {/if}\n
\n {/each}\n \n {/if}\n
\n\n\n\n" - }, - { - "contract_id": "initializeForm", + "contract_id": "initializeForm:Function", "contract_type": "Function", "file_path": "frontend/src/components/DynamicForm.svelte", - "start_line": 33, - "end_line": 46, + "start_line": 35, + "end_line": 48, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -107926,123 +76568,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:initializeForm:Function]\n /**\n * @purpose Initialize form data with default values from the schema.\n * @pre schema is provided and contains properties.\n * @post formData is initialized with default values or empty strings.\n */\n function initializeForm() {\n if (schema && schema.properties) {\n for (const key in schema.properties) {\n formData[key] = schema.properties[key].default || '';\n }\n }\n }\n // [/DEF:initializeForm:Function]\n" - }, - { - "contract_id": "EnvSelector", - "contract_type": "Component", - "file_path": "frontend/src/components/EnvSelector.svelte", - "start_line": 1, - "end_line": 59, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Source and target environments must be selectable from the list of configured environments.", - "LAYER": "Feature", - "PURPOSE": "Provides a UI component for selecting source and target environments.", - "SEMANTICS": [ - "environment", - "selector", - "dropdown", - "migration" - ] - }, - "relations": [ - { - "source_id": "EnvSelector", - "relation_type": "BINDS_TO", - "target_id": "environments store", - "target_ref": "environments store" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "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": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n
\n \n \n \n {#each environments as env}\n \n {/each}\n \n
\n\n\n\n\n" + "anchor_syntax": "region", + "body": " // #region initializeForm:Function [TYPE Function]\n /**\n * @purpose Initialize form data with default values from the schema.\n * @pre schema is provided and contains properties.\n * @post formData is initialized with default values or empty strings.\n */\n function initializeForm() {\n if (schema && schema.properties) {\n for (const key in schema.properties) {\n formData[key] = schema.properties[key].default || '';\n }\n }\n }\n // #endregion initializeForm:Function\n" }, { "contract_id": "Footer", "contract_type": "Component", "file_path": "frontend/src/components/Footer.svelte", "start_line": 1, - "end_line": 11, - "tier": "TIER_1", - "complexity": 1, + "end_line": 13, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "COMPLEXITY": "1", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Displays the application footer with copyright information.", "SEMANTICS": [ @@ -108051,103 +76589,15 @@ "copyright" ] }, - "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n
\n © 2025 Superset Tools. All rights reserved.\n
\n\n" - }, - { - "contract_id": "MappingTable", - "contract_type": "Component", - "file_path": "frontend/src/components/MappingTable.svelte", - "start_line": 1, - "end_line": 106, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Each source database can be mapped to one target database.", - "LAYER": "Feature", - "PURPOSE": "Displays and allows editing of database mappings.", - "SEMANTICS": [ - "mapping", - "table", - "database", - "editor" - ] - }, "relations": [ { - "source_id": "MappingTable", - "relation_type": "BINDS_TO", - "target_id": "mappings state", - "target_ref": "mappings state" + "source_id": "Footer", + "relation_type": "DEPENDS_ON", + "target_id": "RootLayout", + "target_ref": "[RootLayout]" } ], "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, { "code": "tag_not_for_contract_type", "tag": "LAYER", @@ -108164,18 +76614,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -108196,31 +76637,31 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C1", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n
\n \n \n \n \n \n \n \n \n \n {#each sourceDatabases as sDb}\n {@const mapping = mappings.find(m => m.source_db_uuid === sDb.uuid)}\n {@const suggestion = getSuggestion(sDb.uuid)}\n \n \n \n \n \n {/each}\n \n
{$t.dashboard?.source_database }{$t.dashboard?.target_database }{$t.dashboard?.status }
\n {sDb.database_name}\n \n updateMapping(sDb.uuid, (e.target as HTMLSelectElement).value)}\n >\n \n {#each targetDatabases as tDb}\n \n {/each}\n \n \n {#if mapping}\n {$t.dashboard?.saved }\n {:else if suggestion}\n {$t.dashboard?.suggested } ({Math.round(suggestion.confidence * 100)}%)\n {:else}\n {$t.dashboard?.not_mapped }\n {/if}\n
\n
\n\n\n\n\n" + "anchor_syntax": "region", + "body": "\n\n\n\n\n
\n © 2025 Superset Tools. All rights reserved.\n
\n\n" }, { - "contract_id": "updateMapping", + "contract_id": "updateMapping:Function", "contract_type": "Function", "file_path": "frontend/src/components/MappingTable.svelte", - "start_line": 27, - "end_line": 45, + "start_line": 29, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -108258,15 +76699,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:updateMapping:Function]\n /**\n * @purpose Updates a mapping for a specific source database.\n * @pre sourceUuid and targetUuid are provided.\n * @post Parent callback receives normalized mapping payload.\n */\n function updateMapping(sourceUuid: string, targetUuid: string) {\n const sDb = sourceDatabases.find(d => d.uuid === sourceUuid);\n const tDb = targetDatabases.find(d => d.uuid === targetUuid);\n \n onupdate({ \n sourceUuid, \n targetUuid,\n sourceName: sDb?.database_name || \"\",\n targetName: tDb?.database_name || \"\",\n engine: sDb?.engine || \"\"\n });\n }\n // [/DEF:updateMapping:Function]\n" + "anchor_syntax": "region", + "body": " // #region updateMapping:Function [TYPE Function]\n /**\n * @purpose Updates a mapping for a specific source database.\n * @pre sourceUuid and targetUuid are provided.\n * @post Parent callback receives normalized mapping payload.\n */\n function updateMapping(sourceUuid: string, targetUuid: string) {\n const sDb = sourceDatabases.find(d => d.uuid === sourceUuid);\n const tDb = targetDatabases.find(d => d.uuid === targetUuid);\n \n onupdate({ \n sourceUuid, \n targetUuid,\n sourceName: sDb?.database_name || \"\",\n targetName: tDb?.database_name || \"\",\n engine: sDb?.engine || \"\"\n });\n }\n // #endregion updateMapping:Function\n" }, { - "contract_id": "getSuggestion", + "contract_id": "getSuggestion:Function", "contract_type": "Function", "file_path": "frontend/src/components/MappingTable.svelte", - "start_line": 47, - "end_line": 56, + "start_line": 49, + "end_line": 58, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -108304,18 +76745,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:getSuggestion:Function]\n /**\n * @purpose Finds a suggestion for a source database.\n * @pre sourceUuid is provided.\n * @post Returns matching suggestion object or undefined.\n */\n function getSuggestion(sourceUuid: string) {\n return suggestions.find(s => s.source_db_uuid === sourceUuid);\n }\n // [/DEF:getSuggestion:Function]\n" + "anchor_syntax": "region", + "body": " // #region getSuggestion:Function [TYPE Function]\n /**\n * @purpose Finds a suggestion for a source database.\n * @pre sourceUuid is provided.\n * @post Returns matching suggestion object or undefined.\n */\n function getSuggestion(sourceUuid: string) {\n return suggestions.find(s => s.source_db_uuid === sourceUuid);\n }\n // #endregion getSuggestion:Function\n" }, { "contract_id": "MissingMappingModal", "contract_type": "Component", "file_path": "frontend/src/components/MissingMappingModal.svelte", "start_line": 1, - "end_line": 116, - "tier": "TIER_1", - "complexity": 1, + "end_line": 118, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": 3, "INVARIANT": "Modal blocks migration progress until resolved or cancelled.", "LAYER": "Feature", "PURPOSE": "Prompts the user to provide a database mapping when one is missing during migration.", @@ -108344,9 +76786,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C1", + "message": "@INVARIANT is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -108366,18 +76808,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -108398,31 +76831,31 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C1", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } } ], - "anchor_syntax": "def", - "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 Missing Database Mapping\n

\n
\n

\n The database {sourceDbName} is used in the assets being migrated but has no mapping to the target environment. Please select a target database.\n

\n
\n
\n
\n\n
\n \n \n {#each targetDatabases as tDb}\n \n {/each}\n \n
\n\n
\n \n Apply & Continue\n \n \n Cancel Migration\n \n
\n
\n
\n
\n{/if}\n\n\n\n\n" + "anchor_syntax": "region", + "body": "\n\n\n\n\n\n\n\n{#if show}\n
\n
\n
\n\n \n\n
\n
\n
\n \n \n \n
\n
\n

\n Missing Database Mapping\n

\n
\n

\n The database {sourceDbName} is used in the assets being migrated but has no mapping to the target environment. Please select a target database.\n

\n
\n
\n
\n\n
\n \n \n {#each targetDatabases as tDb}\n \n {/each}\n \n
\n\n
\n \n Apply & Continue\n \n \n Cancel Migration\n \n
\n
\n
\n
\n{/if}\n\n\n\n\n" }, { - "contract_id": "resolve", + "contract_id": "resolve:Function", "contract_type": "Function", "file_path": "frontend/src/components/MissingMappingModal.svelte", - "start_line": 27, - "end_line": 40, + "start_line": 29, + "end_line": 42, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -108460,15 +76893,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:resolve:Function]\n // @PURPOSE: Resolves the missing mapping via callback prop.\n // @PRE: selectedTargetUuid must be set.\n // @POST: Parent callback receives mapping payload and modal closes.\n function resolve() {\n if (!selectedTargetUuid) return;\n onresolve({\n sourceDbUuid,\n targetDbUuid: selectedTargetUuid,\n targetDbName: targetDatabases.find(d => d.uuid === selectedTargetUuid)?.database_name\n });\n show = false;\n }\n // [/DEF:resolve:Function]\n" + "anchor_syntax": "region", + "body": " // #region resolve:Function [TYPE Function]\n // @PURPOSE: Resolves the missing mapping via callback prop.\n // @PRE: selectedTargetUuid must be set.\n // @POST: Parent callback receives mapping payload and modal closes.\n function resolve() {\n if (!selectedTargetUuid) return;\n onresolve({\n sourceDbUuid,\n targetDbUuid: selectedTargetUuid,\n targetDbName: targetDatabases.find(d => d.uuid === selectedTargetUuid)?.database_name\n });\n show = false;\n }\n // #endregion resolve:Function\n" }, { - "contract_id": "cancel", + "contract_id": "cancel:Function", "contract_type": "Function", "file_path": "frontend/src/components/MissingMappingModal.svelte", - "start_line": 42, - "end_line": 50, + "start_line": 44, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -108506,19 +76939,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:cancel:Function]\n // @PURPOSE: Cancels the mapping resolution modal.\n // @PRE: Modal is open.\n // @POST: Parent cancel callback is invoked and modal is hidden.\n function cancel() {\n oncancel();\n show = false;\n }\n // [/DEF:cancel:Function]\n" + "anchor_syntax": "region", + "body": " // #region cancel:Function [TYPE Function]\n // @PURPOSE: Cancels the mapping resolution modal.\n // @PRE: Modal is open.\n // @POST: Parent cancel callback is invoked and modal is hidden.\n function cancel() {\n oncancel();\n show = false;\n }\n // #endregion cancel:Function\n" }, { "contract_id": "Navbar", "contract_type": "Component", "file_path": "frontend/src/components/Navbar.svelte", "start_line": 1, - "end_line": 89, + "end_line": 90, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Main navigation bar for the application.", "SEMANTICS": [ @@ -108596,18 +77029,19 @@ } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n
\n \n Superset Tools\n \n \n
\n\n" + "anchor_syntax": "region", + "body": "\n\n\n\n\n\n
\n \n Superset Tools\n \n \n
\n\n" }, { "contract_id": "PasswordPrompt", "contract_type": "Component", "file_path": "frontend/src/components/PasswordPrompt.svelte", "start_line": 1, - "end_line": 182, - "tier": "TIER_1", - "complexity": 1, + "end_line": 184, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "A modal component to prompt the user for database passwords when a migration task is paused.", "SEMANTICS": [ @@ -108639,221 +77073,6 @@ } ], "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "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" - } - } - ], - "anchor_syntax": "def", - "body": "\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", - "contract_type": "Function", - "file_path": "frontend/src/components/PasswordPrompt.svelte", - "start_line": 22, - "end_line": 40, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Parent resume callback receives passwords payload.", - "PRE": "All database passwords must be entered.", - "PURPOSE": "Validates and forwards passwords to resume the task." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " // [DEF:handleSubmit:Function]\n // @PURPOSE: Validates and forwards passwords to resume the task.\n // @PRE: All database passwords must be entered.\n // @POST: Parent resume callback receives passwords payload.\n function handleSubmit() {\n if (submitting) return;\n\n // Validate all passwords entered\n const missing = databases.filter((db) => !passwords[db]);\n if (missing.length > 0) {\n alert(`Please enter passwords for: ${missing.join(\", \")}`);\n return;\n }\n\n submitting = true;\n onresume({ passwords });\n // Reset submitting state is handled by parent or on close\n }\n // [/DEF:handleSubmit:Function]\n" - }, - { - "contract_id": "handleCancel", - "contract_type": "Function", - "file_path": "frontend/src/components/PasswordPrompt.svelte", - "start_line": 42, - "end_line": 50, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "POST": "Parent cancel callback is invoked and show is set to false.", - "PRE": "Modal is open.", - "PURPOSE": "Cancels the password prompt." - }, - "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": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Function' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Function" - } - } - ], - "anchor_syntax": "def", - "body": " // [DEF:handleCancel:Function]\n // @PURPOSE: Cancels the password prompt.\n // @PRE: Modal is open.\n // @POST: Parent cancel callback is invoked and show is set to false.\n function handleCancel() {\n oncancel();\n show = false;\n }\n // [/DEF:handleCancel:Function]\n" - }, - { - "contract_id": "DashboardGrid", - "contract_type": "Component", - "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 1, - "end_line": 697, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Selected IDs must be a subset of available dashboards.", - "LAYER": "Component", - "PURPOSE": "Displays a grid of dashboards with selection and pagination.", - "SEMANTICS": [ - "dashboard", - "grid", - "selection", - "pagination" - ] - }, - "relations": [ - { - "source_id": "DashboardGrid", - "relation_type": "USED_BY", - "target_id": "frontend/src/routes/migration/+page.svelte", - "target_ref": "frontend/src/routes/migration/+page.svelte" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Component" - } - }, { "code": "tag_not_for_contract_type", "tag": "LAYER", @@ -108911,7 +77130,7 @@ { "code": "invalid_relation_predicate", "tag": "RELATION", - "message": "Predicate USED_BY is not in allowed_predicates", + "message": "Predicate USES is not in allowed_predicates", "detail": { "allowed": [ "DEPENDS_ON", @@ -108923,19 +77142,111 @@ "CALLED_BY", "VERIFIES" ], - "predicate": "USED_BY" + "predicate": "USES" } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n
\n \n
\n \n
\n\n {#if selectedIds.length > 0}\n
\n {#if !repositoriesOnly}\n \n {$t.git?.manage_selected}\n \n \n {$t.git?.init_repo || \"Инициализировать Git-репозиторий\"}\n \n {/if}\n \n {#if !repositoriesOnly}\n \n {/if}\n \n \n {#if repositoriesOnly}\n \n {/if}\n \n {$t.git?.selected_count.replace(\n \"{count}\",\n String(selectedIds.length),\n )}\n \n
\n {/if}\n\n \n
\n \n \n \n \n handleSort(\"title\")}\n >\n {$t.dashboard.title}\n {sortColumn === \"title\"\n ? sortDirection === \"asc\"\n ? \"↑\"\n : \"↓\"\n : \"\"}\n \n handleSort(\"last_modified\")}\n >\n {$t.dashboard.last_modified}\n {sortColumn === \"last_modified\"\n ? sortDirection === \"asc\"\n ? \"↑\"\n : \"↓\"\n : \"\"}\n \n handleSort(\"status\")}\n >\n {$t.dashboard.status}\n {sortColumn === \"status\"\n ? sortDirection === \"asc\"\n ? \"↑\"\n : \"↓\"\n : \"\"}\n \n \n \n \n {#each paginatedDashboards as dashboard (dashboard.id)}\n \n \n \n \n \n {/each}\n \n
\n \n handleSelectAll((e.target as HTMLInputElement).checked)}\n class=\"h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500\"\n />\n
\n \n handleSelectionChange(\n dashboard.id,\n (e.target as HTMLInputElement).checked,\n )}\n class=\"h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500\"\n />\n \n openGitManagerForDashboard(dashboard)}\n >\n {dashboard.title}\n \n {new Date(dashboard.last_modified).toLocaleDateString()}\n \n \n {getStatusLabel(dashboard)}\n \n
\n
\n\n \n
\n
\n {($t.dashboard?.showing )\n .replace(\"{start}\", (currentPage * pageSize + 1).toString())\n .replace(\n \"{end}\",\n Math.min(\n (currentPage + 1) * pageSize,\n sortedDashboards.length,\n ).toString(),\n )\n .replace(\"{total}\", sortedDashboards.length.toString())}\n
\n
\n goToPage(currentPage - 1)}\n >\n {$t.dashboard.previous}\n \n = totalPages - 1}\n onclick={() => goToPage(currentPage + 1)}\n >\n {$t.dashboard.next}\n \n
\n
\n
\n\n{#if showGitManager && gitDashboardId}\n \n{/if}\n\n\n\n\n" + "anchor_syntax": "region", + "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": "handleSort", + "contract_id": "handleSubmit:Function", + "contract_type": "Function", + "file_path": "frontend/src/components/PasswordPrompt.svelte", + "start_line": 24, + "end_line": 42, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Parent resume callback receives passwords payload.", + "PRE": "All database passwords must be entered.", + "PURPOSE": "Validates and forwards passwords to resume the task." + }, + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "body": " // #region handleSubmit:Function [TYPE Function]\n // @PURPOSE: Validates and forwards passwords to resume the task.\n // @PRE: All database passwords must be entered.\n // @POST: Parent resume callback receives passwords payload.\n function handleSubmit() {\n if (submitting) return;\n\n // Validate all passwords entered\n const missing = databases.filter((db) => !passwords[db]);\n if (missing.length > 0) {\n alert(`Please enter passwords for: ${missing.join(\", \")}`);\n return;\n }\n\n submitting = true;\n onresume({ passwords });\n // Reset submitting state is handled by parent or on close\n }\n // #endregion handleSubmit:Function\n" + }, + { + "contract_id": "handleCancel:Function", + "contract_type": "Function", + "file_path": "frontend/src/components/PasswordPrompt.svelte", + "start_line": 44, + "end_line": 52, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Parent cancel callback is invoked and show is set to false.", + "PRE": "Modal is open.", + "PURPOSE": "Cancels the password prompt." + }, + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "body": " // #region handleCancel:Function [TYPE Function]\n // @PURPOSE: Cancels the password prompt.\n // @PRE: Modal is open.\n // @POST: Parent cancel callback is invoked and show is set to false.\n function handleCancel() {\n oncancel();\n show = false;\n }\n // #endregion handleCancel:Function\n" + }, + { + "contract_id": "handleSort:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 99, - "end_line": 111, + "start_line": 100, + "end_line": 112, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -108973,15 +77284,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleSort:Function]\n // @PURPOSE: Toggles sort direction or changes sort column.\n // @PRE: column name is provided.\n // @POST: sortColumn and sortDirection state updated.\n function handleSort(column: keyof DashboardMetadata) {\n if (sortColumn === column) {\n sortDirection = sortDirection === \"asc\" ? \"desc\" : \"asc\";\n } else {\n sortColumn = column;\n sortDirection = \"asc\";\n }\n }\n // [/DEF:handleSort:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleSort:Function [TYPE Function]\n // @PURPOSE: Toggles sort direction or changes sort column.\n // @PRE: column name is provided.\n // @POST: sortColumn and sortDirection state updated.\n function handleSort(column: keyof DashboardMetadata) {\n if (sortColumn === column) {\n sortDirection = sortDirection === \"asc\" ? \"desc\" : \"asc\";\n } else {\n sortColumn = column;\n sortDirection = \"asc\";\n }\n }\n // #endregion handleSort:Function\n" }, { - "contract_id": "handleSelectionChange", + "contract_id": "handleSelectionChange:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 113, - "end_line": 126, + "start_line": 114, + "end_line": 127, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109019,15 +77330,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleSelectionChange:Function]\n // @PURPOSE: Handles individual checkbox changes.\n // @PRE: dashboard ID and checked status provided.\n // @POST: selectedIds array updated.\n function handleSelectionChange(id: number, checked: boolean) {\n let newSelected = [...selectedIds];\n if (checked) {\n if (!newSelected.includes(id)) newSelected.push(id);\n } else {\n newSelected = newSelected.filter((sid) => sid !== id);\n }\n selectedIds = newSelected;\n }\n // [/DEF:handleSelectionChange:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleSelectionChange:Function [TYPE Function]\n // @PURPOSE: Handles individual checkbox changes.\n // @PRE: dashboard ID and checked status provided.\n // @POST: selectedIds array updated.\n function handleSelectionChange(id: number, checked: boolean) {\n let newSelected = [...selectedIds];\n if (checked) {\n if (!newSelected.includes(id)) newSelected.push(id);\n } else {\n newSelected = newSelected.filter((sid) => sid !== id);\n }\n selectedIds = newSelected;\n }\n // #endregion handleSelectionChange:Function\n" }, { - "contract_id": "handleSelectAll", + "contract_id": "handleSelectAll:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 128, - "end_line": 145, + "start_line": 129, + "end_line": 146, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109065,15 +77376,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleSelectAll:Function]\n // @PURPOSE: Handles select all checkbox.\n // @PRE: checked status provided.\n // @POST: selectedIds array updated for all paginated items.\n function handleSelectAll(checked: boolean) {\n let newSelected = [...selectedIds];\n if (checked) {\n paginatedDashboards.forEach((d) => {\n if (!newSelected.includes(d.id)) newSelected.push(d.id);\n });\n } else {\n paginatedDashboards.forEach((d) => {\n newSelected = newSelected.filter((sid) => sid !== d.id);\n });\n }\n selectedIds = newSelected;\n }\n // [/DEF:handleSelectAll:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleSelectAll:Function [TYPE Function]\n // @PURPOSE: Handles select all checkbox.\n // @PRE: checked status provided.\n // @POST: selectedIds array updated for all paginated items.\n function handleSelectAll(checked: boolean) {\n let newSelected = [...selectedIds];\n if (checked) {\n paginatedDashboards.forEach((d) => {\n if (!newSelected.includes(d.id)) newSelected.push(d.id);\n });\n } else {\n paginatedDashboards.forEach((d) => {\n newSelected = newSelected.filter((sid) => sid !== d.id);\n });\n }\n selectedIds = newSelected;\n }\n // #endregion handleSelectAll:Function\n" }, { - "contract_id": "goToPage", + "contract_id": "goToPage:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 147, - "end_line": 156, + "start_line": 148, + "end_line": 157, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109111,15 +77422,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:goToPage:Function]\n // @PURPOSE: Changes current page.\n // @PRE: page index is provided.\n // @POST: currentPage state updated if within valid range.\n function goToPage(page: number) {\n if (page >= 0 && page < totalPages) {\n currentPage = page;\n }\n }\n // [/DEF:goToPage:Function]\n" + "anchor_syntax": "region", + "body": " // #region goToPage:Function [TYPE Function]\n // @PURPOSE: Changes current page.\n // @PRE: page index is provided.\n // @POST: currentPage state updated if within valid range.\n function goToPage(page: number) {\n if (page >= 0 && page < totalPages) {\n currentPage = page;\n }\n }\n // #endregion goToPage:Function\n" }, { - "contract_id": "getRepositoryStatusToken", + "contract_id": "getRepositoryStatusToken:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 158, - "end_line": 167, + "start_line": 159, + "end_line": 168, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109157,15 +77468,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:getRepositoryStatusToken:Function]\n /**\n * @purpose Returns normalized repository status token for a dashboard.\n * @pre Dashboard exists.\n * @post Returns one of loading|no_repo|synced|changes|behind_remote|ahead_remote|diverged|error.\n */\n function getRepositoryStatusToken(dashboardId: number): string {\n return repositoryStatusByDashboardId[dashboardId] || \"loading\";\n }\n // [/DEF:getRepositoryStatusToken:Function]\n" + "anchor_syntax": "region", + "body": " // #region getRepositoryStatusToken:Function [TYPE Function]\n /**\n * @purpose Returns normalized repository status token for a dashboard.\n * @pre Dashboard exists.\n * @post Returns one of loading|no_repo|synced|changes|behind_remote|ahead_remote|diverged|error.\n */\n function getRepositoryStatusToken(dashboardId: number): string {\n return repositoryStatusByDashboardId[dashboardId] || \"loading\";\n }\n // #endregion getRepositoryStatusToken:Function\n" }, { - "contract_id": "isRepositoryReady", + "contract_id": "isRepositoryReady:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 169, - "end_line": 175, + "start_line": 170, + "end_line": 176, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109183,15 +77494,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:isRepositoryReady:Function]\n // @PURPOSE: Determines whether git actions can run for a dashboard.\n function isRepositoryReady(dashboardId: number): boolean {\n const token = getRepositoryStatusToken(dashboardId);\n return token !== \"loading\" && token !== \"no_repo\" && token !== \"error\";\n }\n // [/DEF:isRepositoryReady:Function]\n" + "anchor_syntax": "region", + "body": " // #region isRepositoryReady:Function [TYPE Function]\n // @PURPOSE: Determines whether git actions can run for a dashboard.\n function isRepositoryReady(dashboardId: number): boolean {\n const token = getRepositoryStatusToken(dashboardId);\n return token !== \"loading\" && token !== \"no_repo\" && token !== \"error\";\n }\n // #endregion isRepositoryReady:Function\n" }, { - "contract_id": "invalidateRepositoryStatuses", + "contract_id": "invalidateRepositoryStatuses:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 177, - "end_line": 187, + "start_line": 178, + "end_line": 188, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109209,15 +77520,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:invalidateRepositoryStatuses:Function]\n // @PURPOSE: Marks dashboard statuses as loading so they are refetched.\n function invalidateRepositoryStatuses(dashboardIds: number[]): void {\n if (dashboardIds.length === 0) return;\n const nextStatuses = { ...repositoryStatusByDashboardId };\n dashboardIds.forEach((dashboardId) => {\n nextStatuses[dashboardId] = \"loading\";\n });\n repositoryStatusByDashboardId = nextStatuses;\n }\n // [/DEF:invalidateRepositoryStatuses:Function]\n" + "anchor_syntax": "region", + "body": " // #region invalidateRepositoryStatuses:Function [TYPE Function]\n // @PURPOSE: Marks dashboard statuses as loading so they are refetched.\n function invalidateRepositoryStatuses(dashboardIds: number[]): void {\n if (dashboardIds.length === 0) return;\n const nextStatuses = { ...repositoryStatusByDashboardId };\n dashboardIds.forEach((dashboardId) => {\n nextStatuses[dashboardId] = \"loading\";\n });\n repositoryStatusByDashboardId = nextStatuses;\n }\n // #endregion invalidateRepositoryStatuses:Function\n" }, { - "contract_id": "resolveRepositoryStatusToken", + "contract_id": "resolveRepositoryStatusToken:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 189, - "end_line": 221, + "start_line": 190, + "end_line": 222, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109235,15 +77546,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:resolveRepositoryStatusToken:Function]\n /**\n * @purpose Converts git status payload into a stable UI status token.\n */\n function resolveRepositoryStatusToken(status: any): string {\n const syncState = String(status?.sync_state || \"\").toUpperCase();\n if (syncState === \"DIVERGED\") return \"diverged\";\n if (syncState === \"BEHIND_REMOTE\") return \"behind_remote\";\n if (syncState === \"AHEAD_REMOTE\") return \"ahead_remote\";\n if (syncState === \"CHANGES\") return \"changes\";\n if (syncState === \"SYNCED\") return \"synced\";\n\n const syncStatus = String(status?.sync_status || \"\").toUpperCase();\n if (syncStatus === \"NO_REPO\") return \"no_repo\";\n if (syncStatus === \"ERROR\") return \"error\";\n if (syncStatus === \"DIFF\") return \"changes\";\n if (syncStatus === \"OK\") return \"synced\";\n\n const aheadCount = Number(status?.ahead_count || 0);\n const behindCount = Number(status?.behind_count || 0);\n if (aheadCount > 0 && behindCount > 0) return \"diverged\";\n if (behindCount > 0) return \"behind_remote\";\n if (aheadCount > 0) return \"ahead_remote\";\n\n const hasChanges =\n Boolean(status?.is_dirty) ||\n (status?.untracked_files?.length || 0) > 0 ||\n (status?.modified_files?.length || 0) > 0 ||\n (status?.staged_files?.length || 0) > 0;\n\n return hasChanges ? \"changes\" : \"synced\";\n }\n // [/DEF:resolveRepositoryStatusToken:Function]\n" + "anchor_syntax": "region", + "body": " // #region resolveRepositoryStatusToken:Function [TYPE Function]\n /**\n * @purpose Converts git status payload into a stable UI status token.\n */\n function resolveRepositoryStatusToken(status: any): string {\n const syncState = String(status?.sync_state || \"\").toUpperCase();\n if (syncState === \"DIVERGED\") return \"diverged\";\n if (syncState === \"BEHIND_REMOTE\") return \"behind_remote\";\n if (syncState === \"AHEAD_REMOTE\") return \"ahead_remote\";\n if (syncState === \"CHANGES\") return \"changes\";\n if (syncState === \"SYNCED\") return \"synced\";\n\n const syncStatus = String(status?.sync_status || \"\").toUpperCase();\n if (syncStatus === \"NO_REPO\") return \"no_repo\";\n if (syncStatus === \"ERROR\") return \"error\";\n if (syncStatus === \"DIFF\") return \"changes\";\n if (syncStatus === \"OK\") return \"synced\";\n\n const aheadCount = Number(status?.ahead_count || 0);\n const behindCount = Number(status?.behind_count || 0);\n if (aheadCount > 0 && behindCount > 0) return \"diverged\";\n if (behindCount > 0) return \"behind_remote\";\n if (aheadCount > 0) return \"ahead_remote\";\n\n const hasChanges =\n Boolean(status?.is_dirty) ||\n (status?.untracked_files?.length || 0) > 0 ||\n (status?.modified_files?.length || 0) > 0 ||\n (status?.staged_files?.length || 0) > 0;\n\n return hasChanges ? \"changes\" : \"synced\";\n }\n // #endregion resolveRepositoryStatusToken:Function\n" }, { - "contract_id": "loadRepositoryStatuses", + "contract_id": "loadRepositoryStatuses:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 223, - "end_line": 280, + "start_line": 224, + "end_line": 281, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109261,15 +77572,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:loadRepositoryStatuses:Function]\n /**\n * @purpose Hydrates repository status map for dashboards in repository mode.\n */\n async function loadRepositoryStatuses() {\n if (statusMode !== \"repository\") {\n repositoryStatusByDashboardId = {};\n return;\n }\n\n const requestId = repositoryStatusRequestId + 1;\n repositoryStatusRequestId = requestId;\n\n const visibleDashboards = paginatedDashboards;\n const visibleIds = visibleDashboards.map((dashboard) => dashboard.id);\n\n if (visibleDashboards.length === 0) {\n repositoryStatusByDashboardId = {};\n return;\n }\n\n const missingIds = visibleIds.filter((dashboardId) => {\n const token = repositoryStatusByDashboardId[dashboardId];\n return token === undefined || token === \"loading\";\n });\n\n if (missingIds.length === 0) {\n return;\n }\n\n const loadingStatuses = { ...repositoryStatusByDashboardId };\n missingIds.forEach((dashboardId) => {\n loadingStatuses[dashboardId] = \"loading\";\n });\n repositoryStatusByDashboardId = loadingStatuses;\n\n let entries: Array = [];\n try {\n const batchResult = await gitService.getStatusesBatch(missingIds);\n const statusesByDashboardId = batchResult?.statuses || {};\n entries = missingIds.map((dashboardId) => {\n const status =\n statusesByDashboardId[dashboardId] ??\n statusesByDashboardId[String(dashboardId)];\n if (!status) return [dashboardId, \"error\"] as const;\n return [dashboardId, resolveRepositoryStatusToken(status)] as const;\n });\n } catch (error) {\n entries = missingIds.map((dashboardId) => [dashboardId, \"error\"] as const);\n }\n\n if (requestId !== repositoryStatusRequestId) return;\n repositoryStatusByDashboardId = {\n ...repositoryStatusByDashboardId,\n ...Object.fromEntries(entries),\n };\n }\n // [/DEF:loadRepositoryStatuses:Function]\n" + "anchor_syntax": "region", + "body": " // #region loadRepositoryStatuses:Function [TYPE Function]\n /**\n * @purpose Hydrates repository status map for dashboards in repository mode.\n */\n async function loadRepositoryStatuses() {\n if (statusMode !== \"repository\") {\n repositoryStatusByDashboardId = {};\n return;\n }\n\n const requestId = repositoryStatusRequestId + 1;\n repositoryStatusRequestId = requestId;\n\n const visibleDashboards = paginatedDashboards;\n const visibleIds = visibleDashboards.map((dashboard) => dashboard.id);\n\n if (visibleDashboards.length === 0) {\n repositoryStatusByDashboardId = {};\n return;\n }\n\n const missingIds = visibleIds.filter((dashboardId) => {\n const token = repositoryStatusByDashboardId[dashboardId];\n return token === undefined || token === \"loading\";\n });\n\n if (missingIds.length === 0) {\n return;\n }\n\n const loadingStatuses = { ...repositoryStatusByDashboardId };\n missingIds.forEach((dashboardId) => {\n loadingStatuses[dashboardId] = \"loading\";\n });\n repositoryStatusByDashboardId = loadingStatuses;\n\n let entries: Array = [];\n try {\n const batchResult = await gitService.getStatusesBatch(missingIds);\n const statusesByDashboardId = batchResult?.statuses || {};\n entries = missingIds.map((dashboardId) => {\n const status =\n statusesByDashboardId[dashboardId] ??\n statusesByDashboardId[String(dashboardId)];\n if (!status) return [dashboardId, \"error\"] as const;\n return [dashboardId, resolveRepositoryStatusToken(status)] as const;\n });\n } catch (error) {\n entries = missingIds.map((dashboardId) => [dashboardId, \"error\"] as const);\n }\n\n if (requestId !== repositoryStatusRequestId) return;\n repositoryStatusByDashboardId = {\n ...repositoryStatusByDashboardId,\n ...Object.fromEntries(entries),\n };\n }\n // #endregion loadRepositoryStatuses:Function\n" }, { - "contract_id": "runBulkGitAction", + "contract_id": "runBulkGitAction:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 282, - "end_line": 334, + "start_line": 283, + "end_line": 335, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109287,71 +77598,71 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:runBulkGitAction:Function]\n // @PURPOSE: Executes git action for selected dashboards with limited parallelism.\n async function runBulkGitAction(\n actionToken: string,\n action: (dashboardId: number) => Promise,\n ): Promise {\n if (bulkActionRunning) return;\n const selectedDashboardIds = selectedIds.filter((dashboardId) =>\n isRepositoryReady(dashboardId),\n );\n\n if (selectedDashboardIds.length === 0) {\n addToast($t.git?.no_repositories_selected, \"error\");\n return;\n }\n\n bulkActionRunning = true;\n const concurrency = 3;\n const idsQueue = [...selectedDashboardIds];\n let successCount = 0;\n let failedCount = 0;\n\n const worker = async () => {\n while (idsQueue.length > 0) {\n const dashboardId = idsQueue.shift();\n if (dashboardId === undefined) break;\n try {\n await action(dashboardId);\n successCount += 1;\n } catch (_error) {\n failedCount += 1;\n }\n }\n };\n\n try {\n await Promise.all(\n Array.from({ length: Math.min(concurrency, selectedDashboardIds.length) }, () => worker()),\n );\n invalidateRepositoryStatuses(selectedDashboardIds);\n const actionLabel = $t.git?.[`bulk_action_${actionToken}`] || actionToken;\n addToast(\n $t.git?.bulk_result\n .replace(\"{action}\", actionLabel)\n .replace(\"{success}\", String(successCount))\n .replace(\"{failed}\", String(failedCount)),\n failedCount > 0 ? \"warning\" : \"success\",\n );\n } finally {\n bulkActionRunning = false;\n }\n }\n // [/DEF:runBulkGitAction:Function]\n" + "anchor_syntax": "region", + "body": " // #region runBulkGitAction:Function [TYPE Function]\n // @PURPOSE: Executes git action for selected dashboards with limited parallelism.\n async function runBulkGitAction(\n actionToken: string,\n action: (dashboardId: number) => Promise,\n ): Promise {\n if (bulkActionRunning) return;\n const selectedDashboardIds = selectedIds.filter((dashboardId) =>\n isRepositoryReady(dashboardId),\n );\n\n if (selectedDashboardIds.length === 0) {\n addToast($t.git?.no_repositories_selected, \"error\");\n return;\n }\n\n bulkActionRunning = true;\n const concurrency = 3;\n const idsQueue = [...selectedDashboardIds];\n let successCount = 0;\n let failedCount = 0;\n\n const worker = async () => {\n while (idsQueue.length > 0) {\n const dashboardId = idsQueue.shift();\n if (dashboardId === undefined) break;\n try {\n await action(dashboardId);\n successCount += 1;\n } catch (_error) {\n failedCount += 1;\n }\n }\n };\n\n try {\n await Promise.all(\n Array.from({ length: Math.min(concurrency, selectedDashboardIds.length) }, () => worker()),\n );\n invalidateRepositoryStatuses(selectedDashboardIds);\n const actionLabel = $t.git?.[`bulk_action_${actionToken}`] || actionToken;\n addToast(\n $t.git?.bulk_result\n .replace(\"{action}\", actionLabel)\n .replace(\"{success}\", String(successCount))\n .replace(\"{failed}\", String(failedCount)),\n failedCount > 0 ? \"warning\" : \"success\",\n );\n } finally {\n bulkActionRunning = false;\n }\n }\n // #endregion runBulkGitAction:Function\n" }, { - "contract_id": "handleBulkSync", + "contract_id": "handleBulkSync:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 336, - "end_line": 340, + "start_line": 337, + "end_line": 341, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": " // [DEF:handleBulkSync:Function]\n async function handleBulkSync(): Promise {\n await runBulkGitAction(\"sync\", (dashboardId) => gitService.sync(dashboardId, null, envId));\n }\n // [/DEF:handleBulkSync:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleBulkSync:Function [TYPE Function]\n async function handleBulkSync(): Promise {\n await runBulkGitAction(\"sync\", (dashboardId) => gitService.sync(dashboardId, null, envId));\n }\n // #endregion handleBulkSync:Function\n" }, { - "contract_id": "handleBulkCommit", + "contract_id": "handleBulkCommit:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 342, - "end_line": 350, + "start_line": 343, + "end_line": 351, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": " // [DEF:handleBulkCommit:Function]\n async function handleBulkCommit(): Promise {\n const message = prompt($t.git?.commit_message);\n if (!message?.trim()) return;\n await runBulkGitAction(\"commit\", (dashboardId) =>\n gitService.commit(dashboardId, message.trim(), [], envId),\n );\n }\n // [/DEF:handleBulkCommit:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleBulkCommit:Function [TYPE Function]\n async function handleBulkCommit(): Promise {\n const message = prompt($t.git?.commit_message);\n if (!message?.trim()) return;\n await runBulkGitAction(\"commit\", (dashboardId) =>\n gitService.commit(dashboardId, message.trim(), [], envId),\n );\n }\n // #endregion handleBulkCommit:Function\n" }, { - "contract_id": "handleBulkPull", + "contract_id": "handleBulkPull:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 352, - "end_line": 356, + "start_line": 353, + "end_line": 357, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": " // [DEF:handleBulkPull:Function]\n async function handleBulkPull(): Promise {\n await runBulkGitAction(\"pull\", (dashboardId) => gitService.pull(dashboardId, envId));\n }\n // [/DEF:handleBulkPull:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleBulkPull:Function [TYPE Function]\n async function handleBulkPull(): Promise {\n await runBulkGitAction(\"pull\", (dashboardId) => gitService.pull(dashboardId, envId));\n }\n // #endregion handleBulkPull:Function\n" }, { - "contract_id": "handleBulkPush", + "contract_id": "handleBulkPush:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 358, - "end_line": 362, + "start_line": 359, + "end_line": 363, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": " // [DEF:handleBulkPush:Function]\n async function handleBulkPush(): Promise {\n await runBulkGitAction(\"push\", (dashboardId) => gitService.push(dashboardId, envId));\n }\n // [/DEF:handleBulkPush:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleBulkPush:Function [TYPE Function]\n async function handleBulkPush(): Promise {\n await runBulkGitAction(\"push\", (dashboardId) => gitService.push(dashboardId, envId));\n }\n // #endregion handleBulkPush:Function\n" }, { - "contract_id": "handleBulkDelete", + "contract_id": "handleBulkDelete:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 364, - "end_line": 375, + "start_line": 365, + "end_line": 376, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109369,15 +77680,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleBulkDelete:Function]\n // @PURPOSE: Removes selected repositories from storage and binding table.\n async function handleBulkDelete(): Promise {\n if (!confirm($t.git?.confirm_delete_repo || \"Удалить выбранные репозитории?\")) return;\n const idsToDelete = [...selectedIds];\n await runBulkGitAction(\"delete\", (dashboardId) =>\n gitService.deleteRepository(dashboardId, envId),\n );\n dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id));\n selectedIds = [];\n }\n // [/DEF:handleBulkDelete:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleBulkDelete:Function [TYPE Function]\n // @PURPOSE: Removes selected repositories from storage and binding table.\n async function handleBulkDelete(): Promise {\n if (!confirm($t.git?.confirm_delete_repo || \"Удалить выбранные репозитории?\")) return;\n const idsToDelete = [...selectedIds];\n await runBulkGitAction(\"delete\", (dashboardId) =>\n gitService.deleteRepository(dashboardId, envId),\n );\n dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id));\n selectedIds = [];\n }\n // #endregion handleBulkDelete:Function\n" }, { - "contract_id": "handleManageSelected", + "contract_id": "handleManageSelected:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 377, - "end_line": 391, + "start_line": 378, + "end_line": 392, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109395,15 +77706,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleManageSelected:Function]\n // @PURPOSE: Opens Git manager for exactly one selected dashboard.\n async function handleManageSelected(): Promise {\n if (selectedIds.length !== 1) {\n addToast($t.git?.select_single_for_manage, \"warning\");\n return;\n }\n\n const selectedDashboardId = selectedIds[0];\n const selectedDashboard = dashboards.find(\n (dashboard) => dashboard.id === selectedDashboardId,\n );\n openGitManagerForDashboard(selectedDashboard || null);\n }\n // [/DEF:handleManageSelected:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleManageSelected:Function [TYPE Function]\n // @PURPOSE: Opens Git manager for exactly one selected dashboard.\n async function handleManageSelected(): Promise {\n if (selectedIds.length !== 1) {\n addToast($t.git?.select_single_for_manage, \"warning\");\n return;\n }\n\n const selectedDashboardId = selectedIds[0];\n const selectedDashboard = dashboards.find(\n (dashboard) => dashboard.id === selectedDashboardId,\n );\n openGitManagerForDashboard(selectedDashboard || null);\n }\n // #endregion handleManageSelected:Function\n" }, { - "contract_id": "resolveDashboardRef", + "contract_id": "resolveDashboardRef:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 393, - "end_line": 412, + "start_line": 394, + "end_line": 413, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109441,15 +77752,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:resolveDashboardRef:Function]\n // @PURPOSE: Resolves dashboard slug from payload fields.\n // @PRE: Dashboard metadata is provided.\n // @POST: Returns slug string or null if unavailable.\n function resolveDashboardRef(dashboard: DashboardMetadata): string | null {\n const directSlug = String(\n dashboard.slug ||\n dashboard.dashboard_slug ||\n dashboard.url_slug ||\n \"\",\n ).trim();\n if (directSlug) return directSlug;\n\n const dashboardUrl = String(dashboard.url || \"\").trim();\n if (!dashboardUrl) return null;\n const slugMatch = dashboardUrl.match(/\\/dashboard\\/([^/?#]+)/i);\n if (!slugMatch?.[1]) return null;\n return decodeURIComponent(slugMatch[1]);\n }\n // [/DEF:resolveDashboardRef:Function]\n" + "anchor_syntax": "region", + "body": " // #region resolveDashboardRef:Function [TYPE Function]\n // @PURPOSE: Resolves dashboard slug from payload fields.\n // @PRE: Dashboard metadata is provided.\n // @POST: Returns slug string or null if unavailable.\n function resolveDashboardRef(dashboard: DashboardMetadata): string | null {\n const directSlug = String(\n dashboard.slug ||\n dashboard.dashboard_slug ||\n dashboard.url_slug ||\n \"\",\n ).trim();\n if (directSlug) return directSlug;\n\n const dashboardUrl = String(dashboard.url || \"\").trim();\n if (!dashboardUrl) return null;\n const slugMatch = dashboardUrl.match(/\\/dashboard\\/([^/?#]+)/i);\n if (!slugMatch?.[1]) return null;\n return decodeURIComponent(slugMatch[1]);\n }\n // #endregion resolveDashboardRef:Function\n" }, { - "contract_id": "openGitManagerForDashboard", + "contract_id": "openGitManagerForDashboard:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 414, - "end_line": 427, + "start_line": 415, + "end_line": 428, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109467,15 +77778,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:openGitManagerForDashboard:Function]\n // @PURPOSE: Opens Git manager for provided dashboard metadata.\n function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {\n if (!dashboard) return;\n const dashboardRef = resolveDashboardRef(dashboard);\n if (!dashboardRef) {\n addToast($t.git?.select_dashboard_with_slug || \"Dashboard slug is required to open GitManager\", \"error\");\n return;\n }\n gitDashboardId = dashboardRef;\n gitDashboardTitle = dashboard.title || \"\";\n showGitManager = true;\n }\n // [/DEF:openGitManagerForDashboard:Function]\n" + "anchor_syntax": "region", + "body": " // #region openGitManagerForDashboard:Function [TYPE Function]\n // @PURPOSE: Opens Git manager for provided dashboard metadata.\n function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {\n if (!dashboard) return;\n const dashboardRef = resolveDashboardRef(dashboard);\n if (!dashboardRef) {\n addToast($t.git?.select_dashboard_with_slug || \"Dashboard slug is required to open GitManager\", \"error\");\n return;\n }\n gitDashboardId = dashboardRef;\n gitDashboardTitle = dashboard.title || \"\";\n showGitManager = true;\n }\n // #endregion openGitManagerForDashboard:Function\n" }, { - "contract_id": "handleInitializeRepositories", + "contract_id": "handleInitializeRepositories:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 429, - "end_line": 440, + "start_line": 430, + "end_line": 441, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109493,15 +77804,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleInitializeRepositories:Function]\n // @PURPOSE: Opens Git manager from bulk actions to initialize selected repository.\n async function handleInitializeRepositories(): Promise {\n if (selectedIds.length !== 1) {\n addToast($t.git?.select_single_for_manage, \"warning\");\n return;\n }\n const selectedDashboardId = selectedIds[0];\n const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null;\n openGitManagerForDashboard(selectedDashboard);\n }\n // [/DEF:handleInitializeRepositories:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleInitializeRepositories:Function [TYPE Function]\n // @PURPOSE: Opens Git manager from bulk actions to initialize selected repository.\n async function handleInitializeRepositories(): Promise {\n if (selectedIds.length !== 1) {\n addToast($t.git?.select_single_for_manage, \"warning\");\n return;\n }\n const selectedDashboardId = selectedIds[0];\n const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null;\n openGitManagerForDashboard(selectedDashboard);\n }\n // #endregion handleInitializeRepositories:Function\n" }, { - "contract_id": "getSortStatusValue", + "contract_id": "getSortStatusValue:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 442, - "end_line": 452, + "start_line": 443, + "end_line": 453, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109519,15 +77830,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:getSortStatusValue:Function]\n /**\n * @purpose Returns sort value for status column based on mode.\n */\n function getSortStatusValue(dashboard: DashboardMetadata): string {\n if (statusMode === \"repository\") {\n return getRepositoryStatusToken(dashboard.id);\n }\n return String(dashboard.status || \"\").toLowerCase();\n }\n // [/DEF:getSortStatusValue:Function]\n" + "anchor_syntax": "region", + "body": " // #region getSortStatusValue:Function [TYPE Function]\n /**\n * @purpose Returns sort value for status column based on mode.\n */\n function getSortStatusValue(dashboard: DashboardMetadata): string {\n if (statusMode === \"repository\") {\n return getRepositoryStatusToken(dashboard.id);\n }\n return String(dashboard.status || \"\").toLowerCase();\n }\n // #endregion getSortStatusValue:Function\n" }, { - "contract_id": "getStatusLabel", + "contract_id": "getStatusLabel:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 454, - "end_line": 465, + "start_line": 455, + "end_line": 466, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109545,15 +77856,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:getStatusLabel:Function]\n /**\n * @purpose Returns localized label for status column.\n */\n function getStatusLabel(dashboard: DashboardMetadata): string {\n if (statusMode !== \"repository\") {\n return String(dashboard.status || \"\");\n }\n const token = getRepositoryStatusToken(dashboard.id);\n return $t.git?.repo_status?.[token] || token;\n }\n // [/DEF:getStatusLabel:Function]\n" + "anchor_syntax": "region", + "body": " // #region getStatusLabel:Function [TYPE Function]\n /**\n * @purpose Returns localized label for status column.\n */\n function getStatusLabel(dashboard: DashboardMetadata): string {\n if (statusMode !== \"repository\") {\n return String(dashboard.status || \"\");\n }\n const token = getRepositoryStatusToken(dashboard.id);\n return $t.git?.repo_status?.[token] || token;\n }\n // #endregion getStatusLabel:Function\n" }, { - "contract_id": "getStatusBadgeClass", + "contract_id": "getStatusBadgeClass:Function", "contract_type": "Function", "file_path": "frontend/src/components/RepositoryDashboardGrid.svelte", - "start_line": 467, - "end_line": 488, + "start_line": 468, + "end_line": 489, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109571,85 +77882,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:getStatusBadgeClass:Function]\n /**\n * @purpose Returns badge style for status column.\n */\n function getStatusBadgeClass(dashboard: DashboardMetadata): string {\n if (statusMode !== \"repository\") {\n return dashboard.status === \"published\"\n ? \"bg-green-100 text-green-800\"\n : \"bg-gray-100 text-gray-800\";\n }\n\n const token = getRepositoryStatusToken(dashboard.id);\n if (token === \"loading\") return \"bg-slate-100 text-slate-500\";\n if (token === \"no_repo\") return \"bg-gray-100 text-gray-800\";\n if (token === \"synced\") return \"bg-green-100 text-green-800\";\n if (token === \"changes\") return \"bg-amber-100 text-amber-800\";\n if (token === \"behind_remote\") return \"bg-blue-100 text-blue-800\";\n if (token === \"ahead_remote\") return \"bg-indigo-100 text-indigo-800\";\n if (token === \"diverged\") return \"bg-purple-100 text-purple-800\";\n return \"bg-rose-100 text-rose-800\";\n }\n // [/DEF:getStatusBadgeClass:Function]\n" - }, - { - "contract_id": "StartupEnvironmentWizard", - "contract_type": "Component", - "file_path": "frontend/src/components/StartupEnvironmentWizard.svelte", - "start_line": 1, - "end_line": 210, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "When open, wizard keeps user on an actionable setup path until the first environment exists.", - "LAYER": "UI", - "PURPOSE": "Blocking startup wizard for creating the first Superset environment from zero-state screens.", - "UX_FEEDBACK": "Toast on success, inline banner on failure.", - "UX_RECOVERY": "User can switch to advanced settings or retry save with corrected data.", - "UX_STATE": "Error -> Shows validation or backend error inline." - }, - "relations": [ - { - "source_id": "StartupEnvironmentWizard", - "relation_type": "CALLS", - "target_id": "api", - "target_ref": "api" - }, - { - "source_id": "StartupEnvironmentWizard", - "relation_type": "CALLS", - "target_id": "environmentContext", - "target_ref": "environmentContext" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C3", - "detail": { - "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": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n{#if open}\n
\n
\n
\n
\n
\n

{$t.nav?.dashboards}

\n

{$t.dashboard?.setup_title || \"Configure your first environment\"}

\n

{$t.dashboard?.setup_intro || \"Dashboards need at least one Superset environment. Create it here instead of landing on an empty screen.\"}

\n
\n \n
\n
\n\n {#if step === \"intro\"}\n
\n
\n
\n

{$t.dashboard?.setup_card_title || \"What happens next\"}

\n

{$t.dashboard?.setup_card_body || \"The wizard saves a Superset endpoint, validates login, and immediately makes the environment available in the global selector.\"}

\n
\n
\n

{$t.dashboard?.setup_checklist_title || \"Prepare these values\"}

\n
    \n
  • {$t.dashboard?.setup_checklist_url || \"Superset base URL without /api/v1\"}
  • \n
  • {$t.dashboard?.setup_checklist_user || \"Service username with access to dashboards\"}
  • \n
  • {$t.dashboard?.setup_checklist_pass || \"Password for the selected Superset account\"}
  • \n
\n
\n
\n\n
\n

{$t.dashboard?.setup_step_title || \"Starter flow\"}

\n
\n

1. {$t.dashboard?.setup_step_one || \"Create the first environment\"}

\n

2. {$t.dashboard?.setup_step_two || \"Select it automatically for the current session\"}

\n

3. {$t.dashboard?.setup_step_three || \"Load dashboard inventory for that environment\"}

\n
\n \n
\n
\n {:else}\n
\n
\n \n \n \n \n \n \n \n
\n\n {#if submitError}\n
{submitError}
\n {/if}\n\n
\n \n
\n \n \n
\n
\n
\n {/if}\n
\n
\n{/if}\n\n" + "anchor_syntax": "region", + "body": " // #region getStatusBadgeClass:Function [TYPE Function]\n /**\n * @purpose Returns badge style for status column.\n */\n function getStatusBadgeClass(dashboard: DashboardMetadata): string {\n if (statusMode !== \"repository\") {\n return dashboard.status === \"published\"\n ? \"bg-green-100 text-green-800\"\n : \"bg-gray-100 text-gray-800\";\n }\n\n const token = getRepositoryStatusToken(dashboard.id);\n if (token === \"loading\") return \"bg-slate-100 text-slate-500\";\n if (token === \"no_repo\") return \"bg-gray-100 text-gray-800\";\n if (token === \"synced\") return \"bg-green-100 text-green-800\";\n if (token === \"changes\") return \"bg-amber-100 text-amber-800\";\n if (token === \"behind_remote\") return \"bg-blue-100 text-blue-800\";\n if (token === \"ahead_remote\") return \"bg-indigo-100 text-indigo-800\";\n if (token === \"diverged\") return \"bg-purple-100 text-purple-800\";\n return \"bg-rose-100 text-rose-800\";\n }\n // #endregion getStatusBadgeClass:Function\n" }, { "contract_id": "TaskHistory", "contract_type": "Component", "file_path": "frontend/src/components/TaskHistory.svelte", "start_line": 1, - "end_line": 200, - "tier": "TIER_1", - "complexity": 1, + "end_line": 202, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Displays a list of recent tasks with their status and allows selecting them for viewing logs.", "SEMANTICS": [ @@ -109691,18 +77936,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -109723,18 +77959,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C1", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -109775,15 +78011,15 @@ } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n
\n
\n

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

\n
\n
\n \n \n
\n \n
\n \n {$t.common?.refresh }\n \n
\n
\n \n {#if loading && tasks.length === 0}\n
{$t.tasks?.loading }
\n {:else if error}\n
{error}
\n {:else if tasks.length === 0}\n
{$t.tasks?.no_tasks }
\n {:else}\n
    \n {#each tasks as task}\n
  • \n \n
  • \n {/each}\n
\n {/if}\n
\n\n" + "anchor_syntax": "region", + "body": "\n\n\n\n\n\n
\n
\n

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

\n
\n
\n \n \n
\n \n
\n \n {$t.common?.refresh }\n \n
\n
\n \n {#if loading && tasks.length === 0}\n
{$t.tasks?.loading }
\n {:else if error}\n
{error}
\n {:else if tasks.length === 0}\n
{$t.tasks?.no_tasks }
\n {:else}\n
    \n {#each tasks as task}\n
  • \n \n
  • \n {/each}\n
\n {/if}\n
\n\n" }, { - "contract_id": "getStatusColor", + "contract_id": "getStatusColor:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskHistory.svelte", - "start_line": 84, - "end_line": 98, + "start_line": 86, + "end_line": 100, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109821,15 +78057,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:getStatusColor:Function]\n // @PURPOSE: Returns the CSS color class for a given task status.\n // @PRE: status string is provided.\n // @POST: Returns tailwind color class string.\n function getStatusColor(status) {\n switch (status) {\n case 'SUCCESS': return 'bg-green-100 text-green-800';\n case 'FAILED': return 'bg-red-100 text-red-800';\n case 'RUNNING': return 'bg-blue-100 text-blue-800';\n case 'AWAITING_INPUT': return 'bg-orange-100 text-orange-800';\n case 'AWAITING_MAPPING': return 'bg-yellow-100 text-yellow-800';\n default: return 'bg-gray-100 text-gray-800';\n }\n }\n // [/DEF:getStatusColor:Function]\n" + "anchor_syntax": "region", + "body": " // #region getStatusColor:Function [TYPE Function]\n // @PURPOSE: Returns the CSS color class for a given task status.\n // @PRE: status string is provided.\n // @POST: Returns tailwind color class string.\n function getStatusColor(status) {\n switch (status) {\n case 'SUCCESS': return 'bg-green-100 text-green-800';\n case 'FAILED': return 'bg-red-100 text-red-800';\n case 'RUNNING': return 'bg-blue-100 text-blue-800';\n case 'AWAITING_INPUT': return 'bg-orange-100 text-orange-800';\n case 'AWAITING_MAPPING': return 'bg-yellow-100 text-yellow-800';\n default: return 'bg-gray-100 text-gray-800';\n }\n }\n // #endregion getStatusColor:Function\n" }, { - "contract_id": "onDestroy", + "contract_id": "onDestroy:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskHistory.svelte", - "start_line": 110, - "end_line": 117, + "start_line": 112, + "end_line": 119, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -109867,18 +78103,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:onDestroy:Function]\n // @PURPOSE: Cleans up the polling interval when the component is destroyed.\n // @PRE: Component is being destroyed.\n // @POST: Polling interval is cleared.\n onDestroy(() => {\n clearInterval(interval);\n });\n // [/DEF:onDestroy:Function]\n" + "anchor_syntax": "region", + "body": " // #region onDestroy:Function [TYPE Function]\n // @PURPOSE: Cleans up the polling interval when the component is destroyed.\n // @PRE: Component is being destroyed.\n // @POST: Polling interval is cleared.\n onDestroy(() => {\n clearInterval(interval);\n });\n // #endregion onDestroy:Function\n" }, { "contract_id": "TaskList", "contract_type": "Component", "file_path": "frontend/src/components/TaskList.svelte", "start_line": 1, - "end_line": 111, - "tier": "TIER_1", - "complexity": 1, + "end_line": 113, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": 3, "LAYER": "Component", "PURPOSE": "Displays a list of tasks with their status and execution details.", "SEMANTICS": [ @@ -109913,18 +78150,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -109945,18 +78173,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C1", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -109979,15 +78207,15 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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", + "contract_id": "handleTaskClick:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskList.svelte", - "start_line": 51, - "end_line": 58, + "start_line": 53, + "end_line": 60, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110025,197 +78253,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleTaskClick: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 // [/DEF:handleTaskClick:Function]\n" + "anchor_syntax": "region", + "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": "TaskLogViewer", - "contract_type": "Component", + "contract_id": "handleRealTimeLogs:Action", + "contract_type": "Function", "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 1, - "end_line": 306, - "tier": "CRITICAL", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Real-time logs are always appended without duplicates.", - "LAYER": "UI", - "POST": "task logs are displayed and updated in real time.", - "PRE": "Needs a valid taskId to fetch logs for.", - "PURPOSE": "Displays detailed logs for a specific task inline or in a modal using TaskLogPanel.", - "SEMANTICS": [ - "task", - "log", - "viewer", - "inline", - "realtime" - ], - "TEST_CONTRACT": "Component_TaskLogViewer ->", - "TIER": "CRITICAL", - "UX_FEEDBACK": "Auto-scroll keeps newest logs visible", - "UX_RECOVERY": "Refresh button re-fetches logs from API", - "UX_STATE": "Error -> Shows error message with recovery option" - }, - "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": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C3", - "detail": { - "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": "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" - } - }, - { - "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": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Component'", - "detail": { - "actual_type": "Component", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Component' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Component" - } - }, - { - "code": "unknown_tag", - "tag": "TIER", - "message": "@TIER is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "def", - "body": "\n\n\n\n{#if shouldShow}\n \n\n \n \n \n \n {#if inline}\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 {$t.common?.retry}\n \n {:else}\n \n {/if}\n \n \n {:else}\n \n\n \n \n \n \n \n \n {\n show = false;\n onclose();\n }}\n onkeydown={(e) => {\n if (e.key === \"Escape\") {\n show = false;\n onclose();\n }\n }}\n role=\"presentation\"\n >\n\n \n\n \n
\n \n \n {$t.tasks?.logs_title}\n \n {\n show = false;\n onclose();\n }}\n aria-label={$t.common?.close}\n >\n \n \n
\n
\n {#if loading && logs.length === 0}\n \n
\n

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

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

\n {error}\n

\n \n {$t.common?.retry}\n \n \n {:else}\n \n {/if}\n \n \n \n \n \n {/if}\n{/if}\n\n\n\n" - }, - { - "contract_id": "handleRealTimeLogs", - "contract_type": "Action", - "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 63, - "end_line": 80, + "start_line": 64, + "end_line": 81, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110225,91 +78271,43 @@ }, "relations": [], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "POST", - "message": "@POST is not allowed for contract type 'Action'", - "detail": { - "actual_type": "Action", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] - } - }, { "code": "tag_forbidden_by_complexity", "tag": "POST", - "message": "@POST is forbidden for contract type 'Action' at C1", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Action" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PRE", - "message": "@PRE is not allowed for contract type 'Action'", - "detail": { - "actual_type": "Action", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Agent" - ] + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Action' at C1", + "message": "@PRE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Action" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Action'", - "detail": { - "actual_type": "Action", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Action' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Action" + "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleRealTimeLogs:Action]\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 // [/DEF:handleRealTimeLogs:Action]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchLogs:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 82, - "end_line": 102, + "start_line": 83, + "end_line": 103, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110347,15 +78345,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:fetchLogs: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 // [/DEF:fetchLogs:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handleFilterChange:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 104, - "end_line": 112, + "start_line": 105, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110393,15 +78391,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleFilterChange: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 // [/DEF:handleFilterChange:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handleRefresh:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 114, - "end_line": 122, + "start_line": 115, + "end_line": 123, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110439,281 +78437,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleRefresh: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 // [/DEF:handleRefresh:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "showInline", - "contract_type": "Component", - "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 150, - "end_line": 192, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "UI", - "PURPOSE": "Shows inline logs", - "SEMANTICS": [ - "logs", - "inline" - ], - "UX_STATE": "Loading -> Default" - }, - "relations": [ - { - "source_id": "showInline", - "relation_type": "USES", - "target_id": "App", - "target_ref": "App" - } - ], - "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_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" - } - } - ], - "anchor_syntax": "def", - "body": " \n\n \n \n \n \n {#if inline}\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 {$t.common?.retry}\n \n {:else}\n \n {/if}\n \n \n" - }, - { - "contract_id": "showModal", - "contract_type": "Component", - "file_path": "frontend/src/components/TaskLogViewer.svelte", - "start_line": 194, - "end_line": 304, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "UI", - "PURPOSE": "Shows modal logs", - "SEMANTICS": [ - "logs", - "modal" - ], - "UX_STATE": "Loading -> Default" - }, - "relations": [ - { - "source_id": "showModal", - "relation_type": "USES", - "target_id": "App", - "target_ref": "App" - } - ], - "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_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" - } - } - ], - "anchor_syntax": "def", - "body": " \n\n \n \n \n \n \n \n {\n show = false;\n onclose();\n }}\n onkeydown={(e) => {\n if (e.key === \"Escape\") {\n show = false;\n onclose();\n }\n }}\n role=\"presentation\"\n >\n\n \n\n \n
\n \n \n {$t.tasks?.logs_title}\n \n {\n show = false;\n onclose();\n }}\n aria-label={$t.common?.close}\n >\n \n \n
\n
\n {#if loading && logs.length === 0}\n \n
\n

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

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

\n {error}\n

\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": "TaskRunner", - "contract_type": "Component", - "file_path": "frontend/src/components/TaskRunner.svelte", - "start_line": 1, - "end_line": 411, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "UI", - "PURPOSE": "Connects to a WebSocket to display real-time logs for a running task with filtering support.", - "SEMANTICS": [ - "task", - "runner", - "logs", - "websocket" - ], - "UX_STATE": "Loading -> Default" - }, - "relations": [ - { - "source_id": "TaskRunner", - "relation_type": "DEPENDS_ON", - "target_id": "TaskLogPanel", - "target_ref": "TaskLogPanel" - } - ], - "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_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" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n
\n {#if $selectedTask}\n
\n

{$t.tasks?.task_label}: {$selectedTask.plugin_id}

\n
\n {#if connectionStatus === 'connecting'}\n \n \n \n \n {$t.tasks?.connecting}\n {:else if connectionStatus === 'connected'}\n \n {$t.tasks?.live}\n {:else if connectionStatus === 'completed'}\n \n {$t.tasks?.completed}\n {:else if connectionStatus === 'awaiting_mapping'}\n \n {$t.tasks?.awaiting_mapping}\n {:else if connectionStatus === 'awaiting_input'}\n \n {$t.tasks?.awaiting_input}\n {:else}\n \n {$t.tasks?.disconnected}\n {/if}\n
\n
\n\n \n
\n
\n {$t.tasks?.details_parameters}\n
\n
\n
{$t.common?.id}: {$selectedTask.id}
\n
{$t.dashboard?.status}: {$selectedTask.status}
\n
{$t.tasks?.started_label}: {new Date($selectedTask.started_at || $selectedTask.created_at || Date.now()).toLocaleString()}
\n
{$t.tasks?.plugin}: {$selectedTask.plugin_id}
\n
\n
\n {$t.tasks?.parameters}:\n
{JSON.stringify($selectedTask.params, null, 2)}
\n
\n
\n
\n
\n\n
\n \n \n {#if waitingForData && connectionStatus === 'connected'}\n
\n {$t.tasks?.waiting_logs}\n
\n {/if}\n
\n {:else}\n

{$t.tasks?.select_task}

\n {/if}\n
\n\n { connectionStatus = 'disconnected'; ws.close(); }}\n/>\n\n { showPasswordPrompt = false; }}\n/>\n\n\n\n" - }, - { - "contract_id": "connect", + "contract_id": "connect:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskRunner.svelte", - "start_line": 43, - "end_line": 145, + "start_line": 44, + "end_line": 146, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110751,15 +78483,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:connect: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 // [/DEF:connect:Function]\n" + "anchor_syntax": "region", + "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": "fetchTargetDatabases", + "contract_id": "fetchTargetDatabases:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskRunner.svelte", - "start_line": 173, - "end_line": 194, + "start_line": 174, + "end_line": 195, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110797,15 +78529,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:fetchTargetDatabases:Function]\n /**\n * @purpose Fetches available databases from target environment for mapping.\n * @pre selectedTask must have to_env parameter set.\n * @post targetDatabases array populated with available databases.\n */\n async function fetchTargetDatabases() {\n const task = get(selectedTask);\n if (!task || !task.params.to_env) return;\n \n try {\n const envs = await api.fetchApi('/environments');\n const targetEnv = envs.find(e => e.name === task.params.to_env);\n \n if (targetEnv) {\n targetDatabases = await api.fetchApi(`/environments/${targetEnv.id}/databases`);\n }\n } catch (e) {\n console.error('Failed to fetch target databases', e);\n }\n }\n // [/DEF:fetchTargetDatabases:Function]\n" + "anchor_syntax": "region", + "body": " // #region fetchTargetDatabases:Function [TYPE Function]\n /**\n * @purpose Fetches available databases from target environment for mapping.\n * @pre selectedTask must have to_env parameter set.\n * @post targetDatabases array populated with available databases.\n */\n async function fetchTargetDatabases() {\n const task = get(selectedTask);\n if (!task || !task.params.to_env) return;\n \n try {\n const envs = await api.fetchApi('/environments');\n const targetEnv = envs.find(e => e.name === task.params.to_env);\n \n if (targetEnv) {\n targetDatabases = await api.fetchApi(`/environments/${targetEnv.id}/databases`);\n }\n } catch (e) {\n console.error('Failed to fetch target databases', e);\n }\n }\n // #endregion fetchTargetDatabases:Function\n" }, { - "contract_id": "handleMappingResolve", + "contract_id": "handleMappingResolve:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskRunner.svelte", - "start_line": 196, - "end_line": 230, + "start_line": 197, + "end_line": 231, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110843,15 +78575,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleMappingResolve:Function]\n /**\n * @purpose Resolves missing database mapping and continues migration.\n * @pre event.detail contains sourceDbUuid, targetDbUuid, targetDbName.\n * @post Mapping created in backend, task resumed with resolution params.\n */\n async function handleMappingResolve(event) {\n const task = get(selectedTask);\n const { sourceDbUuid, targetDbUuid, targetDbName } = event.detail;\n \n try {\n const envs = await api.fetchApi('/environments');\n const srcEnv = envs.find(e => e.name === task.params.from_env);\n const tgtEnv = envs.find(e => e.name === task.params.to_env);\n\n await api.postApi('/mappings', {\n source_env_id: srcEnv.id,\n target_env_id: tgtEnv.id,\n source_db_uuid: sourceDbUuid,\n target_db_uuid: targetDbUuid,\n source_db_name: missingDbInfo.name,\n target_db_name: targetDbName\n });\n\n await api.postApi(`/tasks/${task.id}/resolve`, {\n resolution_params: { resolved_mapping: { [sourceDbUuid]: targetDbUuid } }\n });\n\n connectionStatus = 'connected';\n addToast($t.tasks?.mapping_resolved, 'success');\n } catch (e) {\n addToast($t.tasks?.mapping_resolve_failed?.replace('{error}', e.message), 'error');\n }\n }\n // [/DEF:handleMappingResolve:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleMappingResolve:Function [TYPE Function]\n /**\n * @purpose Resolves missing database mapping and continues migration.\n * @pre event.detail contains sourceDbUuid, targetDbUuid, targetDbName.\n * @post Mapping created in backend, task resumed with resolution params.\n */\n async function handleMappingResolve(event) {\n const task = get(selectedTask);\n const { sourceDbUuid, targetDbUuid, targetDbName } = event.detail;\n \n try {\n const envs = await api.fetchApi('/environments');\n const srcEnv = envs.find(e => e.name === task.params.from_env);\n const tgtEnv = envs.find(e => e.name === task.params.to_env);\n\n await api.postApi('/mappings', {\n source_env_id: srcEnv.id,\n target_env_id: tgtEnv.id,\n source_db_uuid: sourceDbUuid,\n target_db_uuid: targetDbUuid,\n source_db_name: missingDbInfo.name,\n target_db_name: targetDbName\n });\n\n await api.postApi(`/tasks/${task.id}/resolve`, {\n resolution_params: { resolved_mapping: { [sourceDbUuid]: targetDbUuid } }\n });\n\n connectionStatus = 'connected';\n addToast($t.tasks?.mapping_resolved, 'success');\n } catch (e) {\n addToast($t.tasks?.mapping_resolve_failed?.replace('{error}', e.message), 'error');\n }\n }\n // #endregion handleMappingResolve:Function\n" }, { - "contract_id": "handlePasswordResume", + "contract_id": "handlePasswordResume:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskRunner.svelte", - "start_line": 232, - "end_line": 252, + "start_line": 233, + "end_line": 253, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110889,15 +78621,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handlePasswordResume:Function]\n /**\n * @purpose Submits passwords and resumes paused migration task.\n * @pre event.detail contains passwords object.\n * @post Task resumed with passwords, connection status restored to connected.\n */\n async function handlePasswordResume(event) {\n const task = get(selectedTask);\n const { passwords } = event.detail;\n \n try {\n await api.postApi(`/tasks/${task.id}/resume`, { passwords });\n \n showPasswordPrompt = false;\n connectionStatus = 'connected';\n addToast($t.tasks?.passwords_submitted, 'success');\n } catch (e) {\n addToast($t.tasks?.resume_failed?.replace('{error}', e.message), 'error');\n }\n }\n // [/DEF:handlePasswordResume:Function]\n" + "anchor_syntax": "region", + "body": " // #region handlePasswordResume:Function [TYPE Function]\n /**\n * @purpose Submits passwords and resumes paused migration task.\n * @pre event.detail contains passwords object.\n * @post Task resumed with passwords, connection status restored to connected.\n */\n async function handlePasswordResume(event) {\n const task = get(selectedTask);\n const { passwords } = event.detail;\n \n try {\n await api.postApi(`/tasks/${task.id}/resume`, { passwords });\n \n showPasswordPrompt = false;\n connectionStatus = 'connected';\n addToast($t.tasks?.passwords_submitted, 'success');\n } catch (e) {\n addToast($t.tasks?.resume_failed?.replace('{error}', e.message), 'error');\n }\n }\n // #endregion handlePasswordResume:Function\n" }, { - "contract_id": "startDataTimeout", + "contract_id": "startDataTimeout:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskRunner.svelte", - "start_line": 254, - "end_line": 268, + "start_line": 255, + "end_line": 269, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110935,15 +78667,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:startDataTimeout:Function]\n /**\n * @purpose Starts timeout timer to detect idle connection.\n * @pre connectionStatus is 'connected'.\n * @post waitingForData set to true after 5 seconds if no data received.\n */\n function startDataTimeout() {\n waitingForData = false;\n dataTimeout = setTimeout(() => {\n if (connectionStatus === 'connected') {\n waitingForData = true;\n }\n }, 5000);\n }\n // [/DEF:startDataTimeout:Function]\n" + "anchor_syntax": "region", + "body": " // #region startDataTimeout:Function [TYPE Function]\n /**\n * @purpose Starts timeout timer to detect idle connection.\n * @pre connectionStatus is 'connected'.\n * @post waitingForData set to true after 5 seconds if no data received.\n */\n function startDataTimeout() {\n waitingForData = false;\n dataTimeout = setTimeout(() => {\n if (connectionStatus === 'connected') {\n waitingForData = true;\n }\n }, 5000);\n }\n // #endregion startDataTimeout:Function\n" }, { - "contract_id": "resetDataTimeout", + "contract_id": "resetDataTimeout:Function", "contract_type": "Function", "file_path": "frontend/src/components/TaskRunner.svelte", - "start_line": 270, - "end_line": 281, + "start_line": 271, + "end_line": 282, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -110981,19 +78713,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:resetDataTimeout:Function]\n /**\n * @purpose Resets data timeout timer when new data arrives.\n * @pre dataTimeout must be set.\n * @post waitingForData reset to false, new timeout started.\n */\n function resetDataTimeout() {\n clearTimeout(dataTimeout);\n waitingForData = false;\n startDataTimeout();\n }\n // [/DEF:resetDataTimeout:Function]\n" + "anchor_syntax": "region", + "body": " // #region resetDataTimeout:Function [TYPE Function]\n /**\n * @purpose Resets data timeout timer when new data arrives.\n * @pre dataTimeout must be set.\n * @post waitingForData reset to false, new timeout started.\n */\n function resetDataTimeout() {\n clearTimeout(dataTimeout);\n waitingForData = false;\n startDataTimeout();\n }\n // #endregion resetDataTimeout:Function\n" }, { "contract_id": "Toast", "contract_type": "Component", "file_path": "frontend/src/components/Toast.svelte", "start_line": 1, - "end_line": 32, - "tier": "TIER_1", - "complexity": 2, + "end_line": 33, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Displays transient notifications (toasts) in the bottom-right corner.", "SEMANTICS": [ @@ -111029,9 +78761,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C2", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 2, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -111052,35 +78784,25 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C2", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 2, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C2", - "detail": { - "actual_complexity": 2, + "actual_complexity": 3, "contract_type": "Component" } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n
\n {#each $toasts as toast (toast.id)}\n
\n {toast.message}\n
\n {/each}\n
\n\n\n\n" + "anchor_syntax": "region", + "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", - "contract_type": "Module", + "contract_id": "TaskLogViewerTest:Module", + "contract_type": "Function", "file_path": "frontend/src/components/__tests__/task_log_viewer.test.js", "start_line": 1, - "end_line": 170, - "tier": "TIER_2", - "complexity": 3, + "end_line": 169, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "INVARIANT": "Duplicate logs are never appended. Polling only active for in-progress tasks.", "LAYER": "UI (Tests)", "PURPOSE": "Unit tests for TaskLogViewer component by mounting it and observing the DOM.", @@ -111099,7 +78821,7 @@ }, "relations": [ { - "source_id": "TaskLogViewerTest", + "source_id": "TaskLogViewerTest:Module", "relation_type": "DEPENDS_ON", "target_id": "TaskLogViewer", "target_ref": "[TaskLogViewer]" @@ -111109,476 +78831,105 @@ { "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": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "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 C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], - "anchor_syntax": "def", - "body": "// [DEF:TaskLogViewerTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: tests, task-log, viewer, mount, components\n// @PURPOSE: Unit tests for TaskLogViewer component by mounting it and observing the DOM.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [TaskLogViewer]\n// @INVARIANT: Duplicate logs are never appended. Polling only active for in-progress tasks.\n// @TEST_CONTRACT: TaskLogViewerPropsAndLogStream -> RenderedLogTimeline\n// @TEST_SCENARIO: historical_and_realtime_merge -> Historical logs render and realtime logs append without duplication.\n// @TEST_FIXTURE: valid_viewer -> INLINE_JSON\n// @TEST_EDGE: no_task_id -> Null taskId does not trigger fetch.\n// @TEST_EDGE: fetch_failure -> Network failure renders recoverable error state with retry action.\n// @TEST_EDGE: duplicate_realtime_entry -> Existing log is not duplicated when repeated in realtime stream.\n// @TEST_INVARIANT: no_duplicate_log_rows -> VERIFIED_BY: [historical_and_realtime_merge, duplicate_realtime_entry]\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { render, screen, waitFor } from '@testing-library/svelte';\nimport TaskLogViewer from '../TaskLogViewer.svelte';\nimport { getTaskLogs } from '../../services/taskService.js';\n\nvi.mock('../../services/taskService.js', () => ({\n getTaskLogs: vi.fn()\n}));\n\nconst getTaskLogsMock = vi.mocked(getTaskLogs);\n\nvi.mock('../../lib/i18n', () => ({\n t: {\n subscribe: (fn) => {\n fn({\n tasks: {\n loading: 'Loading...',\n logs_title: 'Task Logs'\n },\n common: {\n retry: 'Retry',\n close: 'Close'\n }\n });\n return () => { };\n }\n }\n}));\n\ndescribe('TaskLogViewer Component', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it('renders loading state initially', () => {\n getTaskLogsMock.mockResolvedValue([]);\n render(TaskLogViewer, { inline: true, taskId: 'task-123' });\n expect(screen.getByText('Loading...')).toBeDefined();\n });\n\n it('fetches and displays historical logs', async () => {\n getTaskLogsMock.mockResolvedValue([\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Historical log entry' }\n ]);\n\n render(TaskLogViewer, { inline: true, taskId: 'task-123' });\n\n await waitFor(() => {\n expect(screen.getByText(/Historical log entry/)).toBeDefined();\n });\n\n expect(getTaskLogs).toHaveBeenCalledWith('task-123');\n });\n\n it('displays error message on fetch failure', async () => {\n getTaskLogsMock.mockRejectedValue(new Error('Network error fetching logs'));\n\n render(TaskLogViewer, { inline: true, taskId: 'task-123' });\n\n await waitFor(() => {\n expect(screen.getByText('Network error fetching logs')).toBeDefined();\n expect(screen.getByText('Retry')).toBeDefined();\n });\n });\n\n it('appends real-time logs passed as props', async () => {\n getTaskLogsMock.mockResolvedValue([\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Historical log entry' }\n ]);\n\n const { rerender } = render(TaskLogViewer, {\n inline: true,\n taskId: 'task-123',\n realTimeLogs: []\n });\n\n await waitFor(() => {\n expect(screen.getByText(/Historical log entry/)).toBeDefined();\n });\n\n // Simulate receiving a new real-time log\n await rerender({\n inline: true,\n taskId: 'task-123',\n realTimeLogs: [\n { timestamp: '2024-01-01T00:00:01', level: 'DEBUG', message: 'Realtime log entry' }\n ]\n });\n\n await waitFor(() => {\n expect(screen.getByText(/Realtime log entry/)).toBeDefined();\n });\n });\n\n it('deduplicates real-time logs that are already in historical logs', async () => {\n getTaskLogsMock.mockResolvedValue([\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Duplicate log entry' }\n ]);\n\n const { rerender } = render(TaskLogViewer, {\n inline: true,\n taskId: 'task-123',\n realTimeLogs: []\n });\n\n await waitFor(() => {\n expect(screen.getByText(/Duplicate log entry/)).toBeDefined();\n });\n\n // Pass the exact same log as realtime\n await rerender({\n inline: true,\n taskId: 'task-123',\n realTimeLogs: [\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Duplicate log entry' }\n ]\n });\n\n // Wait a bit to ensure no explosive re-renders or double additions\n await new Promise((r) => setTimeout(r, 50));\n\n // In RTL, if there were duplicates, getAllByText would return > 1 elements.\n // getByText asserts there is exactly *one* match.\n expect(() => screen.getByText(/Duplicate log entry/)).not.toThrow();\n });\n\n // @TEST_FIXTURE: valid_viewer -> INLINE_JSON\n it('fetches and displays historical logs in modal mode under valid_viewer fixture', async () => {\n getTaskLogsMock.mockResolvedValue([\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Modal log entry' }\n ]);\n\n render(TaskLogViewer, { show: true, inline: false, taskId: 'task-123' });\n\n await waitFor(() => {\n expect(screen.getByText(/Modal log entry/)).toBeDefined();\n expect(screen.getByText('Task Logs')).toBeDefined();\n });\n\n expect(getTaskLogs).toHaveBeenCalledWith('task-123');\n });\n\n // @TEST_EDGE: no_task_id -> Null taskId does not trigger fetch.\n it('does not fetch logs if taskId is null', () => {\n render(TaskLogViewer, { inline: true, taskId: null });\n expect(getTaskLogs).not.toHaveBeenCalled();\n });\n\n // @UX_FEEDBACK\n it('passes autoScroll feedback properly down to the panel by rendering without crashing', () => {\n const { component } = render(TaskLogViewer, { inline: true, taskId: 'task-123' });\n expect(component).toBeDefined();\n });\n});\n// [/DEF:TaskLogViewerTest:Module]\n" - }, - { - "contract_id": "ProtectedRouteModule", - "contract_type": "Module", - "file_path": "frontend/src/components/auth/ProtectedRoute.svelte", - "start_line": 1, - "end_line": 144, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Unauthenticated users are redirected to /login, unauthorized users are redirected to fallbackPath, and protected slot renders only when access is verified.", - "LAYER": "UI", - "PURPOSE": "Enforces authenticated and authorized access before protected route content is rendered.", - "SEMANTICS": [ - "auth", - "route-guard", - "permission", - "redirect", - "session-validation" - ], - "TEST_CONTRACT": "[token:user:requiredPermission] -> [redirect:/login | redirect:fallbackPath | render:slot]", - "TEST_EDGE": "external_fail -> /auth/me network/API failure triggers logout and /login redirect.", - "TEST_FIXTURE": "AuthGuardStateMatrix -> INLINE_JSON", - "TEST_INVARIANT": "ProtectedRenderGate -> VERIFIED_BY: [AuthorizedRender]", - "TEST_SCENARIO": "AuthorizedRender -> Renders slot when authenticated and permission passes.", - "UX_FEEDBACK": "Spinner feedback during Loading and navigation redirect feedback on Error/Unauthorized outcomes.", - "UX_REACTIVITY": "Props are bound via $props; local mutable UI flags use $state; auth store is consumed through Svelte store subscription ($auth).", - "UX_RECOVERY": "Re-authenticate via /login after logout; retry occurs automatically on next protected navigation.", - "UX_STATE": "Success -> Protected slot content is rendered for authenticated users with valid access." - }, - "relations": [ - { - "source_id": "ProtectedRouteModule", - "relation_type": "[BINDS_TO]", - "target_id": "authStore", - "target_ref": "[authStore]" - }, - { - "source_id": "ProtectedRouteModule", - "relation_type": "[CALLS]", - "target_id": "goto", - "target_ref": "[goto]" - }, - { - "source_id": "ProtectedRouteModule", - "relation_type": "[DEPENDS_ON]", - "target_id": "Permissions", - "target_ref": "[Permissions]" - }, - { - "source_id": "ProtectedRouteModule", - "relation_type": "[CALLS]", - "target_id": "fetchApi", - "target_ref": "[fetchApi]" - } - ], - "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": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "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 C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Component" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "UX_FEEDBACK", - "message": "@UX_FEEDBACK is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Component" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "UX_REACTIVITY", - "message": "@UX_REACTIVITY is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Component" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "UX_RECOVERY", - "message": "@UX_RECOVERY is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "UX_STATE", - "message": "@UX_STATE is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Component" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "UX_STATE", - "message": "@UX_STATE is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [BINDS_TO] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[BINDS_TO]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - }, - { - "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]" - } - }, - { - "code": "invalid_relation_predicate", - "tag": "RELATION", - "message": "Predicate [CALLS] is not in allowed_predicates", - "detail": { - "allowed": [ - "DEPENDS_ON", - "CALLS", - "INHERITS", - "IMPLEMENTS", - "DISPATCHES", - "BINDS_TO", - "CALLED_BY", - "VERIFIES" - ], - "predicate": "[CALLS]" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n\n\n{#if $auth.loading || isCheckingAccess}\n
\n
\n
\n{:else if $auth.isAuthenticated && hasRouteAccess}\n {@render children?.()}\n{/if}\n\n\n\n" - }, - { - "contract_id": "ProtectedRoute", - "contract_type": "Component", - "file_path": "frontend/src/components/auth/ProtectedRoute.svelte", - "start_line": 31, - "end_line": 143, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "DATA_CONTRACT": "Input[$props.requiredPermission?: string|null, $props.fallbackPath?: string] -> Output[UIState{isCheckingAccess:boolean, hasRouteAccess:boolean}]", - "POST": "Slot renders only when $auth.isAuthenticated and hasRouteAccess are both true.", - "PRE": "auth store and navigation API are available in runtime; component is mounted in a browser context.", - "PURPOSE": "Wraps protected slot content with session and permission verification guards.", - "SIDE_EFFECT": "Performs /auth/me request, mutates auth store state, emits console instrumentation logs, and executes navigation redirects." - }, - "relations": [], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C1", + "message": "@INVARIANT is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Component" + "contract_type": "Function" + } + }, + { + "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": "POST", - "message": "@POST is forbidden for contract type 'Component' at C1", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Component" + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "Skill", + "Agent" + ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "SIDE_EFFECT", - "message": "@SIDE_EFFECT is forbidden for contract type 'Component' at C1", + "tag": "SEMANTICS", + "message": "@SEMANTICS is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Component" + "contract_type": "Function" + } + }, + { + "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 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" } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n{#if $auth.loading || isCheckingAccess}\n
\n
\n
\n{:else if $auth.isAuthenticated && hasRouteAccess}\n {@render children?.()}\n{/if}\n\n\n" + "anchor_syntax": "region", + "body": "// #region TaskLogViewerTest:Module [TYPE Function]\n// @SEMANTICS: tests, task-log, viewer, mount, components\n// @PURPOSE: Unit tests for TaskLogViewer component by mounting it and observing the DOM.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [TaskLogViewer]\n// @INVARIANT: Duplicate logs are never appended. Polling only active for in-progress tasks.\n// @TEST_CONTRACT: TaskLogViewerPropsAndLogStream -> RenderedLogTimeline\n// @TEST_SCENARIO: historical_and_realtime_merge -> Historical logs render and realtime logs append without duplication.\n// @TEST_FIXTURE: valid_viewer -> INLINE_JSON\n// @TEST_EDGE: no_task_id -> Null taskId does not trigger fetch.\n// @TEST_EDGE: fetch_failure -> Network failure renders recoverable error state with retry action.\n// @TEST_EDGE: duplicate_realtime_entry -> Existing log is not duplicated when repeated in realtime stream.\n// @TEST_INVARIANT: no_duplicate_log_rows -> VERIFIED_BY: [historical_and_realtime_merge, duplicate_realtime_entry]\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { render, screen, waitFor } from '@testing-library/svelte';\nimport TaskLogViewer from '../TaskLogViewer.svelte';\nimport { getTaskLogs } from '../../services/taskService.js';\n\nvi.mock('../../services/taskService.js', () => ({\n getTaskLogs: vi.fn()\n}));\n\nconst getTaskLogsMock = vi.mocked(getTaskLogs);\n\nvi.mock('../../lib/i18n', () => ({\n t: {\n subscribe: (fn) => {\n fn({\n tasks: {\n loading: 'Loading...',\n logs_title: 'Task Logs'\n },\n common: {\n retry: 'Retry',\n close: 'Close'\n }\n });\n return () => { };\n }\n }\n}));\n\ndescribe('TaskLogViewer Component', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it('renders loading state initially', () => {\n getTaskLogsMock.mockResolvedValue([]);\n render(TaskLogViewer, { inline: true, taskId: 'task-123' });\n expect(screen.getByText('Loading...')).toBeDefined();\n });\n\n it('fetches and displays historical logs', async () => {\n getTaskLogsMock.mockResolvedValue([\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Historical log entry' }\n ]);\n\n render(TaskLogViewer, { inline: true, taskId: 'task-123' });\n\n await waitFor(() => {\n expect(screen.getByText(/Historical log entry/)).toBeDefined();\n });\n\n expect(getTaskLogs).toHaveBeenCalledWith('task-123');\n });\n\n it('displays error message on fetch failure', async () => {\n getTaskLogsMock.mockRejectedValue(new Error('Network error fetching logs'));\n\n render(TaskLogViewer, { inline: true, taskId: 'task-123' });\n\n await waitFor(() => {\n expect(screen.getByText('Network error fetching logs')).toBeDefined();\n expect(screen.getByText('Retry')).toBeDefined();\n });\n });\n\n it('appends real-time logs passed as props', async () => {\n getTaskLogsMock.mockResolvedValue([\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Historical log entry' }\n ]);\n\n const { rerender } = render(TaskLogViewer, {\n inline: true,\n taskId: 'task-123',\n realTimeLogs: []\n });\n\n await waitFor(() => {\n expect(screen.getByText(/Historical log entry/)).toBeDefined();\n });\n\n // Simulate receiving a new real-time log\n await rerender({\n inline: true,\n taskId: 'task-123',\n realTimeLogs: [\n { timestamp: '2024-01-01T00:00:01', level: 'DEBUG', message: 'Realtime log entry' }\n ]\n });\n\n await waitFor(() => {\n expect(screen.getByText(/Realtime log entry/)).toBeDefined();\n });\n });\n\n it('deduplicates real-time logs that are already in historical logs', async () => {\n getTaskLogsMock.mockResolvedValue([\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Duplicate log entry' }\n ]);\n\n const { rerender } = render(TaskLogViewer, {\n inline: true,\n taskId: 'task-123',\n realTimeLogs: []\n });\n\n await waitFor(() => {\n expect(screen.getByText(/Duplicate log entry/)).toBeDefined();\n });\n\n // Pass the exact same log as realtime\n await rerender({\n inline: true,\n taskId: 'task-123',\n realTimeLogs: [\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Duplicate log entry' }\n ]\n });\n\n // Wait a bit to ensure no explosive re-renders or double additions\n await new Promise((r) => setTimeout(r, 50));\n\n // In RTL, if there were duplicates, getAllByText would return > 1 elements.\n // getByText asserts there is exactly *one* match.\n expect(() => screen.getByText(/Duplicate log entry/)).not.toThrow();\n });\n\n // @TEST_FIXTURE: valid_viewer -> INLINE_JSON\n it('fetches and displays historical logs in modal mode under valid_viewer fixture', async () => {\n getTaskLogsMock.mockResolvedValue([\n { timestamp: '2024-01-01T00:00:00', level: 'INFO', message: 'Modal log entry' }\n ]);\n\n render(TaskLogViewer, { show: true, inline: false, taskId: 'task-123' });\n\n await waitFor(() => {\n expect(screen.getByText(/Modal log entry/)).toBeDefined();\n expect(screen.getByText('Task Logs')).toBeDefined();\n });\n\n expect(getTaskLogs).toHaveBeenCalledWith('task-123');\n });\n\n // @TEST_EDGE: no_task_id -> Null taskId does not trigger fetch.\n it('does not fetch logs if taskId is null', () => {\n render(TaskLogViewer, { inline: true, taskId: null });\n expect(getTaskLogs).not.toHaveBeenCalled();\n });\n\n // @UX_FEEDBACK\n it('passes autoScroll feedback properly down to the panel by rendering without crashing', () => {\n const { component } = render(TaskLogViewer, { inline: true, taskId: 'task-123' });\n expect(component).toBeDefined();\n });\n});\n// #endregion TaskLogViewerTest:Module\n" }, { - "contract_id": "verifySessionAndAccess", + "contract_id": "verifySessionAndAccess:Function", "contract_type": "Function", "file_path": "frontend/src/components/auth/ProtectedRoute.svelte", - "start_line": 61, - "end_line": 128, + "start_line": 62, + "end_line": 129, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -111636,271 +78987,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:verifySessionAndAccess:Function]\n /**\n * @PURPOSE: Validates session and optional permission gate before allowing protected content render.\n * @PRE: auth store is initialized and can provide token/user state; navigation is available.\n * @POST: hasRouteAccess=true only when user identity is valid and permission check (if provided) passes.\n * @SIDE_EFFECT: Mutates auth loading/user state, performs API I/O to /auth/me, and may redirect.\n * @DATA_CONTRACT: Input[AuthState, requiredPermission, fallbackPath] -> Output[RouteDecision{login_redirect|fallback_redirect|grant}]\n */\n async function verifySessionAndAccess() {\n return belief_scope(\"ProtectedRoute.verifySessionAndAccess\", async () => {\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REASON] Starting route access verification\");\n isCheckingAccess = true;\n\n try {\n if (!$auth.token) {\n auth.setLoading(false);\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] Missing token, redirecting to /login\");\n await goto(\"/login\");\n return;\n }\n\n let currentUser = $auth.user;\n if (!currentUser) {\n auth.setLoading(true);\n try {\n const user = await api.fetchApi(\"/auth/me\");\n auth.setUser(user);\n currentUser = user;\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REASON] Session user hydrated from /auth/me\");\n } catch (error) {\n console.warn(\"[ProtectedRoute.verifySessionAndAccess][EXPLORE] Session validation failed\", { error });\n auth.logout();\n await goto(\"/login\");\n return;\n } finally {\n auth.setLoading(false);\n }\n }\n\n if (!currentUser) {\n auth.logout();\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] User unresolved, redirecting to /login\");\n await goto(\"/login\");\n return;\n }\n\n if (requiredPermission && !hasPermission(currentUser, requiredPermission, \"READ\")) {\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] Permission denied, redirecting to fallback\", {\n requiredPermission,\n fallbackPath,\n });\n hasRouteAccess = false;\n await goto(fallbackPath);\n return;\n }\n\n hasRouteAccess = true;\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] Access granted\");\n } finally {\n isCheckingAccess = false;\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] Verification cycle completed\", {\n isCheckingAccess,\n hasRouteAccess,\n });\n }\n });\n }\n // [/DEF:verifySessionAndAccess:Function]\n" + "anchor_syntax": "region", + "body": " // #region verifySessionAndAccess:Function [TYPE Function]\n /**\n * @PURPOSE: Validates session and optional permission gate before allowing protected content render.\n * @PRE: auth store is initialized and can provide token/user state; navigation is available.\n * @POST: hasRouteAccess=true only when user identity is valid and permission check (if provided) passes.\n * @SIDE_EFFECT: Mutates auth loading/user state, performs API I/O to /auth/me, and may redirect.\n * @DATA_CONTRACT: Input[AuthState, requiredPermission, fallbackPath] -> Output[RouteDecision{login_redirect|fallback_redirect|grant}]\n */\n async function verifySessionAndAccess() {\n return belief_scope(\"ProtectedRoute.verifySessionAndAccess\", async () => {\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REASON] Starting route access verification\");\n isCheckingAccess = true;\n\n try {\n if (!$auth.token) {\n auth.setLoading(false);\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] Missing token, redirecting to /login\");\n await goto(\"/login\");\n return;\n }\n\n let currentUser = $auth.user;\n if (!currentUser) {\n auth.setLoading(true);\n try {\n const user = await api.fetchApi(\"/auth/me\");\n auth.setUser(user);\n currentUser = user;\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REASON] Session user hydrated from /auth/me\");\n } catch (error) {\n console.warn(\"[ProtectedRoute.verifySessionAndAccess][EXPLORE] Session validation failed\", { error });\n auth.logout();\n await goto(\"/login\");\n return;\n } finally {\n auth.setLoading(false);\n }\n }\n\n if (!currentUser) {\n auth.logout();\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] User unresolved, redirecting to /login\");\n await goto(\"/login\");\n return;\n }\n\n if (requiredPermission && !hasPermission(currentUser, requiredPermission, \"READ\")) {\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] Permission denied, redirecting to fallback\", {\n requiredPermission,\n fallbackPath,\n });\n hasRouteAccess = false;\n await goto(fallbackPath);\n return;\n }\n\n hasRouteAccess = true;\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] Access granted\");\n } finally {\n isCheckingAccess = false;\n console.info(\"[ProtectedRoute.verifySessionAndAccess][REFLECT] Verification cycle completed\", {\n isCheckingAccess,\n hasRouteAccess,\n });\n }\n });\n }\n // #endregion verifySessionAndAccess:Function\n" }, { - "contract_id": "BackupList", - "contract_type": "Component", - "file_path": "frontend/src/components/backups/BackupList.svelte", - "start_line": 1, - "end_line": 144, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "LAYER": "Component", - "PURPOSE": "Displays a list of existing backups.", - "SEMANTICS": [ - "backup", - "list", - "table" - ] - }, - "relations": [ - { - "source_id": "BackupList", - "relation_type": "USED_BY", - "target_id": "frontend/src/components/backups/BackupManager.svelte", - "target_ref": "frontend/src/components/backups/BackupManager.svelte" - } - ], - "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "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": "def", - "body": "\n\n\n\n\n\n
\n onNavigate('backups')}\n class=\"hover:text-indigo-600\"\n >\n {$t.storage.root}\n \n {#each getPathParts(currentPath).slice(1) as part, index}\n /\n navigateToPathIndex(index + 1)}\n class=\"hover:text-indigo-600\"\n >\n {part}\n \n {/each}\n
\n\n
\n \n {$t.common?.back}\n \n
\n\n
\n \n \n \n \n \n \n \n \n \n \n {#each backups as backup}\n \n \n \n \n \n \n {:else}\n \n \n \n {/each}\n \n
\n {$t.storage.table.name}\n \n {$t.tasks.target_env}\n \n {$t.storage.table.created_at}\n \n {$t.storage.table.actions}\n
\n {#if isDirectory(backup)}\n onNavigate(backup.path)}\n class=\"text-indigo-600 hover:text-indigo-900\"\n >\n {backup.name}\n \n {:else}\n {backup.name}\n {/if}\n \n {backup.environment}\n \n {formatCreatedAt(backup)}\n \n {#if isDirectory(backup)}\n onNavigate(backup.path)}\n >\n {$t.storage.table.go_to_storage}\n \n {/if}\n
\n {$t.storage.no_files}\n
\n
\n\n\n\n\n" - }, - { - "contract_id": "BackupManager", - "contract_type": "Component", - "file_path": "frontend/src/components/backups/BackupManager.svelte", - "start_line": 1, - "end_line": 281, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Only one backup task can be triggered at a time from the UI.", - "LAYER": "Feature", - "PURPOSE": "Main container for backup management, handling creation and listing.", - "SEMANTICS": [ - "backup", - "manager", - "orchestrator" - ] - }, - "relations": [ - { - "source_id": "BackupManager", - "relation_type": "USES", - "target_id": "BackupList", - "target_ref": "BackupList" - }, - { - "source_id": "BackupManager", - "relation_type": "USES", - "target_id": "api", - "target_ref": "api" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "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": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "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": "def", - "body": "\n\n\n\n\n\n
\n \n
\n
\n
\n ({ value: e.id, label: e.name }))\n ]}\n />\n
\n \n {creating ? $t.common.loading : $t.tasks.start_backup}\n \n
\n\n {#if selectedEnvId}\n
\n

\n \n \n \n {$t.tasks.backup_schedule}\n

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

{$t.tasks.cron_hint}

\n
\n\n
\n \n {#if savingSchedule}\n \n \n \n \n \n {$t.common.loading}\n \n {:else}\n {$t.common.save}\n {/if}\n \n
\n
\n
\n
\n {/if}\n
\n
\n\n
\n

{$t.storage.backups}

\n {#if loading}\n
{$t.common.loading}
\n {:else}\n \n {/if}\n
\n
\n\n\n\n" - }, - { - "contract_id": "handleCreateBackup", + "contract_id": "handleCreateBackup:Function", "contract_type": "Function", "file_path": "frontend/src/components/backups/BackupManager.svelte", - "start_line": 104, - "end_line": 165, + "start_line": 106, + "end_line": 167, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -111955,15 +79050,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleCreateBackup:Function]\n /**\n * @purpose Triggers a new backup task for the selected environment.\n *\n * @pre selectedEnvId must be a valid environment ID.\n * @post A new task is created on the backend.\n *\n * @returns {Promise}\n * @side_effect Dispatches a toast notification.\n */\n // @RELATION: CALLS -> api.createTask\n // [DEF:handleUpdateSchedule:Function]\n /**\n * @purpose Updates the backup schedule for the selected environment.\n * @pre selectedEnvId must be set.\n * @post Environment config is updated on the backend.\n */\n async function handleUpdateSchedule() {\n if (!selectedEnvId) return;\n\n console.log(`[BackupManager][Action] Updating schedule for env: ${selectedEnvId}`);\n savingSchedule = true;\n try {\n await api.updateEnvironmentSchedule(selectedEnvId, {\n enabled: scheduleEnabled,\n cron_expression: cronExpression\n });\n addToast($t.common.success, 'success');\n \n // Update local state\n environments = environments.map(e =>\n e.id === selectedEnvId\n ? { ...e, backup_schedule: { enabled: scheduleEnabled, cron_expression: cronExpression } }\n : e\n );\n } catch (error) {\n console.error(\"[BackupManager][Coherence:Failed] Schedule update failed\", error);\n } finally {\n savingSchedule = false;\n }\n }\n // [/DEF:handleUpdateSchedule:Function]\n\n async function handleCreateBackup() {\n if (!selectedEnvId) {\n addToast($t.tasks.select_env, 'error');\n return;\n }\n\n console.log(`[BackupManager][Action] Triggering backup for env: ${selectedEnvId}`);\n creating = true;\n try {\n await api.createTask('superset-backup', { environment_id: selectedEnvId });\n addToast($t.common.success, 'success');\n console.log(\"[BackupManager][Coherence:OK] Backup task triggered.\");\n } catch (error) {\n console.error(\"[BackupManager][Coherence:Failed] Create failed\", error);\n } finally {\n creating = false;\n }\n }\n // [/DEF:handleCreateBackup:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleCreateBackup:Function [TYPE Function]\n /**\n * @purpose Triggers a new backup task for the selected environment.\n *\n * @pre selectedEnvId must be a valid environment ID.\n * @post A new task is created on the backend.\n *\n * @returns {Promise}\n * @side_effect Dispatches a toast notification.\n */\n // @RELATION: CALLS -> api.createTask\n // #region handleUpdateSchedule:Function [TYPE Function]\n /**\n * @purpose Updates the backup schedule for the selected environment.\n * @pre selectedEnvId must be set.\n * @post Environment config is updated on the backend.\n */\n async function handleUpdateSchedule() {\n if (!selectedEnvId) return;\n\n console.log(`[BackupManager][Action] Updating schedule for env: ${selectedEnvId}`);\n savingSchedule = true;\n try {\n await api.updateEnvironmentSchedule(selectedEnvId, {\n enabled: scheduleEnabled,\n cron_expression: cronExpression\n });\n addToast($t.common.success, 'success');\n \n // Update local state\n environments = environments.map(e =>\n e.id === selectedEnvId\n ? { ...e, backup_schedule: { enabled: scheduleEnabled, cron_expression: cronExpression } }\n : e\n );\n } catch (error) {\n console.error(\"[BackupManager][Coherence:Failed] Schedule update failed\", error);\n } finally {\n savingSchedule = false;\n }\n }\n // #endregion handleUpdateSchedule:Function\n\n async function handleCreateBackup() {\n if (!selectedEnvId) {\n addToast($t.tasks.select_env, 'error');\n return;\n }\n\n console.log(`[BackupManager][Action] Triggering backup for env: ${selectedEnvId}`);\n creating = true;\n try {\n await api.createTask('superset-backup', { environment_id: selectedEnvId });\n addToast($t.common.success, 'success');\n console.log(\"[BackupManager][Coherence:OK] Backup task triggered.\");\n } catch (error) {\n console.error(\"[BackupManager][Coherence:Failed] Create failed\", error);\n } finally {\n creating = false;\n }\n }\n // #endregion handleCreateBackup:Function\n" }, { - "contract_id": "handleUpdateSchedule", + "contract_id": "handleUpdateSchedule:Function", "contract_type": "Function", "file_path": "frontend/src/components/backups/BackupManager.svelte", - "start_line": 115, - "end_line": 145, + "start_line": 117, + "end_line": 147, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112001,127 +79096,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleUpdateSchedule:Function]\n /**\n * @purpose Updates the backup schedule for the selected environment.\n * @pre selectedEnvId must be set.\n * @post Environment config is updated on the backend.\n */\n async function handleUpdateSchedule() {\n if (!selectedEnvId) return;\n\n console.log(`[BackupManager][Action] Updating schedule for env: ${selectedEnvId}`);\n savingSchedule = true;\n try {\n await api.updateEnvironmentSchedule(selectedEnvId, {\n enabled: scheduleEnabled,\n cron_expression: cronExpression\n });\n addToast($t.common.success, 'success');\n \n // Update local state\n environments = environments.map(e =>\n e.id === selectedEnvId\n ? { ...e, backup_schedule: { enabled: scheduleEnabled, cron_expression: cronExpression } }\n : e\n );\n } catch (error) {\n console.error(\"[BackupManager][Coherence:Failed] Schedule update failed\", error);\n } finally {\n savingSchedule = false;\n }\n }\n // [/DEF:handleUpdateSchedule:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleUpdateSchedule:Function [TYPE Function]\n /**\n * @purpose Updates the backup schedule for the selected environment.\n * @pre selectedEnvId must be set.\n * @post Environment config is updated on the backend.\n */\n async function handleUpdateSchedule() {\n if (!selectedEnvId) return;\n\n console.log(`[BackupManager][Action] Updating schedule for env: ${selectedEnvId}`);\n savingSchedule = true;\n try {\n await api.updateEnvironmentSchedule(selectedEnvId, {\n enabled: scheduleEnabled,\n cron_expression: cronExpression\n });\n addToast($t.common.success, 'success');\n \n // Update local state\n environments = environments.map(e =>\n e.id === selectedEnvId\n ? { ...e, backup_schedule: { enabled: scheduleEnabled, cron_expression: cronExpression } }\n : e\n );\n } catch (error) {\n console.error(\"[BackupManager][Coherence:Failed] Schedule update failed\", error);\n } finally {\n savingSchedule = false;\n }\n }\n // #endregion handleUpdateSchedule:Function\n" }, { - "contract_id": "BranchSelector", - "contract_type": "Component", - "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 1, - "end_line": 180, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "LAYER": "Component", - "PURPOSE": "UI для выбора и создания веток Git.", - "SEMANTICS": [ - "git", - "branch", - "selection", - "checkout" - ] - }, - "relations": [ - { - "source_id": "BranchSelector", - "relation_type": "CALLS", - "target_id": "gitService.getBranches", - "target_ref": "gitService.getBranches" - }, - { - "source_id": "BranchSelector", - "relation_type": "CALLS", - "target_id": "GitServiceBranch", - "target_ref": "GitServiceBranch" - }, - { - "source_id": "BranchSelector", - "relation_type": "CALLS", - "target_id": "gitService.createBranch", - "target_ref": "gitService.createBranch" - }, - { - "source_id": "BranchSelector", - "relation_type": "BINDS_TO", - "target_id": "onchange", - "target_ref": "onchange" - } - ], - "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n
\n
\n
\n ({ value: b.name, label: b.name }))}\n />\n
\n\n showCreate = !showCreate}\n disabled={loading}\n class=\"text-blue-600\"\n >\n + {$t.git.new_branch}\n \n
\n\n {#if showCreate}\n
\n
\n \n
\n \n {$t.git.create}\n \n showCreate = false}\n disabled={loading}\n >\n {$t.common.cancel}\n \n
\n {/if}\n
\n\n\n\n" - }, - { - "contract_id": "loadBranches", + "contract_id": "loadBranches:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 48, - "end_line": 67, + "start_line": 50, + "end_line": 69, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112159,15 +79142,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:loadBranches:Function]\n /**\n * @purpose Загружает список веток для дашборда.\n * @pre dashboardId is provided.\n * @post branches обновлен.\n */\n async function loadBranches() {\n console.log(`[BranchSelector][Action] Loading branches for dashboard ${dashboardId}`);\n loading = true;\n try {\n branches = await gitService.getBranches(dashboardId, envId);\n console.log(`[BranchSelector][Coherence:OK] Loaded ${branches.length} branches`);\n } catch (e) {\n console.error(`[BranchSelector][Coherence:Failed] ${e.message}`);\n toast($t.git?.load_branches_failed, 'error');\n } finally {\n loading = false;\n }\n }\n // [/DEF:loadBranches:Function]\n" + "anchor_syntax": "region", + "body": " // #region loadBranches:Function [TYPE Function]\n /**\n * @purpose Загружает список веток для дашборда.\n * @pre dashboardId is provided.\n * @post branches обновлен.\n */\n async function loadBranches() {\n console.log(`[BranchSelector][Action] Loading branches for dashboard ${dashboardId}`);\n loading = true;\n try {\n branches = await gitService.getBranches(dashboardId, envId);\n console.log(`[BranchSelector][Coherence:OK] Loaded ${branches.length} branches`);\n } catch (e) {\n console.error(`[BranchSelector][Coherence:Failed] ${e.message}`);\n toast($t.git?.load_branches_failed, 'error');\n } finally {\n loading = false;\n }\n }\n // #endregion loadBranches:Function\n" }, { - "contract_id": "handleSelect", + "contract_id": "handleSelect:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 69, - "end_line": 78, + "start_line": 71, + "end_line": 80, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112205,15 +79188,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleSelect:Function]\n /**\n * @purpose Handles branch selection from dropdown.\n * @pre event contains branch name.\n * @post handleCheckout is called with selected branch.\n */\n function handleSelect(event) {\n handleCheckout(event.target.value);\n }\n // [/DEF:handleSelect:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleSelect:Function [TYPE Function]\n /**\n * @purpose Handles branch selection from dropdown.\n * @pre event contains branch name.\n * @post handleCheckout is called with selected branch.\n */\n function handleSelect(event) {\n handleCheckout(event.target.value);\n }\n // #endregion handleSelect:Function\n" }, { - "contract_id": "handleCheckout", + "contract_id": "handleCheckout:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 80, - "end_line": 99, + "start_line": 82, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112248,15 +79231,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleCheckout:Function]\n /**\n * @purpose Переключает текущую ветку.\n * @param {string} branchName - Имя ветки.\n * @post currentBranch обновлен, родительский callback вызван.\n */\n async function handleCheckout(branchName) {\n console.log(`[BranchSelector][Action] Checking out branch ${branchName}`);\n try {\n await gitService.checkoutBranch(dashboardId, branchName, envId);\n currentBranch = branchName;\n onchange({ branch: branchName });\n toast($t.git?.switched_to?.replace('{branch}', branchName), 'success');\n console.log(`[BranchSelector][Coherence:OK] Checked out ${branchName}`);\n } catch (e) {\n console.error(`[BranchSelector][Coherence:Failed] ${e.message}`);\n toast(e.message, 'error');\n }\n }\n // [/DEF:handleCheckout:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleCheckout:Function [TYPE Function]\n /**\n * @purpose Переключает текущую ветку.\n * @param {string} branchName - Имя ветки.\n * @post currentBranch обновлен, родительский callback вызван.\n */\n async function handleCheckout(branchName) {\n console.log(`[BranchSelector][Action] Checking out branch ${branchName}`);\n try {\n await gitService.checkoutBranch(dashboardId, branchName, envId);\n currentBranch = branchName;\n onchange({ branch: branchName });\n toast($t.git?.switched_to?.replace('{branch}', branchName), 'success');\n console.log(`[BranchSelector][Coherence:OK] Checked out ${branchName}`);\n } catch (e) {\n console.error(`[BranchSelector][Coherence:Failed] ${e.message}`);\n toast(e.message, 'error');\n }\n }\n // #endregion handleCheckout:Function\n" }, { - "contract_id": "handleCreate", + "contract_id": "handleCreate:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 101, - "end_line": 122, + "start_line": 103, + "end_line": 124, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112294,109 +79277,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleCreate:Function]\n /**\n * @purpose Создает новую ветку.\n * @pre newBranchName is not empty.\n * @post Новая ветка создана и загружена; showCreate reset.\n */\n async function handleCreate() {\n if (!newBranchName) return;\n console.log(`[BranchSelector][Action] Creating branch ${newBranchName} from ${currentBranch}`);\n try {\n await gitService.createBranch(dashboardId, newBranchName, currentBranch, envId);\n toast($t.git?.created_branch?.replace('{branch}', newBranchName), 'success');\n showCreate = false;\n newBranchName = '';\n await loadBranches();\n console.log(`[BranchSelector][Coherence:OK] Branch created`);\n } catch (e) {\n console.error(`[BranchSelector][Coherence:Failed] ${e.message}`);\n toast(e.message, 'error');\n }\n }\n // [/DEF:handleCreate:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleCreate:Function [TYPE Function]\n /**\n * @purpose Создает новую ветку.\n * @pre newBranchName is not empty.\n * @post Новая ветка создана и загружена; showCreate reset.\n */\n async function handleCreate() {\n if (!newBranchName) return;\n console.log(`[BranchSelector][Action] Creating branch ${newBranchName} from ${currentBranch}`);\n try {\n await gitService.createBranch(dashboardId, newBranchName, currentBranch, envId);\n toast($t.git?.created_branch?.replace('{branch}', newBranchName), 'success');\n showCreate = false;\n newBranchName = '';\n await loadBranches();\n console.log(`[BranchSelector][Coherence:OK] Branch created`);\n } catch (e) {\n console.error(`[BranchSelector][Coherence:Failed] ${e.message}`);\n toast(e.message, 'error');\n }\n }\n // #endregion handleCreate:Function\n" }, { - "contract_id": "CommitHistory", - "contract_type": "Component", - "file_path": "frontend/src/components/git/CommitHistory.svelte", - "start_line": 1, - "end_line": 99, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "LAYER": "Component", - "PURPOSE": "Displays the commit history for a specific dashboard.", - "SEMANTICS": [ - "git", - "history", - "commits", - "audit" - ] - }, - "relations": [ - { - "source_id": "CommitHistory", - "relation_type": "CALLS", - "target_id": "gitService.getHistory", - "target_ref": "gitService.getHistory" - } - ], - "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n
\n
\n

\n {$t.git.history}\n

\n \n
\n\n {#if loading}\n
\n
\n
\n {:else if history.length === 0}\n

{$t.git.no_commits}

\n {:else}\n
\n {#each history as commit}\n
\n
\n {commit.message}\n {commit.hash.substring(0, 7)}\n
\n
\n {commit.author} • {new Date(commit.timestamp).toLocaleString()}\n
\n
\n {/each}\n
\n {/if}\n
\n\n\n\n" - }, - { - "contract_id": "loadHistory", + "contract_id": "loadHistory:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/CommitHistory.svelte", - "start_line": 42, - "end_line": 61, + "start_line": 44, + "end_line": 63, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112434,94 +79323,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:loadHistory:Function]\n /**\n * @purpose Fetch commit history from the backend.\n * @pre dashboardId is valid.\n * @post history state is updated.\n */\n async function loadHistory() {\n console.log(`[CommitHistory][Action] Loading history for dashboard ${dashboardId}`);\n loading = true;\n try {\n history = await gitService.getHistory(dashboardId, 50, envId);\n console.log(`[CommitHistory][Coherence:OK] Loaded ${history.length} commits`);\n } catch (e) {\n console.error(`[CommitHistory][Coherence:Failed] ${e.message}`);\n toast('Failed to load commit history', 'error');\n } finally {\n loading = false;\n }\n }\n // [/DEF:loadHistory:Function]\n" + "anchor_syntax": "region", + "body": " // #region loadHistory:Function [TYPE Function]\n /**\n * @purpose Fetch commit history from the backend.\n * @pre dashboardId is valid.\n * @post history state is updated.\n */\n async function loadHistory() {\n console.log(`[CommitHistory][Action] Loading history for dashboard ${dashboardId}`);\n loading = true;\n try {\n history = await gitService.getHistory(dashboardId, 50, envId);\n console.log(`[CommitHistory][Coherence:OK] Loaded ${history.length} commits`);\n } catch (e) {\n console.error(`[CommitHistory][Coherence:Failed] ${e.message}`);\n toast('Failed to load commit history', 'error');\n } finally {\n loading = false;\n }\n }\n // #endregion loadHistory:Function\n" }, { - "contract_id": "CommitModal", - "contract_type": "Component", - "file_path": "frontend/src/components/git/CommitModal.svelte", - "start_line": 1, - "end_line": 268, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "Component", - "PURPOSE": "Модальное окно для создания коммита с просмотром изменений (diff).", - "SEMANTICS": [ - "git", - "commit", - "modal", - "version_control", - "diff" - ], - "UX_STATE": "Submitting -> Commit actions stay disabled while the commit request is in flight." - }, - "relations": [ - { - "source_id": "CommitModal", - "relation_type": "CALLS", - "target_id": "GitServiceClient", - "target_ref": "[GitServiceClient]" - } - ], - "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_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" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n{#if show}\n \n \n

{$t.git?.commit}

\n\n
\n \n
\n
\n
\n {$t.git?.commit_message}\n \n {#if generatingMessage}\n {$t.mapper?.generating}\n {:else}\n {$t.git?.generate_with_ai}\n {/if}\n \n
\n \n
\n\n {#if status}\n
\n \n {$t.git?.changed_files}\n \n
    \n {#each status.staged_files as file}\n \n S\n {file}\n \n {/each}\n {#each status.modified_files as file}\n \n M\n {file}\n \n {/each}\n {#each status.untracked_files as file}\n \n ?\n {file}\n \n {/each}\n
\n
\n {/if}\n
\n\n \n \n \n {$t.git?.changes_preview}\n
\n
\n {#if loading}\n \n {$t.git?.loading_diff}\n
\n {:else if diff}\n {diff}\n {:else}\n \n {$t.git?.no_changes}\n \n {/if}\n \n \n \n\n
\n (show = false)}\n class=\"px-4 py-2 text-gray-600 hover:bg-gray-100 rounded\"\n >\n {$t.common?.cancel}\n \n \n {committing ? $t.git?.committing : $t.git?.commit}\n \n
\n \n \n{/if}\n\n\n\n" - }, - { - "contract_id": "handleGenerateMessage", + "contract_id": "handleGenerateMessage:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/CommitModal.svelte", - "start_line": 36, - "end_line": 59, + "start_line": 37, + "end_line": 60, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112539,15 +79349,51 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleGenerateMessage:Function]\n /**\n * @purpose Generates a commit message using LLM.\n */\n async function handleGenerateMessage() {\n generatingMessage = true;\n try {\n console.log(\n `[CommitModal][Action] Generating commit message for dashboard ${dashboardId}`,\n );\n // postApi returns the JSON data directly or throws an error\n const data = await api.postApi(\n `/git/repositories/${encodeURIComponent(String(dashboardId))}/generate-message${envId ? `?env_id=${encodeURIComponent(String(envId))}` : \"\"}`,\n );\n message = data.message;\n toast($t.git?.commit_message_generated, \"success\");\n } catch (e) {\n console.error(`[CommitModal][Coherence:Failed] ${e.message}`);\n toast(e.message || $t.git?.commit_message_failed, \"error\");\n } finally {\n generatingMessage = false;\n }\n }\n // [/DEF:handleGenerateMessage:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "handleCommit", + "contract_id": "loadStatus:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/CommitModal.svelte", - "start_line": 104, - "end_line": 129, + "start_line": 62, + "end_line": 103, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "PRE": "dashboardId должен быть валидным.", + "PURPOSE": "Загружает текущий статус репозитория и diff." + }, + "relations": [], + "schema_warnings": [ + { + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "body": " // #region loadStatus:Function [TYPE Function]\n /**\n * @purpose Загружает текущий статус репозитория и diff.\n * @pre dashboardId должен быть валидным.\n */\n async function loadStatus() {\n if (!dashboardId || !show) return;\n loading = true;\n try {\n console.log(\n `[CommitModal][Action] Loading status and diff for ${dashboardId}`,\n );\n status = await gitService.getStatus(dashboardId, envId);\n // Fetch both unstaged and staged diffs to show complete picture\n const unstagedDiff = await gitService.getDiff(\n dashboardId,\n null,\n false,\n envId,\n );\n const stagedDiff = await gitService.getDiff(\n dashboardId,\n null,\n true,\n envId,\n );\n\n diff = \"\";\n if (stagedDiff)\n diff += \"--- STAGED CHANGES ---\\n\" + stagedDiff + \"\\n\\n\";\n if (unstagedDiff)\n diff += \"--- UNSTAGED CHANGES ---\\n\" + unstagedDiff;\n\n if (!diff) diff = \"\";\n } catch (e) {\n console.error(`[CommitModal][Coherence:Failed] ${e.message}`);\n toast($t.git?.load_changes_failed, \"error\");\n } finally {\n loading = false;\n }\n }\n // #endregion loadStatus:Function\n" + }, + { + "contract_id": "handleCommit:Function", + "contract_type": "Function", + "file_path": "frontend/src/components/git/CommitModal.svelte", + "start_line": 105, + "end_line": 130, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112585,243 +79431,29 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleCommit:Function]\n /**\n * @purpose Создает коммит с указанным сообщением.\n * @pre message не должно быть пустым.\n * @post Коммит создан и модальное окно закрыто.\n */\n async function handleCommit() {\n if (!message) return;\n console.log(\n `[CommitModal][Action] Committing changes for dashboard ${dashboardId}`,\n );\n committing = true;\n try {\n await gitService.commit(dashboardId, message, [], envId);\n toast($t.git?.commit_success, \"success\");\n show = false;\n message = \"\";\n console.log(`[CommitModal][Coherence:OK] Committed`);\n } catch (e) {\n console.error(`[CommitModal][Coherence:Failed] ${e.message}`);\n toast(e.message, \"error\");\n } finally {\n committing = false;\n }\n }\n // [/DEF:handleCommit:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleCommit:Function [TYPE Function]\n /**\n * @purpose Создает коммит с указанным сообщением.\n * @pre message не должно быть пустым.\n * @post Коммит создан и модальное окно закрыто.\n */\n async function handleCommit() {\n if (!message) return;\n console.log(\n `[CommitModal][Action] Committing changes for dashboard ${dashboardId}`,\n );\n committing = true;\n try {\n await gitService.commit(dashboardId, message, [], envId);\n toast($t.git?.commit_success, \"success\");\n show = false;\n message = \"\";\n console.log(`[CommitModal][Coherence:OK] Committed`);\n } catch (e) {\n console.error(`[CommitModal][Coherence:Failed] ${e.message}`);\n toast(e.message, \"error\");\n } finally {\n committing = false;\n }\n }\n // #endregion handleCommit:Function\n" }, { - "contract_id": "ConflictResolver", - "contract_type": "Component", - "file_path": "frontend/src/components/git/ConflictResolver.svelte", - "start_line": 1, - "end_line": 179, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "User must resolve all conflicts before saving.", - "LAYER": "Component", - "PURPOSE": "UI for resolving merge conflicts (Keep Mine / Keep Theirs).", - "SEMANTICS": [ - "git", - "conflict", - "resolution", - "merge" - ] - }, - "relations": [ - { - "source_id": "ConflictResolver", - "relation_type": "BINDS_TO", - "target_id": "onresolve", - "target_ref": "onresolve" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "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": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n{#if show}\n \n \n

\n Merge Conflicts Detected\n

\n

\n The following files have conflicts. Please choose how to resolve\n them.\n

\n\n
\n {#each conflicts as conflict}\n
\n \n {conflict.file_path}\n {#if resolutions[conflict.file_path]}\n \n Resolved: {resolutions[conflict.file_path]}\n \n {/if}\n
\n \n
\n \n Your Changes (Mine)\n
\n
\n {conflict.mine}\n
\n \n resolve(conflict.file_path, \"mine\")}\n >\n Keep Mine\n \n
\n
\n \n Remote Changes (Theirs)\n
\n
\n {conflict.theirs}\n
\n \n resolve(conflict.file_path, \"theirs\")}\n >\n Keep Theirs\n \n \n \n \n {/each}\n \n\n
\n (show = false)}\n class=\"px-4 py-2 text-gray-600 hover:bg-gray-100 rounded transition-colors\"\n >\n Cancel\n \n \n Resolve & Continue\n \n
\n \n \n{/if}\n\n\n\n" - }, - { - "contract_id": "DeploymentModal", - "contract_type": "Component", + "contract_id": "loadStatus:Watcher", + "contract_type": "Function", "file_path": "frontend/src/components/git/DeploymentModal.svelte", - "start_line": 1, - "end_line": 238, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "INVARIANT": "Cannot deploy without a selected environment.", - "LAYER": "Component", - "PURPOSE": "Modal for deploying a dashboard to a target environment.", - "SEMANTICS": [ - "deployment", - "git", - "environment", - "modal" - ] - }, - "relations": [ - { - "source_id": "DeploymentModal", - "relation_type": "CALLS", - "target_id": "GitService", - "target_ref": "GitService" - }, - { - "source_id": "DeploymentModal", - "relation_type": "DISPATCHES", - "target_id": "deploy", - "target_ref": "deploy" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "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": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n{#if show}\n \n
\n

{$t.git?.deploy}

\n\n {#if loading}\n

{$t.migration?.loading_envs}

\n {:else if deploymentCandidates.length === 0}\n

\n {$t.git?.no_deploy_envs}\n

\n
\n (show = false)}\n class=\"px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300\"\n >\n {$t.common?.close}\n \n
\n {:else}\n {#if normalizedPreferredStage}\n

\n GitFlow target stage: {normalizedPreferredStage}\n

\n {/if}\n
\n
\n {#if isProdTarget}\n

\n Внимание: выбрано PROD окружение. Потребуется подтверждение перед deploy.\n

\n {/if}\n\n
\n (show = false)}\n class=\"px-4 py-2 text-gray-600 hover:bg-gray-100 rounded\"\n >\n {$t.common?.cancel}\n \n \n {#if deploying}\n \n \n \n \n {$t.git?.deploying}\n {:else}\n {$t.git?.deploy}\n {/if}\n \n
\n {/if}\n
\n \n{/if}\n\n\n\n" - }, - { - "contract_id": "loadStatus", - "contract_type": "Watcher", - "file_path": "frontend/src/components/git/DeploymentModal.svelte", - "start_line": 47, - "end_line": 51, + "start_line": 49, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": " // [DEF:loadStatus:Watcher]\n $effect(() => {\n if (show) loadEnvironments();\n });\n // [/DEF:loadStatus:Watcher]\n" + "anchor_syntax": "region", + "body": " // #region loadStatus:Watcher [TYPE Function]\n $effect(() => {\n if (show) loadEnvironments();\n });\n // #endregion loadStatus:Watcher\n" }, { - "contract_id": "normalizeEnvStage", + "contract_id": "normalizeEnvStage:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/DeploymentModal.svelte", - "start_line": 53, - "end_line": 64, + "start_line": 55, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112849,15 +79481,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:normalizeEnvStage:Function]\n /**\n * @purpose Normalize environment stage with legacy production fallback.\n * @post Returns DEV/PREPROD/PROD.\n */\n 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 // [/DEF:normalizeEnvStage:Function]\n" + "anchor_syntax": "region", + "body": " // #region normalizeEnvStage:Function [TYPE Function]\n /**\n * @purpose Normalize environment stage with legacy production fallback.\n * @post Returns DEV/PREPROD/PROD.\n */\n 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 // #endregion normalizeEnvStage:Function\n" }, { - "contract_id": "resolveEnvUrl", + "contract_id": "resolveEnvUrl:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/DeploymentModal.svelte", - "start_line": 66, - "end_line": 74, + "start_line": 68, + "end_line": 76, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112885,15 +79517,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:resolveEnvUrl:Function]\n /**\n * @purpose Resolve environment URL from consolidated or git-specific payload shape.\n * @post Returns stable URL string.\n */\n function resolveEnvUrl(env) {\n return String(env?.superset_url || env?.url || \"\");\n }\n // [/DEF:resolveEnvUrl:Function]\n" + "anchor_syntax": "region", + "body": " // #region resolveEnvUrl:Function [TYPE Function]\n /**\n * @purpose Resolve environment URL from consolidated or git-specific payload shape.\n * @post Returns stable URL string.\n */\n function resolveEnvUrl(env) {\n return String(env?.superset_url || env?.url || \"\");\n }\n // #endregion resolveEnvUrl:Function\n" }, { - "contract_id": "loadEnvironments", + "contract_id": "loadEnvironments:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/DeploymentModal.svelte", - "start_line": 76, - "end_line": 104, + "start_line": 78, + "end_line": 106, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112931,15 +79563,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:loadEnvironments:Function]\n /**\n * @purpose Fetch available environments from API.\n * @post environments state is populated.\n * @side_effect Updates environments state.\n */\n async function loadEnvironments() {\n console.log(`[DeploymentModal][Action] Loading environments`);\n loading = true;\n try {\n environments = await api.getEnvironmentsList();\n const candidates = (environments || []).filter((env) => env?.id !== envId);\n if (normalizedPreferredStage) {\n const stageMatched = candidates.filter((env) => normalizeEnvStage(env) === normalizedPreferredStage);\n selectedEnv = (stageMatched[0]?.id) || (candidates[0]?.id) || \"\";\n } else {\n selectedEnv = (candidates[0]?.id) || \"\";\n }\n console.log(\n `[DeploymentModal][Coherence:OK] Loaded ${environments.length} environments`,\n );\n } catch (e) {\n console.error(`[DeploymentModal][Coherence:Failed] ${e.message}`);\n toast($t.migration?.loading_envs_failed, \"error\");\n } finally {\n loading = false;\n }\n }\n // [/DEF:loadEnvironments:Function]\n" + "anchor_syntax": "region", + "body": " // #region loadEnvironments:Function [TYPE Function]\n /**\n * @purpose Fetch available environments from API.\n * @post environments state is populated.\n * @side_effect Updates environments state.\n */\n async function loadEnvironments() {\n console.log(`[DeploymentModal][Action] Loading environments`);\n loading = true;\n try {\n environments = await api.getEnvironmentsList();\n const candidates = (environments || []).filter((env) => env?.id !== envId);\n if (normalizedPreferredStage) {\n const stageMatched = candidates.filter((env) => normalizeEnvStage(env) === normalizedPreferredStage);\n selectedEnv = (stageMatched[0]?.id) || (candidates[0]?.id) || \"\";\n } else {\n selectedEnv = (candidates[0]?.id) || \"\";\n }\n console.log(\n `[DeploymentModal][Coherence:OK] Loaded ${environments.length} environments`,\n );\n } catch (e) {\n console.error(`[DeploymentModal][Coherence:Failed] ${e.message}`);\n toast($t.migration?.loading_envs_failed, \"error\");\n } finally {\n loading = false;\n }\n }\n // #endregion loadEnvironments:Function\n" }, { - "contract_id": "handleDeploy", + "contract_id": "handleDeploy:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/DeploymentModal.svelte", - "start_line": 106, - "end_line": 140, + "start_line": 108, + "end_line": 142, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -112987,159 +79619,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleDeploy: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 // [/DEF:handleDeploy:Function]\n" + "anchor_syntax": "region", + "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": "GitManager", - "contract_type": "Component", - "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 1, - "end_line": 1212, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "LAYER": "Component", - "PURPOSE": "Центральный UI управления Git с фокусом на рабочий поток аналитика (commit -> promote -> deploy).", - "SEMANTICS": [ - "git", - "manager", - "dashboard", - "version_control", - "tabs", - "workflow" - ] - }, - "relations": [ - { - "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": "CALLS", - "target_id": "gitService", - "target_ref": "gitService" - } - ], - "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "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": "def", - "body": "\n\n\n\n\n\n{#if show}\n {\n if (event.key === 'Escape') closeModal();\n }}\n role=\"button\"\n tabindex=\"0\"\n aria-label={$t.common?.close || 'Close'}\n >\n \n \n \n \n \n \n\n \n
slug: {dashboardId}
\n
\n\n {#if checkingStatus}\n
\n
\n
\n {:else if !initialized}\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 {:else}\n
\n {#if hasOriginConfigMismatch}\n
\n
Git server mismatch detected
\n
\n Configured server host: {configHost}, repository origin host: {originHost}.\n Next push will auto-realign origin host to configured server.\n
\n
\n {/if}\n
\n
\n \n {currentEnvStage || 'DEV'}\n \n Текущая ветка: {currentBranch}\n
\n
\n \n
\n
\n\n
\n (activeTab = 'workspace')}\n >\n 📝 Фиксация изменений\n \n (activeTab = 'release')}\n >\n 🚀 Релиз\n \n (activeTab = 'operations')}\n >\n ⚙️ Серверные операции\n \n
\n\n {#if activeTab === 'workspace'}\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 if workspaceDiff}\n
{workspaceDiff}
\n {:else}\n
\n Нет изменений для коммита\n
\n {/if}\n
\n
\n
\n {:else if activeTab === 'release'}\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 {:else}\n
\n \n ⬇ Pull\n \n \n ⬆ Push{#if workspaceStatus?.ahead_count > 0} ({workspaceStatus.ahead_count}){/if}\n \n \n 🚀 Deploy\n \n
\n {/if}\n
\n {/if}\n \n\n {#if showUnfinishedMergeDialog && 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{/if}\n\n\n\n\n\n\n\n" - }, - { - "contract_id": "stageBadgeClass", + "contract_id": "stageBadgeClass:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 110, - "end_line": 120, + "start_line": 112, + "end_line": 122, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113167,15 +79655,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:stageBadgeClass: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 // [/DEF:stageBadgeClass:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "resolveCurrentEnvironmentId", + "contract_id": "resolveCurrentEnvironmentId:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 122, - "end_line": 132, + "start_line": 124, + "end_line": 134, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113203,15 +79691,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:resolveCurrentEnvironmentId: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 // [/DEF:resolveCurrentEnvironmentId:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "applyGitflowStageDefaults", + "contract_id": "applyGitflowStageDefaults:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 134, - "end_line": 157, + "start_line": 136, + "end_line": 159, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113239,15 +79727,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:applyGitflowStageDefaults: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 // [/DEF:applyGitflowStageDefaults:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "loadCurrentEnvironmentStage", + "contract_id": "loadCurrentEnvironmentStage:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 159, - "end_line": 177, + "start_line": 161, + "end_line": 179, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113275,15 +79763,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:loadCurrentEnvironmentStage: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 // [/DEF:loadCurrentEnvironmentStage:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "isNumericDashboardRef", + "contract_id": "isNumericDashboardRef:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 179, - "end_line": 187, + "start_line": 181, + "end_line": 189, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113311,15 +79799,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:isNumericDashboardRef: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 // [/DEF:isNumericDashboardRef:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "checkStatus", + "contract_id": "checkStatus:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 189, - "end_line": 215, + "start_line": 191, + "end_line": 217, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113347,15 +79835,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:checkStatus: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 // [/DEF:checkStatus:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "loadWorkspace:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 217, - "end_line": 237, + "start_line": 219, + "end_line": 239, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113383,15 +79871,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:loadWorkspace: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 // [/DEF:loadWorkspace:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handleSync:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 239, - "end_line": 261, + "start_line": 241, + "end_line": 263, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113419,15 +79907,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleSync: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 // [/DEF:handleSync:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handlePull:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 310, - "end_line": 507, + "start_line": 312, + "end_line": 509, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113455,15 +79943,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handlePull: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 // [/DEF:handlePull:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handlePush:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 509, - "end_line": 526, + "start_line": 511, + "end_line": 528, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113491,15 +79979,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handlePush: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 // [/DEF:handlePush:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handlePromote:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 528, - "end_line": 576, + "start_line": 530, + "end_line": 578, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113527,15 +80015,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handlePromote: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 // [/DEF:handlePromote:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "openDeployModal:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 578, - "end_line": 594, + "start_line": 580, + "end_line": 596, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113563,15 +80051,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:openDeployModal: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 // [/DEF:openDeployModal:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "getSelectedConfig:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 596, - "end_line": 604, + "start_line": 598, + "end_line": 606, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113599,15 +80087,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:getSelectedConfig: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 // [/DEF:getSelectedConfig:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "resolveDefaultConfig:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 606, - "end_line": 620, + "start_line": 608, + "end_line": 622, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113635,15 +80123,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:resolveDefaultConfig: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 // [/DEF:resolveDefaultConfig:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "resolvePushProviderLabel:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 622, - "end_line": 634, + "start_line": 624, + "end_line": 636, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113671,15 +80159,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:resolvePushProviderLabel: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 // [/DEF:resolvePushProviderLabel:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "extractHttpHost:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 636, - "end_line": 652, + "start_line": 638, + "end_line": 654, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113707,15 +80195,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:extractHttpHost: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 // [/DEF:extractHttpHost:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "buildSuggestedRepoName:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 654, - "end_line": 667, + "start_line": 656, + "end_line": 669, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113743,15 +80231,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:buildSuggestedRepoName: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 // [/DEF:buildSuggestedRepoName:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handleCreateRemoteRepo:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 669, - "end_line": 707, + "start_line": 671, + "end_line": 709, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113779,15 +80267,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleCreateRemoteRepo: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 // [/DEF:handleCreateRemoteRepo:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handleInit:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 709, - "end_line": 733, + "start_line": 711, + "end_line": 735, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113815,15 +80303,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleInit: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 // [/DEF:handleInit:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "closeModal:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 735, - "end_line": 743, + "start_line": 737, + "end_line": 745, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113851,15 +80339,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:closeModal:Function]\n /**\n * @purpose Close git manager modal.\n * @post show=false.\n */\n function closeModal() {\n show = false;\n }\n // [/DEF:closeModal:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "handleBackdropClick:Function", "contract_type": "Function", "file_path": "frontend/src/components/git/GitManager.svelte", - "start_line": 745, - "end_line": 755, + "start_line": 747, + "end_line": 757, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -113887,19 +80375,18 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleBackdropClick: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 // [/DEF:handleBackdropClick:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "GitManagerUnfinishedMergeIntegrationTest", - "contract_type": "Module", + "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": 43, - "tier": "TIER_2", - "complexity": 3, + "end_line": 42, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "UI Tests", "PURPOSE": "Protect unresolved-merge dialog contract in GitManager pull flow.", "SEMANTICS": [ @@ -113911,26 +80398,90 @@ }, "relations": [ { - "source_id": "GitManagerUnfinishedMergeIntegrationTest", + "source_id": "GitManagerUnfinishedMergeIntegrationTest:Module", "relation_type": "DEPENDS_ON", "target_id": "GitManager", "target_ref": "[GitManager]" } ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "// [DEF:GitManagerUnfinishedMergeIntegrationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: git-manager, unfinished-merge, dialog, integration-test\n// @PURPOSE: Protect unresolved-merge dialog contract in GitManager pull flow.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [GitManager]\n\nimport { describe, it, expect } from 'vitest';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst COMPONENT_PATH = path.resolve(\n process.cwd(),\n 'src/components/git/GitManager.svelte',\n);\n\ndescribe('GitManager unfinished merge dialog contract', () => {\n it('keeps 409 unfinished-merge detection and WebUI dialog recovery flow in pull handler', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('Number(error?.status) !== 409');\n expect(source).toContain(\"payload.error_code !== 'GIT_UNFINISHED_MERGE'\");\n expect(source).toContain('function openUnfinishedMergeDialogFromError(error)');\n expect(source).toContain('showUnfinishedMergeDialog = true;');\n expect(source).toContain('const handledByDialog = openUnfinishedMergeDialogFromError(e);');\n expect(source).toContain('await loadMergeRecoveryState();');\n });\n\n it('renders unresolved-merge dialog details and web recovery actions', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('{#if showUnfinishedMergeDialog && unfinishedMergeContext}');\n expect(source).toContain('unfinishedMergeContext.repositoryPath');\n expect(source).toContain('unfinishedMergeContext.currentBranch');\n expect(source).toContain('unfinishedMergeContext.commands.join');\n expect(source).toContain('handleCopyUnfinishedMergeCommands');\n expect(source).toContain('handleOpenConflictResolver');\n expect(source).toContain('handleAbortUnfinishedMerge');\n expect(source).toContain('handleContinueUnfinishedMerge');\n expect(source).toContain('$t.git?.unfinished_merge?.copy_commands');\n });\n});\n// [/DEF:GitManagerUnfinishedMergeIntegrationTest:Module]\n" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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" + } + } + ], + "anchor_syntax": "region", + "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" }, { "contract_id": "DocPreview", "contract_type": "Component", "file_path": "frontend/src/components/llm/DocPreview.svelte", "start_line": 1, - "end_line": 88, + "end_line": 89, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "UI component for previewing generated dataset documentation before saving.", "TYPE": "{Object}", @@ -113974,19 +80525,19 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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", "contract_type": "Component", "file_path": "frontend/src/components/llm/ProviderConfig.svelte", "start_line": 1, - "end_line": 535, + "end_line": 536, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "UI form for managing LLM provider configurations.", "UX_STATE": "Loading -> Default" @@ -114023,134 +80574,37 @@ } } ], - "anchor_syntax": "def", - "body": "\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 \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 \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" + "anchor_syntax": "region", + "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 \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 \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": "ProviderConfigIntegrationTest", - "contract_type": "Module", - "file_path": "frontend/src/components/llm/__tests__/provider_config.integration.test.js", - "start_line": 1, - "end_line": 61, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Edit action keeps explicit click handler and opens normalized edit form.", - "LAYER": "UI Tests", - "PURPOSE": "Protect edit and delete interaction contracts in LLM provider settings UI.", - "SEMANTICS": [ - "llm", - "provider-config", - "integration-test", - "edit-flow", - "delete-flow" - ] - }, - "relations": [ - { - "source_id": "ProviderConfigIntegrationTest", - "relation_type": "DEPENDS_ON", - "target_id": "ProviderConfig", - "target_ref": "[ProviderConfig]" - } - ], - "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" - } - } - ], - "anchor_syntax": "def", - "body": "// [DEF:ProviderConfigIntegrationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: llm, provider-config, integration-test, edit-flow, delete-flow\n// @PURPOSE: Protect edit and delete interaction contracts in LLM provider settings UI.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [ProviderConfig]\n// @INVARIANT: Edit action keeps explicit click handler and opens normalized edit form.\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/llm/ProviderConfig.svelte',\n);\n\n// [DEF:provider_config_edit_contract_tests:Function]\n// @RELATION: BINDS_TO -> [ProviderConfigIntegrationTest]\n// @COMPLEXITY: 3\n// @PURPOSE: Validate edit and delete handler wiring plus normalized edit form state mapping.\n// @PRE: ProviderConfig component source exists in expected path.\n// @POST: Contract checks ensure edit click cannot degrade into no-op flow.\ndescribe('ProviderConfig edit interaction contract', () => {\n it('keeps explicit edit click handler with guarded button semantics', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('type=\"button\"');\n expect(source).toContain('onclick={() => handleEdit(provider)}');\n });\n\n it('normalizes provider payload into editable form shape', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('formData = {');\n expect(source).toContain('name: provider?.name ?? \"\"');\n expect(source).toContain('provider_type: provider?.provider_type ?? \"openai\"');\n expect(source).toContain('default_model: provider?.default_model ?? \"gpt-4o\"');\n expect(source).toContain('showForm = true;');\n });\n\n it('keeps explicit delete flow with confirmation and delete request', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('async function handleDelete(provider)');\n expect(source).toContain('$t.llm.delete_confirm.replace(\"{name}\", provider.name || provider.id)');\n expect(source).toContain('await requestApi(`/llm/providers/${provider.id}`, \"DELETE\")');\n expect(source).toContain('onclick={() => handleDelete(provider)}');\n });\n\n it('does not forward masked api_key when toggling provider activation', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('const updatePayload = {');\n expect(source).toContain('provider_type: provider.provider_type');\n expect(source).toContain('default_model: provider.default_model');\n expect(source).not.toContain('await requestApi(`/llm/providers/${provider.id}`, \"PUT\", {\\n ...provider,');\n });\n});\n// [/DEF:provider_config_edit_contract_tests:Function]\n// [/DEF:ProviderConfigIntegrationTest:Module]\n" - }, - { - "contract_id": "provider_config_edit_contract_tests", - "contract_type": "Function", - "file_path": "frontend/src/components/llm/__tests__/provider_config.integration.test.js", - "start_line": 18, - "end_line": 60, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "Contract checks ensure edit click cannot degrade into no-op flow.", - "PRE": "ProviderConfig component source exists in expected path.", - "PURPOSE": "Validate edit and delete handler wiring plus normalized edit form state mapping." - }, - "relations": [ - { - "source_id": "provider_config_edit_contract_tests", - "relation_type": "BINDS_TO", - "target_id": "ProviderConfigIntegrationTest", - "target_ref": "[ProviderConfigIntegrationTest]" - } - ], - "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": "def", - "body": "// [DEF:provider_config_edit_contract_tests:Function]\n// @RELATION: BINDS_TO -> [ProviderConfigIntegrationTest]\n// @COMPLEXITY: 3\n// @PURPOSE: Validate edit and delete handler wiring plus normalized edit form state mapping.\n// @PRE: ProviderConfig component source exists in expected path.\n// @POST: Contract checks ensure edit click cannot degrade into no-op flow.\ndescribe('ProviderConfig edit interaction contract', () => {\n it('keeps explicit edit click handler with guarded button semantics', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('type=\"button\"');\n expect(source).toContain('onclick={() => handleEdit(provider)}');\n });\n\n it('normalizes provider payload into editable form shape', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('formData = {');\n expect(source).toContain('name: provider?.name ?? \"\"');\n expect(source).toContain('provider_type: provider?.provider_type ?? \"openai\"');\n expect(source).toContain('default_model: provider?.default_model ?? \"gpt-4o\"');\n expect(source).toContain('showForm = true;');\n });\n\n it('keeps explicit delete flow with confirmation and delete request', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('async function handleDelete(provider)');\n expect(source).toContain('$t.llm.delete_confirm.replace(\"{name}\", provider.name || provider.id)');\n expect(source).toContain('await requestApi(`/llm/providers/${provider.id}`, \"DELETE\")');\n expect(source).toContain('onclick={() => handleDelete(provider)}');\n });\n\n it('does not forward masked api_key when toggling provider activation', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('const updatePayload = {');\n expect(source).toContain('provider_type: provider.provider_type');\n expect(source).toContain('default_model: provider.default_model');\n expect(source).not.toContain('await requestApi(`/llm/providers/${provider.id}`, \"PUT\", {\\n ...provider,');\n });\n});\n// [/DEF:provider_config_edit_contract_tests:Function]\n" - }, - { - "contract_id": "FileList", + "contract_id": "ValidationReport", "contract_type": "Component", - "file_path": "frontend/src/components/storage/FileList.svelte", + "file_path": "frontend/src/components/llm/ValidationReport.svelte", "start_line": 1, - "end_line": 161, + "end_line": 82, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "EVENTS": "ondelete/onnavigate callback props - Raised when a file is deleted or navigation is requested.", + "COMPLEXITY": 3, "LAYER": "UI", - "PROPS": "files (Array) - List of StoredFile objects.", - "PURPOSE": "Displays a table of files with metadata and actions.", - "SEMANTICS": [ - "storage", - "files", - "list", - "table" - ], - "UX_STATE": "Loading -> Default" + "PURPOSE": "Displays the results of an LLM-based dashboard validation task." }, "relations": [ { - "source_id": "FileList", + "source_id": "ValidationReport", "relation_type": "DEPENDS_ON", - "target_id": "storageService", - "target_ref": "storageService" + "target_id": "DashboardValidationService", + "target_ref": "[DashboardValidationService]" + }, + { + "source_id": "ValidationReport", + "relation_type": "CALLS", + "target_id": "api_module", + "target_ref": "[api_module]" } ], "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "EVENTS", - "message": "@EVENTS is not defined in axiom_config.yaml tags", - "detail": null - }, { "code": "tag_not_for_contract_type", "tag": "LAYER", @@ -114174,17 +80628,93 @@ } }, { - "code": "unknown_tag", - "tag": "PROPS", - "message": "@PROPS is not defined in axiom_config.yaml tags", - "detail": null + "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", + "body": "\n\n\n\n\n\n\n\n\n{#if result}\n
\n
\n

{$t.llm?.validation_report_title}

\n \n {result.status}\n \n
\n\n
\n

{$t.reports?.summary}

\n

{result.summary}

\n
\n\n {#if result.issues && result.issues.length > 0}\n
\n

{$t.tasks?.issues}

\n
\n \n \n \n \n \n \n \n \n \n {#each result.issues as issue}\n \n \n \n \n \n {/each}\n \n
{$t.reports?.severity}{$t.reports?.message}{$t.tasks?.location}
\n \n {issue.severity}\n \n {issue.message}{issue.location || $t.common?.not_available}
\n
\n
\n {/if}\n\n {#if result.screenshot_path}\n
\n

{$t.dashboard?.screenshot_strategy}

\n {$t.dashboard?.dashboard_validation}\n
\n {/if}\n
\n{:else}\n
\n

{$t.llm?.no_validation_result}

\n
\n{/if}\n\n\n" + }, + { + "contract_id": "ProviderConfigIntegrationTest:Module", + "contract_type": "Function", + "file_path": "frontend/src/components/llm/__tests__/provider_config.integration.test.js", + "start_line": 1, + "end_line": 59, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "INVARIANT": "Edit action keeps explicit click handler and opens normalized edit form.", + "LAYER": "UI Tests", + "PURPOSE": "Protect edit and delete interaction contracts in LLM provider settings UI.", + "SEMANTICS": [ + "llm", + "provider-config", + "integration-test", + "edit-flow", + "delete-flow" + ] + }, + "relations": [ + { + "source_id": "ProviderConfigIntegrationTest:Module", + "relation_type": "DEPENDS_ON", + "target_id": "ProviderConfig", + "target_ref": "[ProviderConfig]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } }, { "code": "tag_not_for_contract_type", "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Component'", + "message": "@SEMANTICS is not allowed for contract type 'Function'", "detail": { - "actual_type": "Component", + "actual_type": "Function", "allowed_types": [ "Module", "Block", @@ -114196,22 +80726,93 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", + "message": "@SEMANTICS is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Component" + "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" } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n
\n \n \n \n \n \n \n \n \n \n \n \n {#each files as file}\n \n \n \n \n \n \n \n {:else}\n \n \n \n {/each}\n \n
{$t.storage.table.name}{$t.storage.table.category}{$t.storage.table.size}{$t.storage.table.created_at}{$t.storage.table.actions}
\n {#if isDirectory(file)}\n onnavigate(file.path)}\n class=\"flex items-center text-indigo-600 hover:text-indigo-900\"\n >\n \n \n \n {file.name}\n \n {:else}\n
\n \n \n \n {file.name}\n
\n {/if}\n
{file.category}\n {isDirectory(file) ? '--' : formatSize(file.size)}\n {formatDate(file.created_at)}\n {#if !isDirectory(file)}\n handleDownload(file)}\n class=\"text-indigo-600 hover:text-indigo-900 mr-4\"\n >\n {$t.storage.table.download}\n \n {/if}\n ondelete({ category: file.category, path: file.path, name: file.name })}\n class=\"text-red-600 hover:text-red-900\"\n >\n {$t.storage.table.delete}\n \n
\n {$t.storage.no_files}\n
\n
\n\n\n\n\n" + "anchor_syntax": "region", + "body": "// #region ProviderConfigIntegrationTest:Module [TYPE Function]\n// @SEMANTICS: llm, provider-config, integration-test, edit-flow, delete-flow\n// @PURPOSE: Protect edit and delete interaction contracts in LLM provider settings UI.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [ProviderConfig]\n// @INVARIANT: Edit action keeps explicit click handler and opens normalized edit form.\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/llm/ProviderConfig.svelte',\n);\n\n// #region provider_config_edit_contract_tests:Function [TYPE Function]\n// @RELATION: BINDS_TO -> [ProviderConfigIntegrationTest]\n// @PURPOSE: Validate edit and delete handler wiring plus normalized edit form state mapping.\n// @PRE: ProviderConfig component source exists in expected path.\n// @POST: Contract checks ensure edit click cannot degrade into no-op flow.\ndescribe('ProviderConfig edit interaction contract', () => {\n it('keeps explicit edit click handler with guarded button semantics', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('type=\"button\"');\n expect(source).toContain('onclick={() => handleEdit(provider)}');\n });\n\n it('normalizes provider payload into editable form shape', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('formData = {');\n expect(source).toContain('name: provider?.name ?? \"\"');\n expect(source).toContain('provider_type: provider?.provider_type ?? \"openai\"');\n expect(source).toContain('default_model: provider?.default_model ?? \"gpt-4o\"');\n expect(source).toContain('showForm = true;');\n });\n\n it('keeps explicit delete flow with confirmation and delete request', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('async function handleDelete(provider)');\n expect(source).toContain('$t.llm.delete_confirm.replace(\"{name}\", provider.name || provider.id)');\n expect(source).toContain('await requestApi(`/llm/providers/${provider.id}`, \"DELETE\")');\n expect(source).toContain('onclick={() => handleDelete(provider)}');\n });\n\n it('does not forward masked api_key when toggling provider activation', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('const updatePayload = {');\n expect(source).toContain('provider_type: provider.provider_type');\n expect(source).toContain('default_model: provider.default_model');\n expect(source).not.toContain('await requestApi(`/llm/providers/${provider.id}`, \"PUT\", {\\n ...provider,');\n });\n});\n// #endregion provider_config_edit_contract_tests:Function\n// #endregion ProviderConfigIntegrationTest:Module\n" }, { - "contract_id": "isDirectory", + "contract_id": "provider_config_edit_contract_tests:Function", + "contract_type": "Function", + "file_path": "frontend/src/components/llm/__tests__/provider_config.integration.test.js", + "start_line": 17, + "end_line": 58, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "Contract checks ensure edit click cannot degrade into no-op flow.", + "PRE": "ProviderConfig component source exists in expected path.", + "PURPOSE": "Validate edit and delete handler wiring plus normalized edit form state mapping." + }, + "relations": [ + { + "source_id": "provider_config_edit_contract_tests:Function", + "relation_type": "BINDS_TO", + "target_id": "ProviderConfigIntegrationTest", + "target_ref": "[ProviderConfigIntegrationTest]" + } + ], + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + } + ], + "anchor_syntax": "region", + "body": "// #region provider_config_edit_contract_tests:Function [TYPE Function]\n// @RELATION: BINDS_TO -> [ProviderConfigIntegrationTest]\n// @PURPOSE: Validate edit and delete handler wiring plus normalized edit form state mapping.\n// @PRE: ProviderConfig component source exists in expected path.\n// @POST: Contract checks ensure edit click cannot degrade into no-op flow.\ndescribe('ProviderConfig edit interaction contract', () => {\n it('keeps explicit edit click handler with guarded button semantics', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('type=\"button\"');\n expect(source).toContain('onclick={() => handleEdit(provider)}');\n });\n\n it('normalizes provider payload into editable form shape', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('formData = {');\n expect(source).toContain('name: provider?.name ?? \"\"');\n expect(source).toContain('provider_type: provider?.provider_type ?? \"openai\"');\n expect(source).toContain('default_model: provider?.default_model ?? \"gpt-4o\"');\n expect(source).toContain('showForm = true;');\n });\n\n it('keeps explicit delete flow with confirmation and delete request', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('async function handleDelete(provider)');\n expect(source).toContain('$t.llm.delete_confirm.replace(\"{name}\", provider.name || provider.id)');\n expect(source).toContain('await requestApi(`/llm/providers/${provider.id}`, \"DELETE\")');\n expect(source).toContain('onclick={() => handleDelete(provider)}');\n });\n\n it('does not forward masked api_key when toggling provider activation', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('const updatePayload = {');\n expect(source).toContain('provider_type: provider.provider_type');\n expect(source).toContain('default_model: provider.default_model');\n expect(source).not.toContain('await requestApi(`/llm/providers/${provider.id}`, \"PUT\", {\\n ...provider,');\n });\n});\n// #endregion provider_config_edit_contract_tests:Function\n" + }, + { + "contract_id": "isDirectory:Function", "contract_type": "Function", "file_path": "frontend/src/components/storage/FileList.svelte", - "start_line": 28, - "end_line": 40, + "start_line": 29, + "end_line": 41, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -114263,15 +80864,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " // [DEF:isDirectory:Function]\n /**\n * @purpose Checks if a file object represents a directory.\n * @pre file object has mime_type property.\n * @post Returns boolean.\n * @param {Object} file - The file object to check.\n * @return {boolean} True if it's a directory, false otherwise.\n */\n function isDirectory(file) {\n console.log(\"[isDirectory][Action] Checking file type\");\n return file.mime_type === 'directory';\n }\n // [/DEF:isDirectory:Function]\n" + "anchor_syntax": "region", + "body": " // #region isDirectory:Function [TYPE Function]\n /**\n * @purpose Checks if a file object represents a directory.\n * @pre file object has mime_type property.\n * @post Returns boolean.\n * @param {Object} file - The file object to check.\n * @return {boolean} True if it's a directory, false otherwise.\n */\n function isDirectory(file) {\n console.log(\"[isDirectory][Action] Checking file type\");\n return file.mime_type === 'directory';\n }\n // #endregion isDirectory:Function\n" }, { - "contract_id": "formatSize", + "contract_id": "formatSize:Function", "contract_type": "Function", "file_path": "frontend/src/components/storage/FileList.svelte", - "start_line": 42, - "end_line": 58, + "start_line": 43, + "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -114323,15 +80924,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " // [DEF:formatSize:Function]\n /**\n * @purpose Formats file size in bytes into a human-readable string.\n * @pre bytes is a number.\n * @post Returns formatted string.\n * @param {number} bytes - The size in bytes.\n * @return {string} Formatted size (e.g., \"1.2 MB\").\n */\n function formatSize(bytes) {\n console.log(`[formatSize][Action] Formatting ${bytes} bytes`);\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n }\n // [/DEF:formatSize:Function]\n" + "anchor_syntax": "region", + "body": " // #region formatSize:Function [TYPE Function]\n /**\n * @purpose Formats file size in bytes into a human-readable string.\n * @pre bytes is a number.\n * @post Returns formatted string.\n * @param {number} bytes - The size in bytes.\n * @return {string} Formatted size (e.g., \"1.2 MB\").\n */\n function formatSize(bytes) {\n console.log(`[formatSize][Action] Formatting ${bytes} bytes`);\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n }\n // #endregion formatSize:Function\n" }, { - "contract_id": "formatDate", + "contract_id": "formatDate:Function", "contract_type": "Function", "file_path": "frontend/src/components/storage/FileList.svelte", - "start_line": 60, - "end_line": 72, + "start_line": 61, + "end_line": 73, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -114383,15 +80984,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " // [DEF:formatDate:Function]\n /**\n * @purpose Formats an ISO date string into a localized readable format.\n * @pre dateStr is a valid date string.\n * @post Returns localized string.\n * @param {string} dateStr - The date string to format.\n * @return {string} Localized date and time.\n */\n function formatDate(dateStr) {\n console.log(\"[formatDate][Action] Formatting date string\");\n return new Date(dateStr).toLocaleString();\n }\n // [/DEF:formatDate:Function]\n" + "anchor_syntax": "region", + "body": " // #region formatDate:Function [TYPE Function]\n /**\n * @purpose Formats an ISO date string into a localized readable format.\n * @pre dateStr is a valid date string.\n * @post Returns localized string.\n * @param {string} dateStr - The date string to format.\n * @return {string} Localized date and time.\n */\n function formatDate(dateStr) {\n console.log(\"[formatDate][Action] Formatting date string\");\n return new Date(dateStr).toLocaleString();\n }\n // #endregion formatDate:Function\n" }, { - "contract_id": "handleDownload", + "contract_id": "handleDownload:Function", "contract_type": "Function", "file_path": "frontend/src/components/storage/FileList.svelte", - "start_line": 74, - "end_line": 87, + "start_line": 75, + "end_line": 88, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -114429,106 +81030,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleDownload:Function]\n /**\n * @purpose Downloads selected file through authenticated API request.\n * @pre file is a non-directory storage entry with category/path.\n * @post Browser download starts or user sees toast on failure.\n */\n async function handleDownload(file) {\n try {\n await downloadFile(file.category, file.path, file.name);\n } catch (error) {\n addToast(error?.message || 'Download failed', 'error');\n }\n }\n // [/DEF:handleDownload:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleDownload:Function [TYPE Function]\n /**\n * @purpose Downloads selected file through authenticated API request.\n * @pre file is a non-directory storage entry with category/path.\n * @post Browser download starts or user sees toast on failure.\n */\n async function handleDownload(file) {\n try {\n await downloadFile(file.category, file.path, file.name);\n } catch (error) {\n addToast(error?.message || 'Download failed', 'error');\n }\n }\n // #endregion handleDownload:Function\n" }, { - "contract_id": "FileUpload", - "contract_type": "Component", - "file_path": "frontend/src/components/storage/FileUpload.svelte", - "start_line": 1, - "end_line": 139, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "EVENTS": "onuploaded callback prop - Invoked when a file is successfully uploaded.", - "LAYER": "UI", - "PROPS": "None", - "PURPOSE": "Provides a form for uploading files to a specific category.", - "SEMANTICS": [ - "storage", - "upload", - "files" - ], - "UX_STATE": "Loading -> Default" - }, - "relations": [ - { - "source_id": "FileUpload", - "relation_type": "DEPENDS_ON", - "target_id": "storageService", - "target_ref": "storageService" - } - ], - "schema_warnings": [ - { - "code": "unknown_tag", - "tag": "EVENTS", - "message": "@EVENTS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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": "unknown_tag", - "tag": "PROPS", - "message": "@PROPS is not defined in axiom_config.yaml tags", - "detail": null - }, - { - "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" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n
\n

{$t.storage.upload_title}

\n \n
\n
\n \n \n \n \n \n
\n\n
{ event.preventDefault(); dragOver = true; }}\n ondragleave={(event) => { event.preventDefault(); dragOver = false; }}\n ondrop={(event) => { event.preventDefault(); handleDrop(event); }}\n >\n
\n \n \n \n
\n \n

{$t.storage.drag_drop}

\n
\n

{$t.storage.supported_formats}

\n
\n
\n\n {#if isUploading}\n
\n
\n {$t.storage.uploading}\n
\n {/if}\n
\n
\n\n\n\n\n" - }, - { - "contract_id": "handleUpload", + "contract_id": "handleUpload:Function", "contract_type": "Function", "file_path": "frontend/src/components/storage/FileUpload.svelte", - "start_line": 22, - "end_line": 60, + "start_line": 23, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -114566,15 +81076,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleUpload:Function]\n /**\n * @purpose Handles the file upload process.\n * @pre A file must be selected in the file input.\n * @post The file is uploaded to the server and a success toast is shown.\n */\n let fileInput;\n let {\n category = 'backups',\n path = '',\n onuploaded = () => {},\n } = $props();\n\n let isUploading = $state(false);\n let dragOver = $state(false);\n\n async function handleUpload() {\n const file = fileInput.files[0];\n if (!file) return;\n\n isUploading = true;\n try {\n // path is relative to root, but upload endpoint expects path within category\n // FileList.path is like \"backup/folder\", we need just \"folder\"\n const subpath = path.startsWith(category)\n ? path.substring(category.length).replace(/^\\/+/, '')\n : path;\n\n await uploadFile(file, category, subpath);\n addToast($t.storage.messages.upload_success.replace('{name}', file.name), 'success');\n fileInput.value = '';\n onuploaded();\n } catch (error) {\n addToast($t.storage.messages.upload_failed.replace('{error}', error.message), 'error');\n } finally {\n isUploading = false;\n }\n }\n // [/DEF:handleUpload:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleUpload:Function [TYPE Function]\n /**\n * @purpose Handles the file upload process.\n * @pre A file must be selected in the file input.\n * @post The file is uploaded to the server and a success toast is shown.\n */\n let fileInput;\n let {\n category = 'backups',\n path = '',\n onuploaded = () => {},\n } = $props();\n\n let isUploading = $state(false);\n let dragOver = $state(false);\n\n async function handleUpload() {\n const file = fileInput.files[0];\n if (!file) return;\n\n isUploading = true;\n try {\n // path is relative to root, but upload endpoint expects path within category\n // FileList.path is like \"backup/folder\", we need just \"folder\"\n const subpath = path.startsWith(category)\n ? path.substring(category.length).replace(/^\\/+/, '')\n : path;\n\n await uploadFile(file, category, subpath);\n addToast($t.storage.messages.upload_success.replace('{name}', file.name), 'success');\n fileInput.value = '';\n onuploaded();\n } catch (error) {\n addToast($t.storage.messages.upload_failed.replace('{error}', error.message), 'error');\n } finally {\n isUploading = false;\n }\n }\n // #endregion handleUpload:Function\n" }, { - "contract_id": "handleDrop", + "contract_id": "handleDrop:Function", "contract_type": "Function", "file_path": "frontend/src/components/storage/FileUpload.svelte", - "start_line": 62, - "end_line": 76, + "start_line": 63, + "end_line": 77, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -114599,19 +81109,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleDrop:Function]\n /**\n * @purpose Handles the file drop event for drag-and-drop.\n * @param {DragEvent} event - The drop event.\n */\n function handleDrop(event) {\n event.preventDefault();\n dragOver = false;\n const files = event.dataTransfer.files;\n if (files.length > 0) {\n fileInput.files = files;\n handleUpload();\n }\n }\n // [/DEF:handleDrop:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleDrop:Function [TYPE Function]\n /**\n * @purpose Handles the file drop event for drag-and-drop.\n * @param {DragEvent} event - The drop event.\n */\n function handleDrop(event) {\n event.preventDefault();\n dragOver = false;\n const files = event.dataTransfer.files;\n if (files.length > 0) {\n fileInput.files = files;\n handleUpload();\n }\n }\n // #endregion handleDrop:Function\n" }, { "contract_id": "LogEntryRow", "contract_type": "Component", "file_path": "frontend/src/components/tasks/LogEntryRow.svelte", "start_line": 1, - "end_line": 102, + "end_line": 103, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Renders a single log entry with stacked layout optimized for narrow drawer panels.", "SEMANTICS": [ @@ -114702,15 +81212,15 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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", + "contract_id": "formatTime:Function", "contract_type": "Function", "file_path": "frontend/src/components/tasks/LogEntryRow.svelte", - "start_line": 15, - "end_line": 27, + "start_line": 16, + "end_line": 28, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -114728,19 +81238,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:formatTime:Function]\n /** @PURPOSE Format ISO timestamp to HH:MM:SS */\n function formatTime(timestamp) {\n if (!timestamp) return \"\";\n const date = new Date(timestamp);\n return date.toLocaleTimeString(\"en-US\", {\n hour12: false,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n }\n // [/DEF:formatTime:Function]\n" + "anchor_syntax": "region", + "body": " // #region formatTime:Function [TYPE Function]\n /** @PURPOSE Format ISO timestamp to HH:MM:SS */\n function formatTime(timestamp) {\n if (!timestamp) return \"\";\n const date = new Date(timestamp);\n return date.toLocaleTimeString(\"en-US\", {\n hour12: false,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n }\n // #endregion formatTime:Function\n" }, { "contract_id": "LogFilterBar", "contract_type": "Component", "file_path": "frontend/src/components/tasks/LogFilterBar.svelte", "start_line": 1, - "end_line": 142, + "end_line": 143, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Compact filter toolbar for logs — level, source, and text search in a single dense row.", "SEMANTICS": [ @@ -114823,19 +81333,19 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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" }, { "contract_id": "TaskLogPanel", "contract_type": "Component", "file_path": "frontend/src/components/tasks/TaskLogPanel.svelte", "start_line": 1, - "end_line": 151, + "end_line": 152, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "INVARIANT": "Must always display logs in chronological order and respect auto-scroll preference.", "LAYER": "UI", "PURPOSE": "Combines log filtering and display into a single cohesive dark-themed panel.", @@ -114954,32 +81464,29 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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": "ConnectionForm", + "contract_id": "TaskResultPanel", "contract_type": "Component", - "file_path": "frontend/src/components/tools/ConnectionForm.svelte", + "file_path": "frontend/src/components/tasks/TaskResultPanel.svelte", "start_line": 1, - "end_line": 93, + "end_line": 123, "tier": "TIER_1", - "complexity": 1, + "complexity": 2, "metadata": { + "COMPLEXITY": 2, "LAYER": "UI", - "PURPOSE": "UI component for creating a new database connection configuration.", - "SEMANTICS": [ - "connection", - "form", - "settings" - ] + "PURPOSE": "Displays formatted task result summary with status badges and result details.", + "UX_STATE": "Loaded -> Task result displayed with status color coding." }, "relations": [ { - "source_id": "ConnectionForm", - "relation_type": "USES", - "target_id": "frontend/src/services/connectionService.js", - "target_ref": "frontend/src/services/connectionService.js" + "source_id": "TaskResultPanel", + "relation_type": "DEPENDS_ON", + "target_id": "i18n", + "target_ref": "[i18n]" } ], "schema_warnings": [ @@ -114999,84 +81506,35 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", + "message": "@LAYER is forbidden for contract type 'Component' at C2", "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 2, "contract_type": "Component" } }, { "code": "tag_forbidden_by_complexity", "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", + "message": "@RELATION is forbidden for contract type 'Component' at C2", "detail": { - "actual_complexity": 1, + "actual_complexity": 2, "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" - } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n
{ event.preventDefault(); handleSubmit(); }} class=\"space-y-6\">\n \n \n
\n \n \n
\n\n \n\n
\n \n \n
\n\n
\n \n
\n
\n
\n\n\n" + "anchor_syntax": "region", + "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", "contract_type": "Component", "file_path": "frontend/src/components/tools/ConnectionList.svelte", "start_line": 1, - "end_line": 86, - "tier": "TIER_1", - "complexity": 1, + "end_line": 88, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "UI component for listing and deleting saved database connection configurations.", "SEMANTICS": [ @@ -115110,18 +81568,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -115142,18 +81591,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C1", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -115176,15 +81625,15 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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", + "contract_id": "fetchConnections:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/ConnectionList.svelte", - "start_line": 20, - "end_line": 34, + "start_line": 22, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115222,127 +81671,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:fetchConnections:Function]\n // @PURPOSE: Fetches the list of connections from the backend.\n // @PRE: None.\n // @POST: connections array is populated.\n async function fetchConnections() {\n isLoading = true;\n try {\n connections = await getConnections();\n } catch (e) {\n addToast($t.connections?.fetch_failed, 'error');\n } finally {\n isLoading = false;\n }\n }\n // [/DEF:fetchConnections:Function]\n" + "anchor_syntax": "region", + "body": " // #region fetchConnections:Function [TYPE Function]\n // @PURPOSE: Fetches the list of connections from the backend.\n // @PRE: None.\n // @POST: connections array is populated.\n async function fetchConnections() {\n isLoading = true;\n try {\n connections = await getConnections();\n } catch (e) {\n addToast($t.connections?.fetch_failed, 'error');\n } finally {\n isLoading = false;\n }\n }\n // #endregion fetchConnections:Function\n" }, { - "contract_id": "DebugTool", - "contract_type": "Component", - "file_path": "frontend/src/components/tools/DebugTool.svelte", - "start_line": 1, - "end_line": 191, - "tier": "TIER_1", - "complexity": 1, - "metadata": { - "LAYER": "UI", - "PURPOSE": "UI component for system diagnostics and debugging API responses.", - "SEMANTICS": [ - "debug", - "tool", - "api", - "structure" - ] - }, - "relations": [ - { - "source_id": "DebugTool", - "relation_type": "USES", - "target_id": "frontend/src/services/toolsService.js", - "target_ref": "frontend/src/services/toolsService.js" - } - ], - "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "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 C1", - "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, - "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" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n
\n
\n

{$t.debug?.title}

\n \n
\n \n \n
\n\n {#if action === 'test-db-api'}\n
\n
\n \n \n
\n
\n \n \n
\n
\n {:else}\n
\n
\n \n \n
\n
\n \n \n
\n
\n {/if}\n\n
\n \n
\n
\n\n {#if results}\n
\n
\n

{$t.debug?.output}

\n
\n
\n
{JSON.stringify(results, null, 2)}
\n
\n
\n {/if}\n
\n\n" - }, - { - "contract_id": "handleRunDebug", + "contract_id": "handleRunDebug:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/DebugTool.svelte", - "start_line": 44, - "end_line": 85, + "start_line": 46, + "end_line": 87, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115387,15 +81724,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " // [DEF:handleRunDebug:Function]\n /**\n * @purpose Triggers the debug task.\n * @pre Required fields are selected.\n * @post Task is started and polling begins.\n * @returns {Promise}\n */\n async function handleRunDebug() {\n isRunning = true;\n results = null;\n try {\n let params = { action };\n if (action === 'test-db-api') {\n if (!sourceEnv || !targetEnv) {\n addToast($t.debug?.source_target_required, 'warning');\n isRunning = false;\n return;\n }\n const sEnv = envs.find(e => e.id === sourceEnv);\n const tEnv = envs.find(e => e.id === targetEnv);\n params.source_env = sEnv.name;\n params.target_env = tEnv.name;\n } else {\n if (!selectedEnv || !datasetId) {\n addToast($t.debug?.env_dataset_required, 'warning');\n isRunning = false;\n return;\n }\n const env = envs.find(e => e.id === selectedEnv);\n params.env = env.name;\n params.dataset_id = parseInt(datasetId);\n }\n\n const task = await runTask('system-debug', params);\n selectedTask.set(task);\n startPolling(task.id);\n } catch (e) {\n isRunning = false;\n addToast(e.message, 'error');\n }\n }\n // [/DEF:handleRunDebug:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleRunDebug:Function [TYPE Function]\n /**\n * @purpose Triggers the debug task.\n * @pre Required fields are selected.\n * @post Task is started and polling begins.\n * @returns {Promise}\n */\n async function handleRunDebug() {\n isRunning = true;\n results = null;\n try {\n let params = { action };\n if (action === 'test-db-api') {\n if (!sourceEnv || !targetEnv) {\n addToast($t.debug?.source_target_required, 'warning');\n isRunning = false;\n return;\n }\n const sEnv = envs.find(e => e.id === sourceEnv);\n const tEnv = envs.find(e => e.id === targetEnv);\n params.source_env = sEnv.name;\n params.target_env = tEnv.name;\n } else {\n if (!selectedEnv || !datasetId) {\n addToast($t.debug?.env_dataset_required, 'warning');\n isRunning = false;\n return;\n }\n const env = envs.find(e => e.id === selectedEnv);\n params.env = env.name;\n params.dataset_id = parseInt(datasetId);\n }\n\n const task = await runTask('system-debug', params);\n selectedTask.set(task);\n startPolling(task.id);\n } catch (e) {\n isRunning = false;\n addToast(e.message, 'error');\n }\n }\n // #endregion handleRunDebug:Function\n" }, { - "contract_id": "startPolling", + "contract_id": "startPolling:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/DebugTool.svelte", - "start_line": 87, - "end_line": 117, + "start_line": 89, + "end_line": 119, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115447,18 +81784,19 @@ "detail": null } ], - "anchor_syntax": "def", - "body": " // [DEF:startPolling:Function]\n /**\n * @purpose Polls for task completion.\n * @pre Task ID is valid.\n * @post Polls until success/failure.\n * @param {string} taskId - ID of the task.\n * @returns {void}\n */\n function startPolling(taskId) {\n if (pollInterval) clearInterval(pollInterval);\n pollInterval = setInterval(async () => {\n try {\n const task = await getTaskStatus(taskId);\n selectedTask.set(task);\n if (task.status === 'SUCCESS') {\n clearInterval(pollInterval);\n isRunning = false;\n results = task.result;\n addToast($t.debug?.completed, 'success');\n } else if (task.status === 'FAILED') {\n clearInterval(pollInterval);\n isRunning = false;\n addToast($t.debug?.failed, 'error');\n }\n } catch (e) {\n clearInterval(pollInterval);\n isRunning = false;\n }\n }, 2000);\n }\n // [/DEF:startPolling:Function]\n" + "anchor_syntax": "region", + "body": " // #region startPolling:Function [TYPE Function]\n /**\n * @purpose Polls for task completion.\n * @pre Task ID is valid.\n * @post Polls until success/failure.\n * @param {string} taskId - ID of the task.\n * @returns {void}\n */\n function startPolling(taskId) {\n if (pollInterval) clearInterval(pollInterval);\n pollInterval = setInterval(async () => {\n try {\n const task = await getTaskStatus(taskId);\n selectedTask.set(task);\n if (task.status === 'SUCCESS') {\n clearInterval(pollInterval);\n isRunning = false;\n results = task.result;\n addToast($t.debug?.completed, 'success');\n } else if (task.status === 'FAILED') {\n clearInterval(pollInterval);\n isRunning = false;\n addToast($t.debug?.failed, 'error');\n }\n } catch (e) {\n clearInterval(pollInterval);\n isRunning = false;\n }\n }, 2000);\n }\n // #endregion startPolling:Function\n" }, { "contract_id": "MapperTool", "contract_type": "Component", "file_path": "frontend/src/components/tools/MapperTool.svelte", "start_line": 1, - "end_line": 250, - "tier": "TIER_1", - "complexity": 1, + "end_line": 252, + "tier": "TIER_2", + "complexity": 3, "metadata": { + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "UI component for mapping dataset column verbose names using the MapperPlugin.", "SEMANTICS": [ @@ -115500,18 +81838,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C1", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, - "contract_type": "Component" - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Component' at C1", - "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -115532,18 +81861,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C1", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, { - "code": "tag_forbidden_by_complexity", - "tag": "RELATION", - "message": "@RELATION is forbidden for contract type 'Component' at C1", + "code": "missing_required_tag", + "tag": "UX_STATE", + "message": "@UX_STATE is required for contract type 'Component' at C3", "detail": { - "actual_complexity": 1, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -115584,15 +81913,15 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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", + "contract_id": "fetchData:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/MapperTool.svelte", - "start_line": 35, - "end_line": 47, + "start_line": 37, + "end_line": 49, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115630,15 +81959,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:fetchData:Function]\n // @PURPOSE: Fetches environments and saved connections.\n // @PRE: None.\n // @POST: envs and connections arrays are populated.\n async function fetchData() {\n try {\n envs = await api.fetchApi('/environments');\n connections = await getConnections();\n } catch (e) {\n addToast($t.mapper.errors.fetch_failed, 'error');\n }\n }\n // [/DEF:fetchData:Function]\n" + "anchor_syntax": "region", + "body": " // #region fetchData:Function [TYPE Function]\n // @PURPOSE: Fetches environments and saved connections.\n // @PRE: None.\n // @POST: envs and connections arrays are populated.\n async function fetchData() {\n try {\n envs = await api.fetchApi('/environments');\n connections = await getConnections();\n } catch (e) {\n addToast($t.mapper.errors.fetch_failed, 'error');\n }\n }\n // #endregion fetchData:Function\n" }, { - "contract_id": "handleRunMapper", + "contract_id": "handleRunMapper:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/MapperTool.svelte", - "start_line": 49, - "end_line": 90, + "start_line": 51, + "end_line": 92, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115676,15 +82005,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleRunMapper:Function]\n // @PURPOSE: Triggers the MapperPlugin task.\n // @PRE: selectedEnv and datasetId are set; source-specific fields are valid.\n // @POST: Mapper task is started and selectedTask is updated.\n async function handleRunMapper() {\n if (!selectedEnv || !datasetId) {\n addToast($t.mapper.errors.required_fields, 'warning');\n return;\n }\n\n if (source === 'postgres' && (!selectedConnection || !tableName)) {\n addToast($t.mapper.errors.postgres_required, 'warning');\n return;\n }\n\n if (source === 'excel' && !excelPath) {\n addToast($t.mapper.errors.excel_required, 'warning');\n return;\n }\n\n isRunning = true;\n try {\n const env = envs.find(e => e.id === selectedEnv);\n const task = await runTask('dataset-mapper', {\n env: env.name,\n dataset_id: parseInt(datasetId),\n source,\n connection_id: selectedConnection,\n table_name: tableName,\n table_schema: tableSchema,\n excel_path: excelPath\n });\n \n selectedTask.set(task);\n addToast($t.mapper.success.started, 'success');\n } catch (e) {\n addToast(e.message, 'error');\n } finally {\n isRunning = false;\n }\n }\n // [/DEF:handleRunMapper:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleRunMapper:Function [TYPE Function]\n // @PURPOSE: Triggers the MapperPlugin task.\n // @PRE: selectedEnv and datasetId are set; source-specific fields are valid.\n // @POST: Mapper task is started and selectedTask is updated.\n async function handleRunMapper() {\n if (!selectedEnv || !datasetId) {\n addToast($t.mapper.errors.required_fields, 'warning');\n return;\n }\n\n if (source === 'postgres' && (!selectedConnection || !tableName)) {\n addToast($t.mapper.errors.postgres_required, 'warning');\n return;\n }\n\n if (source === 'excel' && !excelPath) {\n addToast($t.mapper.errors.excel_required, 'warning');\n return;\n }\n\n isRunning = true;\n try {\n const env = envs.find(e => e.id === selectedEnv);\n const task = await runTask('dataset-mapper', {\n env: env.name,\n dataset_id: parseInt(datasetId),\n source,\n connection_id: selectedConnection,\n table_name: tableName,\n table_schema: tableSchema,\n excel_path: excelPath\n });\n \n selectedTask.set(task);\n addToast($t.mapper.success.started, 'success');\n } catch (e) {\n addToast(e.message, 'error');\n } finally {\n isRunning = false;\n }\n }\n // #endregion handleRunMapper:Function\n" }, { - "contract_id": "handleGenerateDocs", + "contract_id": "handleGenerateDocs:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/MapperTool.svelte", - "start_line": 92, - "end_line": 127, + "start_line": 94, + "end_line": 129, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115722,19 +82051,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:handleGenerateDocs:Function]\n // @PURPOSE: Triggers the LLM Documentation task.\n // @PRE: selectedEnv and datasetId are set.\n // @POST: Documentation task is started.\n async function handleGenerateDocs() {\n if (!selectedEnv || !datasetId) {\n addToast($t.mapper.errors.required_fields, 'warning');\n return;\n }\n\n isGeneratingDocs = true;\n try {\n // Fetch active provider first\n const providers = await api.fetchApi('/llm/providers');\n const activeProvider = providers.find(p => p.is_active);\n \n if (!activeProvider) {\n addToast($t.mapper?.errors?.no_active_llm_provider, 'error');\n return;\n }\n\n const task = await runTask('llm_documentation', {\n dataset_id: datasetId,\n environment_id: selectedEnv,\n provider_id: activeProvider.id\n });\n \n selectedTask.set(task);\n addToast($t.mapper?.success?.docs_started, 'success');\n } catch (e) {\n addToast(e.message || $t.mapper?.errors?.docs_start_failed, 'error');\n } finally {\n isGeneratingDocs = false;\n }\n }\n // [/DEF:handleGenerateDocs:Function]\n" + "anchor_syntax": "region", + "body": " // #region handleGenerateDocs:Function [TYPE Function]\n // @PURPOSE: Triggers the LLM Documentation task.\n // @PRE: selectedEnv and datasetId are set.\n // @POST: Documentation task is started.\n async function handleGenerateDocs() {\n if (!selectedEnv || !datasetId) {\n addToast($t.mapper.errors.required_fields, 'warning');\n return;\n }\n\n isGeneratingDocs = true;\n try {\n // Fetch active provider first\n const providers = await api.fetchApi('/llm/providers');\n const activeProvider = providers.find(p => p.is_active);\n \n if (!activeProvider) {\n addToast($t.mapper?.errors?.no_active_llm_provider, 'error');\n return;\n }\n\n const task = await runTask('llm_documentation', {\n dataset_id: datasetId,\n environment_id: selectedEnv,\n provider_id: activeProvider.id\n });\n \n selectedTask.set(task);\n addToast($t.mapper?.success?.docs_started, 'success');\n } catch (e) {\n addToast(e.message || $t.mapper?.errors?.docs_start_failed, 'error');\n } finally {\n isGeneratingDocs = false;\n }\n }\n // #endregion handleGenerateDocs:Function\n" }, { "contract_id": "Counter", "contract_type": "Component", "file_path": "frontend/src/lib/Counter.svelte", "start_line": 1, - "end_line": 20, + "end_line": 21, "tier": "TIER_1", "complexity": 2, "metadata": { - "COMPLEXITY": "2", + "COMPLEXITY": 2, "LAYER": "UI", "PURPOSE": "Simple counter demo component", "UX_STATE": "Incremented -> Count increases immediately after button activation." @@ -115764,19 +82093,66 @@ } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n" + "anchor_syntax": "region", + "body": "\n\n\n\n\n\n\n\n" }, { - "contract_id": "api_module", + "contract_id": "ApiModule", "contract_type": "Module", "file_path": "frontend/src/lib/api.js", "start_line": 1, - "end_line": 423, + "end_line": 432, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, + "PURPOSE": "Handles all communication with the backend API via fetch wrappers with auth, error normalization, and toast feedback." + }, + "relations": [ + { + "source_id": "ApiModule", + "relation_type": "DEPENDS_ON", + "target_id": "ToastsModule", + "target_ref": "[ToastsModule]" + }, + { + "source_id": "ApiModule", + "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", + "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": [ @@ -115788,13 +82164,76 @@ }, "relations": [ { - "source_id": "api_module", + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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", @@ -115814,15 +82253,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:api_module:Module]\n// @COMPLEXITY: 3\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// [DEF:buildApiError: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// [/DEF:buildApiError:Function]\n\n// [DEF:notifyApiError: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// [/DEF:notifyApiError:Function]\n\n// [DEF:shouldSuppressApiErrorToast: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// [/DEF:shouldSuppressApiErrorToast:Function]\n\n// [DEF:getWsUrl: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// [/DEF:getWsUrl:Function]\n\n// [DEF:getAuthHeaders:Function]\n// @PURPOSE: Returns headers with Authorization if token exists.\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = {\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n// [/DEF:getAuthHeaders:Function]\n\n// [DEF:fetchApi: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// [/DEF:fetchApi:Function]\n\n// [DEF:fetchApiBlob: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// [/DEF:fetchApiBlob:Function]\n\n// [DEF:postApi: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// [/DEF:postApi:Function]\n\n// [DEF:deleteApi: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// [/DEF:deleteApi:Function]\n\n// [DEF:requestApi: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// [/DEF:requestApi:Function]\n\n// [DEF:api:Data]\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\n // Health\n getHealthSummary: (environmentId) => {\n const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';\n return fetchApi(`/health/summary${query}`, { suppressToast: true });\n },\n};\n// [/DEF:api:Data]\n\n// [/DEF:api_module:Module]\n" + "anchor_syntax": "region", + "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", + "contract_id": "buildApiError:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 13, - "end_line": 35, + "start_line": 20, + "end_line": 42, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115867,15 +82306,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "// [DEF:buildApiError: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// [/DEF:buildApiError:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "notifyApiError:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 37, - "end_line": 52, + "start_line": 44, + "end_line": 59, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115913,15 +82352,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:notifyApiError: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// [/DEF:notifyApiError:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "shouldSuppressApiErrorToast:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 54, - "end_line": 89, + "start_line": 61, + "end_line": 96, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -115959,15 +82398,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:shouldSuppressApiErrorToast: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// [/DEF:shouldSuppressApiErrorToast:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "getWsUrl:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 91, - "end_line": 106, + "start_line": 98, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116019,15 +82458,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "// [DEF:getWsUrl: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// [/DEF:getWsUrl:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "getAuthHeaders:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 108, - "end_line": 123, + "start_line": 115, + "end_line": 130, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116045,15 +82484,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:getAuthHeaders:Function]\n// @PURPOSE: Returns headers with Authorization if token exists.\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = {\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n }\n return headers;\n}\n// [/DEF:getAuthHeaders:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchApi:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 125, - "end_line": 151, + "start_line": 132, + "end_line": 158, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116105,15 +82544,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchApi: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// [/DEF:fetchApi:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchApiBlob:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 153, - "end_line": 181, + "start_line": 160, + "end_line": 188, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116158,15 +82597,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchApiBlob: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// [/DEF:fetchApiBlob:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "postApi:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 183, - "end_line": 210, + "start_line": 190, + "end_line": 217, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116218,15 +82657,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "// [DEF:postApi: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// [/DEF:postApi:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "deleteApi:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 212, - "end_line": 237, + "start_line": 219, + "end_line": 244, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116278,15 +82717,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "// [DEF:deleteApi: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// [/DEF:deleteApi:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "requestApi:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 239, - "end_line": 273, + "start_line": 246, + "end_line": 280, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116324,15 +82763,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:requestApi: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// [/DEF:requestApi:Function]\n" + "anchor_syntax": "region", + "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", - "contract_type": "Data", + "contract_id": "api:Data", + "contract_type": "Function", "file_path": "frontend/src/lib/api.js", - "start_line": 275, - "end_line": 421, + "start_line": 282, + "end_line": 429, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116340,47 +82779,52 @@ }, "relations": [], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Data'", - "detail": { - "actual_type": "Data", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, { "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Data' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Data" + "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": "// [DEF:api:Data]\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\n // Health\n getHealthSummary: (environmentId) => {\n const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';\n return fetchApi(`/health/summary${query}`, { suppressToast: true });\n },\n};\n// [/DEF:api:Data]\n" + "anchor_syntax": "region", + "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" }, { "contract_id": "ReportsApiTest", "contract_type": "Module", "file_path": "frontend/src/lib/api/__tests__/reports_api.test.js", "start_line": 1, - "end_line": 213, + "end_line": 217, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, + "PURPOSE": "Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers." + }, + "relations": [ + { + "source_id": "ReportsApiTest", + "relation_type": "DEPENDS_ON", + "target_id": "ReportsApi", + "target_ref": "[ReportsApi]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "// #region ReportsApiTest [C:3] [TYPE Module] [SEMANTICS report, api, test, search, filter]\n// @BRIEF Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.\n// @RELATION DEPENDS_ON -> [ReportsApi]\n\n// #region ReportsApiTest:Module [TYPE Function]\n// @SEMANTICS: tests, reports, api-client, query-string, error-normalization\n// @PURPOSE: Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.\n// @LAYER: Infra (Tests)\n// @RELATION: DEPENDS_ON -> [ReportsApi]\n// @INVARIANT: Pure functions produce deterministic output. Async wrappers propagate structured errors.\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\n\n// Mock SvelteKit environment modules before any source imports\nvi.mock('$env/static/public', () => ({\n PUBLIC_WS_URL: 'ws://localhost:8000'\n}));\n\n// Mock toasts to prevent import side-effects\nvi.mock('../../toasts.js', () => ({\n addToast: vi.fn()\n}));\n\n// Mock the api module\nvi.mock('../../api.js', () => ({\n api: {\n fetchApi: vi.fn()\n }\n}));\n\nimport { buildReportQueryString, normalizeApiError } from '../reports.js';\n\n// #region TestBuildReportQueryString:Class [TYPE Function]\n// @PURPOSE: Validate query string construction from filter options.\n// @PRE: Options object with various filter fields.\n// @POST: Correct URLSearchParams string produced.\ndescribe('buildReportQueryString', () => {\n it('returns empty string for empty options', () => {\n expect(buildReportQueryString()).toBe('');\n expect(buildReportQueryString({})).toBe('');\n });\n\n it('serializes page and page_size', () => {\n const qs = buildReportQueryString({ page: 2, page_size: 10 });\n expect(qs).toContain('page=2');\n expect(qs).toContain('page_size=10');\n });\n\n it('serializes task_types array', () => {\n const qs = buildReportQueryString({ task_types: ['backup', 'migration'] });\n expect(qs).toContain('task_types=backup%2Cmigration');\n });\n\n it('serializes statuses array', () => {\n const qs = buildReportQueryString({ statuses: ['success', 'failed'] });\n expect(qs).toContain('statuses=success%2Cfailed');\n });\n\n it('ignores empty arrays', () => {\n const qs = buildReportQueryString({ task_types: [], statuses: [] });\n expect(qs).toBe('');\n });\n\n it('serializes time range and search', () => {\n const qs = buildReportQueryString({\n time_from: '2024-01-01',\n time_to: '2024-12-31',\n search: 'backup'\n });\n expect(qs).toContain('time_from=2024-01-01');\n expect(qs).toContain('time_to=2024-12-31');\n expect(qs).toContain('search=backup');\n });\n\n it('serializes sort options', () => {\n const qs = buildReportQueryString({ sort_by: 'status', sort_order: 'asc' });\n expect(qs).toContain('sort_by=status');\n expect(qs).toContain('sort_order=asc');\n });\n\n it('handles all options combined', () => {\n const qs = buildReportQueryString({\n page: 1,\n page_size: 20,\n task_types: ['backup'],\n statuses: ['success'],\n search: 'test',\n sort_by: 'updated_at',\n sort_order: 'desc'\n });\n expect(qs).toContain('page=1');\n expect(qs).toContain('page_size=20');\n expect(qs).toContain('task_types=backup');\n expect(qs).toContain('statuses=success');\n expect(qs).toContain('search=test');\n });\n});\n// #endregion TestBuildReportQueryString:Class\n\n// #region TestNormalizeApiError:Class [TYPE Function]\n// @PURPOSE: Validate error normalization for UI-state mapping.\n// @PRE: Various error types (Error, string, object).\n// @POST: Always returns {message, code, retryable} object.\ndescribe('normalizeApiError', () => {\n it('extracts message from Error object', () => {\n const result = normalizeApiError(new Error('Connection failed'));\n expect(result.message).toBe('Connection failed');\n expect(result.code).toBe('REPORTS_API_ERROR');\n expect(result.retryable).toBe(true);\n });\n\n it('uses string error directly', () => {\n const result = normalizeApiError('Something went wrong');\n expect(result.message).toBe('Something went wrong');\n });\n\n it('falls back to default message for null/undefined', () => {\n expect(normalizeApiError(null).message).toBe('Failed to load reports');\n expect(normalizeApiError(undefined).message).toBe('Failed to load reports');\n });\n\n it('falls back for object without message', () => {\n const result = normalizeApiError({ status: 500 });\n expect(result.message).toBe('Failed to load reports');\n });\n\n it('always includes code and retryable fields', () => {\n const result = normalizeApiError('test');\n expect(result).toHaveProperty('code');\n expect(result).toHaveProperty('retryable');\n });\n});\n// #endregion TestNormalizeApiError:Class\n\n// #region TestGetReportsAsync:Class [TYPE Function]\n// @PURPOSE: Validate getReports and getReportDetail with mocked api.fetchApi.\n// @PRE: api.fetchApi is mocked via vi.mock.\n// @POST: Functions call correct endpoints and propagate results/errors.\n\ndescribe('getReports', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReports } = await import('../reports.js');\n const mockResponse = { items: [], total: 0 };\n api.fetchApi.mockResolvedValue(mockResponse);\n\n const result = await getReports();\n expect(api.fetchApi).toHaveBeenCalledWith('/reports');\n expect(result).toEqual(mockResponse);\n });\n\n it('appends query string when options provided', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockResolvedValue({ items: [] });\n\n await getReports({ page: 2, page_size: 5 });\n const call = api.fetchApi.mock.calls[0][0];\n expect(call).toContain('/reports?');\n expect(call).toContain('page=2');\n expect(call).toContain('page_size=5');\n });\n\n it('throws normalized error on failure', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Network error'));\n\n await expect(getReports()).rejects.toEqual(\n expect.objectContaining({\n message: 'Network error',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n\ndescribe('getReportDetail', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReportDetail } = await import('../reports.js');\n const mockDetail = { report: { report_id: 'r1' } };\n api.fetchApi.mockResolvedValue(mockDetail);\n\n const result = await getReportDetail('r1');\n expect(api.fetchApi).toHaveBeenCalledWith('/reports/r1');\n expect(result).toEqual(mockDetail);\n });\n\n it('throws normalized error on failure', async () => {\n const { getReportDetail } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Not found'));\n\n await expect(getReportDetail('nonexistent')).rejects.toEqual(\n expect.objectContaining({\n message: 'Not found',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n// #endregion TestGetReportsAsync:Class\n\n// #endregion ReportsApiTest:Module\n// #endregion ReportsApiTest\n" + }, + { + "contract_id": "ReportsApiTest:Module", + "contract_type": "Function", + "file_path": "frontend/src/lib/api/__tests__/reports_api.test.js", + "start_line": 5, + "end_line": 216, + "tier": "TIER_1", + "complexity": 1, + "metadata": { "INVARIANT": "Pure functions produce deterministic output. Async wrappers propagate structured errors.", "LAYER": "Infra (Tests)", "PURPOSE": "Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.", @@ -116394,7 +82838,7 @@ }, "relations": [ { - "source_id": "ReportsApiTest", + "source_id": "ReportsApiTest:Module", "relation_type": "DEPENDS_ON", "target_id": "ReportsApi", "target_ref": "[ReportsApi]" @@ -116404,22 +82848,85 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "message": "@INVARIANT is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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" } } ], - "anchor_syntax": "def", - "body": "// [DEF:ReportsApiTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: tests, reports, api-client, query-string, error-normalization\n// @PURPOSE: Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.\n// @LAYER: Infra (Tests)\n// @RELATION: DEPENDS_ON -> [ReportsApi]\n// @INVARIANT: Pure functions produce deterministic output. Async wrappers propagate structured errors.\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\n\n// Mock SvelteKit environment modules before any source imports\nvi.mock('$env/static/public', () => ({\n PUBLIC_WS_URL: 'ws://localhost:8000'\n}));\n\n// Mock toasts to prevent import side-effects\nvi.mock('../../toasts.js', () => ({\n addToast: vi.fn()\n}));\n\n// Mock the api module\nvi.mock('../../api.js', () => ({\n api: {\n fetchApi: vi.fn()\n }\n}));\n\nimport { buildReportQueryString, normalizeApiError } from '../reports.js';\n\n// [DEF:TestBuildReportQueryString:Class]\n// @PURPOSE: Validate query string construction from filter options.\n// @PRE: Options object with various filter fields.\n// @POST: Correct URLSearchParams string produced.\ndescribe('buildReportQueryString', () => {\n it('returns empty string for empty options', () => {\n expect(buildReportQueryString()).toBe('');\n expect(buildReportQueryString({})).toBe('');\n });\n\n it('serializes page and page_size', () => {\n const qs = buildReportQueryString({ page: 2, page_size: 10 });\n expect(qs).toContain('page=2');\n expect(qs).toContain('page_size=10');\n });\n\n it('serializes task_types array', () => {\n const qs = buildReportQueryString({ task_types: ['backup', 'migration'] });\n expect(qs).toContain('task_types=backup%2Cmigration');\n });\n\n it('serializes statuses array', () => {\n const qs = buildReportQueryString({ statuses: ['success', 'failed'] });\n expect(qs).toContain('statuses=success%2Cfailed');\n });\n\n it('ignores empty arrays', () => {\n const qs = buildReportQueryString({ task_types: [], statuses: [] });\n expect(qs).toBe('');\n });\n\n it('serializes time range and search', () => {\n const qs = buildReportQueryString({\n time_from: '2024-01-01',\n time_to: '2024-12-31',\n search: 'backup'\n });\n expect(qs).toContain('time_from=2024-01-01');\n expect(qs).toContain('time_to=2024-12-31');\n expect(qs).toContain('search=backup');\n });\n\n it('serializes sort options', () => {\n const qs = buildReportQueryString({ sort_by: 'status', sort_order: 'asc' });\n expect(qs).toContain('sort_by=status');\n expect(qs).toContain('sort_order=asc');\n });\n\n it('handles all options combined', () => {\n const qs = buildReportQueryString({\n page: 1,\n page_size: 20,\n task_types: ['backup'],\n statuses: ['success'],\n search: 'test',\n sort_by: 'updated_at',\n sort_order: 'desc'\n });\n expect(qs).toContain('page=1');\n expect(qs).toContain('page_size=20');\n expect(qs).toContain('task_types=backup');\n expect(qs).toContain('statuses=success');\n expect(qs).toContain('search=test');\n });\n});\n// [/DEF:TestBuildReportQueryString:Class]\n\n// [DEF:TestNormalizeApiError:Class]\n// @PURPOSE: Validate error normalization for UI-state mapping.\n// @PRE: Various error types (Error, string, object).\n// @POST: Always returns {message, code, retryable} object.\ndescribe('normalizeApiError', () => {\n it('extracts message from Error object', () => {\n const result = normalizeApiError(new Error('Connection failed'));\n expect(result.message).toBe('Connection failed');\n expect(result.code).toBe('REPORTS_API_ERROR');\n expect(result.retryable).toBe(true);\n });\n\n it('uses string error directly', () => {\n const result = normalizeApiError('Something went wrong');\n expect(result.message).toBe('Something went wrong');\n });\n\n it('falls back to default message for null/undefined', () => {\n expect(normalizeApiError(null).message).toBe('Failed to load reports');\n expect(normalizeApiError(undefined).message).toBe('Failed to load reports');\n });\n\n it('falls back for object without message', () => {\n const result = normalizeApiError({ status: 500 });\n expect(result.message).toBe('Failed to load reports');\n });\n\n it('always includes code and retryable fields', () => {\n const result = normalizeApiError('test');\n expect(result).toHaveProperty('code');\n expect(result).toHaveProperty('retryable');\n });\n});\n// [/DEF:TestNormalizeApiError:Class]\n\n// [DEF:TestGetReportsAsync:Class]\n// @PURPOSE: Validate getReports and getReportDetail with mocked api.fetchApi.\n// @PRE: api.fetchApi is mocked via vi.mock.\n// @POST: Functions call correct endpoints and propagate results/errors.\n\ndescribe('getReports', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReports } = await import('../reports.js');\n const mockResponse = { items: [], total: 0 };\n api.fetchApi.mockResolvedValue(mockResponse);\n\n const result = await getReports();\n expect(api.fetchApi).toHaveBeenCalledWith('/reports');\n expect(result).toEqual(mockResponse);\n });\n\n it('appends query string when options provided', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockResolvedValue({ items: [] });\n\n await getReports({ page: 2, page_size: 5 });\n const call = api.fetchApi.mock.calls[0][0];\n expect(call).toContain('/reports?');\n expect(call).toContain('page=2');\n expect(call).toContain('page_size=5');\n });\n\n it('throws normalized error on failure', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Network error'));\n\n await expect(getReports()).rejects.toEqual(\n expect.objectContaining({\n message: 'Network error',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n\ndescribe('getReportDetail', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReportDetail } = await import('../reports.js');\n const mockDetail = { report: { report_id: 'r1' } };\n api.fetchApi.mockResolvedValue(mockDetail);\n\n const result = await getReportDetail('r1');\n expect(api.fetchApi).toHaveBeenCalledWith('/reports/r1');\n expect(result).toEqual(mockDetail);\n });\n\n it('throws normalized error on failure', async () => {\n const { getReportDetail } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Not found'));\n\n await expect(getReportDetail('nonexistent')).rejects.toEqual(\n expect.objectContaining({\n message: 'Not found',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n// [/DEF:TestGetReportsAsync:Class]\n\n// [/DEF:ReportsApiTest:Module]\n" + "anchor_syntax": "region", + "body": "// #region ReportsApiTest:Module [TYPE Function]\n// @SEMANTICS: tests, reports, api-client, query-string, error-normalization\n// @PURPOSE: Unit tests for reports API client functions: query string building, error normalization, and fetch wrappers.\n// @LAYER: Infra (Tests)\n// @RELATION: DEPENDS_ON -> [ReportsApi]\n// @INVARIANT: Pure functions produce deterministic output. Async wrappers propagate structured errors.\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\n\n// Mock SvelteKit environment modules before any source imports\nvi.mock('$env/static/public', () => ({\n PUBLIC_WS_URL: 'ws://localhost:8000'\n}));\n\n// Mock toasts to prevent import side-effects\nvi.mock('../../toasts.js', () => ({\n addToast: vi.fn()\n}));\n\n// Mock the api module\nvi.mock('../../api.js', () => ({\n api: {\n fetchApi: vi.fn()\n }\n}));\n\nimport { buildReportQueryString, normalizeApiError } from '../reports.js';\n\n// #region TestBuildReportQueryString:Class [TYPE Function]\n// @PURPOSE: Validate query string construction from filter options.\n// @PRE: Options object with various filter fields.\n// @POST: Correct URLSearchParams string produced.\ndescribe('buildReportQueryString', () => {\n it('returns empty string for empty options', () => {\n expect(buildReportQueryString()).toBe('');\n expect(buildReportQueryString({})).toBe('');\n });\n\n it('serializes page and page_size', () => {\n const qs = buildReportQueryString({ page: 2, page_size: 10 });\n expect(qs).toContain('page=2');\n expect(qs).toContain('page_size=10');\n });\n\n it('serializes task_types array', () => {\n const qs = buildReportQueryString({ task_types: ['backup', 'migration'] });\n expect(qs).toContain('task_types=backup%2Cmigration');\n });\n\n it('serializes statuses array', () => {\n const qs = buildReportQueryString({ statuses: ['success', 'failed'] });\n expect(qs).toContain('statuses=success%2Cfailed');\n });\n\n it('ignores empty arrays', () => {\n const qs = buildReportQueryString({ task_types: [], statuses: [] });\n expect(qs).toBe('');\n });\n\n it('serializes time range and search', () => {\n const qs = buildReportQueryString({\n time_from: '2024-01-01',\n time_to: '2024-12-31',\n search: 'backup'\n });\n expect(qs).toContain('time_from=2024-01-01');\n expect(qs).toContain('time_to=2024-12-31');\n expect(qs).toContain('search=backup');\n });\n\n it('serializes sort options', () => {\n const qs = buildReportQueryString({ sort_by: 'status', sort_order: 'asc' });\n expect(qs).toContain('sort_by=status');\n expect(qs).toContain('sort_order=asc');\n });\n\n it('handles all options combined', () => {\n const qs = buildReportQueryString({\n page: 1,\n page_size: 20,\n task_types: ['backup'],\n statuses: ['success'],\n search: 'test',\n sort_by: 'updated_at',\n sort_order: 'desc'\n });\n expect(qs).toContain('page=1');\n expect(qs).toContain('page_size=20');\n expect(qs).toContain('task_types=backup');\n expect(qs).toContain('statuses=success');\n expect(qs).toContain('search=test');\n });\n});\n// #endregion TestBuildReportQueryString:Class\n\n// #region TestNormalizeApiError:Class [TYPE Function]\n// @PURPOSE: Validate error normalization for UI-state mapping.\n// @PRE: Various error types (Error, string, object).\n// @POST: Always returns {message, code, retryable} object.\ndescribe('normalizeApiError', () => {\n it('extracts message from Error object', () => {\n const result = normalizeApiError(new Error('Connection failed'));\n expect(result.message).toBe('Connection failed');\n expect(result.code).toBe('REPORTS_API_ERROR');\n expect(result.retryable).toBe(true);\n });\n\n it('uses string error directly', () => {\n const result = normalizeApiError('Something went wrong');\n expect(result.message).toBe('Something went wrong');\n });\n\n it('falls back to default message for null/undefined', () => {\n expect(normalizeApiError(null).message).toBe('Failed to load reports');\n expect(normalizeApiError(undefined).message).toBe('Failed to load reports');\n });\n\n it('falls back for object without message', () => {\n const result = normalizeApiError({ status: 500 });\n expect(result.message).toBe('Failed to load reports');\n });\n\n it('always includes code and retryable fields', () => {\n const result = normalizeApiError('test');\n expect(result).toHaveProperty('code');\n expect(result).toHaveProperty('retryable');\n });\n});\n// #endregion TestNormalizeApiError:Class\n\n// #region TestGetReportsAsync:Class [TYPE Function]\n// @PURPOSE: Validate getReports and getReportDetail with mocked api.fetchApi.\n// @PRE: api.fetchApi is mocked via vi.mock.\n// @POST: Functions call correct endpoints and propagate results/errors.\n\ndescribe('getReports', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReports } = await import('../reports.js');\n const mockResponse = { items: [], total: 0 };\n api.fetchApi.mockResolvedValue(mockResponse);\n\n const result = await getReports();\n expect(api.fetchApi).toHaveBeenCalledWith('/reports');\n expect(result).toEqual(mockResponse);\n });\n\n it('appends query string when options provided', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockResolvedValue({ items: [] });\n\n await getReports({ page: 2, page_size: 5 });\n const call = api.fetchApi.mock.calls[0][0];\n expect(call).toContain('/reports?');\n expect(call).toContain('page=2');\n expect(call).toContain('page_size=5');\n });\n\n it('throws normalized error on failure', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Network error'));\n\n await expect(getReports()).rejects.toEqual(\n expect.objectContaining({\n message: 'Network error',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n\ndescribe('getReportDetail', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReportDetail } = await import('../reports.js');\n const mockDetail = { report: { report_id: 'r1' } };\n api.fetchApi.mockResolvedValue(mockDetail);\n\n const result = await getReportDetail('r1');\n expect(api.fetchApi).toHaveBeenCalledWith('/reports/r1');\n expect(result).toEqual(mockDetail);\n });\n\n it('throws normalized error on failure', async () => {\n const { getReportDetail } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Not found'));\n\n await expect(getReportDetail('nonexistent')).rejects.toEqual(\n expect.objectContaining({\n message: 'Not found',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n// #endregion TestGetReportsAsync:Class\n\n// #endregion ReportsApiTest:Module\n" }, { - "contract_id": "TestBuildReportQueryString", - "contract_type": "Class", + "contract_id": "TestBuildReportQueryString:Class", + "contract_type": "Function", "file_path": "frontend/src/lib/api/__tests__/reports_api.test.js", - "start_line": 30, - "end_line": 95, + "start_line": 33, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116432,40 +82939,40 @@ { "code": "tag_forbidden_by_complexity", "tag": "POST", - "message": "@POST is forbidden for contract type 'Class' at C1", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Class' at C1", + "message": "@PRE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": "// [DEF:TestBuildReportQueryString:Class]\n// @PURPOSE: Validate query string construction from filter options.\n// @PRE: Options object with various filter fields.\n// @POST: Correct URLSearchParams string produced.\ndescribe('buildReportQueryString', () => {\n it('returns empty string for empty options', () => {\n expect(buildReportQueryString()).toBe('');\n expect(buildReportQueryString({})).toBe('');\n });\n\n it('serializes page and page_size', () => {\n const qs = buildReportQueryString({ page: 2, page_size: 10 });\n expect(qs).toContain('page=2');\n expect(qs).toContain('page_size=10');\n });\n\n it('serializes task_types array', () => {\n const qs = buildReportQueryString({ task_types: ['backup', 'migration'] });\n expect(qs).toContain('task_types=backup%2Cmigration');\n });\n\n it('serializes statuses array', () => {\n const qs = buildReportQueryString({ statuses: ['success', 'failed'] });\n expect(qs).toContain('statuses=success%2Cfailed');\n });\n\n it('ignores empty arrays', () => {\n const qs = buildReportQueryString({ task_types: [], statuses: [] });\n expect(qs).toBe('');\n });\n\n it('serializes time range and search', () => {\n const qs = buildReportQueryString({\n time_from: '2024-01-01',\n time_to: '2024-12-31',\n search: 'backup'\n });\n expect(qs).toContain('time_from=2024-01-01');\n expect(qs).toContain('time_to=2024-12-31');\n expect(qs).toContain('search=backup');\n });\n\n it('serializes sort options', () => {\n const qs = buildReportQueryString({ sort_by: 'status', sort_order: 'asc' });\n expect(qs).toContain('sort_by=status');\n expect(qs).toContain('sort_order=asc');\n });\n\n it('handles all options combined', () => {\n const qs = buildReportQueryString({\n page: 1,\n page_size: 20,\n task_types: ['backup'],\n statuses: ['success'],\n search: 'test',\n sort_by: 'updated_at',\n sort_order: 'desc'\n });\n expect(qs).toContain('page=1');\n expect(qs).toContain('page_size=20');\n expect(qs).toContain('task_types=backup');\n expect(qs).toContain('statuses=success');\n expect(qs).toContain('search=test');\n });\n});\n// [/DEF:TestBuildReportQueryString:Class]\n" + "anchor_syntax": "region", + "body": "// #region TestBuildReportQueryString:Class [TYPE Function]\n// @PURPOSE: Validate query string construction from filter options.\n// @PRE: Options object with various filter fields.\n// @POST: Correct URLSearchParams string produced.\ndescribe('buildReportQueryString', () => {\n it('returns empty string for empty options', () => {\n expect(buildReportQueryString()).toBe('');\n expect(buildReportQueryString({})).toBe('');\n });\n\n it('serializes page and page_size', () => {\n const qs = buildReportQueryString({ page: 2, page_size: 10 });\n expect(qs).toContain('page=2');\n expect(qs).toContain('page_size=10');\n });\n\n it('serializes task_types array', () => {\n const qs = buildReportQueryString({ task_types: ['backup', 'migration'] });\n expect(qs).toContain('task_types=backup%2Cmigration');\n });\n\n it('serializes statuses array', () => {\n const qs = buildReportQueryString({ statuses: ['success', 'failed'] });\n expect(qs).toContain('statuses=success%2Cfailed');\n });\n\n it('ignores empty arrays', () => {\n const qs = buildReportQueryString({ task_types: [], statuses: [] });\n expect(qs).toBe('');\n });\n\n it('serializes time range and search', () => {\n const qs = buildReportQueryString({\n time_from: '2024-01-01',\n time_to: '2024-12-31',\n search: 'backup'\n });\n expect(qs).toContain('time_from=2024-01-01');\n expect(qs).toContain('time_to=2024-12-31');\n expect(qs).toContain('search=backup');\n });\n\n it('serializes sort options', () => {\n const qs = buildReportQueryString({ sort_by: 'status', sort_order: 'asc' });\n expect(qs).toContain('sort_by=status');\n expect(qs).toContain('sort_order=asc');\n });\n\n it('handles all options combined', () => {\n const qs = buildReportQueryString({\n page: 1,\n page_size: 20,\n task_types: ['backup'],\n statuses: ['success'],\n search: 'test',\n sort_by: 'updated_at',\n sort_order: 'desc'\n });\n expect(qs).toContain('page=1');\n expect(qs).toContain('page_size=20');\n expect(qs).toContain('task_types=backup');\n expect(qs).toContain('statuses=success');\n expect(qs).toContain('search=test');\n });\n});\n// #endregion TestBuildReportQueryString:Class\n" }, { - "contract_id": "TestNormalizeApiError", - "contract_type": "Class", + "contract_id": "TestNormalizeApiError:Class", + "contract_type": "Function", "file_path": "frontend/src/lib/api/__tests__/reports_api.test.js", - "start_line": 97, - "end_line": 130, + "start_line": 100, + "end_line": 133, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116478,40 +82985,40 @@ { "code": "tag_forbidden_by_complexity", "tag": "POST", - "message": "@POST is forbidden for contract type 'Class' at C1", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Class' at C1", + "message": "@PRE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": "// [DEF:TestNormalizeApiError:Class]\n// @PURPOSE: Validate error normalization for UI-state mapping.\n// @PRE: Various error types (Error, string, object).\n// @POST: Always returns {message, code, retryable} object.\ndescribe('normalizeApiError', () => {\n it('extracts message from Error object', () => {\n const result = normalizeApiError(new Error('Connection failed'));\n expect(result.message).toBe('Connection failed');\n expect(result.code).toBe('REPORTS_API_ERROR');\n expect(result.retryable).toBe(true);\n });\n\n it('uses string error directly', () => {\n const result = normalizeApiError('Something went wrong');\n expect(result.message).toBe('Something went wrong');\n });\n\n it('falls back to default message for null/undefined', () => {\n expect(normalizeApiError(null).message).toBe('Failed to load reports');\n expect(normalizeApiError(undefined).message).toBe('Failed to load reports');\n });\n\n it('falls back for object without message', () => {\n const result = normalizeApiError({ status: 500 });\n expect(result.message).toBe('Failed to load reports');\n });\n\n it('always includes code and retryable fields', () => {\n const result = normalizeApiError('test');\n expect(result).toHaveProperty('code');\n expect(result).toHaveProperty('retryable');\n });\n});\n// [/DEF:TestNormalizeApiError:Class]\n" + "anchor_syntax": "region", + "body": "// #region TestNormalizeApiError:Class [TYPE Function]\n// @PURPOSE: Validate error normalization for UI-state mapping.\n// @PRE: Various error types (Error, string, object).\n// @POST: Always returns {message, code, retryable} object.\ndescribe('normalizeApiError', () => {\n it('extracts message from Error object', () => {\n const result = normalizeApiError(new Error('Connection failed'));\n expect(result.message).toBe('Connection failed');\n expect(result.code).toBe('REPORTS_API_ERROR');\n expect(result.retryable).toBe(true);\n });\n\n it('uses string error directly', () => {\n const result = normalizeApiError('Something went wrong');\n expect(result.message).toBe('Something went wrong');\n });\n\n it('falls back to default message for null/undefined', () => {\n expect(normalizeApiError(null).message).toBe('Failed to load reports');\n expect(normalizeApiError(undefined).message).toBe('Failed to load reports');\n });\n\n it('falls back for object without message', () => {\n const result = normalizeApiError({ status: 500 });\n expect(result.message).toBe('Failed to load reports');\n });\n\n it('always includes code and retryable fields', () => {\n const result = normalizeApiError('test');\n expect(result).toHaveProperty('code');\n expect(result).toHaveProperty('retryable');\n });\n});\n// #endregion TestNormalizeApiError:Class\n" }, { - "contract_id": "TestGetReportsAsync", - "contract_type": "Class", + "contract_id": "TestGetReportsAsync:Class", + "contract_type": "Function", "file_path": "frontend/src/lib/api/__tests__/reports_api.test.js", - "start_line": 132, - "end_line": 211, + "start_line": 135, + "end_line": 214, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116524,44 +83031,67 @@ { "code": "tag_forbidden_by_complexity", "tag": "POST", - "message": "@POST is forbidden for contract type 'Class' at C1", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Class' at C1", + "message": "@PRE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Class' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Class" + "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": "// [DEF:TestGetReportsAsync:Class]\n// @PURPOSE: Validate getReports and getReportDetail with mocked api.fetchApi.\n// @PRE: api.fetchApi is mocked via vi.mock.\n// @POST: Functions call correct endpoints and propagate results/errors.\n\ndescribe('getReports', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReports } = await import('../reports.js');\n const mockResponse = { items: [], total: 0 };\n api.fetchApi.mockResolvedValue(mockResponse);\n\n const result = await getReports();\n expect(api.fetchApi).toHaveBeenCalledWith('/reports');\n expect(result).toEqual(mockResponse);\n });\n\n it('appends query string when options provided', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockResolvedValue({ items: [] });\n\n await getReports({ page: 2, page_size: 5 });\n const call = api.fetchApi.mock.calls[0][0];\n expect(call).toContain('/reports?');\n expect(call).toContain('page=2');\n expect(call).toContain('page_size=5');\n });\n\n it('throws normalized error on failure', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Network error'));\n\n await expect(getReports()).rejects.toEqual(\n expect.objectContaining({\n message: 'Network error',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n\ndescribe('getReportDetail', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReportDetail } = await import('../reports.js');\n const mockDetail = { report: { report_id: 'r1' } };\n api.fetchApi.mockResolvedValue(mockDetail);\n\n const result = await getReportDetail('r1');\n expect(api.fetchApi).toHaveBeenCalledWith('/reports/r1');\n expect(result).toEqual(mockDetail);\n });\n\n it('throws normalized error on failure', async () => {\n const { getReportDetail } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Not found'));\n\n await expect(getReportDetail('nonexistent')).rejects.toEqual(\n expect.objectContaining({\n message: 'Not found',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n// [/DEF:TestGetReportsAsync:Class]\n" + "anchor_syntax": "region", + "body": "// #region TestGetReportsAsync:Class [TYPE Function]\n// @PURPOSE: Validate getReports and getReportDetail with mocked api.fetchApi.\n// @PRE: api.fetchApi is mocked via vi.mock.\n// @POST: Functions call correct endpoints and propagate results/errors.\n\ndescribe('getReports', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReports } = await import('../reports.js');\n const mockResponse = { items: [], total: 0 };\n api.fetchApi.mockResolvedValue(mockResponse);\n\n const result = await getReports();\n expect(api.fetchApi).toHaveBeenCalledWith('/reports');\n expect(result).toEqual(mockResponse);\n });\n\n it('appends query string when options provided', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockResolvedValue({ items: [] });\n\n await getReports({ page: 2, page_size: 5 });\n const call = api.fetchApi.mock.calls[0][0];\n expect(call).toContain('/reports?');\n expect(call).toContain('page=2');\n expect(call).toContain('page_size=5');\n });\n\n it('throws normalized error on failure', async () => {\n const { getReports } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Network error'));\n\n await expect(getReports()).rejects.toEqual(\n expect.objectContaining({\n message: 'Network error',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n\ndescribe('getReportDetail', () => {\n let api;\n\n beforeEach(async () => {\n vi.clearAllMocks();\n const apiModule = await import('../../api.js');\n api = apiModule.api;\n });\n\n it('calls fetchApi with correct endpoint', async () => {\n const { getReportDetail } = await import('../reports.js');\n const mockDetail = { report: { report_id: 'r1' } };\n api.fetchApi.mockResolvedValue(mockDetail);\n\n const result = await getReportDetail('r1');\n expect(api.fetchApi).toHaveBeenCalledWith('/reports/r1');\n expect(result).toEqual(mockDetail);\n });\n\n it('throws normalized error on failure', async () => {\n const { getReportDetail } = await import('../reports.js');\n api.fetchApi.mockRejectedValue(new Error('Not found'));\n\n await expect(getReportDetail('nonexistent')).rejects.toEqual(\n expect.objectContaining({\n message: 'Not found',\n code: 'REPORTS_API_ERROR'\n })\n );\n });\n});\n// #endregion TestGetReportsAsync:Class\n" }, { "contract_id": "AssistantApi", "contract_type": "Module", "file_path": "frontend/src/lib/api/assistant.js", "start_line": 1, - "end_line": 102, + "end_line": 106, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, + "PURPOSE": "API client wrapper for assistant chat, session-scoped prompts, confirmation actions, conversation management, and history retrieval." + }, + "relations": [ + { + "source_id": "AssistantApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "// #region AssistantApi [C:3] [TYPE Module] [SEMANTICS assistant, api, chat, session, history]\n// @BRIEF API client wrapper for assistant chat, session-scoped prompts, confirmation actions, conversation management, and history retrieval.\n// @RELATION DEPENDS_ON -> [ApiModule]\n\n// #region AssistantApi:Module [TYPE Function]\n// @SEMANTICS: assistant, api, client, chat, confirmation\n// @PURPOSE: API client wrapper for assistant chat, session-scoped prompts, confirmation actions, and history retrieval.\n// @LAYER: Infra-API\n// @RELATION: DEPENDS_ON -> api_module\n// @INVARIANT: All assistant requests must use requestApi wrapper (no native fetch).\n\nimport { requestApi } from '$lib/api.js';\n\n// #region sendAssistantMessage:Function [TYPE Function]\n// @PURPOSE: Send a user message to assistant orchestrator endpoint.\n// @PRE: payload.message is a non-empty string.\n// @POST: Returns assistant response object with deterministic state.\nexport function sendAssistantMessage(payload) {\n return requestApi('/assistant/messages', 'POST', payload);\n}\n// #endregion sendAssistantMessage:Function\n\n// #region buildAssistantSeedMessage:Function [TYPE Function]\n// @PURPOSE: Compose visible assistant seed text from context label and prompt body.\n// @PRE: prompt contains the user-visible request.\n// @POST: Returns trimmed seed message string for prefilled input.\nexport function buildAssistantSeedMessage({ label = '', prompt = '' } = {}) {\n const normalizedLabel = String(label || '').trim();\n const normalizedPrompt = String(prompt || '').trim();\n if (normalizedLabel && normalizedPrompt) {\n return `${normalizedLabel}: ${normalizedPrompt}`;\n }\n return normalizedPrompt || normalizedLabel;\n}\n// #endregion buildAssistantSeedMessage:Function\n\n// #region confirmAssistantOperation:Function [TYPE Function]\n// @PURPOSE: Confirm a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Returns execution response (started/success/failed).\nexport function confirmAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/confirm`, 'POST');\n}\n// #endregion confirmAssistantOperation:Function\n\n// #region cancelAssistantOperation:Function [TYPE Function]\n// @PURPOSE: Cancel a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Operation is cancelled and cannot be executed by this token.\nexport function cancelAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/cancel`, 'POST');\n}\n// #endregion cancelAssistantOperation:Function\n\n// #region getAssistantHistory:Function [TYPE Function]\n// @PURPOSE: Retrieve paginated assistant conversation history.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns a paginated payload with history items.\nexport function getAssistantHistory(page = 1, pageSize = 20, conversationId = null, fromLatest = false) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (conversationId) {\n params.append('conversation_id', conversationId);\n }\n if (fromLatest) {\n params.append('from_latest', 'true');\n }\n return requestApi(`/assistant/history?${params.toString()}`, 'GET');\n}\n// #endregion getAssistantHistory:Function\n\n// #region getAssistantConversations:Function [TYPE Function]\n// @PURPOSE: Retrieve paginated conversation list for assistant sidebar/history switcher.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns paginated conversation summaries.\nexport function getAssistantConversations(\n page = 1,\n pageSize = 20,\n includeArchived = false,\n search = '',\n archivedOnly = false,\n) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (includeArchived) {\n params.append('include_archived', 'true');\n }\n if (archivedOnly) {\n params.append('archived_only', 'true');\n }\n if (search?.trim()) {\n params.append('search', search.trim());\n }\n return requestApi(`/assistant/conversations?${params.toString()}`, 'GET');\n}\n// #endregion getAssistantConversations:Function\n\n// #region deleteAssistantConversation:Function [TYPE Function]\n// @PURPOSE: Soft-delete or hard-delete a conversation.\n// @PRE: conversationId string is provided.\n// @POST: Returns success status.\nexport function deleteAssistantConversation(conversationId) {\n return requestApi(`/assistant/conversations/${conversationId}`, 'DELETE');\n}\n// #endregion deleteAssistantConversation:Function\n\n// #endregion AssistantApi:Module\n// #endregion AssistantApi\n" + }, + { + "contract_id": "AssistantApi:Module", + "contract_type": "Function", + "file_path": "frontend/src/lib/api/assistant.js", + "start_line": 5, + "end_line": 105, + "tier": "TIER_1", + "complexity": 1, + "metadata": { "INVARIANT": "All assistant requests must use requestApi wrapper (no native fetch).", "LAYER": "Infra-API", "PURPOSE": "API client wrapper for assistant chat, session-scoped prompts, confirmation actions, and history retrieval.", @@ -116575,7 +83105,7 @@ }, "relations": [ { - "source_id": "AssistantApi", + "source_id": "AssistantApi:Module", "relation_type": "DEPENDS_ON", "target_id": "api_module", "target_ref": "api_module" @@ -116585,22 +83115,85 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "message": "@INVARIANT is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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" } } ], - "anchor_syntax": "def", - "body": "// [DEF:AssistantApi:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: assistant, api, client, chat, confirmation\n// @PURPOSE: API client wrapper for assistant chat, session-scoped prompts, confirmation actions, and history retrieval.\n// @LAYER: Infra-API\n// @RELATION: DEPENDS_ON -> api_module\n// @INVARIANT: All assistant requests must use requestApi wrapper (no native fetch).\n\nimport { requestApi } from '$lib/api.js';\n\n// [DEF:sendAssistantMessage:Function]\n// @PURPOSE: Send a user message to assistant orchestrator endpoint.\n// @PRE: payload.message is a non-empty string.\n// @POST: Returns assistant response object with deterministic state.\nexport function sendAssistantMessage(payload) {\n return requestApi('/assistant/messages', 'POST', payload);\n}\n// [/DEF:sendAssistantMessage:Function]\n\n// [DEF:buildAssistantSeedMessage:Function]\n// @PURPOSE: Compose visible assistant seed text from context label and prompt body.\n// @PRE: prompt contains the user-visible request.\n// @POST: Returns trimmed seed message string for prefilled input.\nexport function buildAssistantSeedMessage({ label = '', prompt = '' } = {}) {\n const normalizedLabel = String(label || '').trim();\n const normalizedPrompt = String(prompt || '').trim();\n if (normalizedLabel && normalizedPrompt) {\n return `${normalizedLabel}: ${normalizedPrompt}`;\n }\n return normalizedPrompt || normalizedLabel;\n}\n// [/DEF:buildAssistantSeedMessage:Function]\n\n// [DEF:confirmAssistantOperation:Function]\n// @PURPOSE: Confirm a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Returns execution response (started/success/failed).\nexport function confirmAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/confirm`, 'POST');\n}\n// [/DEF:confirmAssistantOperation:Function]\n\n// [DEF:cancelAssistantOperation:Function]\n// @PURPOSE: Cancel a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Operation is cancelled and cannot be executed by this token.\nexport function cancelAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/cancel`, 'POST');\n}\n// [/DEF:cancelAssistantOperation:Function]\n\n// [DEF:getAssistantHistory:Function]\n// @PURPOSE: Retrieve paginated assistant conversation history.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns a paginated payload with history items.\nexport function getAssistantHistory(page = 1, pageSize = 20, conversationId = null, fromLatest = false) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (conversationId) {\n params.append('conversation_id', conversationId);\n }\n if (fromLatest) {\n params.append('from_latest', 'true');\n }\n return requestApi(`/assistant/history?${params.toString()}`, 'GET');\n}\n// [/DEF:getAssistantHistory:Function]\n\n// [DEF:getAssistantConversations:Function]\n// @PURPOSE: Retrieve paginated conversation list for assistant sidebar/history switcher.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns paginated conversation summaries.\nexport function getAssistantConversations(\n page = 1,\n pageSize = 20,\n includeArchived = false,\n search = '',\n archivedOnly = false,\n) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (includeArchived) {\n params.append('include_archived', 'true');\n }\n if (archivedOnly) {\n params.append('archived_only', 'true');\n }\n if (search?.trim()) {\n params.append('search', search.trim());\n }\n return requestApi(`/assistant/conversations?${params.toString()}`, 'GET');\n}\n// [/DEF:getAssistantConversations:Function]\n\n// [DEF:deleteAssistantConversation:Function]\n// @PURPOSE: Soft-delete or hard-delete a conversation.\n// @PRE: conversationId string is provided.\n// @POST: Returns success status.\nexport function deleteAssistantConversation(conversationId) {\n return requestApi(`/assistant/conversations/${conversationId}`, 'DELETE');\n}\n// [/DEF:deleteAssistantConversation:Function]\n\n// [/DEF:AssistantApi:Module]\n" + "anchor_syntax": "region", + "body": "// #region AssistantApi:Module [TYPE Function]\n// @SEMANTICS: assistant, api, client, chat, confirmation\n// @PURPOSE: API client wrapper for assistant chat, session-scoped prompts, confirmation actions, and history retrieval.\n// @LAYER: Infra-API\n// @RELATION: DEPENDS_ON -> api_module\n// @INVARIANT: All assistant requests must use requestApi wrapper (no native fetch).\n\nimport { requestApi } from '$lib/api.js';\n\n// #region sendAssistantMessage:Function [TYPE Function]\n// @PURPOSE: Send a user message to assistant orchestrator endpoint.\n// @PRE: payload.message is a non-empty string.\n// @POST: Returns assistant response object with deterministic state.\nexport function sendAssistantMessage(payload) {\n return requestApi('/assistant/messages', 'POST', payload);\n}\n// #endregion sendAssistantMessage:Function\n\n// #region buildAssistantSeedMessage:Function [TYPE Function]\n// @PURPOSE: Compose visible assistant seed text from context label and prompt body.\n// @PRE: prompt contains the user-visible request.\n// @POST: Returns trimmed seed message string for prefilled input.\nexport function buildAssistantSeedMessage({ label = '', prompt = '' } = {}) {\n const normalizedLabel = String(label || '').trim();\n const normalizedPrompt = String(prompt || '').trim();\n if (normalizedLabel && normalizedPrompt) {\n return `${normalizedLabel}: ${normalizedPrompt}`;\n }\n return normalizedPrompt || normalizedLabel;\n}\n// #endregion buildAssistantSeedMessage:Function\n\n// #region confirmAssistantOperation:Function [TYPE Function]\n// @PURPOSE: Confirm a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Returns execution response (started/success/failed).\nexport function confirmAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/confirm`, 'POST');\n}\n// #endregion confirmAssistantOperation:Function\n\n// #region cancelAssistantOperation:Function [TYPE Function]\n// @PURPOSE: Cancel a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Operation is cancelled and cannot be executed by this token.\nexport function cancelAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/cancel`, 'POST');\n}\n// #endregion cancelAssistantOperation:Function\n\n// #region getAssistantHistory:Function [TYPE Function]\n// @PURPOSE: Retrieve paginated assistant conversation history.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns a paginated payload with history items.\nexport function getAssistantHistory(page = 1, pageSize = 20, conversationId = null, fromLatest = false) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (conversationId) {\n params.append('conversation_id', conversationId);\n }\n if (fromLatest) {\n params.append('from_latest', 'true');\n }\n return requestApi(`/assistant/history?${params.toString()}`, 'GET');\n}\n// #endregion getAssistantHistory:Function\n\n// #region getAssistantConversations:Function [TYPE Function]\n// @PURPOSE: Retrieve paginated conversation list for assistant sidebar/history switcher.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns paginated conversation summaries.\nexport function getAssistantConversations(\n page = 1,\n pageSize = 20,\n includeArchived = false,\n search = '',\n archivedOnly = false,\n) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (includeArchived) {\n params.append('include_archived', 'true');\n }\n if (archivedOnly) {\n params.append('archived_only', 'true');\n }\n if (search?.trim()) {\n params.append('search', search.trim());\n }\n return requestApi(`/assistant/conversations?${params.toString()}`, 'GET');\n}\n// #endregion getAssistantConversations:Function\n\n// #region deleteAssistantConversation:Function [TYPE Function]\n// @PURPOSE: Soft-delete or hard-delete a conversation.\n// @PRE: conversationId string is provided.\n// @POST: Returns success status.\nexport function deleteAssistantConversation(conversationId) {\n return requestApi(`/assistant/conversations/${conversationId}`, 'DELETE');\n}\n// #endregion deleteAssistantConversation:Function\n\n// #endregion AssistantApi:Module\n" }, { - "contract_id": "sendAssistantMessage", + "contract_id": "sendAssistantMessage:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/assistant.js", - "start_line": 11, - "end_line": 18, + "start_line": 14, + "end_line": 21, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116638,15 +83231,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:sendAssistantMessage:Function]\n// @PURPOSE: Send a user message to assistant orchestrator endpoint.\n// @PRE: payload.message is a non-empty string.\n// @POST: Returns assistant response object with deterministic state.\nexport function sendAssistantMessage(payload) {\n return requestApi('/assistant/messages', 'POST', payload);\n}\n// [/DEF:sendAssistantMessage:Function]\n" + "anchor_syntax": "region", + "body": "// #region sendAssistantMessage:Function [TYPE Function]\n// @PURPOSE: Send a user message to assistant orchestrator endpoint.\n// @PRE: payload.message is a non-empty string.\n// @POST: Returns assistant response object with deterministic state.\nexport function sendAssistantMessage(payload) {\n return requestApi('/assistant/messages', 'POST', payload);\n}\n// #endregion sendAssistantMessage:Function\n" }, { - "contract_id": "buildAssistantSeedMessage", + "contract_id": "buildAssistantSeedMessage:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/assistant.js", - "start_line": 20, - "end_line": 32, + "start_line": 23, + "end_line": 35, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116684,15 +83277,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:buildAssistantSeedMessage:Function]\n// @PURPOSE: Compose visible assistant seed text from context label and prompt body.\n// @PRE: prompt contains the user-visible request.\n// @POST: Returns trimmed seed message string for prefilled input.\nexport function buildAssistantSeedMessage({ label = '', prompt = '' } = {}) {\n const normalizedLabel = String(label || '').trim();\n const normalizedPrompt = String(prompt || '').trim();\n if (normalizedLabel && normalizedPrompt) {\n return `${normalizedLabel}: ${normalizedPrompt}`;\n }\n return normalizedPrompt || normalizedLabel;\n}\n// [/DEF:buildAssistantSeedMessage:Function]\n" + "anchor_syntax": "region", + "body": "// #region buildAssistantSeedMessage:Function [TYPE Function]\n// @PURPOSE: Compose visible assistant seed text from context label and prompt body.\n// @PRE: prompt contains the user-visible request.\n// @POST: Returns trimmed seed message string for prefilled input.\nexport function buildAssistantSeedMessage({ label = '', prompt = '' } = {}) {\n const normalizedLabel = String(label || '').trim();\n const normalizedPrompt = String(prompt || '').trim();\n if (normalizedLabel && normalizedPrompt) {\n return `${normalizedLabel}: ${normalizedPrompt}`;\n }\n return normalizedPrompt || normalizedLabel;\n}\n// #endregion buildAssistantSeedMessage:Function\n" }, { - "contract_id": "confirmAssistantOperation", + "contract_id": "confirmAssistantOperation:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/assistant.js", - "start_line": 34, - "end_line": 41, + "start_line": 37, + "end_line": 44, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116730,15 +83323,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:confirmAssistantOperation:Function]\n// @PURPOSE: Confirm a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Returns execution response (started/success/failed).\nexport function confirmAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/confirm`, 'POST');\n}\n// [/DEF:confirmAssistantOperation:Function]\n" + "anchor_syntax": "region", + "body": "// #region confirmAssistantOperation:Function [TYPE Function]\n// @PURPOSE: Confirm a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Returns execution response (started/success/failed).\nexport function confirmAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/confirm`, 'POST');\n}\n// #endregion confirmAssistantOperation:Function\n" }, { - "contract_id": "cancelAssistantOperation", + "contract_id": "cancelAssistantOperation:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/assistant.js", - "start_line": 43, - "end_line": 50, + "start_line": 46, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116776,15 +83369,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:cancelAssistantOperation:Function]\n// @PURPOSE: Cancel a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Operation is cancelled and cannot be executed by this token.\nexport function cancelAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/cancel`, 'POST');\n}\n// [/DEF:cancelAssistantOperation:Function]\n" + "anchor_syntax": "region", + "body": "// #region cancelAssistantOperation:Function [TYPE Function]\n// @PURPOSE: Cancel a pending risky assistant operation.\n// @PRE: confirmationId references an existing pending token.\n// @POST: Operation is cancelled and cannot be executed by this token.\nexport function cancelAssistantOperation(confirmationId) {\n return requestApi(`/assistant/confirmations/${confirmationId}/cancel`, 'POST');\n}\n// #endregion cancelAssistantOperation:Function\n" }, { - "contract_id": "getAssistantHistory", + "contract_id": "getAssistantHistory:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/assistant.js", - "start_line": 52, - "end_line": 66, + "start_line": 55, + "end_line": 69, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116822,15 +83415,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:getAssistantHistory:Function]\n// @PURPOSE: Retrieve paginated assistant conversation history.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns a paginated payload with history items.\nexport function getAssistantHistory(page = 1, pageSize = 20, conversationId = null, fromLatest = false) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (conversationId) {\n params.append('conversation_id', conversationId);\n }\n if (fromLatest) {\n params.append('from_latest', 'true');\n }\n return requestApi(`/assistant/history?${params.toString()}`, 'GET');\n}\n// [/DEF:getAssistantHistory:Function]\n" + "anchor_syntax": "region", + "body": "// #region getAssistantHistory:Function [TYPE Function]\n// @PURPOSE: Retrieve paginated assistant conversation history.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns a paginated payload with history items.\nexport function getAssistantHistory(page = 1, pageSize = 20, conversationId = null, fromLatest = false) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (conversationId) {\n params.append('conversation_id', conversationId);\n }\n if (fromLatest) {\n params.append('from_latest', 'true');\n }\n return requestApi(`/assistant/history?${params.toString()}`, 'GET');\n}\n// #endregion getAssistantHistory:Function\n" }, { - "contract_id": "getAssistantConversations", + "contract_id": "getAssistantConversations:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/assistant.js", - "start_line": 68, - "end_line": 91, + "start_line": 71, + "end_line": 94, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116868,15 +83461,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:getAssistantConversations:Function]\n// @PURPOSE: Retrieve paginated conversation list for assistant sidebar/history switcher.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns paginated conversation summaries.\nexport function getAssistantConversations(\n page = 1,\n pageSize = 20,\n includeArchived = false,\n search = '',\n archivedOnly = false,\n) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (includeArchived) {\n params.append('include_archived', 'true');\n }\n if (archivedOnly) {\n params.append('archived_only', 'true');\n }\n if (search?.trim()) {\n params.append('search', search.trim());\n }\n return requestApi(`/assistant/conversations?${params.toString()}`, 'GET');\n}\n// [/DEF:getAssistantConversations:Function]\n" + "anchor_syntax": "region", + "body": "// #region getAssistantConversations:Function [TYPE Function]\n// @PURPOSE: Retrieve paginated conversation list for assistant sidebar/history switcher.\n// @PRE: page/pageSize are positive integers.\n// @POST: Returns paginated conversation summaries.\nexport function getAssistantConversations(\n page = 1,\n pageSize = 20,\n includeArchived = false,\n search = '',\n archivedOnly = false,\n) {\n const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });\n if (includeArchived) {\n params.append('include_archived', 'true');\n }\n if (archivedOnly) {\n params.append('archived_only', 'true');\n }\n if (search?.trim()) {\n params.append('search', search.trim());\n }\n return requestApi(`/assistant/conversations?${params.toString()}`, 'GET');\n}\n// #endregion getAssistantConversations:Function\n" }, { - "contract_id": "deleteAssistantConversation", + "contract_id": "deleteAssistantConversation:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/assistant.js", - "start_line": 93, - "end_line": 100, + "start_line": 96, + "end_line": 103, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116914,19 +83507,42 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:deleteAssistantConversation:Function]\n// @PURPOSE: Soft-delete or hard-delete a conversation.\n// @PRE: conversationId string is provided.\n// @POST: Returns success status.\nexport function deleteAssistantConversation(conversationId) {\n return requestApi(`/assistant/conversations/${conversationId}`, 'DELETE');\n}\n// [/DEF:deleteAssistantConversation:Function]\n" + "anchor_syntax": "region", + "body": "// #region deleteAssistantConversation:Function [TYPE Function]\n// @PURPOSE: Soft-delete or hard-delete a conversation.\n// @PRE: conversationId string is provided.\n// @POST: Returns success status.\nexport function deleteAssistantConversation(conversationId) {\n return requestApi(`/assistant/conversations/${conversationId}`, 'DELETE');\n}\n// #endregion deleteAssistantConversation:Function\n" }, { "contract_id": "DatasetReviewApi", "contract_type": "Module", "file_path": "frontend/src/lib/api/datasetReview.js", "start_line": 1, - "end_line": 132, + "end_line": 136, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, + "PURPOSE": "Shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and session DTO normalization." + }, + "relations": [ + { + "source_id": "DatasetReviewApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "// #region DatasetReviewApi [C:3] [TYPE Module] [SEMANTICS dataset, review, api, session, conflict]\n// @BRIEF Shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and session DTO normalization.\n// @RELATION DEPENDS_ON -> [ApiModule]\n\n// #region DatasetReviewApi:Module [TYPE Function]\n// @SEMANTICS: dataset-review, api, session-version, headers, conflict-handling\n// @PURPOSE: Provide shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and refreshed session DTO normalization.\n// @LAYER: Infra-API\n// @RELATION: DEPENDS_ON -> api_module\n\nimport { requestApi } from '$lib/api.js';\n\n// #region buildDatasetReviewRequestOptions:Function [TYPE Function]\n// @PURPOSE: Attach optimistic-lock session version header when the current version is known.\n// @PRE: sessionVersion may be null when no loaded session version exists yet.\n// @POST: Returns requestApi-compatible options object.\nexport function buildDatasetReviewRequestOptions(sessionVersion) {\n if (sessionVersion === null || sessionVersion === undefined || sessionVersion === '') {\n return {};\n }\n return {\n headers: {\n 'X-Session-Version': String(sessionVersion),\n },\n };\n}\n// #endregion buildDatasetReviewRequestOptions:Function\n\n// #region requestDatasetReviewApi:Function [TYPE Function]\n// @PURPOSE: Proxy dataset review mutations through requestApi with optional optimistic-lock headers.\n// @PRE: endpoint and method are valid requestApi inputs.\n// @POST: Executes requestApi with X-Session-Version only when a session version is known.\nexport function requestDatasetReviewApi(endpoint, method = 'GET', body = null, sessionVersion = null) {\n const requestOptions = buildDatasetReviewRequestOptions(sessionVersion);\n if (requestOptions.headers) {\n return requestApi(endpoint, method, body, requestOptions);\n }\n return requestApi(endpoint, method, body);\n}\n// #endregion requestDatasetReviewApi:Function\n\n// #region isDatasetReviewConflictError:Function [TYPE Function]\n// @PURPOSE: Detect optimistic-lock conflicts from dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns true when the mutation failed with 409 conflict semantics.\nexport function isDatasetReviewConflictError(error) {\n return Number(error?.status) === 409;\n}\n// #endregion isDatasetReviewConflictError:Function\n\n// #region getDatasetReviewConflictMessage:Function [TYPE Function]\n// @PURPOSE: Return explicit 409-style guidance for stale dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns a user-facing conflict message string.\nexport function getDatasetReviewConflictMessage(error) {\n return (\n error?.message ||\n 'This review session changed in another action. Reload the latest session state and retry the update.'\n );\n}\n// #endregion getDatasetReviewConflictMessage:Function\n\n// #region extractDatasetReviewVersion:Function [TYPE Function]\n// @PURPOSE: Resolve the latest session version from refreshed DTO fragments.\n// @PRE: payload may be a session summary, a detail payload, or a mutation fragment.\n// @POST: Returns the newest known session version or null.\nexport function extractDatasetReviewVersion(payload) {\n if (!payload) {\n return null;\n }\n\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const version = extractDatasetReviewVersion(item);\n if (version !== null) {\n return version;\n }\n }\n return null;\n }\n\n if (payload.session_version !== undefined && payload.session_version !== null) {\n return payload.session_version;\n }\n if (payload.version !== undefined && payload.version !== null) {\n return payload.version;\n }\n if (payload.session) {\n return extractDatasetReviewVersion(payload.session);\n }\n\n return null;\n}\n// #endregion extractDatasetReviewVersion:Function\n\n// #region normalizeDatasetReviewDetail:Function [TYPE Function]\n// @PURPOSE: Normalize refreshed session-detail DTOs into the legacy route-friendly workspace shape.\n// @PRE: detail may already be legacy-flat or refreshed with nested session/session_version fields.\n// @POST: Returns a shape with flattened summary fields plus refreshed collections and version metadata.\nexport function normalizeDatasetReviewDetail(detail) {\n if (!detail) {\n return null;\n }\n\n if (!detail.session && detail.session_id) {\n return detail;\n }\n\n const summary = detail.session || {};\n const preview = detail.preview || null;\n\n return {\n ...summary,\n session_version:\n detail.session_version ?? summary.session_version ?? summary.version ?? null,\n version: detail.session_version ?? summary.session_version ?? summary.version ?? null,\n profile: detail.profile || null,\n findings: detail.findings || [],\n semantic_sources: detail.semantic_sources || [],\n semantic_fields: detail.semantic_fields || [],\n imported_filters: detail.filters || detail.imported_filters || [],\n template_variables: detail.template_variables || [],\n execution_mappings: detail.mappings || detail.execution_mappings || [],\n clarification: detail.clarification || null,\n clarification_sessions: detail.clarification\n ? [detail.clarification.clarification_session].filter(Boolean)\n : detail.clarification_sessions || [],\n previews: preview ? [preview] : detail.previews || [],\n preview,\n latest_run_context: detail.latest_run_context || null,\n };\n}\n// #endregion normalizeDatasetReviewDetail:Function\n\n// #endregion DatasetReviewApi:Module\n// #endregion DatasetReviewApi\n" + }, + { + "contract_id": "DatasetReviewApi:Module", + "contract_type": "Function", + "file_path": "frontend/src/lib/api/datasetReview.js", + "start_line": 5, + "end_line": 135, + "tier": "TIER_1", + "complexity": 1, + "metadata": { "LAYER": "Infra-API", "PURPOSE": "Provide shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and refreshed session DTO normalization.", "SEMANTICS": [ @@ -116939,22 +83555,86 @@ }, "relations": [ { - "source_id": "DatasetReviewApi", + "source_id": "DatasetReviewApi:Module", "relation_type": "DEPENDS_ON", "target_id": "api_module", "target_ref": "api_module" } ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "// [DEF:DatasetReviewApi:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, api, session-version, headers, conflict-handling\n// @PURPOSE: Provide shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and refreshed session DTO normalization.\n// @LAYER: Infra-API\n// @RELATION: DEPENDS_ON -> api_module\n\nimport { requestApi } from '$lib/api.js';\n\n// [DEF:buildDatasetReviewRequestOptions:Function]\n// @PURPOSE: Attach optimistic-lock session version header when the current version is known.\n// @PRE: sessionVersion may be null when no loaded session version exists yet.\n// @POST: Returns requestApi-compatible options object.\nexport function buildDatasetReviewRequestOptions(sessionVersion) {\n if (sessionVersion === null || sessionVersion === undefined || sessionVersion === '') {\n return {};\n }\n return {\n headers: {\n 'X-Session-Version': String(sessionVersion),\n },\n };\n}\n// [/DEF:buildDatasetReviewRequestOptions:Function]\n\n// [DEF:requestDatasetReviewApi:Function]\n// @PURPOSE: Proxy dataset review mutations through requestApi with optional optimistic-lock headers.\n// @PRE: endpoint and method are valid requestApi inputs.\n// @POST: Executes requestApi with X-Session-Version only when a session version is known.\nexport function requestDatasetReviewApi(endpoint, method = 'GET', body = null, sessionVersion = null) {\n const requestOptions = buildDatasetReviewRequestOptions(sessionVersion);\n if (requestOptions.headers) {\n return requestApi(endpoint, method, body, requestOptions);\n }\n return requestApi(endpoint, method, body);\n}\n// [/DEF:requestDatasetReviewApi:Function]\n\n// [DEF:isDatasetReviewConflictError:Function]\n// @PURPOSE: Detect optimistic-lock conflicts from dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns true when the mutation failed with 409 conflict semantics.\nexport function isDatasetReviewConflictError(error) {\n return Number(error?.status) === 409;\n}\n// [/DEF:isDatasetReviewConflictError:Function]\n\n// [DEF:getDatasetReviewConflictMessage:Function]\n// @PURPOSE: Return explicit 409-style guidance for stale dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns a user-facing conflict message string.\nexport function getDatasetReviewConflictMessage(error) {\n return (\n error?.message ||\n 'This review session changed in another action. Reload the latest session state and retry the update.'\n );\n}\n// [/DEF:getDatasetReviewConflictMessage:Function]\n\n// [DEF:extractDatasetReviewVersion:Function]\n// @PURPOSE: Resolve the latest session version from refreshed DTO fragments.\n// @PRE: payload may be a session summary, a detail payload, or a mutation fragment.\n// @POST: Returns the newest known session version or null.\nexport function extractDatasetReviewVersion(payload) {\n if (!payload) {\n return null;\n }\n\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const version = extractDatasetReviewVersion(item);\n if (version !== null) {\n return version;\n }\n }\n return null;\n }\n\n if (payload.session_version !== undefined && payload.session_version !== null) {\n return payload.session_version;\n }\n if (payload.version !== undefined && payload.version !== null) {\n return payload.version;\n }\n if (payload.session) {\n return extractDatasetReviewVersion(payload.session);\n }\n\n return null;\n}\n// [/DEF:extractDatasetReviewVersion:Function]\n\n// [DEF:normalizeDatasetReviewDetail:Function]\n// @PURPOSE: Normalize refreshed session-detail DTOs into the legacy route-friendly workspace shape.\n// @PRE: detail may already be legacy-flat or refreshed with nested session/session_version fields.\n// @POST: Returns a shape with flattened summary fields plus refreshed collections and version metadata.\nexport function normalizeDatasetReviewDetail(detail) {\n if (!detail) {\n return null;\n }\n\n if (!detail.session && detail.session_id) {\n return detail;\n }\n\n const summary = detail.session || {};\n const preview = detail.preview || null;\n\n return {\n ...summary,\n session_version:\n detail.session_version ?? summary.session_version ?? summary.version ?? null,\n version: detail.session_version ?? summary.session_version ?? summary.version ?? null,\n profile: detail.profile || null,\n findings: detail.findings || [],\n semantic_sources: detail.semantic_sources || [],\n semantic_fields: detail.semantic_fields || [],\n imported_filters: detail.filters || detail.imported_filters || [],\n template_variables: detail.template_variables || [],\n execution_mappings: detail.mappings || detail.execution_mappings || [],\n clarification: detail.clarification || null,\n clarification_sessions: detail.clarification\n ? [detail.clarification.clarification_session].filter(Boolean)\n : detail.clarification_sessions || [],\n previews: preview ? [preview] : detail.previews || [],\n preview,\n latest_run_context: detail.latest_run_context || null,\n };\n}\n// [/DEF:normalizeDatasetReviewDetail:Function]\n\n// [/DEF:DatasetReviewApi:Module]\n" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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" + } + } + ], + "anchor_syntax": "region", + "body": "// #region DatasetReviewApi:Module [TYPE Function]\n// @SEMANTICS: dataset-review, api, session-version, headers, conflict-handling\n// @PURPOSE: Provide shared frontend helpers for dataset review session-scoped API requests, optimistic-lock propagation, and refreshed session DTO normalization.\n// @LAYER: Infra-API\n// @RELATION: DEPENDS_ON -> api_module\n\nimport { requestApi } from '$lib/api.js';\n\n// #region buildDatasetReviewRequestOptions:Function [TYPE Function]\n// @PURPOSE: Attach optimistic-lock session version header when the current version is known.\n// @PRE: sessionVersion may be null when no loaded session version exists yet.\n// @POST: Returns requestApi-compatible options object.\nexport function buildDatasetReviewRequestOptions(sessionVersion) {\n if (sessionVersion === null || sessionVersion === undefined || sessionVersion === '') {\n return {};\n }\n return {\n headers: {\n 'X-Session-Version': String(sessionVersion),\n },\n };\n}\n// #endregion buildDatasetReviewRequestOptions:Function\n\n// #region requestDatasetReviewApi:Function [TYPE Function]\n// @PURPOSE: Proxy dataset review mutations through requestApi with optional optimistic-lock headers.\n// @PRE: endpoint and method are valid requestApi inputs.\n// @POST: Executes requestApi with X-Session-Version only when a session version is known.\nexport function requestDatasetReviewApi(endpoint, method = 'GET', body = null, sessionVersion = null) {\n const requestOptions = buildDatasetReviewRequestOptions(sessionVersion);\n if (requestOptions.headers) {\n return requestApi(endpoint, method, body, requestOptions);\n }\n return requestApi(endpoint, method, body);\n}\n// #endregion requestDatasetReviewApi:Function\n\n// #region isDatasetReviewConflictError:Function [TYPE Function]\n// @PURPOSE: Detect optimistic-lock conflicts from dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns true when the mutation failed with 409 conflict semantics.\nexport function isDatasetReviewConflictError(error) {\n return Number(error?.status) === 409;\n}\n// #endregion isDatasetReviewConflictError:Function\n\n// #region getDatasetReviewConflictMessage:Function [TYPE Function]\n// @PURPOSE: Return explicit 409-style guidance for stale dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns a user-facing conflict message string.\nexport function getDatasetReviewConflictMessage(error) {\n return (\n error?.message ||\n 'This review session changed in another action. Reload the latest session state and retry the update.'\n );\n}\n// #endregion getDatasetReviewConflictMessage:Function\n\n// #region extractDatasetReviewVersion:Function [TYPE Function]\n// @PURPOSE: Resolve the latest session version from refreshed DTO fragments.\n// @PRE: payload may be a session summary, a detail payload, or a mutation fragment.\n// @POST: Returns the newest known session version or null.\nexport function extractDatasetReviewVersion(payload) {\n if (!payload) {\n return null;\n }\n\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const version = extractDatasetReviewVersion(item);\n if (version !== null) {\n return version;\n }\n }\n return null;\n }\n\n if (payload.session_version !== undefined && payload.session_version !== null) {\n return payload.session_version;\n }\n if (payload.version !== undefined && payload.version !== null) {\n return payload.version;\n }\n if (payload.session) {\n return extractDatasetReviewVersion(payload.session);\n }\n\n return null;\n}\n// #endregion extractDatasetReviewVersion:Function\n\n// #region normalizeDatasetReviewDetail:Function [TYPE Function]\n// @PURPOSE: Normalize refreshed session-detail DTOs into the legacy route-friendly workspace shape.\n// @PRE: detail may already be legacy-flat or refreshed with nested session/session_version fields.\n// @POST: Returns a shape with flattened summary fields plus refreshed collections and version metadata.\nexport function normalizeDatasetReviewDetail(detail) {\n if (!detail) {\n return null;\n }\n\n if (!detail.session && detail.session_id) {\n return detail;\n }\n\n const summary = detail.session || {};\n const preview = detail.preview || null;\n\n return {\n ...summary,\n session_version:\n detail.session_version ?? summary.session_version ?? summary.version ?? null,\n version: detail.session_version ?? summary.session_version ?? summary.version ?? null,\n profile: detail.profile || null,\n findings: detail.findings || [],\n semantic_sources: detail.semantic_sources || [],\n semantic_fields: detail.semantic_fields || [],\n imported_filters: detail.filters || detail.imported_filters || [],\n template_variables: detail.template_variables || [],\n execution_mappings: detail.mappings || detail.execution_mappings || [],\n clarification: detail.clarification || null,\n clarification_sessions: detail.clarification\n ? [detail.clarification.clarification_session].filter(Boolean)\n : detail.clarification_sessions || [],\n previews: preview ? [preview] : detail.previews || [],\n preview,\n latest_run_context: detail.latest_run_context || null,\n };\n}\n// #endregion normalizeDatasetReviewDetail:Function\n\n// #endregion DatasetReviewApi:Module\n" }, { - "contract_id": "buildDatasetReviewRequestOptions", + "contract_id": "buildDatasetReviewRequestOptions:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/datasetReview.js", - "start_line": 10, - "end_line": 24, + "start_line": 13, + "end_line": 27, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -116992,15 +83672,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:buildDatasetReviewRequestOptions:Function]\n// @PURPOSE: Attach optimistic-lock session version header when the current version is known.\n// @PRE: sessionVersion may be null when no loaded session version exists yet.\n// @POST: Returns requestApi-compatible options object.\nexport function buildDatasetReviewRequestOptions(sessionVersion) {\n if (sessionVersion === null || sessionVersion === undefined || sessionVersion === '') {\n return {};\n }\n return {\n headers: {\n 'X-Session-Version': String(sessionVersion),\n },\n };\n}\n// [/DEF:buildDatasetReviewRequestOptions:Function]\n" + "anchor_syntax": "region", + "body": "// #region buildDatasetReviewRequestOptions:Function [TYPE Function]\n// @PURPOSE: Attach optimistic-lock session version header when the current version is known.\n// @PRE: sessionVersion may be null when no loaded session version exists yet.\n// @POST: Returns requestApi-compatible options object.\nexport function buildDatasetReviewRequestOptions(sessionVersion) {\n if (sessionVersion === null || sessionVersion === undefined || sessionVersion === '') {\n return {};\n }\n return {\n headers: {\n 'X-Session-Version': String(sessionVersion),\n },\n };\n}\n// #endregion buildDatasetReviewRequestOptions:Function\n" }, { - "contract_id": "requestDatasetReviewApi", + "contract_id": "requestDatasetReviewApi:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/datasetReview.js", - "start_line": 26, - "end_line": 37, + "start_line": 29, + "end_line": 40, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117038,15 +83718,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:requestDatasetReviewApi:Function]\n// @PURPOSE: Proxy dataset review mutations through requestApi with optional optimistic-lock headers.\n// @PRE: endpoint and method are valid requestApi inputs.\n// @POST: Executes requestApi with X-Session-Version only when a session version is known.\nexport function requestDatasetReviewApi(endpoint, method = 'GET', body = null, sessionVersion = null) {\n const requestOptions = buildDatasetReviewRequestOptions(sessionVersion);\n if (requestOptions.headers) {\n return requestApi(endpoint, method, body, requestOptions);\n }\n return requestApi(endpoint, method, body);\n}\n// [/DEF:requestDatasetReviewApi:Function]\n" + "anchor_syntax": "region", + "body": "// #region requestDatasetReviewApi:Function [TYPE Function]\n// @PURPOSE: Proxy dataset review mutations through requestApi with optional optimistic-lock headers.\n// @PRE: endpoint and method are valid requestApi inputs.\n// @POST: Executes requestApi with X-Session-Version only when a session version is known.\nexport function requestDatasetReviewApi(endpoint, method = 'GET', body = null, sessionVersion = null) {\n const requestOptions = buildDatasetReviewRequestOptions(sessionVersion);\n if (requestOptions.headers) {\n return requestApi(endpoint, method, body, requestOptions);\n }\n return requestApi(endpoint, method, body);\n}\n// #endregion requestDatasetReviewApi:Function\n" }, { - "contract_id": "isDatasetReviewConflictError", + "contract_id": "isDatasetReviewConflictError:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/datasetReview.js", - "start_line": 39, - "end_line": 46, + "start_line": 42, + "end_line": 49, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117084,15 +83764,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:isDatasetReviewConflictError:Function]\n// @PURPOSE: Detect optimistic-lock conflicts from dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns true when the mutation failed with 409 conflict semantics.\nexport function isDatasetReviewConflictError(error) {\n return Number(error?.status) === 409;\n}\n// [/DEF:isDatasetReviewConflictError:Function]\n" + "anchor_syntax": "region", + "body": "// #region isDatasetReviewConflictError:Function [TYPE Function]\n// @PURPOSE: Detect optimistic-lock conflicts from dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns true when the mutation failed with 409 conflict semantics.\nexport function isDatasetReviewConflictError(error) {\n return Number(error?.status) === 409;\n}\n// #endregion isDatasetReviewConflictError:Function\n" }, { - "contract_id": "getDatasetReviewConflictMessage", + "contract_id": "getDatasetReviewConflictMessage:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/datasetReview.js", - "start_line": 48, - "end_line": 58, + "start_line": 51, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117130,15 +83810,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:getDatasetReviewConflictMessage:Function]\n// @PURPOSE: Return explicit 409-style guidance for stale dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns a user-facing conflict message string.\nexport function getDatasetReviewConflictMessage(error) {\n return (\n error?.message ||\n 'This review session changed in another action. Reload the latest session state and retry the update.'\n );\n}\n// [/DEF:getDatasetReviewConflictMessage:Function]\n" + "anchor_syntax": "region", + "body": "// #region getDatasetReviewConflictMessage:Function [TYPE Function]\n// @PURPOSE: Return explicit 409-style guidance for stale dataset review mutations.\n// @PRE: error may be null or a normalized API error.\n// @POST: Returns a user-facing conflict message string.\nexport function getDatasetReviewConflictMessage(error) {\n return (\n error?.message ||\n 'This review session changed in another action. Reload the latest session state and retry the update.'\n );\n}\n// #endregion getDatasetReviewConflictMessage:Function\n" }, { - "contract_id": "extractDatasetReviewVersion", + "contract_id": "extractDatasetReviewVersion:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/datasetReview.js", - "start_line": 60, - "end_line": 91, + "start_line": 63, + "end_line": 94, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117176,15 +83856,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:extractDatasetReviewVersion:Function]\n// @PURPOSE: Resolve the latest session version from refreshed DTO fragments.\n// @PRE: payload may be a session summary, a detail payload, or a mutation fragment.\n// @POST: Returns the newest known session version or null.\nexport function extractDatasetReviewVersion(payload) {\n if (!payload) {\n return null;\n }\n\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const version = extractDatasetReviewVersion(item);\n if (version !== null) {\n return version;\n }\n }\n return null;\n }\n\n if (payload.session_version !== undefined && payload.session_version !== null) {\n return payload.session_version;\n }\n if (payload.version !== undefined && payload.version !== null) {\n return payload.version;\n }\n if (payload.session) {\n return extractDatasetReviewVersion(payload.session);\n }\n\n return null;\n}\n// [/DEF:extractDatasetReviewVersion:Function]\n" + "anchor_syntax": "region", + "body": "// #region extractDatasetReviewVersion:Function [TYPE Function]\n// @PURPOSE: Resolve the latest session version from refreshed DTO fragments.\n// @PRE: payload may be a session summary, a detail payload, or a mutation fragment.\n// @POST: Returns the newest known session version or null.\nexport function extractDatasetReviewVersion(payload) {\n if (!payload) {\n return null;\n }\n\n if (Array.isArray(payload)) {\n for (const item of payload) {\n const version = extractDatasetReviewVersion(item);\n if (version !== null) {\n return version;\n }\n }\n return null;\n }\n\n if (payload.session_version !== undefined && payload.session_version !== null) {\n return payload.session_version;\n }\n if (payload.version !== undefined && payload.version !== null) {\n return payload.version;\n }\n if (payload.session) {\n return extractDatasetReviewVersion(payload.session);\n }\n\n return null;\n}\n// #endregion extractDatasetReviewVersion:Function\n" }, { - "contract_id": "normalizeDatasetReviewDetail", + "contract_id": "normalizeDatasetReviewDetail:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/datasetReview.js", - "start_line": 93, - "end_line": 130, + "start_line": 96, + "end_line": 133, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117222,19 +83902,45 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:normalizeDatasetReviewDetail:Function]\n// @PURPOSE: Normalize refreshed session-detail DTOs into the legacy route-friendly workspace shape.\n// @PRE: detail may already be legacy-flat or refreshed with nested session/session_version fields.\n// @POST: Returns a shape with flattened summary fields plus refreshed collections and version metadata.\nexport function normalizeDatasetReviewDetail(detail) {\n if (!detail) {\n return null;\n }\n\n if (!detail.session && detail.session_id) {\n return detail;\n }\n\n const summary = detail.session || {};\n const preview = detail.preview || null;\n\n return {\n ...summary,\n session_version:\n detail.session_version ?? summary.session_version ?? summary.version ?? null,\n version: detail.session_version ?? summary.session_version ?? summary.version ?? null,\n profile: detail.profile || null,\n findings: detail.findings || [],\n semantic_sources: detail.semantic_sources || [],\n semantic_fields: detail.semantic_fields || [],\n imported_filters: detail.filters || detail.imported_filters || [],\n template_variables: detail.template_variables || [],\n execution_mappings: detail.mappings || detail.execution_mappings || [],\n clarification: detail.clarification || null,\n clarification_sessions: detail.clarification\n ? [detail.clarification.clarification_session].filter(Boolean)\n : detail.clarification_sessions || [],\n previews: preview ? [preview] : detail.previews || [],\n preview,\n latest_run_context: detail.latest_run_context || null,\n };\n}\n// [/DEF:normalizeDatasetReviewDetail:Function]\n" + "anchor_syntax": "region", + "body": "// #region normalizeDatasetReviewDetail:Function [TYPE Function]\n// @PURPOSE: Normalize refreshed session-detail DTOs into the legacy route-friendly workspace shape.\n// @PRE: detail may already be legacy-flat or refreshed with nested session/session_version fields.\n// @POST: Returns a shape with flattened summary fields plus refreshed collections and version metadata.\nexport function normalizeDatasetReviewDetail(detail) {\n if (!detail) {\n return null;\n }\n\n if (!detail.session && detail.session_id) {\n return detail;\n }\n\n const summary = detail.session || {};\n const preview = detail.preview || null;\n\n return {\n ...summary,\n session_version:\n detail.session_version ?? summary.session_version ?? summary.version ?? null,\n version: detail.session_version ?? summary.session_version ?? summary.version ?? null,\n profile: detail.profile || null,\n findings: detail.findings || [],\n semantic_sources: detail.semantic_sources || [],\n semantic_fields: detail.semantic_fields || [],\n imported_filters: detail.filters || detail.imported_filters || [],\n template_variables: detail.template_variables || [],\n execution_mappings: detail.mappings || detail.execution_mappings || [],\n clarification: detail.clarification || null,\n clarification_sessions: detail.clarification\n ? [detail.clarification.clarification_session].filter(Boolean)\n : detail.clarification_sessions || [],\n previews: preview ? [preview] : detail.previews || [],\n preview,\n latest_run_context: detail.latest_run_context || null,\n };\n}\n// #endregion normalizeDatasetReviewDetail:Function\n" }, { "contract_id": "ReportsApi", "contract_type": "Module", "file_path": "frontend/src/lib/api/reports.js", "start_line": 1, - "end_line": 109, + "end_line": 116, "tier": "TIER_2", "complexity": 4, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 4, + "POST": "Report list and detail helpers return backend payloads or normalized UI-safe errors.", + "PRE": "Shared API wrapper is configured for the current frontend auth/session context.", + "PURPOSE": "Wrapper-based reports API client for list/detail retrieval through the shared API wrapper.", + "SIDE_EFFECT": "Performs authenticated report HTTP requests through the shared API wrapper." + }, + "relations": [ + { + "source_id": "ReportsApi", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "// #region ReportsApi [C:4] [TYPE Module] [SEMANTICS report, api, search, filter, query]\n// @BRIEF Wrapper-based reports API client for list/detail retrieval through the shared API wrapper.\n// @RELATION DEPENDS_ON -> [ApiModule]\n// @PRE Shared API wrapper is configured for the current frontend auth/session context.\n// @POST Report list and detail helpers return backend payloads or normalized UI-safe errors.\n// @SIDE_EFFECT Performs authenticated report HTTP requests through the shared API wrapper.\n\n// #region ReportsApi:Module [TYPE Function]\n// @SEMANTICS: frontend, api_client, reports, wrapper\n// @PURPOSE: Wrapper-based reports API client for list/detail retrieval without direct native fetch usage.\n// @LAYER: Infra\n// @RELATION: DEPENDS_ON -> [api_module]\n// @PRE: Shared API wrapper is configured for the current frontend auth/session context.\n// @POST: Report list and detail helpers return backend payloads or normalized UI-safe errors.\n// @SIDE_EFFECT: Performs authenticated report HTTP requests through the shared API wrapper.\n// @INVARIANT: Uses existing api wrapper methods and returns structured errors for UI-state mapping.\nimport { api } from '../api.js';\n\n// #region buildReportQueryString:Function [TYPE Function]\n// @PURPOSE: Build query string for reports list endpoint from filter options.\n// @PRE: options is an object with optional report query fields.\n// @POST: Returns URL query string without leading '?'.\nexport function buildReportQueryString(options = {}) {\n\tconsole.log(\"[reports][api][buildReportQueryString:START]\");\n\tconst params = new URLSearchParams();\n\n\tif (options.page != null) params.append('page', String(options.page));\n\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\n\tif (Array.isArray(options.task_types) && options.task_types.length > 0) {\n\t\tparams.append('task_types', options.task_types.join(','));\n\t}\n\tif (Array.isArray(options.statuses) && options.statuses.length > 0) {\n\t\tparams.append('statuses', options.statuses.join(','));\n\t}\n\n\tif (options.time_from) params.append('time_from', options.time_from);\n\tif (options.time_to) params.append('time_to', options.time_to);\n\tif (options.search) params.append('search', options.search);\n\tif (options.sort_by) params.append('sort_by', options.sort_by);\n\tif (options.sort_order) params.append('sort_order', options.sort_order);\n\n\treturn params.toString();\n}\n// #endregion buildReportQueryString:Function\n\n// #region normalizeApiError: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.\nexport function normalizeApiError(error) {\n\tconsole.log(\"[reports][api][normalizeApiError:START]\");\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\t'Failed to load reports';\n\n\treturn {\n\t\tmessage,\n\t\tcode: 'REPORTS_API_ERROR',\n\t\tretryable: true\n\t};\n}\n// #endregion normalizeApiError:Function\n\n// #region getReports:Function [TYPE Function]\n// @PURPOSE: Fetch unified report list using existing request wrapper.\n// @PRE: valid auth context for protected endpoint.\n// @POST: Returns parsed payload or structured error for UI-state mapping.\n//\n// @TEST_CONTRACT: GetReportsApi ->\n// {\n// required_fields: {},\n// optional_fields: {options: Object},\n// invariants: [\n// \"Fetches from /reports with built query string\",\n// \"Returns response payload on success\",\n// \"Catches and normalizes errors using normalizeApiError\"\n// ]\n// }\n// @TEST_FIXTURE: valid_get_reports -> {\"options\": {\"page\": 1}}\n// @TEST_EDGE: api_fetch_failure -> api.fetchApi throws error\n// @TEST_INVARIANT: error_normalization -> verifies: [api_fetch_failure]\nexport async function getReports(options = {}) {\n\ttry {\n\t\tconsole.log(\"[reports][api][getReports:STARTED]\", options);\n\t\tconst query = buildReportQueryString(options);\n\t\tconst res = await api.fetchApi(`/reports${query ? `?${query}` : ''}`);\n\t\tconsole.log(\"[reports][api][getReports:SUCCESS]\", res);\n\t\treturn res;\n\t} catch (error) {\n\t\tconsole.error(\"[reports][api][getReports:FAILED]\", error);\n\t\tthrow normalizeApiError(error);\n\t}\n}\n// #endregion getReports:Function\n\n// #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\n// #endregion ReportsApi:Module\n// #endregion ReportsApi\n" + }, + { + "contract_id": "ReportsApi:Module", + "contract_type": "Function", + "file_path": "frontend/src/lib/api/reports.js", + "start_line": 8, + "end_line": 115, + "tier": "TIER_1", + "complexity": 1, + "metadata": { "INVARIANT": "Uses existing api wrapper methods and returns structured errors for UI-state mapping.", "LAYER": "Infra", "POST": "Report list and detail helpers return backend payloads or normalized UI-safe errors.", @@ -117250,7 +83956,7 @@ }, "relations": [ { - "source_id": "ReportsApi", + "source_id": "ReportsApi:Module", "relation_type": "DEPENDS_ON", "target_id": "api_module", "target_ref": "[api_module]" @@ -117260,22 +83966,112 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C4", + "message": "@INVARIANT is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 4, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "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": "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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": "SIDE_EFFECT", + "message": "@SIDE_EFFECT 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" } } ], - "anchor_syntax": "def", - "body": "// [DEF:ReportsApi:Module]\n// @COMPLEXITY: 4\n// @SEMANTICS: frontend, api_client, reports, wrapper\n// @PURPOSE: Wrapper-based reports API client for list/detail retrieval without direct native fetch usage.\n// @LAYER: Infra\n// @RELATION: DEPENDS_ON -> [api_module]\n// @PRE: Shared API wrapper is configured for the current frontend auth/session context.\n// @POST: Report list and detail helpers return backend payloads or normalized UI-safe errors.\n// @SIDE_EFFECT: Performs authenticated report HTTP requests through the shared API wrapper.\n// @INVARIANT: Uses existing api wrapper methods and returns structured errors for UI-state mapping.\nimport { api } from '../api.js';\n\n// [DEF:buildReportQueryString:Function]\n// @PURPOSE: Build query string for reports list endpoint from filter options.\n// @PRE: options is an object with optional report query fields.\n// @POST: Returns URL query string without leading '?'.\nexport function buildReportQueryString(options = {}) {\n\tconsole.log(\"[reports][api][buildReportQueryString:START]\");\n\tconst params = new URLSearchParams();\n\n\tif (options.page != null) params.append('page', String(options.page));\n\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\n\tif (Array.isArray(options.task_types) && options.task_types.length > 0) {\n\t\tparams.append('task_types', options.task_types.join(','));\n\t}\n\tif (Array.isArray(options.statuses) && options.statuses.length > 0) {\n\t\tparams.append('statuses', options.statuses.join(','));\n\t}\n\n\tif (options.time_from) params.append('time_from', options.time_from);\n\tif (options.time_to) params.append('time_to', options.time_to);\n\tif (options.search) params.append('search', options.search);\n\tif (options.sort_by) params.append('sort_by', options.sort_by);\n\tif (options.sort_order) params.append('sort_order', options.sort_order);\n\n\treturn params.toString();\n}\n// [/DEF:buildReportQueryString:Function]\n\n// [DEF:normalizeApiError: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.\nexport function normalizeApiError(error) {\n\tconsole.log(\"[reports][api][normalizeApiError:START]\");\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\t'Failed to load reports';\n\n\treturn {\n\t\tmessage,\n\t\tcode: 'REPORTS_API_ERROR',\n\t\tretryable: true\n\t};\n}\n// [/DEF:normalizeApiError:Function]\n\n// [DEF:getReports:Function]\n// @PURPOSE: Fetch unified report list using existing request wrapper.\n// @PRE: valid auth context for protected endpoint.\n// @POST: Returns parsed payload or structured error for UI-state mapping.\n//\n// @TEST_CONTRACT: GetReportsApi ->\n// {\n// required_fields: {},\n// optional_fields: {options: Object},\n// invariants: [\n// \"Fetches from /reports with built query string\",\n// \"Returns response payload on success\",\n// \"Catches and normalizes errors using normalizeApiError\"\n// ]\n// }\n// @TEST_FIXTURE: valid_get_reports -> {\"options\": {\"page\": 1}}\n// @TEST_EDGE: api_fetch_failure -> api.fetchApi throws error\n// @TEST_INVARIANT: error_normalization -> verifies: [api_fetch_failure]\nexport async function getReports(options = {}) {\n\ttry {\n\t\tconsole.log(\"[reports][api][getReports:STARTED]\", options);\n\t\tconst query = buildReportQueryString(options);\n\t\tconst res = await api.fetchApi(`/reports${query ? `?${query}` : ''}`);\n\t\tconsole.log(\"[reports][api][getReports:SUCCESS]\", res);\n\t\treturn res;\n\t} catch (error) {\n\t\tconsole.error(\"[reports][api][getReports:FAILED]\", error);\n\t\tthrow normalizeApiError(error);\n\t}\n}\n// [/DEF:getReports:Function]\n\n// [DEF:getReportDetail: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// [/DEF:getReportDetail:Function]\n\n// [/DEF:ReportsApi:Module]\n" + "anchor_syntax": "region", + "body": "// #region ReportsApi:Module [TYPE Function]\n// @SEMANTICS: frontend, api_client, reports, wrapper\n// @PURPOSE: Wrapper-based reports API client for list/detail retrieval without direct native fetch usage.\n// @LAYER: Infra\n// @RELATION: DEPENDS_ON -> [api_module]\n// @PRE: Shared API wrapper is configured for the current frontend auth/session context.\n// @POST: Report list and detail helpers return backend payloads or normalized UI-safe errors.\n// @SIDE_EFFECT: Performs authenticated report HTTP requests through the shared API wrapper.\n// @INVARIANT: Uses existing api wrapper methods and returns structured errors for UI-state mapping.\nimport { api } from '../api.js';\n\n// #region buildReportQueryString:Function [TYPE Function]\n// @PURPOSE: Build query string for reports list endpoint from filter options.\n// @PRE: options is an object with optional report query fields.\n// @POST: Returns URL query string without leading '?'.\nexport function buildReportQueryString(options = {}) {\n\tconsole.log(\"[reports][api][buildReportQueryString:START]\");\n\tconst params = new URLSearchParams();\n\n\tif (options.page != null) params.append('page', String(options.page));\n\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\n\tif (Array.isArray(options.task_types) && options.task_types.length > 0) {\n\t\tparams.append('task_types', options.task_types.join(','));\n\t}\n\tif (Array.isArray(options.statuses) && options.statuses.length > 0) {\n\t\tparams.append('statuses', options.statuses.join(','));\n\t}\n\n\tif (options.time_from) params.append('time_from', options.time_from);\n\tif (options.time_to) params.append('time_to', options.time_to);\n\tif (options.search) params.append('search', options.search);\n\tif (options.sort_by) params.append('sort_by', options.sort_by);\n\tif (options.sort_order) params.append('sort_order', options.sort_order);\n\n\treturn params.toString();\n}\n// #endregion buildReportQueryString:Function\n\n// #region normalizeApiError: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.\nexport function normalizeApiError(error) {\n\tconsole.log(\"[reports][api][normalizeApiError:START]\");\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\t'Failed to load reports';\n\n\treturn {\n\t\tmessage,\n\t\tcode: 'REPORTS_API_ERROR',\n\t\tretryable: true\n\t};\n}\n// #endregion normalizeApiError:Function\n\n// #region getReports:Function [TYPE Function]\n// @PURPOSE: Fetch unified report list using existing request wrapper.\n// @PRE: valid auth context for protected endpoint.\n// @POST: Returns parsed payload or structured error for UI-state mapping.\n//\n// @TEST_CONTRACT: GetReportsApi ->\n// {\n// required_fields: {},\n// optional_fields: {options: Object},\n// invariants: [\n// \"Fetches from /reports with built query string\",\n// \"Returns response payload on success\",\n// \"Catches and normalizes errors using normalizeApiError\"\n// ]\n// }\n// @TEST_FIXTURE: valid_get_reports -> {\"options\": {\"page\": 1}}\n// @TEST_EDGE: api_fetch_failure -> api.fetchApi throws error\n// @TEST_INVARIANT: error_normalization -> verifies: [api_fetch_failure]\nexport async function getReports(options = {}) {\n\ttry {\n\t\tconsole.log(\"[reports][api][getReports:STARTED]\", options);\n\t\tconst query = buildReportQueryString(options);\n\t\tconst res = await api.fetchApi(`/reports${query ? `?${query}` : ''}`);\n\t\tconsole.log(\"[reports][api][getReports:SUCCESS]\", res);\n\t\treturn res;\n\t} catch (error) {\n\t\tconsole.error(\"[reports][api][getReports:FAILED]\", error);\n\t\tthrow normalizeApiError(error);\n\t}\n}\n// #endregion getReports:Function\n\n// #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\n// #endregion ReportsApi:Module\n" }, { - "contract_id": "buildReportQueryString", + "contract_id": "buildReportQueryString:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/reports.js", - "start_line": 13, - "end_line": 39, + "start_line": 19, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117313,15 +84109,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:buildReportQueryString:Function]\n// @PURPOSE: Build query string for reports list endpoint from filter options.\n// @PRE: options is an object with optional report query fields.\n// @POST: Returns URL query string without leading '?'.\nexport function buildReportQueryString(options = {}) {\n\tconsole.log(\"[reports][api][buildReportQueryString:START]\");\n\tconst params = new URLSearchParams();\n\n\tif (options.page != null) params.append('page', String(options.page));\n\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\n\tif (Array.isArray(options.task_types) && options.task_types.length > 0) {\n\t\tparams.append('task_types', options.task_types.join(','));\n\t}\n\tif (Array.isArray(options.statuses) && options.statuses.length > 0) {\n\t\tparams.append('statuses', options.statuses.join(','));\n\t}\n\n\tif (options.time_from) params.append('time_from', options.time_from);\n\tif (options.time_to) params.append('time_to', options.time_to);\n\tif (options.search) params.append('search', options.search);\n\tif (options.sort_by) params.append('sort_by', options.sort_by);\n\tif (options.sort_order) params.append('sort_order', options.sort_order);\n\n\treturn params.toString();\n}\n// [/DEF:buildReportQueryString:Function]\n" + "anchor_syntax": "region", + "body": "// #region buildReportQueryString:Function [TYPE Function]\n// @PURPOSE: Build query string for reports list endpoint from filter options.\n// @PRE: options is an object with optional report query fields.\n// @POST: Returns URL query string without leading '?'.\nexport function buildReportQueryString(options = {}) {\n\tconsole.log(\"[reports][api][buildReportQueryString:START]\");\n\tconst params = new URLSearchParams();\n\n\tif (options.page != null) params.append('page', String(options.page));\n\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\n\tif (Array.isArray(options.task_types) && options.task_types.length > 0) {\n\t\tparams.append('task_types', options.task_types.join(','));\n\t}\n\tif (Array.isArray(options.statuses) && options.statuses.length > 0) {\n\t\tparams.append('statuses', options.statuses.join(','));\n\t}\n\n\tif (options.time_from) params.append('time_from', options.time_from);\n\tif (options.time_to) params.append('time_to', options.time_to);\n\tif (options.search) params.append('search', options.search);\n\tif (options.sort_by) params.append('sort_by', options.sort_by);\n\tif (options.sort_order) params.append('sort_order', options.sort_order);\n\n\treturn params.toString();\n}\n// #endregion buildReportQueryString:Function\n" }, { - "contract_id": "normalizeApiError", + "contract_id": "normalizeApiError:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/reports.js", - "start_line": 41, - "end_line": 58, + "start_line": 47, + "end_line": 64, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117359,15 +84155,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:normalizeApiError: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.\nexport function normalizeApiError(error) {\n\tconsole.log(\"[reports][api][normalizeApiError:START]\");\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\t'Failed to load reports';\n\n\treturn {\n\t\tmessage,\n\t\tcode: 'REPORTS_API_ERROR',\n\t\tretryable: true\n\t};\n}\n// [/DEF:normalizeApiError:Function]\n" + "anchor_syntax": "region", + "body": "// #region normalizeApiError: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.\nexport function normalizeApiError(error) {\n\tconsole.log(\"[reports][api][normalizeApiError:START]\");\n\tconst message =\n\t\t(error && typeof error.message === 'string' && error.message) ||\n\t\t(typeof error === 'string' && error) ||\n\t\t'Failed to load reports';\n\n\treturn {\n\t\tmessage,\n\t\tcode: 'REPORTS_API_ERROR',\n\t\tretryable: true\n\t};\n}\n// #endregion normalizeApiError:Function\n" }, { - "contract_id": "getReports", + "contract_id": "getReports:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/reports.js", - "start_line": 60, - "end_line": 90, + "start_line": 66, + "end_line": 96, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117406,15 +84202,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:getReports:Function]\n// @PURPOSE: Fetch unified report list using existing request wrapper.\n// @PRE: valid auth context for protected endpoint.\n// @POST: Returns parsed payload or structured error for UI-state mapping.\n//\n// @TEST_CONTRACT: GetReportsApi ->\n// {\n// required_fields: {},\n// optional_fields: {options: Object},\n// invariants: [\n// \"Fetches from /reports with built query string\",\n// \"Returns response payload on success\",\n// \"Catches and normalizes errors using normalizeApiError\"\n// ]\n// }\n// @TEST_FIXTURE: valid_get_reports -> {\"options\": {\"page\": 1}}\n// @TEST_EDGE: api_fetch_failure -> api.fetchApi throws error\n// @TEST_INVARIANT: error_normalization -> verifies: [api_fetch_failure]\nexport async function getReports(options = {}) {\n\ttry {\n\t\tconsole.log(\"[reports][api][getReports:STARTED]\", options);\n\t\tconst query = buildReportQueryString(options);\n\t\tconst res = await api.fetchApi(`/reports${query ? `?${query}` : ''}`);\n\t\tconsole.log(\"[reports][api][getReports:SUCCESS]\", res);\n\t\treturn res;\n\t} catch (error) {\n\t\tconsole.error(\"[reports][api][getReports:FAILED]\", error);\n\t\tthrow normalizeApiError(error);\n\t}\n}\n// [/DEF:getReports:Function]\n" + "anchor_syntax": "region", + "body": "// #region getReports:Function [TYPE Function]\n// @PURPOSE: Fetch unified report list using existing request wrapper.\n// @PRE: valid auth context for protected endpoint.\n// @POST: Returns parsed payload or structured error for UI-state mapping.\n//\n// @TEST_CONTRACT: GetReportsApi ->\n// {\n// required_fields: {},\n// optional_fields: {options: Object},\n// invariants: [\n// \"Fetches from /reports with built query string\",\n// \"Returns response payload on success\",\n// \"Catches and normalizes errors using normalizeApiError\"\n// ]\n// }\n// @TEST_FIXTURE: valid_get_reports -> {\"options\": {\"page\": 1}}\n// @TEST_EDGE: api_fetch_failure -> api.fetchApi throws error\n// @TEST_INVARIANT: error_normalization -> verifies: [api_fetch_failure]\nexport async function getReports(options = {}) {\n\ttry {\n\t\tconsole.log(\"[reports][api][getReports:STARTED]\", options);\n\t\tconst query = buildReportQueryString(options);\n\t\tconst res = await api.fetchApi(`/reports${query ? `?${query}` : ''}`);\n\t\tconsole.log(\"[reports][api][getReports:SUCCESS]\", res);\n\t\treturn res;\n\t} catch (error) {\n\t\tconsole.error(\"[reports][api][getReports:FAILED]\", error);\n\t\tthrow normalizeApiError(error);\n\t}\n}\n// #endregion getReports:Function\n" }, { - "contract_id": "getReportDetail", + "contract_id": "getReportDetail:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/reports.js", - "start_line": 92, - "end_line": 107, + "start_line": 98, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117452,15 +84248,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:getReportDetail: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// [/DEF:getReportDetail:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "normalizeTranslateError:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 12, - "end_line": 24, + "start_line": 15, + "end_line": 27, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117498,15 +84294,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:normalizeTranslateError: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// [/DEF:normalizeTranslateError:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "fetchJobs", + "contract_id": "fetchJobs:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 26, - "end_line": 42, + "start_line": 29, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117544,15 +84340,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchJobs: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// [/DEF:fetchJobs:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "createJob", + "contract_id": "createJob:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 44, - "end_line": 55, + "start_line": 47, + "end_line": 58, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117590,15 +84386,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:createJob: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// [/DEF:createJob:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "updateJob", + "contract_id": "updateJob:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 57, - "end_line": 68, + "start_line": 60, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117636,15 +84432,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:updateJob: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// [/DEF:updateJob:Function]\n" + "anchor_syntax": "region", + "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" }, { - "contract_id": "deleteJob", + "contract_id": "deleteJob:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 70, - "end_line": 81, + "start_line": 73, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117682,15 +84478,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:deleteJob: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// [/DEF:deleteJob:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "duplicateJob:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 83, - "end_line": 94, + "start_line": 86, + "end_line": 97, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117728,29 +84524,29 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:duplicateJob: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// [/DEF:duplicateJob:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchDatasourceColumns:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 96, - "end_line": 206, + "start_line": 99, + "end_line": 209, "tier": "TIER_1", "complexity": 1, "metadata": {}, "relations": [], "schema_warnings": [], - "anchor_syntax": "def", - "body": "// [DEF:fetchDatasourceColumns:Function]\n\nexport async function fetchDatasources(envId, search = '') {\n try {\n return await api.fetchApi(\n `/translate/datasources?env_id=${encodeURIComponent(envId)}&search=${encodeURIComponent(search)}`\n );\n } catch (error) {\n throw normalizeTranslateError(error, 'Failed to fetch datasources');\n }\n}\n\nexport async function fetchDatasourceColumns(datasourceId, envId) {\n\ttry {\n\t\treturn await api.fetchApi(\n\t\t\t`/translate/datasources/${datasourceId}/columns?env_id=${encodeURIComponent(envId)}`\n\t\t);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch datasource columns');\n\t}\n}\n\n// [DEF:fetchPreview:Function]\n// @PURPOSE: Trigger a translation preview for a job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns preview session with rows and cost estimate.\nexport async function fetchPreview(jobId, sampleSize = 10, envId = '') {\n\ttry {\n\t\tconst body = { sample_size: sampleSize };\n\t\tif (envId) {\n\t\t\tbody.env_id = envId;\n\t\t}\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview`, body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to run preview');\n\t}\n}\n// [/DEF:fetchPreview:Function]\n\n// [DEF:approveRow:Function]\n// @PURPOSE: Approve a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function approveRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'approve'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to approve row');\n\t}\n}\n// [/DEF:approveRow:Function]\n\n// [DEF:editRow:Function]\n// @PURPOSE: Edit a preview row's translation.\n// @PRE: jobId and rowKey are non-empty strings; translation is non-empty.\n// @POST: Returns updated row.\nexport async function editRow(jobId, rowKey, translation) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'edit',\n\t\t\ttranslation: translation\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to edit row');\n\t}\n}\n// [/DEF:editRow:Function]\n\n// [DEF:rejectRow:Function]\n// @PURPOSE: Reject a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function rejectRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'reject'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to reject row');\n\t}\n}\n// [/DEF:rejectRow:Function]\n\n// [DEF:acceptPreview:Function]\n// @PURPOSE: Accept a preview session, enabling full translation execution.\n// @PRE: jobId is non-empty string.\n// @POST: Returns accepted preview session.\nexport async function acceptPreview(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview/accept`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to accept preview');\n\t}\n}\n// [/DEF:acceptPreview:Function]\n\n// [DEF:fetchPreviewRecords:Function]\n// @PURPOSE: Fetch records for a preview session.\n// @PRE: sessionId is non-empty string.\n// @POST: Returns list of preview records.\nexport async function fetchPreviewRecords(sessionId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/preview/${sessionId}/records`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch preview records');\n\t}\n}\n// [/DEF:fetchPreviewRecords:Function]\n// [/DEF:fetchDatasourceColumns:Function]\n" + "anchor_syntax": "region", + "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.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function approveRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'approve'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to approve row');\n\t}\n}\n// #endregion approveRow:Function\n\n// #region editRow:Function [TYPE Function]\n// @PURPOSE: Edit a preview row's translation.\n// @PRE: jobId and rowKey are non-empty strings; translation is non-empty.\n// @POST: Returns updated row.\nexport async function editRow(jobId, rowKey, translation) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'edit',\n\t\t\ttranslation: translation\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to edit row');\n\t}\n}\n// #endregion editRow:Function\n\n// #region rejectRow:Function [TYPE Function]\n// @PURPOSE: Reject a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function rejectRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'reject'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to reject row');\n\t}\n}\n// #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", + "contract_id": "fetchPreview:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 118, - "end_line": 133, + "start_line": 121, + "end_line": 136, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117788,15 +84584,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchPreview:Function]\n// @PURPOSE: Trigger a translation preview for a job.\n// @PRE: jobId is non-empty string.\n// @POST: Returns preview session with rows and cost estimate.\nexport async function fetchPreview(jobId, sampleSize = 10, envId = '') {\n\ttry {\n\t\tconst body = { sample_size: sampleSize };\n\t\tif (envId) {\n\t\t\tbody.env_id = envId;\n\t\t}\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview`, body);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to run preview');\n\t}\n}\n// [/DEF:fetchPreview:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "approveRow:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 135, - "end_line": 148, + "start_line": 138, + "end_line": 151, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117834,15 +84630,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:approveRow:Function]\n// @PURPOSE: Approve a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function approveRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'approve'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to approve row');\n\t}\n}\n// [/DEF:approveRow:Function]\n" + "anchor_syntax": "region", + "body": "// #region approveRow:Function [TYPE Function]\n// @PURPOSE: Approve a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function approveRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'approve'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to approve row');\n\t}\n}\n// #endregion approveRow:Function\n" }, { - "contract_id": "editRow", + "contract_id": "editRow:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 150, - "end_line": 164, + "start_line": 153, + "end_line": 167, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117880,15 +84676,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:editRow:Function]\n// @PURPOSE: Edit a preview row's translation.\n// @PRE: jobId and rowKey are non-empty strings; translation is non-empty.\n// @POST: Returns updated row.\nexport async function editRow(jobId, rowKey, translation) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'edit',\n\t\t\ttranslation: translation\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to edit row');\n\t}\n}\n// [/DEF:editRow:Function]\n" + "anchor_syntax": "region", + "body": "// #region editRow:Function [TYPE Function]\n// @PURPOSE: Edit a preview row's translation.\n// @PRE: jobId and rowKey are non-empty strings; translation is non-empty.\n// @POST: Returns updated row.\nexport async function editRow(jobId, rowKey, translation) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'edit',\n\t\t\ttranslation: translation\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to edit row');\n\t}\n}\n// #endregion editRow:Function\n" }, { - "contract_id": "rejectRow", + "contract_id": "rejectRow:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 166, - "end_line": 179, + "start_line": 169, + "end_line": 182, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117926,15 +84722,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:rejectRow:Function]\n// @PURPOSE: Reject a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function rejectRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'reject'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to reject row');\n\t}\n}\n// [/DEF:rejectRow:Function]\n" + "anchor_syntax": "region", + "body": "// #region rejectRow:Function [TYPE Function]\n// @PURPOSE: Reject a preview row.\n// @PRE: jobId and rowKey are non-empty strings.\n// @POST: Returns updated row.\nexport async function rejectRow(jobId, rowKey) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/preview/rows/${rowKey}`, 'PUT', {\n\t\t\taction: 'reject'\n\t\t});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to reject row');\n\t}\n}\n// #endregion rejectRow:Function\n" }, { - "contract_id": "acceptPreview", + "contract_id": "acceptPreview:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 181, - "end_line": 192, + "start_line": 184, + "end_line": 195, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -117972,15 +84768,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:acceptPreview:Function]\n// @PURPOSE: Accept a preview session, enabling full translation execution.\n// @PRE: jobId is non-empty string.\n// @POST: Returns accepted preview session.\nexport async function acceptPreview(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/preview/accept`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to accept preview');\n\t}\n}\n// [/DEF:acceptPreview:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchPreviewRecords:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 194, - "end_line": 205, + "start_line": 197, + "end_line": 208, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118018,15 +84814,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchPreviewRecords:Function]\n// @PURPOSE: Fetch records for a preview session.\n// @PRE: sessionId is non-empty string.\n// @POST: Returns list of preview records.\nexport async function fetchPreviewRecords(sessionId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/preview/${sessionId}/records`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch preview records');\n\t}\n}\n// [/DEF:fetchPreviewRecords:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "triggerRun:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 209, - "end_line": 221, + "start_line": 212, + "end_line": 224, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118064,15 +84860,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:triggerRun: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// [/DEF:triggerRun:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchRunStatus:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 223, - "end_line": 234, + "start_line": 226, + "end_line": 237, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118110,15 +84906,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchRunStatus: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// [/DEF:fetchRunStatus:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchRunHistory:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 236, - "end_line": 251, + "start_line": 239, + "end_line": 254, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118156,15 +84952,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchRunHistory: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// [/DEF:fetchRunHistory:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchRunRecords:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 253, - "end_line": 269, + "start_line": 256, + "end_line": 272, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118202,15 +84998,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchRunRecords: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// [/DEF:fetchRunRecords:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "retryFailedBatches:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 271, - "end_line": 282, + "start_line": 274, + "end_line": 285, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118248,15 +85044,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:retryFailedBatches: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// [/DEF:retryFailedBatches:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "retryInsert:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 284, - "end_line": 295, + "start_line": 287, + "end_line": 298, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118294,15 +85090,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:retryInsert: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// [/DEF:retryInsert:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "cancelRun:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 297, - "end_line": 308, + "start_line": 300, + "end_line": 311, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118340,15 +85136,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:cancelRun: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// [/DEF:cancelRun:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchRunBatches:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 310, - "end_line": 321, + "start_line": 313, + "end_line": 324, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118386,15 +85182,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchRunBatches: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// [/DEF:fetchRunBatches:Function]\n" + "anchor_syntax": "region", + "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", - "contract_type": "Variable", + "contract_id": "dictionaryApi:Variable", + "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 324, - "end_line": 421, + "start_line": 327, + "end_line": 424, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118402,43 +85198,25 @@ }, "relations": [], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Variable'", - "detail": { - "actual_type": "Variable", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, { "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Variable' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Variable" + "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": "// [DEF:dictionaryApi:Variable]\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// [/DEF:dictionaryApi:Variable]\n" + "anchor_syntax": "region", + "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", + "contract_id": "submitCorrection:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 427, - "end_line": 438, + "start_line": 430, + "end_line": 441, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118476,15 +85254,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:submitCorrection: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// [/DEF:submitCorrection:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "submitBulkCorrections:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 440, - "end_line": 451, + "start_line": 443, + "end_line": 454, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118522,15 +85300,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:submitBulkCorrections: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// [/DEF:submitBulkCorrections:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchSchedule:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 457, - "end_line": 466, + "start_line": 460, + "end_line": 469, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118548,15 +85326,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchSchedule:Function]\n// @PURPOSE: Fetch schedule for a translation job.\nexport async function fetchSchedule(jobId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/schedule`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch schedule');\n\t}\n}\n// [/DEF:fetchSchedule:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "setSchedule:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 468, - "end_line": 477, + "start_line": 471, + "end_line": 480, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118574,15 +85352,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:setSchedule:Function]\n// @PURPOSE: Create or update schedule for a translation job.\nexport async function setSchedule(jobId, payload) {\n\ttry {\n\t\treturn await api.requestApi(`/translate/jobs/${jobId}/schedule`, 'PUT', payload);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to set schedule');\n\t}\n}\n// [/DEF:setSchedule:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "deleteSchedule:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 479, - "end_line": 488, + "start_line": 482, + "end_line": 491, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118600,15 +85378,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:deleteSchedule:Function]\n// @PURPOSE: Delete schedule for a translation job.\nexport async function deleteSchedule(jobId) {\n\ttry {\n\t\treturn await api.deleteApi(`/translate/jobs/${jobId}/schedule`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to delete schedule');\n\t}\n}\n// [/DEF:deleteSchedule:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "enableSchedule:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 490, - "end_line": 499, + "start_line": 493, + "end_line": 502, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118626,15 +85404,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:enableSchedule:Function]\n// @PURPOSE: Enable a schedule.\nexport async function enableSchedule(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/schedule/enable`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to enable schedule');\n\t}\n}\n// [/DEF:enableSchedule:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "disableSchedule:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 501, - "end_line": 510, + "start_line": 504, + "end_line": 513, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118652,15 +85430,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:disableSchedule:Function]\n// @PURPOSE: Disable a schedule.\nexport async function disableSchedule(jobId) {\n\ttry {\n\t\treturn await api.postApi(`/translate/jobs/${jobId}/schedule/disable`, {});\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to disable schedule');\n\t}\n}\n// [/DEF:disableSchedule:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchNextExecutions:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 512, - "end_line": 521, + "start_line": 515, + "end_line": 524, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118678,15 +85456,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchNextExecutions:Function]\n// @PURPOSE: Preview next N schedule executions.\nexport async function fetchNextExecutions(jobId, n = 3) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/schedule/next-executions?n=${n}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch next executions');\n\t}\n}\n// [/DEF:fetchNextExecutions:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchAllRuns:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 527, - "end_line": 544, + "start_line": 530, + "end_line": 547, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118704,15 +85482,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchAllRuns:Function]\n// @PURPOSE: Fetch all runs with cross-job filtering and pagination.\nexport async function fetchAllRuns(options = {}) {\n\ttry {\n\t\tconst params = new URLSearchParams();\n\t\tif (options.page != null) params.append('page', String(options.page));\n\t\tif (options.page_size != null) params.append('page_size', String(options.page_size));\n\t\tif (options.job_id) params.append('job_id', options.job_id);\n\t\tif (options.status) params.append('status', options.status);\n\t\tif (options.trigger_type) params.append('trigger_type', options.trigger_type);\n\t\tif (options.created_by) params.append('created_by', options.created_by);\n\t\tconst query = params.toString();\n\t\treturn await api.fetchApi(`/translate/runs${query ? `?${query}` : ''}`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch runs');\n\t}\n}\n// [/DEF:fetchAllRuns:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchRunDetail:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 546, - "end_line": 555, + "start_line": 549, + "end_line": 558, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118730,15 +85508,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchRunDetail:Function]\n// @PURPOSE: Fetch detailed run info with config_snapshot, records, events.\nexport async function fetchRunDetail(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/detail`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch run detail');\n\t}\n}\n// [/DEF:fetchRunDetail:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchJobMetrics:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 557, - "end_line": 566, + "start_line": 560, + "end_line": 569, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118756,15 +85534,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchJobMetrics:Function]\n// @PURPOSE: Fetch aggregated metrics for a specific job.\nexport async function fetchJobMetrics(jobId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/jobs/${jobId}/metrics`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch job metrics');\n\t}\n}\n// [/DEF:fetchJobMetrics:Function]\n" + "anchor_syntax": "region", + "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", + "contract_id": "fetchAllMetrics:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 568, - "end_line": 577, + "start_line": 571, + "end_line": 580, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118782,15 +85560,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:fetchAllMetrics:Function]\n// @PURPOSE: Fetch aggregated metrics for all jobs.\nexport async function fetchAllMetrics() {\n\ttry {\n\t\treturn await api.fetchApi('/translate/metrics');\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to fetch metrics');\n\t}\n}\n// [/DEF:fetchAllMetrics:Function]\n" + "anchor_syntax": "region", + "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": "downloadSkippedCsv", + "contract_id": "downloadSkippedCsv:Function", "contract_type": "Function", "file_path": "frontend/src/lib/api/translate.js", - "start_line": 579, - "end_line": 588, + "start_line": 582, + "end_line": 591, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118808,19 +85586,42 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:downloadSkippedCsv:Function]\n// @PURPOSE: Download skipped records CSV for a run.\nexport async function downloadSkippedCsv(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/skipped.csv`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to download skipped CSV');\n\t}\n}\n// [/DEF:downloadSkippedCsv:Function]\n" + "anchor_syntax": "region", + "body": "// #region downloadSkippedCsv:Function [TYPE Function]\n// @PURPOSE: Download skipped records CSV for a run.\nexport async function downloadSkippedCsv(runId) {\n\ttry {\n\t\treturn await api.fetchApi(`/translate/runs/${runId}/skipped.csv`);\n\t} catch (error) {\n\t\tthrow normalizeTranslateError(error, 'Failed to download skipped CSV');\n\t}\n}\n// #endregion downloadSkippedCsv:Function\n" }, { "contract_id": "PermissionsTest", "contract_type": "Module", "file_path": "frontend/src/lib/auth/__tests__/permissions.test.js", "start_line": 1, - "end_line": 103, + "end_line": 107, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, + "PURPOSE": "Verifies frontend RBAC permission parsing and access checks." + }, + "relations": [ + { + "source_id": "PermissionsTest", + "relation_type": "DEPENDS_ON", + "target_id": "Permissions", + "target_ref": "[Permissions]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "// #region PermissionsTest [C:3] [TYPE Module] [SEMANTICS auth, rbac, permission, test, guard]\n// @BRIEF Verifies frontend RBAC permission parsing and access checks.\n// @RELATION DEPENDS_ON -> [Permissions]\n\n// #region PermissionsTest:Module [TYPE Function]\n// @SEMANTICS: tests, auth, permissions, rbac\n// @PURPOSE: Verifies frontend RBAC permission parsing and access checks.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [Permissions]\n\nimport { describe, it, expect } from \"vitest\";\nimport {\n normalizePermissionRequirement,\n isAdminUser,\n hasPermission,\n} from \"../permissions.js\";\n\ndescribe(\"auth.permissions\", () => {\n it(\"normalizes resource-only requirement with default READ action\", () => {\n expect(normalizePermissionRequirement(\"admin:settings\")).toEqual({\n resource: \"admin:settings\",\n action: \"READ\",\n });\n });\n\n it(\"normalizes explicit resource:action requirement\", () => {\n expect(normalizePermissionRequirement(\"admin:settings:write\")).toEqual({\n resource: \"admin:settings\",\n action: \"WRITE\",\n });\n });\n\n it(\"detects admin role case-insensitively\", () => {\n const user = {\n roles: [{ name: \"ADMIN\" }],\n };\n expect(isAdminUser(user)).toBe(true);\n });\n\n it(\"denies when user is absent and permission is required\", () => {\n expect(hasPermission(null, \"tasks\", \"READ\")).toBe(false);\n });\n\n it(\"grants when permission object matches resource and action\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"tasks\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"tasks\", \"READ\")).toBe(true);\n });\n\n it(\"grants when requirement is provided as resource:action\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"admin:settings\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"admin:settings:READ\")).toBe(true);\n });\n\n it(\"grants when string permission entry matches\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [\"plugin:migration:READ\"],\n },\n ],\n };\n\n expect(hasPermission(user, \"plugin:migration\", \"READ\")).toBe(true);\n });\n\n it(\"denies when action does not match\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"tasks\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"tasks\", \"WRITE\")).toBe(false);\n });\n\n it(\"always grants for admin role regardless of explicit permissions\", () => {\n const adminUser = {\n roles: [{ name: \"Admin\", permissions: [] }],\n };\n\n expect(hasPermission(adminUser, \"admin:users\", \"READ\")).toBe(true);\n expect(hasPermission(adminUser, \"plugin:migration\", \"EXECUTE\")).toBe(true);\n });\n});\n\n// #endregion PermissionsTest:Module\n// #endregion PermissionsTest\n" + }, + { + "contract_id": "PermissionsTest:Module", + "contract_type": "Function", + "file_path": "frontend/src/lib/auth/__tests__/permissions.test.js", + "start_line": 5, + "end_line": 106, + "tier": "TIER_1", + "complexity": 1, + "metadata": { "LAYER": "UI (Tests)", "PURPOSE": "Verifies frontend RBAC permission parsing and access checks.", "SEMANTICS": [ @@ -118832,26 +85633,113 @@ }, "relations": [ { - "source_id": "PermissionsTest", + "source_id": "PermissionsTest:Module", "relation_type": "DEPENDS_ON", "target_id": "Permissions", "target_ref": "[Permissions]" } ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "// [DEF:PermissionsTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: tests, auth, permissions, rbac\n// @PURPOSE: Verifies frontend RBAC permission parsing and access checks.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [Permissions]\n\nimport { describe, it, expect } from \"vitest\";\nimport {\n normalizePermissionRequirement,\n isAdminUser,\n hasPermission,\n} from \"../permissions.js\";\n\ndescribe(\"auth.permissions\", () => {\n it(\"normalizes resource-only requirement with default READ action\", () => {\n expect(normalizePermissionRequirement(\"admin:settings\")).toEqual({\n resource: \"admin:settings\",\n action: \"READ\",\n });\n });\n\n it(\"normalizes explicit resource:action requirement\", () => {\n expect(normalizePermissionRequirement(\"admin:settings:write\")).toEqual({\n resource: \"admin:settings\",\n action: \"WRITE\",\n });\n });\n\n it(\"detects admin role case-insensitively\", () => {\n const user = {\n roles: [{ name: \"ADMIN\" }],\n };\n expect(isAdminUser(user)).toBe(true);\n });\n\n it(\"denies when user is absent and permission is required\", () => {\n expect(hasPermission(null, \"tasks\", \"READ\")).toBe(false);\n });\n\n it(\"grants when permission object matches resource and action\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"tasks\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"tasks\", \"READ\")).toBe(true);\n });\n\n it(\"grants when requirement is provided as resource:action\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"admin:settings\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"admin:settings:READ\")).toBe(true);\n });\n\n it(\"grants when string permission entry matches\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [\"plugin:migration:READ\"],\n },\n ],\n };\n\n expect(hasPermission(user, \"plugin:migration\", \"READ\")).toBe(true);\n });\n\n it(\"denies when action does not match\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"tasks\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"tasks\", \"WRITE\")).toBe(false);\n });\n\n it(\"always grants for admin role regardless of explicit permissions\", () => {\n const adminUser = {\n roles: [{ name: \"Admin\", permissions: [] }],\n };\n\n expect(hasPermission(adminUser, \"admin:users\", \"READ\")).toBe(true);\n expect(hasPermission(adminUser, \"plugin:migration\", \"EXECUTE\")).toBe(true);\n });\n});\n\n// [/DEF:PermissionsTest:Module]\n" + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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" + } + } + ], + "anchor_syntax": "region", + "body": "// #region PermissionsTest:Module [TYPE Function]\n// @SEMANTICS: tests, auth, permissions, rbac\n// @PURPOSE: Verifies frontend RBAC permission parsing and access checks.\n// @LAYER: UI (Tests)\n// @RELATION: DEPENDS_ON -> [Permissions]\n\nimport { describe, it, expect } from \"vitest\";\nimport {\n normalizePermissionRequirement,\n isAdminUser,\n hasPermission,\n} from \"../permissions.js\";\n\ndescribe(\"auth.permissions\", () => {\n it(\"normalizes resource-only requirement with default READ action\", () => {\n expect(normalizePermissionRequirement(\"admin:settings\")).toEqual({\n resource: \"admin:settings\",\n action: \"READ\",\n });\n });\n\n it(\"normalizes explicit resource:action requirement\", () => {\n expect(normalizePermissionRequirement(\"admin:settings:write\")).toEqual({\n resource: \"admin:settings\",\n action: \"WRITE\",\n });\n });\n\n it(\"detects admin role case-insensitively\", () => {\n const user = {\n roles: [{ name: \"ADMIN\" }],\n };\n expect(isAdminUser(user)).toBe(true);\n });\n\n it(\"denies when user is absent and permission is required\", () => {\n expect(hasPermission(null, \"tasks\", \"READ\")).toBe(false);\n });\n\n it(\"grants when permission object matches resource and action\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"tasks\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"tasks\", \"READ\")).toBe(true);\n });\n\n it(\"grants when requirement is provided as resource:action\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"admin:settings\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"admin:settings:READ\")).toBe(true);\n });\n\n it(\"grants when string permission entry matches\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [\"plugin:migration:READ\"],\n },\n ],\n };\n\n expect(hasPermission(user, \"plugin:migration\", \"READ\")).toBe(true);\n });\n\n it(\"denies when action does not match\", () => {\n const user = {\n roles: [\n {\n name: \"Operator\",\n permissions: [{ resource: \"tasks\", action: \"READ\" }],\n },\n ],\n };\n\n expect(hasPermission(user, \"tasks\", \"WRITE\")).toBe(false);\n });\n\n it(\"always grants for admin role regardless of explicit permissions\", () => {\n const adminUser = {\n roles: [{ name: \"Admin\", permissions: [] }],\n };\n\n expect(hasPermission(adminUser, \"admin:users\", \"READ\")).toBe(true);\n expect(hasPermission(adminUser, \"plugin:migration\", \"EXECUTE\")).toBe(true);\n });\n});\n\n// #endregion PermissionsTest:Module\n" }, { - "contract_id": "Permissions", + "contract_id": "PermissionsModule", "contract_type": "Module", "file_path": "frontend/src/lib/auth/permissions.js", "start_line": 1, - "end_line": 102, + "end_line": 106, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, + "PURPOSE": "Shared frontend RBAC utilities for route guards, permission checks, and menu visibility based on user roles." + }, + "relations": [ + { + "source_id": "PermissionsModule", + "relation_type": "DEPENDS_ON", + "target_id": "AuthStore", + "target_ref": "[AuthStore]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "body": "// #region PermissionsModule [C:3] [TYPE Module] [SEMANTICS auth, rbac, permission, role, guard]\n// @BRIEF Shared frontend RBAC utilities for route guards, permission checks, and menu visibility based on user roles.\n// @RELATION DEPENDS_ON -> [AuthStore]\n\n// #region Permissions:Module [TYPE Function]\n// @SEMANTICS: auth, permissions, rbac, roles\n// @PURPOSE: Shared frontend RBAC utilities for route guards and menu visibility.\n// @LAYER: Domain\n// @RELATION: DEPENDS_ON -> [authStore]\n// @INVARIANT: Admin role always bypasses explicit permission checks.\n\nconst KNOWN_ACTIONS = new Set([\"READ\", \"WRITE\", \"EXECUTE\", \"DELETE\"]);\n\nfunction normalizeAction(action, fallback = \"READ\") {\n const normalized = String(action || \"\").trim().toUpperCase();\n if (!normalized) return fallback;\n return normalized;\n}\n\n// #region normalizePermissionRequirement:Function [TYPE Function]\n// @PURPOSE: Convert permission requirement string to canonical resource/action tuple.\n// @PRE: Permission can be \"resource\" or \"resource:ACTION\" where resource itself may contain \":\".\n// @POST: Returns normalized object with action in uppercase.\nexport function normalizePermissionRequirement(permission, defaultAction = \"READ\") {\n const fallbackAction = normalizeAction(defaultAction, \"READ\");\n const rawPermission = String(permission || \"\").trim();\n\n if (!rawPermission) {\n return { resource: null, action: fallbackAction };\n }\n\n const parts = rawPermission.split(\":\");\n if (parts.length > 1) {\n const tail = normalizeAction(parts[parts.length - 1], fallbackAction);\n if (KNOWN_ACTIONS.has(tail)) {\n const resource = parts.slice(0, -1).join(\":\");\n return { resource, action: tail };\n }\n }\n\n return { resource: rawPermission, action: fallbackAction };\n}\n// #endregion normalizePermissionRequirement:Function\n\n// #region isAdminUser:Function [TYPE Function]\n// @PURPOSE: Determine whether user has Admin role.\n// @PRE: user can be null or partially populated.\n// @POST: Returns true when at least one role name equals \"Admin\" (case-insensitive).\nexport function isAdminUser(user) {\n const roles = Array.isArray(user?.roles) ? user.roles : [];\n return roles.some(\n (role) => String(role?.name || \"\").trim().toLowerCase() === \"admin\",\n );\n}\n// #endregion isAdminUser:Function\n\n// #region hasPermission:Function [TYPE Function]\n// @PURPOSE: Check if user has a required resource/action permission.\n// @PRE: user contains roles with permissions from /api/auth/me payload.\n// @POST: Returns true when requirement is empty, user is admin, or matching permission exists.\nexport function hasPermission(user, requirement, action = \"READ\") {\n if (!requirement) return true;\n if (!user) return false;\n if (isAdminUser(user)) return true;\n\n const { resource, action: requiredAction } = normalizePermissionRequirement(\n requirement,\n action,\n );\n if (!resource) return true;\n\n const roles = Array.isArray(user.roles) ? user.roles : [];\n for (const role of roles) {\n const permissions = Array.isArray(role?.permissions) ? role.permissions : [];\n for (const permission of permissions) {\n if (typeof permission === \"string\") {\n const normalized = normalizePermissionRequirement(\n permission,\n requiredAction,\n );\n if (\n normalized.resource === resource &&\n normalized.action === requiredAction\n ) {\n return true;\n }\n continue;\n }\n\n const permissionResource = String(permission?.resource || \"\").trim();\n const permissionAction = normalizeAction(permission?.action, \"\");\n if (\n permissionResource === resource &&\n permissionAction === requiredAction\n ) {\n return true;\n }\n }\n }\n\n return false;\n}\n// #endregion hasPermission:Function\n\n// #endregion Permissions:Module\n// #endregion PermissionsModule\n" + }, + { + "contract_id": "Permissions:Module", + "contract_type": "Function", + "file_path": "frontend/src/lib/auth/permissions.js", + "start_line": 5, + "end_line": 105, + "tier": "TIER_1", + "complexity": 1, + "metadata": { "INVARIANT": "Admin role always bypasses explicit permission checks.", "LAYER": "Domain", "PURPOSE": "Shared frontend RBAC utilities for route guards and menu visibility.", @@ -118864,7 +85752,7 @@ }, "relations": [ { - "source_id": "Permissions", + "source_id": "Permissions:Module", "relation_type": "DEPENDS_ON", "target_id": "authStore", "target_ref": "[authStore]" @@ -118874,22 +85762,85 @@ { "code": "tag_forbidden_by_complexity", "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Module' at C3", + "message": "@INVARIANT is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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" } } ], - "anchor_syntax": "def", - "body": "// [DEF:Permissions:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: auth, permissions, rbac, roles\n// @PURPOSE: Shared frontend RBAC utilities for route guards and menu visibility.\n// @LAYER: Domain\n// @RELATION: DEPENDS_ON -> [authStore]\n// @INVARIANT: Admin role always bypasses explicit permission checks.\n\nconst KNOWN_ACTIONS = new Set([\"READ\", \"WRITE\", \"EXECUTE\", \"DELETE\"]);\n\nfunction normalizeAction(action, fallback = \"READ\") {\n const normalized = String(action || \"\").trim().toUpperCase();\n if (!normalized) return fallback;\n return normalized;\n}\n\n// [DEF:normalizePermissionRequirement:Function]\n// @PURPOSE: Convert permission requirement string to canonical resource/action tuple.\n// @PRE: Permission can be \"resource\" or \"resource:ACTION\" where resource itself may contain \":\".\n// @POST: Returns normalized object with action in uppercase.\nexport function normalizePermissionRequirement(permission, defaultAction = \"READ\") {\n const fallbackAction = normalizeAction(defaultAction, \"READ\");\n const rawPermission = String(permission || \"\").trim();\n\n if (!rawPermission) {\n return { resource: null, action: fallbackAction };\n }\n\n const parts = rawPermission.split(\":\");\n if (parts.length > 1) {\n const tail = normalizeAction(parts[parts.length - 1], fallbackAction);\n if (KNOWN_ACTIONS.has(tail)) {\n const resource = parts.slice(0, -1).join(\":\");\n return { resource, action: tail };\n }\n }\n\n return { resource: rawPermission, action: fallbackAction };\n}\n// [/DEF:normalizePermissionRequirement:Function]\n\n// [DEF:isAdminUser:Function]\n// @PURPOSE: Determine whether user has Admin role.\n// @PRE: user can be null or partially populated.\n// @POST: Returns true when at least one role name equals \"Admin\" (case-insensitive).\nexport function isAdminUser(user) {\n const roles = Array.isArray(user?.roles) ? user.roles : [];\n return roles.some(\n (role) => String(role?.name || \"\").trim().toLowerCase() === \"admin\",\n );\n}\n// [/DEF:isAdminUser:Function]\n\n// [DEF:hasPermission:Function]\n// @PURPOSE: Check if user has a required resource/action permission.\n// @PRE: user contains roles with permissions from /api/auth/me payload.\n// @POST: Returns true when requirement is empty, user is admin, or matching permission exists.\nexport function hasPermission(user, requirement, action = \"READ\") {\n if (!requirement) return true;\n if (!user) return false;\n if (isAdminUser(user)) return true;\n\n const { resource, action: requiredAction } = normalizePermissionRequirement(\n requirement,\n action,\n );\n if (!resource) return true;\n\n const roles = Array.isArray(user.roles) ? user.roles : [];\n for (const role of roles) {\n const permissions = Array.isArray(role?.permissions) ? role.permissions : [];\n for (const permission of permissions) {\n if (typeof permission === \"string\") {\n const normalized = normalizePermissionRequirement(\n permission,\n requiredAction,\n );\n if (\n normalized.resource === resource &&\n normalized.action === requiredAction\n ) {\n return true;\n }\n continue;\n }\n\n const permissionResource = String(permission?.resource || \"\").trim();\n const permissionAction = normalizeAction(permission?.action, \"\");\n if (\n permissionResource === resource &&\n permissionAction === requiredAction\n ) {\n return true;\n }\n }\n }\n\n return false;\n}\n// [/DEF:hasPermission:Function]\n\n// [/DEF:Permissions:Module]\n" + "anchor_syntax": "region", + "body": "// #region Permissions:Module [TYPE Function]\n// @SEMANTICS: auth, permissions, rbac, roles\n// @PURPOSE: Shared frontend RBAC utilities for route guards and menu visibility.\n// @LAYER: Domain\n// @RELATION: DEPENDS_ON -> [authStore]\n// @INVARIANT: Admin role always bypasses explicit permission checks.\n\nconst KNOWN_ACTIONS = new Set([\"READ\", \"WRITE\", \"EXECUTE\", \"DELETE\"]);\n\nfunction normalizeAction(action, fallback = \"READ\") {\n const normalized = String(action || \"\").trim().toUpperCase();\n if (!normalized) return fallback;\n return normalized;\n}\n\n// #region normalizePermissionRequirement:Function [TYPE Function]\n// @PURPOSE: Convert permission requirement string to canonical resource/action tuple.\n// @PRE: Permission can be \"resource\" or \"resource:ACTION\" where resource itself may contain \":\".\n// @POST: Returns normalized object with action in uppercase.\nexport function normalizePermissionRequirement(permission, defaultAction = \"READ\") {\n const fallbackAction = normalizeAction(defaultAction, \"READ\");\n const rawPermission = String(permission || \"\").trim();\n\n if (!rawPermission) {\n return { resource: null, action: fallbackAction };\n }\n\n const parts = rawPermission.split(\":\");\n if (parts.length > 1) {\n const tail = normalizeAction(parts[parts.length - 1], fallbackAction);\n if (KNOWN_ACTIONS.has(tail)) {\n const resource = parts.slice(0, -1).join(\":\");\n return { resource, action: tail };\n }\n }\n\n return { resource: rawPermission, action: fallbackAction };\n}\n// #endregion normalizePermissionRequirement:Function\n\n// #region isAdminUser:Function [TYPE Function]\n// @PURPOSE: Determine whether user has Admin role.\n// @PRE: user can be null or partially populated.\n// @POST: Returns true when at least one role name equals \"Admin\" (case-insensitive).\nexport function isAdminUser(user) {\n const roles = Array.isArray(user?.roles) ? user.roles : [];\n return roles.some(\n (role) => String(role?.name || \"\").trim().toLowerCase() === \"admin\",\n );\n}\n// #endregion isAdminUser:Function\n\n// #region hasPermission:Function [TYPE Function]\n// @PURPOSE: Check if user has a required resource/action permission.\n// @PRE: user contains roles with permissions from /api/auth/me payload.\n// @POST: Returns true when requirement is empty, user is admin, or matching permission exists.\nexport function hasPermission(user, requirement, action = \"READ\") {\n if (!requirement) return true;\n if (!user) return false;\n if (isAdminUser(user)) return true;\n\n const { resource, action: requiredAction } = normalizePermissionRequirement(\n requirement,\n action,\n );\n if (!resource) return true;\n\n const roles = Array.isArray(user.roles) ? user.roles : [];\n for (const role of roles) {\n const permissions = Array.isArray(role?.permissions) ? role.permissions : [];\n for (const permission of permissions) {\n if (typeof permission === \"string\") {\n const normalized = normalizePermissionRequirement(\n permission,\n requiredAction,\n );\n if (\n normalized.resource === resource &&\n normalized.action === requiredAction\n ) {\n return true;\n }\n continue;\n }\n\n const permissionResource = String(permission?.resource || \"\").trim();\n const permissionAction = normalizeAction(permission?.action, \"\");\n if (\n permissionResource === resource &&\n permissionAction === requiredAction\n ) {\n return true;\n }\n }\n }\n\n return false;\n}\n// #endregion hasPermission:Function\n\n// #endregion Permissions:Module\n" }, { - "contract_id": "normalizePermissionRequirement", + "contract_id": "normalizePermissionRequirement:Function", "contract_type": "Function", "file_path": "frontend/src/lib/auth/permissions.js", - "start_line": 17, - "end_line": 40, + "start_line": 20, + "end_line": 43, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118927,15 +85878,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:normalizePermissionRequirement:Function]\n// @PURPOSE: Convert permission requirement string to canonical resource/action tuple.\n// @PRE: Permission can be \"resource\" or \"resource:ACTION\" where resource itself may contain \":\".\n// @POST: Returns normalized object with action in uppercase.\nexport function normalizePermissionRequirement(permission, defaultAction = \"READ\") {\n const fallbackAction = normalizeAction(defaultAction, \"READ\");\n const rawPermission = String(permission || \"\").trim();\n\n if (!rawPermission) {\n return { resource: null, action: fallbackAction };\n }\n\n const parts = rawPermission.split(\":\");\n if (parts.length > 1) {\n const tail = normalizeAction(parts[parts.length - 1], fallbackAction);\n if (KNOWN_ACTIONS.has(tail)) {\n const resource = parts.slice(0, -1).join(\":\");\n return { resource, action: tail };\n }\n }\n\n return { resource: rawPermission, action: fallbackAction };\n}\n// [/DEF:normalizePermissionRequirement:Function]\n" + "anchor_syntax": "region", + "body": "// #region normalizePermissionRequirement:Function [TYPE Function]\n// @PURPOSE: Convert permission requirement string to canonical resource/action tuple.\n// @PRE: Permission can be \"resource\" or \"resource:ACTION\" where resource itself may contain \":\".\n// @POST: Returns normalized object with action in uppercase.\nexport function normalizePermissionRequirement(permission, defaultAction = \"READ\") {\n const fallbackAction = normalizeAction(defaultAction, \"READ\");\n const rawPermission = String(permission || \"\").trim();\n\n if (!rawPermission) {\n return { resource: null, action: fallbackAction };\n }\n\n const parts = rawPermission.split(\":\");\n if (parts.length > 1) {\n const tail = normalizeAction(parts[parts.length - 1], fallbackAction);\n if (KNOWN_ACTIONS.has(tail)) {\n const resource = parts.slice(0, -1).join(\":\");\n return { resource, action: tail };\n }\n }\n\n return { resource: rawPermission, action: fallbackAction };\n}\n// #endregion normalizePermissionRequirement:Function\n" }, { - "contract_id": "isAdminUser", + "contract_id": "isAdminUser:Function", "contract_type": "Function", "file_path": "frontend/src/lib/auth/permissions.js", - "start_line": 42, - "end_line": 52, + "start_line": 45, + "end_line": 55, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -118973,15 +85924,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:isAdminUser:Function]\n// @PURPOSE: Determine whether user has Admin role.\n// @PRE: user can be null or partially populated.\n// @POST: Returns true when at least one role name equals \"Admin\" (case-insensitive).\nexport function isAdminUser(user) {\n const roles = Array.isArray(user?.roles) ? user.roles : [];\n return roles.some(\n (role) => String(role?.name || \"\").trim().toLowerCase() === \"admin\",\n );\n}\n// [/DEF:isAdminUser:Function]\n" + "anchor_syntax": "region", + "body": "// #region isAdminUser:Function [TYPE Function]\n// @PURPOSE: Determine whether user has Admin role.\n// @PRE: user can be null or partially populated.\n// @POST: Returns true when at least one role name equals \"Admin\" (case-insensitive).\nexport function isAdminUser(user) {\n const roles = Array.isArray(user?.roles) ? user.roles : [];\n return roles.some(\n (role) => String(role?.name || \"\").trim().toLowerCase() === \"admin\",\n );\n}\n// #endregion isAdminUser:Function\n" }, { - "contract_id": "hasPermission", + "contract_id": "hasPermission:Function", "contract_type": "Function", "file_path": "frontend/src/lib/auth/permissions.js", - "start_line": 54, - "end_line": 100, + "start_line": 57, + "end_line": 103, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119019,41 +85970,28 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:hasPermission:Function]\n// @PURPOSE: Check if user has a required resource/action permission.\n// @PRE: user contains roles with permissions from /api/auth/me payload.\n// @POST: Returns true when requirement is empty, user is admin, or matching permission exists.\nexport function hasPermission(user, requirement, action = \"READ\") {\n if (!requirement) return true;\n if (!user) return false;\n if (isAdminUser(user)) return true;\n\n const { resource, action: requiredAction } = normalizePermissionRequirement(\n requirement,\n action,\n );\n if (!resource) return true;\n\n const roles = Array.isArray(user.roles) ? user.roles : [];\n for (const role of roles) {\n const permissions = Array.isArray(role?.permissions) ? role.permissions : [];\n for (const permission of permissions) {\n if (typeof permission === \"string\") {\n const normalized = normalizePermissionRequirement(\n permission,\n requiredAction,\n );\n if (\n normalized.resource === resource &&\n normalized.action === requiredAction\n ) {\n return true;\n }\n continue;\n }\n\n const permissionResource = String(permission?.resource || \"\").trim();\n const permissionAction = normalizeAction(permission?.action, \"\");\n if (\n permissionResource === resource &&\n permissionAction === requiredAction\n ) {\n return true;\n }\n }\n }\n\n return false;\n}\n// [/DEF:hasPermission:Function]\n" + "anchor_syntax": "region", + "body": "// #region hasPermission:Function [TYPE Function]\n// @PURPOSE: Check if user has a required resource/action permission.\n// @PRE: user contains roles with permissions from /api/auth/me payload.\n// @POST: Returns true when requirement is empty, user is admin, or matching permission exists.\nexport function hasPermission(user, requirement, action = \"READ\") {\n if (!requirement) return true;\n if (!user) return false;\n if (isAdminUser(user)) return true;\n\n const { resource, action: requiredAction } = normalizePermissionRequirement(\n requirement,\n action,\n );\n if (!resource) return true;\n\n const roles = Array.isArray(user.roles) ? user.roles : [];\n for (const role of roles) {\n const permissions = Array.isArray(role?.permissions) ? role.permissions : [];\n for (const permission of permissions) {\n if (typeof permission === \"string\") {\n const normalized = normalizePermissionRequirement(\n permission,\n requiredAction,\n );\n if (\n normalized.resource === resource &&\n normalized.action === requiredAction\n ) {\n return true;\n }\n continue;\n }\n\n const permissionResource = String(permission?.resource || \"\").trim();\n const permissionAction = normalizeAction(permission?.action, \"\");\n if (\n permissionResource === resource &&\n permissionAction === requiredAction\n ) {\n return true;\n }\n }\n }\n\n return false;\n}\n// #endregion hasPermission:Function\n" }, { - "contract_id": "authStore", + "contract_id": "AuthStore", "contract_type": "Store", "file_path": "frontend/src/lib/auth/store.ts", "start_line": 1, - "end_line": 102, + "end_line": 109, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", - "LAYER": "Feature", - "PURPOSE": "Manages the global authentication state on the frontend.", - "SEMANTICS": [ - "auth", - "store", - "svelte", - "jwt", - "session" - ] + "COMPLEXITY": 3, + "PURPOSE": "Manages the global authentication state including JWT token, user profile, and login/logout lifecycle.", + "UX_STATE": "Unauthenticated -> No valid session, login required" }, "relations": [ { - "source_id": "authStore", - "relation_type": "MODIFIED_BY", - "target_id": "AuthService", - "target_ref": "AuthService" - }, - { - "source_id": "authStore", + "source_id": "AuthStore", "relation_type": "BINDS_TO", - "target_id": "Navbar", - "target_ref": "Navbar" + "target_id": "PermissionsModule", + "target_ref": "[PermissionsModule]" } ], "schema_warnings": [ @@ -119083,28 +86021,6 @@ "contract_type": "Store" } }, - { - "code": "tag_not_for_contract_type", - "tag": "LAYER", - "message": "@LAYER is not allowed for contract type 'Store'", - "detail": { - "actual_type": "Store", - "allowed_types": [ - "Module", - "Skill", - "Agent" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Store' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Store" - } - }, { "code": "tag_not_for_contract_type", "tag": "PURPOSE", @@ -119134,22 +86050,19 @@ }, { "code": "tag_not_for_contract_type", - "tag": "SEMANTICS", - "message": "@SEMANTICS is not allowed for contract type 'Store'", + "tag": "UX_STATE", + "message": "@UX_STATE is not allowed for contract type 'Store'", "detail": { "actual_type": "Store", "allowed_types": [ - "Module", - "Block", - "Skill", - "Agent" + "Component" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Store' at C3", + "tag": "UX_STATE", + "message": "@UX_STATE is forbidden for contract type 'Store' at C3", "detail": { "actual_complexity": 3, "contract_type": "Store" @@ -119163,6 +86076,107 @@ "actual_complexity": 3, "contract_type": "Store" } + } + ], + "anchor_syntax": "region", + "body": "// #region AuthStore [C:3] [TYPE Store] [SEMANTICS auth, store, jwt, token, login, session]\n// @BRIEF Manages the global authentication state including JWT token, user profile, and login/logout lifecycle.\n// @RELATION BINDS_TO -> [PermissionsModule]\n// @UX_STATE Loading -> Auth state being initialized from localStorage\n// @UX_STATE Authenticated -> User profile and token active\n// @UX_STATE Unauthenticated -> No valid session, login required\n\n// #region authStore:Store [TYPE Function]\n// @SEMANTICS: auth, store, svelte, jwt, session\n// @PURPOSE: Manages the global authentication state on the frontend.\n// @LAYER: Feature\n// @RELATION: MODIFIED_BY -> AuthService\n// @RELATION: BINDS_TO -> Navbar\n\nimport { writable } from 'svelte/store';\nimport { browser } from '$app/environment';\n\n// #region AuthState:Interface [TYPE Function]\n/**\n * @purpose Defines the structure of the authentication state.\n */\nexport interface AuthState {\n user: any | null;\n token: string | null;\n isAuthenticated: boolean;\n loading: boolean;\n}\n// #endregion AuthState:Interface\n\nconst initialState: AuthState = {\n user: null,\n token: browser ? localStorage.getItem('auth_token') : null,\n isAuthenticated: false,\n loading: true\n};\n\n// #region createAuthStore:Function [TYPE Function]\n/**\n * @purpose Creates and configures the auth store with helper methods.\n * @pre No preconditions - initialization function.\n * @post Returns configured auth store with subscribe, setToken, setUser, logout, setLoading methods.\n * @returns {Writable}\n */\nfunction createAuthStore() {\n const { subscribe, set, update } = writable(initialState);\n\n return {\n subscribe,\n // #region setToken:Function [TYPE Function]\n /**\n * @purpose Updates the store with a new JWT token.\n * @pre token must be a valid JWT string.\n * @post Store updated with new token, isAuthenticated set to true.\n * @param {string} token - The JWT access token.\n */\n setToken: (token: string) => {\n console.log(\"[setToken][Action] Updating token\");\n if (browser) {\n localStorage.setItem('auth_token', token);\n }\n update(state => ({ ...state, token, isAuthenticated: !!token }));\n },\n // #endregion setToken:Function\n // #region setUser:Function [TYPE Function]\n /**\n * @purpose Sets the current user profile data.\n * @pre User object must contain valid profile data.\n * @post Store updated with user, isAuthenticated true, loading false.\n * @param {any} user - The user profile object.\n */\n setUser: (user: any) => {\n console.log(\"[setUser][Action] Setting user profile\");\n update(state => ({ ...state, user, isAuthenticated: !!user, loading: false }));\n },\n // #endregion setUser:Function\n // #region logout:Function [TYPE Function]\n /**\n * @purpose Clears authentication state and storage.\n * @pre User is currently authenticated.\n * @post Auth token removed from localStorage, store reset to initial state.\n */\n logout: () => {\n console.log(\"[logout][Action] Logging out\");\n if (browser) {\n localStorage.removeItem('auth_token');\n }\n set({ user: null, token: null, isAuthenticated: false, loading: false });\n },\n // #endregion logout:Function\n // #region setLoading:Function [TYPE Function]\n /**\n * @purpose Updates the loading state.\n * @pre None.\n * @post Store loading state updated.\n * @param {boolean} loading - Loading status.\n */\n setLoading: (loading: boolean) => {\n console.log(`[setLoading][Action] Setting loading to ${loading}`);\n update(state => ({ ...state, loading }));\n }\n // #endregion setLoading:Function\n };\n}\n// #endregion createAuthStore:Function\n\nexport const auth = createAuthStore();\n\n// #endregion authStore:Store\n// #endregion AuthStore\n" + }, + { + "contract_id": "authStore:Store", + "contract_type": "Function", + "file_path": "frontend/src/lib/auth/store.ts", + "start_line": 8, + "end_line": 108, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "LAYER": "Feature", + "PURPOSE": "Manages the global authentication state on the frontend.", + "SEMANTICS": [ + "auth", + "store", + "svelte", + "jwt", + "session" + ] + }, + "relations": [ + { + "source_id": "authStore:Store", + "relation_type": "MODIFIED_BY", + "target_id": "AuthService", + "target_ref": "AuthService" + }, + { + "source_id": "authStore:Store", + "relation_type": "BINDS_TO", + "target_id": "Navbar", + "target_ref": "Navbar" + } + ], + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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", @@ -119183,15 +86197,15 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:authStore:Store]\n// @COMPLEXITY: 3\n// @SEMANTICS: auth, store, svelte, jwt, session\n// @PURPOSE: Manages the global authentication state on the frontend.\n// @LAYER: Feature\n// @RELATION: MODIFIED_BY -> AuthService\n// @RELATION: BINDS_TO -> Navbar\n\nimport { writable } from 'svelte/store';\nimport { browser } from '$app/environment';\n\n// [DEF:AuthState:Interface]\n/**\n * @purpose Defines the structure of the authentication state.\n */\nexport interface AuthState {\n user: any | null;\n token: string | null;\n isAuthenticated: boolean;\n loading: boolean;\n}\n// [/DEF:AuthState:Interface]\n\nconst initialState: AuthState = {\n user: null,\n token: browser ? localStorage.getItem('auth_token') : null,\n isAuthenticated: false,\n loading: true\n};\n\n// [DEF:createAuthStore:Function]\n/**\n * @purpose Creates and configures the auth store with helper methods.\n * @pre No preconditions - initialization function.\n * @post Returns configured auth store with subscribe, setToken, setUser, logout, setLoading methods.\n * @returns {Writable}\n */\nfunction createAuthStore() {\n const { subscribe, set, update } = writable(initialState);\n\n return {\n subscribe,\n // [DEF:setToken:Function]\n /**\n * @purpose Updates the store with a new JWT token.\n * @pre token must be a valid JWT string.\n * @post Store updated with new token, isAuthenticated set to true.\n * @param {string} token - The JWT access token.\n */\n setToken: (token: string) => {\n console.log(\"[setToken][Action] Updating token\");\n if (browser) {\n localStorage.setItem('auth_token', token);\n }\n update(state => ({ ...state, token, isAuthenticated: !!token }));\n },\n // [/DEF:setToken:Function]\n // [DEF:setUser:Function]\n /**\n * @purpose Sets the current user profile data.\n * @pre User object must contain valid profile data.\n * @post Store updated with user, isAuthenticated true, loading false.\n * @param {any} user - The user profile object.\n */\n setUser: (user: any) => {\n console.log(\"[setUser][Action] Setting user profile\");\n update(state => ({ ...state, user, isAuthenticated: !!user, loading: false }));\n },\n // [/DEF:setUser:Function]\n // [DEF:logout:Function]\n /**\n * @purpose Clears authentication state and storage.\n * @pre User is currently authenticated.\n * @post Auth token removed from localStorage, store reset to initial state.\n */\n logout: () => {\n console.log(\"[logout][Action] Logging out\");\n if (browser) {\n localStorage.removeItem('auth_token');\n }\n set({ user: null, token: null, isAuthenticated: false, loading: false });\n },\n // [/DEF:logout:Function]\n // [DEF:setLoading:Function]\n /**\n * @purpose Updates the loading state.\n * @pre None.\n * @post Store loading state updated.\n * @param {boolean} loading - Loading status.\n */\n setLoading: (loading: boolean) => {\n console.log(`[setLoading][Action] Setting loading to ${loading}`);\n update(state => ({ ...state, loading }));\n }\n // [/DEF:setLoading:Function]\n };\n}\n// [/DEF:createAuthStore:Function]\n\nexport const auth = createAuthStore();\n\n// [/DEF:authStore:Store]\n" + "anchor_syntax": "region", + "body": "// #region authStore:Store [TYPE Function]\n// @SEMANTICS: auth, store, svelte, jwt, session\n// @PURPOSE: Manages the global authentication state on the frontend.\n// @LAYER: Feature\n// @RELATION: MODIFIED_BY -> AuthService\n// @RELATION: BINDS_TO -> Navbar\n\nimport { writable } from 'svelte/store';\nimport { browser } from '$app/environment';\n\n// #region AuthState:Interface [TYPE Function]\n/**\n * @purpose Defines the structure of the authentication state.\n */\nexport interface AuthState {\n user: any | null;\n token: string | null;\n isAuthenticated: boolean;\n loading: boolean;\n}\n// #endregion AuthState:Interface\n\nconst initialState: AuthState = {\n user: null,\n token: browser ? localStorage.getItem('auth_token') : null,\n isAuthenticated: false,\n loading: true\n};\n\n// #region createAuthStore:Function [TYPE Function]\n/**\n * @purpose Creates and configures the auth store with helper methods.\n * @pre No preconditions - initialization function.\n * @post Returns configured auth store with subscribe, setToken, setUser, logout, setLoading methods.\n * @returns {Writable}\n */\nfunction createAuthStore() {\n const { subscribe, set, update } = writable(initialState);\n\n return {\n subscribe,\n // #region setToken:Function [TYPE Function]\n /**\n * @purpose Updates the store with a new JWT token.\n * @pre token must be a valid JWT string.\n * @post Store updated with new token, isAuthenticated set to true.\n * @param {string} token - The JWT access token.\n */\n setToken: (token: string) => {\n console.log(\"[setToken][Action] Updating token\");\n if (browser) {\n localStorage.setItem('auth_token', token);\n }\n update(state => ({ ...state, token, isAuthenticated: !!token }));\n },\n // #endregion setToken:Function\n // #region setUser:Function [TYPE Function]\n /**\n * @purpose Sets the current user profile data.\n * @pre User object must contain valid profile data.\n * @post Store updated with user, isAuthenticated true, loading false.\n * @param {any} user - The user profile object.\n */\n setUser: (user: any) => {\n console.log(\"[setUser][Action] Setting user profile\");\n update(state => ({ ...state, user, isAuthenticated: !!user, loading: false }));\n },\n // #endregion setUser:Function\n // #region logout:Function [TYPE Function]\n /**\n * @purpose Clears authentication state and storage.\n * @pre User is currently authenticated.\n * @post Auth token removed from localStorage, store reset to initial state.\n */\n logout: () => {\n console.log(\"[logout][Action] Logging out\");\n if (browser) {\n localStorage.removeItem('auth_token');\n }\n set({ user: null, token: null, isAuthenticated: false, loading: false });\n },\n // #endregion logout:Function\n // #region setLoading:Function [TYPE Function]\n /**\n * @purpose Updates the loading state.\n * @pre None.\n * @post Store loading state updated.\n * @param {boolean} loading - Loading status.\n */\n setLoading: (loading: boolean) => {\n console.log(`[setLoading][Action] Setting loading to ${loading}`);\n update(state => ({ ...state, loading }));\n }\n // #endregion setLoading:Function\n };\n}\n// #endregion createAuthStore:Function\n\nexport const auth = createAuthStore();\n\n// #endregion authStore:Store\n" }, { - "contract_id": "AuthState", - "contract_type": "Interface", + "contract_id": "AuthState:Interface", + "contract_type": "Function", "file_path": "frontend/src/lib/auth/store.ts", - "start_line": 12, - "end_line": 22, + "start_line": 18, + "end_line": 28, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119199,43 +86213,25 @@ }, "relations": [], "schema_warnings": [ - { - "code": "tag_not_for_contract_type", - "tag": "PURPOSE", - "message": "@PURPOSE is not allowed for contract type 'Interface'", - "detail": { - "actual_type": "Interface", - "allowed_types": [ - "Module", - "Function", - "Class", - "Component", - "Block", - "ADR", - "Skill", - "Agent" - ] - } - }, { "code": "tag_forbidden_by_complexity", "tag": "PURPOSE", - "message": "@PURPOSE is forbidden for contract type 'Interface' at C1", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", "detail": { "actual_complexity": 1, - "contract_type": "Interface" + "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": "// [DEF:AuthState:Interface]\n/**\n * @purpose Defines the structure of the authentication state.\n */\nexport interface AuthState {\n user: any | null;\n token: string | null;\n isAuthenticated: boolean;\n loading: boolean;\n}\n// [/DEF:AuthState:Interface]\n" + "anchor_syntax": "region", + "body": "// #region AuthState:Interface [TYPE Function]\n/**\n * @purpose Defines the structure of the authentication state.\n */\nexport interface AuthState {\n user: any | null;\n token: string | null;\n isAuthenticated: boolean;\n loading: boolean;\n}\n// #endregion AuthState:Interface\n" }, { "contract_id": "frontend/src/lib/auth/store.ts::AuthState", "contract_type": "Interface", "file_path": "frontend/src/lib/auth/store.ts", - "start_line": 13, - "end_line": 16, + "start_line": 19, + "end_line": 22, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -119245,11 +86241,11 @@ "body": "/**\n * @purpose Defines the structure of the authentication state.\n */" }, { - "contract_id": "createAuthStore", + "contract_id": "createAuthStore:Function", "contract_type": "Function", "file_path": "frontend/src/lib/auth/store.ts", - "start_line": 31, - "end_line": 98, + "start_line": 37, + "end_line": 104, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119294,15 +86290,15 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "// [DEF:createAuthStore:Function]\n/**\n * @purpose Creates and configures the auth store with helper methods.\n * @pre No preconditions - initialization function.\n * @post Returns configured auth store with subscribe, setToken, setUser, logout, setLoading methods.\n * @returns {Writable}\n */\nfunction createAuthStore() {\n const { subscribe, set, update } = writable(initialState);\n\n return {\n subscribe,\n // [DEF:setToken:Function]\n /**\n * @purpose Updates the store with a new JWT token.\n * @pre token must be a valid JWT string.\n * @post Store updated with new token, isAuthenticated set to true.\n * @param {string} token - The JWT access token.\n */\n setToken: (token: string) => {\n console.log(\"[setToken][Action] Updating token\");\n if (browser) {\n localStorage.setItem('auth_token', token);\n }\n update(state => ({ ...state, token, isAuthenticated: !!token }));\n },\n // [/DEF:setToken:Function]\n // [DEF:setUser:Function]\n /**\n * @purpose Sets the current user profile data.\n * @pre User object must contain valid profile data.\n * @post Store updated with user, isAuthenticated true, loading false.\n * @param {any} user - The user profile object.\n */\n setUser: (user: any) => {\n console.log(\"[setUser][Action] Setting user profile\");\n update(state => ({ ...state, user, isAuthenticated: !!user, loading: false }));\n },\n // [/DEF:setUser:Function]\n // [DEF:logout:Function]\n /**\n * @purpose Clears authentication state and storage.\n * @pre User is currently authenticated.\n * @post Auth token removed from localStorage, store reset to initial state.\n */\n logout: () => {\n console.log(\"[logout][Action] Logging out\");\n if (browser) {\n localStorage.removeItem('auth_token');\n }\n set({ user: null, token: null, isAuthenticated: false, loading: false });\n },\n // [/DEF:logout:Function]\n // [DEF:setLoading:Function]\n /**\n * @purpose Updates the loading state.\n * @pre None.\n * @post Store loading state updated.\n * @param {boolean} loading - Loading status.\n */\n setLoading: (loading: boolean) => {\n console.log(`[setLoading][Action] Setting loading to ${loading}`);\n update(state => ({ ...state, loading }));\n }\n // [/DEF:setLoading:Function]\n };\n}\n// [/DEF:createAuthStore:Function]\n" + "anchor_syntax": "region", + "body": "// #region createAuthStore:Function [TYPE Function]\n/**\n * @purpose Creates and configures the auth store with helper methods.\n * @pre No preconditions - initialization function.\n * @post Returns configured auth store with subscribe, setToken, setUser, logout, setLoading methods.\n * @returns {Writable}\n */\nfunction createAuthStore() {\n const { subscribe, set, update } = writable(initialState);\n\n return {\n subscribe,\n // #region setToken:Function [TYPE Function]\n /**\n * @purpose Updates the store with a new JWT token.\n * @pre token must be a valid JWT string.\n * @post Store updated with new token, isAuthenticated set to true.\n * @param {string} token - The JWT access token.\n */\n setToken: (token: string) => {\n console.log(\"[setToken][Action] Updating token\");\n if (browser) {\n localStorage.setItem('auth_token', token);\n }\n update(state => ({ ...state, token, isAuthenticated: !!token }));\n },\n // #endregion setToken:Function\n // #region setUser:Function [TYPE Function]\n /**\n * @purpose Sets the current user profile data.\n * @pre User object must contain valid profile data.\n * @post Store updated with user, isAuthenticated true, loading false.\n * @param {any} user - The user profile object.\n */\n setUser: (user: any) => {\n console.log(\"[setUser][Action] Setting user profile\");\n update(state => ({ ...state, user, isAuthenticated: !!user, loading: false }));\n },\n // #endregion setUser:Function\n // #region logout:Function [TYPE Function]\n /**\n * @purpose Clears authentication state and storage.\n * @pre User is currently authenticated.\n * @post Auth token removed from localStorage, store reset to initial state.\n */\n logout: () => {\n console.log(\"[logout][Action] Logging out\");\n if (browser) {\n localStorage.removeItem('auth_token');\n }\n set({ user: null, token: null, isAuthenticated: false, loading: false });\n },\n // #endregion logout:Function\n // #region setLoading:Function [TYPE Function]\n /**\n * @purpose Updates the loading state.\n * @pre None.\n * @post Store loading state updated.\n * @param {boolean} loading - Loading status.\n */\n setLoading: (loading: boolean) => {\n console.log(`[setLoading][Action] Setting loading to ${loading}`);\n update(state => ({ ...state, loading }));\n }\n // #endregion setLoading:Function\n };\n}\n// #endregion createAuthStore:Function\n" }, { "contract_id": "frontend/src/lib/auth/store.ts::createAuthStore", "contract_type": "Function", "file_path": "frontend/src/lib/auth/store.ts", - "start_line": 32, - "end_line": 38, + "start_line": 38, + "end_line": 44, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119315,11 +86311,11 @@ "body": "/**\n * @purpose Creates and configures the auth store with helper methods.\n * @pre No preconditions - initialization function.\n * @post Returns configured auth store with subscribe, setToken, setUser, logout, setLoading methods.\n * @returns {Writable}\n */" }, { - "contract_id": "setToken", + "contract_id": "setToken:Function", "contract_type": "Function", "file_path": "frontend/src/lib/auth/store.ts", - "start_line": 43, - "end_line": 57, + "start_line": 49, + "end_line": 63, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119364,15 +86360,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:setToken:Function]\n /**\n * @purpose Updates the store with a new JWT token.\n * @pre token must be a valid JWT string.\n * @post Store updated with new token, isAuthenticated set to true.\n * @param {string} token - The JWT access token.\n */\n setToken: (token: string) => {\n console.log(\"[setToken][Action] Updating token\");\n if (browser) {\n localStorage.setItem('auth_token', token);\n }\n update(state => ({ ...state, token, isAuthenticated: !!token }));\n },\n // [/DEF:setToken:Function]\n" + "anchor_syntax": "region", + "body": " // #region setToken:Function [TYPE Function]\n /**\n * @purpose Updates the store with a new JWT token.\n * @pre token must be a valid JWT string.\n * @post Store updated with new token, isAuthenticated set to true.\n * @param {string} token - The JWT access token.\n */\n setToken: (token: string) => {\n console.log(\"[setToken][Action] Updating token\");\n if (browser) {\n localStorage.setItem('auth_token', token);\n }\n update(state => ({ ...state, token, isAuthenticated: !!token }));\n },\n // #endregion setToken:Function\n" }, { - "contract_id": "setUser", + "contract_id": "setUser:Function", "contract_type": "Function", "file_path": "frontend/src/lib/auth/store.ts", - "start_line": 58, - "end_line": 69, + "start_line": 64, + "end_line": 75, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119417,15 +86413,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:setUser:Function]\n /**\n * @purpose Sets the current user profile data.\n * @pre User object must contain valid profile data.\n * @post Store updated with user, isAuthenticated true, loading false.\n * @param {any} user - The user profile object.\n */\n setUser: (user: any) => {\n console.log(\"[setUser][Action] Setting user profile\");\n update(state => ({ ...state, user, isAuthenticated: !!user, loading: false }));\n },\n // [/DEF:setUser:Function]\n" + "anchor_syntax": "region", + "body": " // #region setUser:Function [TYPE Function]\n /**\n * @purpose Sets the current user profile data.\n * @pre User object must contain valid profile data.\n * @post Store updated with user, isAuthenticated true, loading false.\n * @param {any} user - The user profile object.\n */\n setUser: (user: any) => {\n console.log(\"[setUser][Action] Setting user profile\");\n update(state => ({ ...state, user, isAuthenticated: !!user, loading: false }));\n },\n // #endregion setUser:Function\n" }, { - "contract_id": "logout", + "contract_id": "logout:Function", "contract_type": "Function", "file_path": "frontend/src/lib/auth/store.ts", - "start_line": 70, - "end_line": 83, + "start_line": 76, + "end_line": 89, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119463,15 +86459,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:logout:Function]\n /**\n * @purpose Clears authentication state and storage.\n * @pre User is currently authenticated.\n * @post Auth token removed from localStorage, store reset to initial state.\n */\n logout: () => {\n console.log(\"[logout][Action] Logging out\");\n if (browser) {\n localStorage.removeItem('auth_token');\n }\n set({ user: null, token: null, isAuthenticated: false, loading: false });\n },\n // [/DEF:logout:Function]\n" + "anchor_syntax": "region", + "body": " // #region logout:Function [TYPE Function]\n /**\n * @purpose Clears authentication state and storage.\n * @pre User is currently authenticated.\n * @post Auth token removed from localStorage, store reset to initial state.\n */\n logout: () => {\n console.log(\"[logout][Action] Logging out\");\n if (browser) {\n localStorage.removeItem('auth_token');\n }\n set({ user: null, token: null, isAuthenticated: false, loading: false });\n },\n // #endregion logout:Function\n" }, { - "contract_id": "setLoading", + "contract_id": "setLoading:Function", "contract_type": "Function", "file_path": "frontend/src/lib/auth/store.ts", - "start_line": 84, - "end_line": 95, + "start_line": 90, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119516,122 +86512,19 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:setLoading:Function]\n /**\n * @purpose Updates the loading state.\n * @pre None.\n * @post Store loading state updated.\n * @param {boolean} loading - Loading status.\n */\n setLoading: (loading: boolean) => {\n console.log(`[setLoading][Action] Setting loading to ${loading}`);\n update(state => ({ ...state, loading }));\n }\n // [/DEF:setLoading:Function]\n" - }, - { - "contract_id": "AssistantChatPanel", - "contract_type": "Component", - "file_path": "frontend/src/lib/components/assistant/AssistantChatPanel.svelte", - "start_line": 1, - "end_line": 960, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": "5", - "DATA_CONTRACT": "Input[{variant,className,assistantChatStore,datasetReviewSessionId?}] -> Output[{messages,conversations,conversationId,llmReady,panelVisible}]", - "INVARIANT": "Risky operations are executed only through explicit confirm action.", - "LAYER": "UI", - "POST": "User and assistant message state, conversation history, and task affordances stay synchronized with the active conversation scope.", - "PRE": "Assistant chat store, API adapters, and optional dataset review session context are initialized before the panel becomes interactive.", - "PURPOSE": "Assistant chat workspace for mixed-initiative dataset review clarification, contextual actions, and task tracking.", - "SEMANTICS": [ - "assistant-chat", - "confirmation", - "long-running-task", - "progress-tracking" - ], - "SIDE_EFFECT": "Reads and mutates assistant conversation state, may navigate to review routes, opens task drawer affordances, and emits toast feedback.", - "UX_FEEDBACK": "Started operation surfaces task_id and quick action to open task drawer.", - "UX_RECOVERY": "User can retry command or action from input and action buttons.", - "UX_STATE": "Error -> Failed action rendered as assistant failed message." - }, - "relations": [ - { - "source_id": "AssistantChatPanel", - "relation_type": "BINDS_TO", - "target_id": "assistantChat", - "target_ref": "assistantChat" - }, - { - "source_id": "AssistantChatPanel", - "relation_type": "CALLS", - "target_id": "AssistantApi", - "target_ref": "AssistantApi" - }, - { - "source_id": "AssistantChatPanel", - "relation_type": "BINDS_TO", - "target_id": "DatasetReviewWorkspace", - "target_ref": "DatasetReviewWorkspace" - }, - { - "source_id": "AssistantChatPanel", - "relation_type": "DISPATCHES", - "target_id": "taskDrawer", - "target_ref": "taskDrawer" - } - ], - "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 C5", - "detail": { - "actual_complexity": 5, - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n{#if panelVisible}\n {#if variant === \"drawer\" && isOpen}\n \n {/if}\n\n \n \n
\n \n

{$t.assistant?.title}

\n
\n {#if variant === \"drawer\"}\n \n \n \n {:else}\n \n {$t.assistant?.session_scope_title || \"Active review context\"}\n \n {/if}\n \n\n
\n {#if !llmReady}\n
\n
\n {$t.dashboard?.llm_not_configured || \"LLM is not configured\"}\n
\n
\n {#if llmStatusReason === \"no_active_provider\"}\n No active LLM provider is enabled for runtime. Activate one provider in Admin → LLM Settings or verify that this runtime points to the same database where providers were configured.\n {:else if llmStatusReason === \"invalid_api_key\"}\n {$t.dashboard?.llm_configure_key || \"Invalid LLM API key. Update and save a real key in Admin -> LLM Settings.\"}\n {:else}\n {$t.dashboard?.llm_status_unavailable || \"LLM status is unavailable. Check settings and backend logs.\"}\n {/if}\n
\n {#if llmStatusPayload?.reason}\n
\n Runtime status: {llmStatusPayload.reason}\n
\n {/if}\n
\n {/if}\n\n {#if contextSummary}\n
\n
\n
\n
{$t.assistant?.session_scope_title || \"Active review context\"}
\n
{contextSummary}
\n
\n {#if focusTarget?.target}\n \n {$t.assistant?.clear_focus_action || \"Clear focus\"}\n \n {/if}\n
\n
\n {/if}\n\n {#if clarificationBannerVisible}\n
\n
\n
\n
\n {$t.assistant?.clarification_banner_title || \"Clarification queue is active\"}\n
\n
{clarificationMessage?.text}
\n {#if focusTargetKey}\n
\n {$t.assistant?.focus_target_label || \"Focus\"}: {focusTargetKey}\n
\n {/if}\n
\n
\n \n {$t.assistant?.clarification_resume_action || \"Resume\"}\n \n \n {$t.assistant?.clarification_dismiss_action || \"Dismiss\"}\n \n
\n
\n
\n {/if}\n\n setAssistantFocusTarget(nextFocusTarget)}\n />\n\n
\n
\n \n {$t.assistant?.conversations}\n \n \n {$t.assistant?.new}\n \n
\n
\n setConversationFilter(\"active\")}\n >\n {$t.assistant?.active} ({activeConversationsTotal})\n \n setConversationFilter(\"archived\")}\n >\n {$t.assistant?.archived} ({archivedConversationsTotal})\n \n
\n
\n {#each conversations as convo (convo.conversation_id)}\n
\n selectConversation(convo)}\n title={formatConversationTime(convo.updated_at)}\n >\n
{buildConversationTitle(convo)}
\n
{convo.last_message || \"\"}
\n \n
\n {/each}\n {#if loadingConversations}\n
...
\n {/if}\n {#if conversationsHasNext}\n loadConversations(false)}\n >\n {$t.assistant?.more}\n \n {/if}\n
\n
\n\n \n {#if loadingMoreHistory}\n
\n {$t.assistant?.loading_older}\n
\n {/if}\n\n {#if loadingHistory}\n
\n {$t.assistant?.loading_history}\n
\n {:else if messages.length === 0}\n
\n
\n {$t.assistant?.try_commands || \"Start the review dialog from here.\"}\n
\n
\n
• {$t.assistant?.sample_command_branch}
\n
• {$t.assistant?.sample_command_migration}
\n
• {$t.assistant?.sample_command_status}
\n
\n
\n {/if}\n\n {#each messages as message (message.message_id)}\n
\n \n
\n \n {message.role === \"user\" ? $t.assistant?.you : $t.assistant?.assistant}\n \n {#if message.state}\n \n {$t.assistant?.states?.[message.state] || message.state}\n \n {/if}\n
\n\n
{message.text}
\n\n {#if getMessageFocusTarget(message)}\n syncMessageFocus(message)}\n >\n {$t.assistant?.highlight_workspace_action || \"Highlight in workspace\"}\n \n {/if}\n\n {#if isConfirmationCard(message)}\n
\n
\n {$t.assistant?.confirmation_card_title || \"Confirmation required\"}\n
\n
\n {datasetReviewSessionId\n ? `${$t.assistant?.session_scope_label || \"Session\"}: ${datasetReviewSessionId}`\n : $t.assistant?.confirmation_scope_fallback || \"This action affects the current assistant scope.\"}\n
\n {#if message.intent?.operation}\n
\n {$t.assistant?.confirmation_operation_label || \"Action\"}:\n {message.intent.operation}\n
\n {/if}\n {#if hasConfirmationScope(message)}\n
    \n {#each renderConfirmationScope(message) as item}\n
  • • {item}
  • \n {/each}\n
\n {/if}\n
\n {/if}\n\n {#if message.task_id}\n
\n \n {$t.assistant?.task_id}: {message.task_id}\n \n openDrawerForTask(message.task_id)}\n >\n {$t.assistant?.open_task_drawer}\n \n
\n {/if}\n\n {#if message.actions?.length}\n
\n {#each message.actions as action}\n handleAction(action, message)}\n >\n {action.label}\n \n {/each}\n
\n {/if}\n
\n
\n {/each}\n\n {#if loading}\n
\n
\n
\n \n {$t.assistant?.assistant}\n \n
\n
\n {$t.assistant?.thinking}\n \n \n \n
\n
\n
\n {/if}\n \n\n
\n
\n \n \n {loading ? \"...\" : $t.assistant?.send}\n \n
\n
\n \n \n{/if}\n\n\n" + "anchor_syntax": "region", + "body": " // #region setLoading:Function [TYPE Function]\n /**\n * @purpose Updates the loading state.\n * @pre None.\n * @post Store loading state updated.\n * @param {boolean} loading - Loading status.\n */\n setLoading: (loading: boolean) => {\n console.log(`[setLoading][Action] Setting loading to ${loading}`);\n update(state => ({ ...state, loading }));\n }\n // #endregion setLoading:Function\n" }, { "contract_id": "AssistantClarificationCard", "contract_type": "Component", "file_path": "frontend/src/lib/components/assistant/AssistantClarificationCard.svelte", "start_line": 1, - "end_line": 408, + "end_line": 409, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input[{sessionId, disabled}] -> Output[{clarification_state|clarification_session|current_question|session}] via onupdated callback.", "LAYER": "UI", "POST": "Clarification queue state is readable in-chat and answer mutations refresh the active question state without requiring a dedicated dialog.", @@ -119674,9 +86567,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C4", + "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -119696,9 +86589,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C4", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "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" } }, @@ -119719,9 +86630,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C4", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, "contract_type": "Component" } }, @@ -119780,145 +86700,15 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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": "AssistantChatIntegrationTest", - "contract_type": "Module", - "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_chat.integration.test.js", - "start_line": 1, - "end_line": 103, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Critical assistant UX states and action hooks remain present in component source.", - "LAYER": "UI Tests", - "PURPOSE": "Contract-level integration checks for assistant chat panel implementation and localization wiring.", - "SEMANTICS": [ - "assistant", - "integration-test", - "ux-contract", - "i18n" - ], - "TEST_CONTRACT": "AssistantChatSourceArtifacts -> ContractAssertions", - "TEST_EDGE": "missing_locale_key -> Missing assistant locale key in en/ru fails dictionary assertion.", - "TEST_FIXTURE": "assistant_locales_en_ru -> file:src/lib/i18n/locales/en/assistant.json + file:src/lib/i18n/locales/ru/assistant.json", - "TEST_INVARIANT": "assistant_ux_contract_visible -> VERIFIED_BY: [assistant_contract_and_i18n_intact]", - "TEST_SCENARIO": "assistant_contract_and_i18n_intact -> Component semantic/UX anchors and locale keys stay consistent." - }, - "relations": [ - { - "source_id": "AssistantChatIntegrationTest", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantChatPanel", - "target_ref": "[AssistantChatPanel]" - } - ], - "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": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "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 C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_FIXTURE", - "message": "@TEST_FIXTURE is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - } - ], - "anchor_syntax": "def", - "body": "// [DEF:AssistantChatIntegrationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: assistant, integration-test, ux-contract, i18n\n// @PURPOSE: Contract-level integration checks for assistant chat panel implementation and localization wiring.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [AssistantChatPanel]\n// @INVARIANT: Critical assistant UX states and action hooks remain present in component source.\n// @TEST_CONTRACT: AssistantChatSourceArtifacts -> ContractAssertions\n// @TEST_SCENARIO: assistant_contract_and_i18n_intact -> Component semantic/UX anchors and locale keys stay consistent.\n// @TEST_FIXTURE: assistant_locales_en_ru -> file:src/lib/i18n/locales/en/assistant.json + file:src/lib/i18n/locales/ru/assistant.json\n// @TEST_EDGE: missing_component_anchor -> Missing DEF/UX tags fails contract assertion.\n// @TEST_EDGE: missing_action_hook -> Missing confirm/cancel/open_task hooks fails integration assertion.\n// @TEST_EDGE: missing_locale_key -> Missing assistant locale key in en/ru fails dictionary assertion.\n// @TEST_INVARIANT: assistant_ux_contract_visible -> VERIFIED_BY: [assistant_contract_and_i18n_intact]\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/lib/components/assistant/AssistantChatPanel.svelte',\n);\nconst EN_LOCALE_PATH = path.resolve(\n process.cwd(),\n 'src/lib/i18n/locales/en/assistant.json',\n);\nconst RU_LOCALE_PATH = path.resolve(\n process.cwd(),\n 'src/lib/i18n/locales/ru/assistant.json',\n);\n\n// [DEF:readJson:Function]\n// @PURPOSE: Read and parse JSON fixture file from disk.\n// @PRE: filePath points to existing UTF-8 JSON file.\n// @POST: Returns parsed object representation.\nfunction readJson(filePath) {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8'));\n}\n// [/DEF:readJson:Function]\n\n// [DEF:assistant_chat_contract_tests:Function]\n// @PURPOSE: Validate assistant chat component contract and locale integration without DOM runtime dependency.\n// @PRE: Component and locale files exist in expected paths.\n// @POST: Contract checks guarantee assistant UI anchors and i18n wiring remain intact.\ndescribe('AssistantChatPanel integration contract', () => {\n it('contains semantic anchors and UX contract tags', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('');\n expect(source).toContain('@COMPLEXITY: 5');\n expect(source).toContain('@UX_STATE: LoadingHistory');\n expect(source).toContain('@UX_STATE: Sending');\n expect(source).toContain('@UX_STATE: Error');\n expect(source).toContain('@UX_FEEDBACK: Started operation surfaces task_id');\n expect(source).toContain('@UX_RECOVERY: User can retry command');\n expect(source).toContain('');\n });\n\n it('keeps confirmation/task-tracking action hooks in place', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('if (action.type === \"confirm\" && message.confirmation_id)');\n expect(source).toContain('if (action.type === \"cancel\" && message.confirmation_id)');\n expect(source).toContain('if (action.type === \"open_task\" && action.target)');\n expect(source).toContain('openDrawerForTask(action.target)');\n expect(source).toContain('goto(\"/reports\")');\n });\n\n it('guards first-message optimistic flow from premature history overwrite', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('if (panelVisible && initialized && conversationId && !loading)');\n expect(source).toContain('function appendLocalUserMessage(text, targetConversationId = conversationId)');\n expect(source).toContain('conversation_id: targetConversationId || null');\n expect(source).toContain('historyLoadVersion += 1;');\n });\n\n it('uses i18n bindings for assistant UI labels', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('$t.assistant?.title');\n expect(source).toContain('$t.assistant?.input_placeholder');\n expect(source).toContain('$t.assistant?.send');\n expect(source).toContain('$t.assistant?.states?.[message.state]');\n expect(source).toContain('$t.assistant?.open_task_drawer');\n });\n\n it('provides assistant locale keys in both en and ru dictionaries', () => {\n const en = readJson(EN_LOCALE_PATH);\n const ru = readJson(RU_LOCALE_PATH);\n\n expect(en.title).toBeTruthy();\n expect(en.send).toBeTruthy();\n expect(en.states.needs_confirmation).toBeTruthy();\n\n expect(ru.title).toBeTruthy();\n expect(ru.send).toBeTruthy();\n expect(ru.states.needs_confirmation).toBeTruthy();\n });\n});\n// [/DEF:assistant_chat_contract_tests:Function]\n// [/DEF:AssistantChatIntegrationTest:Module]\n" - }, - { - "contract_id": "readJson", + "contract_id": "readJson:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_chat.integration.test.js", - "start_line": 33, - "end_line": 40, + "start_line": 32, + "end_line": 39, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -119956,23 +86746,229 @@ } } ], - "anchor_syntax": "def", - "body": "// [DEF:readJson:Function]\n// @PURPOSE: Read and parse JSON fixture file from disk.\n// @PRE: filePath points to existing UTF-8 JSON file.\n// @POST: Returns parsed object representation.\nfunction readJson(filePath) {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8'));\n}\n// [/DEF:readJson:Function]\n" + "anchor_syntax": "region", + "body": "// #region readJson:Function [TYPE Function]\n// @PURPOSE: Read and parse JSON fixture file from disk.\n// @PRE: filePath points to existing UTF-8 JSON file.\n// @POST: Returns parsed object representation.\nfunction readJson(filePath) {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8'));\n}\n// #endregion readJson:Function\n" }, { - "contract_id": "assistant_chat_contract_tests", + "contract_id": "AssistantClarificationIntegrationTest:Module", "contract_type": "Function", - "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_chat.integration.test.js", - "start_line": 42, - "end_line": 102, + "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_clarification.integration.test.js", + "start_line": 1, + "end_line": 207, "tier": "TIER_1", "complexity": 1, "metadata": { - "POST": "Contract checks guarantee assistant UI anchors and i18n wiring remain intact.", - "PRE": "Component and locale files exist in expected paths.", - "PURPOSE": "Validate assistant chat component contract and locale integration without DOM runtime dependency." + "LAYER": "UI Tests", + "PURPOSE": "Verify AssistantChatPanel hosts the resumable dataset-review clarification flow inside the assistant drawer and refreshes workspace session state after answering.", + "SEMANTICS": [ + "assistant", + "clarification", + "integration-test", + "dataset-review", + "mixed-initiative" + ] }, - "relations": [], + "relations": [ + { + "source_id": "AssistantClarificationIntegrationTest:Module", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantChatPanel", + "target_ref": "[AssistantChatPanel]" + } + ], + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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" + } + } + ], + "anchor_syntax": "region", + "body": "// #region AssistantClarificationIntegrationTest:Module [TYPE Function]\n// @SEMANTICS: assistant, clarification, integration-test, dataset-review, mixed-initiative\n// @PURPOSE: Verify AssistantChatPanel hosts the resumable dataset-review clarification flow inside the assistant drawer and refreshes workspace session state after answering.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [AssistantChatPanel]\n\nimport { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@testing-library/svelte\";\nimport AssistantChatPanel from \"../AssistantChatPanel.svelte\";\nimport * as assistantApi from \"$lib/api/assistant\";\nimport { api, requestApi } from \"$lib/api.js\";\nimport { setSession as setDatasetReviewSession } from \"$lib/stores/datasetReviewSession.js\";\n\nvi.mock(\"$lib/api/assistant\", () => ({\n getAssistantHistory: vi.fn(() => Promise.resolve({ items: [], total: 0, has_next: false })),\n getAssistantConversations: vi.fn(() => Promise.resolve({ items: [], total: 0, has_next: false, active_total: 0, archived_total: 0 })),\n deleteAssistantConversation: vi.fn(),\n confirmAssistantOperation: vi.fn(),\n cancelAssistantOperation: vi.fn(),\n sendAssistantMessage: vi.fn(),\n}));\n\nvi.mock(\"$lib/api.js\", () => ({\n api: {\n getLlmStatus: vi.fn(() => Promise.resolve({ configured: true, reason: \"ok\" })),\n fetchApi: vi.fn(),\n },\n requestApi: vi.fn(),\n}));\n\nvi.mock(\"$lib/stores/datasetReviewSession.js\", () => ({\n setSession: vi.fn(),\n}));\n\nvi.mock(\"$lib/stores/taskDrawer.js\", () => ({\n openDrawerForTask: vi.fn(),\n}));\n\nconst { assistantState } = vi.hoisted(() => ({\n assistantState: (() => {\n let value = {\n isOpen: true,\n conversationId: \"conv-1\",\n datasetReviewSessionId: \"session-1\",\n seedMessage: \"\",\n focusTarget: null,\n };\n const subscribers = new Set();\n return {\n subscribe(fn) {\n subscribers.add(fn);\n fn(value);\n return () => subscribers.delete(fn);\n },\n update(updater) {\n value = updater(value);\n subscribers.forEach((fn) => fn(value));\n },\n };\n })(),\n}));\n\nvi.mock(\"$lib/stores/assistantChat\", () => ({\n assistantChatStore: { subscribe: assistantState.subscribe },\n closeAssistantChat: vi.fn(),\n setAssistantConversationId: vi.fn(),\n setAssistantSeedMessage: vi.fn(),\n setAssistantFocusTarget: vi.fn(),\n}));\n\nvi.mock(\"$app/navigation\", () => ({ goto: vi.fn() }));\nvi.mock(\"../../../../services/gitService.js\", () => ({ gitService: { getDiff: vi.fn() } }));\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: { error: \"Common error\" },\n assistant: {\n title: \"Assistant\",\n close: \"Close\",\n conversations: \"Conversations\",\n new: \"New\",\n active: \"Active\",\n archived: \"Archived\",\n more: \"More\",\n loading_history: \"Loading history\",\n loading_older: \"Loading older\",\n try_commands: \"Try commands\",\n sample_command_branch: \"branch\",\n sample_command_migration: \"migration\",\n sample_command_status: \"status\",\n you: \"You\",\n assistant: \"Assistant\",\n thinking: \"Thinking\",\n input_placeholder: \"Type command\",\n send: \"Send\",\n conversation: \"Conversation\",\n session_scope_title: \"Active review context\",\n session_scope_label: \"Session\",\n focus_target_label: \"Focus\",\n clear_focus_action: \"Clear focus\",\n states: { success: \"Success\" },\n open_task_drawer: \"Open task drawer\",\n },\n dataset_review: {\n clarification: {\n title: \"Clarification queue\",\n progress_label: \"Progress\",\n active_question_label: \"Active question\",\n why_it_matters_label: \"Why it matters\",\n current_guess_label: \"Current guess\",\n topic_label: \"Topic\",\n custom_answer_label: \"Custom answer\",\n answer_action: \"Answer\",\n custom_answer_action: \"Use custom answer\",\n skip_action: \"Skip\",\n expert_review_action: \"Expert review\",\n completed: \"No active clarification question remains.\",\n resume_action: \"Resume clarification\",\n feedback_label: \"Feedback\",\n feedback_prompt: \"Record whether the clarification result was useful.\",\n feedback_up_action: \"Thumbs up\",\n feedback_down_action: \"Thumbs down\",\n messages: {\n saving: \"Saving clarification answer...\",\n saved: \"Clarification answer saved.\",\n resumed: \"Clarification queue resumed.\",\n },\n },\n },\n dashboard: {},\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"AssistantChatPanel clarification integration\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n api.fetchApi.mockImplementation((endpoint) => {\n if (endpoint === \"/dataset-orchestration/sessions/session-1/clarification\") {\n return Promise.resolve({\n clarification_session: {\n clarification_session_id: \"clarification-1\",\n session_id: \"session-1\",\n status: \"active\",\n current_question_id: \"question-1\",\n resolved_count: 1,\n remaining_count: 2,\n },\n current_question: {\n question_id: \"question-1\",\n topic_ref: \"profile.summary\",\n question_text: \"Which customer label should be used?\",\n why_it_matters: \"This label is shown to reviewers.\",\n current_guess: \"Customer name\",\n options: [{ value: \"Customer name\", label: \"Customer name\", is_recommended: true }],\n },\n });\n }\n if (endpoint === \"/dataset-orchestration/sessions/session-1\") {\n return Promise.resolve({ session_id: \"session-1\", session_version: 4, version: 4, profile: { business_summary: \"Updated summary\" } });\n }\n return Promise.resolve(null);\n });\n requestApi.mockResolvedValue({\n clarification_state: {\n clarification_session: {\n clarification_session_id: \"clarification-1\",\n session_id: \"session-1\",\n status: \"active\",\n current_question_id: null,\n resolved_count: 2,\n remaining_count: 1,\n },\n current_question: null,\n },\n session: { session_id: \"session-1\", session_version: 4, version: 4 },\n });\n });\n\n it(\"renders clarification queue inside assistant and refreshes session after answering\", async () => {\n render(AssistantChatPanel);\n\n expect(await screen.findByText(\"Clarification queue\")).toBeTruthy();\n expect(screen.getByText(\"Which customer label should be used?\")).toBeTruthy();\n expect(screen.getByText(\"This label is shown to reviewers.\")).toBeTruthy();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Answer\" }));\n\n await waitFor(() => {\n expect(requestApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/clarification/answers\",\n \"POST\",\n expect.objectContaining({ question_id: \"question-1\", answer_kind: \"selected\", answer_value: \"Customer name\" }),\n );\n });\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledWith(\"/dataset-orchestration/sessions/session-1\");\n expect(setDatasetReviewSession).toHaveBeenCalled();\n });\n });\n});\n// #endregion AssistantClarificationIntegrationTest:Module\n" + }, + { + "contract_id": "AssistantFirstMessageIntegrationTest:Module", + "contract_type": "Function", + "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_first_message.integration.test.js", + "start_line": 1, + "end_line": 239, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "INVARIANT": "Starting a new conversation must not trigger history reload that overwrites the first local user message.", + "LAYER": "UI Tests", + "PURPOSE": "Verify first optimistic user message stays visible while a new conversation request is pending.", + "SEMANTICS": [ + "assistant", + "integration-test", + "optimistic-message", + "conversation-race" + ] + }, + "relations": [ + { + "source_id": "AssistantFirstMessageIntegrationTest:Module", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantChatPanel", + "target_ref": "[AssistantChatPanel]" + } + ], + "schema_warnings": [ + { + "code": "tag_forbidden_by_complexity", + "tag": "INVARIANT", + "message": "@INVARIANT is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "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": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_not_for_contract_type", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", + "detail": { + "actual_type": "Function", + "allowed_types": [ + "Module", + "Block", + "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" + } + } + ], + "anchor_syntax": "region", + "body": "// #region AssistantFirstMessageIntegrationTest:Module [TYPE Function]\n// @SEMANTICS: assistant, integration-test, optimistic-message, conversation-race\n// @PURPOSE: Verify first optimistic user message stays visible while a new conversation request is pending.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [AssistantChatPanel]\n// @INVARIANT: Starting a new conversation must not trigger history reload that overwrites the first local user message.\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { render, screen, fireEvent, waitFor } from '@testing-library/svelte';\nimport AssistantChatPanel from '../AssistantChatPanel.svelte';\nimport * as assistantApi from '$lib/api/assistant';\n\nconst { assistantState } = vi.hoisted(() => ({\n assistantState: (() => {\n let value = {\n isOpen: true,\n conversationId: null,\n datasetReviewSessionId: 'session-1',\n seedMessage: '',\n focusTarget: null,\n };\n const subscribers = new Set();\n return {\n subscribe(fn) {\n subscribers.add(fn);\n fn(value);\n return () => subscribers.delete(fn);\n },\n set(next) {\n value = next;\n subscribers.forEach((fn) => fn(value));\n },\n update(updater) {\n value = updater(value);\n subscribers.forEach((fn) => fn(value));\n },\n };\n })(),\n}));\n\nvi.mock('$lib/api/assistant', () => ({\n getAssistantHistory: vi.fn(() =>\n Promise.resolve({ items: [], total: 0, has_next: false }),\n ),\n getAssistantConversations: vi.fn(() =>\n Promise.resolve({\n items: [],\n total: 0,\n has_next: false,\n active_total: 0,\n archived_total: 0,\n }),\n ),\n deleteAssistantConversation: vi.fn(),\n confirmAssistantOperation: vi.fn(),\n cancelAssistantOperation: vi.fn(),\n sendAssistantMessage: vi.fn(),\n}));\n\nvi.mock('$lib/api', () => ({\n api: {\n getLlmStatus: vi.fn(() => Promise.resolve({ configured: true, reason: 'ok' })),\n fetchApi: vi.fn((endpoint) => {\n if (endpoint === '/dataset-orchestration/sessions/session-1/clarification') {\n return Promise.resolve(null);\n }\n if (endpoint === '/dataset-orchestration/sessions/session-1') {\n return Promise.resolve({ session_id: 'session-1', session_version: 2, version: 2 });\n }\n return Promise.resolve(null);\n }),\n },\n requestApi: vi.fn(),\n}));\n\nvi.mock('$lib/stores/assistantChat', () => ({\n assistantChatStore: {\n subscribe: assistantState.subscribe,\n },\n closeAssistantChat: vi.fn(),\n setAssistantConversationId: vi.fn((conversationId) => {\n assistantState.update((state) => ({ ...state, conversationId }));\n }),\n setAssistantSeedMessage: vi.fn((seedMessage) => {\n assistantState.update((state) => ({ ...state, seedMessage }));\n }),\n setAssistantFocusTarget: vi.fn((focusTarget) => {\n assistantState.update((state) => ({ ...state, focusTarget }));\n }),\n}));\n\nvi.mock('$lib/stores/taskDrawer.js', () => ({\n openDrawerForTask: vi.fn(),\n}));\n\nvi.mock('$app/navigation', () => ({\n goto: vi.fn(),\n}));\n\nvi.mock('../../../../services/gitService.js', () => ({\n gitService: {\n getDiff: vi.fn(),\n },\n}));\n\nvi.mock('$lib/i18n', () => ({\n t: {\n subscribe: (fn) => {\n fn({\n assistant: {\n title: 'Assistant',\n close: 'Close',\n conversations: 'Conversations',\n new: 'New',\n active: 'Active',\n archived: 'Archived',\n more: 'More',\n loading_history: 'Loading history',\n loading_older: 'Loading older',\n try_commands: 'Try commands',\n sample_command_branch: 'branch',\n sample_command_migration: 'migration',\n sample_command_status: 'status',\n you: 'You',\n assistant: 'Assistant',\n thinking: 'Thinking',\n input_placeholder: 'Type command',\n send: 'Send',\n conversation: 'Conversation',\n session_scope_title: 'Active review context',\n session_scope_label: 'Session',\n focus_target_label: 'Focus',\n clear_focus_action: 'Clear focus',\n states: {\n success: 'Success',\n failed: 'Failed',\n started: 'Started',\n },\n open_task_drawer: 'Open task drawer',\n },\n dashboard: {},\n });\n return () => {};\n },\n },\n}));\n\n// #region assistant_first_message_tests:Function [TYPE Function]\n// @RELATION: BINDS_TO -> [AssistantFirstMessageIntegrationTest]\n// @PURPOSE: Guard optimistic first-message UX against history reload race in new conversation flow.\n// @PRE: Assistant panel renders with open state and mocked network dependencies.\n// @POST: First user message remains visible before pending send request resolves.\ndescribe('AssistantChatPanel first message flow', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n assistantState.set({\n isOpen: true,\n conversationId: null,\n datasetReviewSessionId: 'session-1',\n seedMessage: '',\n focusTarget: null,\n });\n });\n\n it('keeps the first optimistic user message visible during pending send in a new conversation', async () => {\n let resolveSend;\n assistantApi.sendAssistantMessage.mockImplementation(\n (payload) =>\n new Promise((resolve) => {\n resolveSend = () =>\n resolve({\n response_id: 'resp-1',\n conversation_id: payload.conversation_id,\n text: 'Started',\n state: 'success',\n created_at: new Date().toISOString(),\n actions: [],\n });\n }),\n );\n\n render(AssistantChatPanel);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n await fireEvent.click(screen.getByText('New'));\n\n const input = screen.getByPlaceholderText('Type command');\n await fireEvent.input(input, { target: { value: 'first prompt' } });\n await fireEvent.click(screen.getByText('Send'));\n\n expect(screen.getByText('first prompt')).toBeTruthy();\n expect(assistantApi.sendAssistantMessage).toHaveBeenCalledTimes(1);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n resolveSend();\n\n await waitFor(() => {\n expect(screen.getByText('Started')).toBeTruthy();\n });\n });\n\n it('passes dataset review session id with first assistant message', async () => {\n assistantApi.sendAssistantMessage.mockResolvedValue({\n response_id: 'resp-2',\n conversation_id: 'conv-2',\n text: 'Started',\n state: 'success',\n created_at: new Date().toISOString(),\n actions: [],\n });\n\n render(AssistantChatPanel);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n const input = screen.getByPlaceholderText('Type command');\n await fireEvent.input(input, { target: { value: 'review this mapping' } });\n await fireEvent.click(screen.getByText('Send'));\n\n await waitFor(() => {\n expect(assistantApi.sendAssistantMessage).toHaveBeenCalledWith(\n expect.objectContaining({\n dataset_review_session_id: 'session-1',\n message: 'review this mapping',\n }),\n );\n });\n });\n});\n// #endregion assistant_first_message_tests:Function\n// #endregion AssistantFirstMessageIntegrationTest:Module\n" + }, + { + "contract_id": "assistant_first_message_tests:Function", + "contract_type": "Function", + "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_first_message.integration.test.js", + "start_line": 148, + "end_line": 238, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "POST": "First user message remains visible before pending send request resolves.", + "PRE": "Assistant panel renders with open state and mocked network dependencies.", + "PURPOSE": "Guard optimistic first-message UX against history reload race in new conversation flow." + }, + "relations": [ + { + "source_id": "assistant_first_message_tests:Function", + "relation_type": "BINDS_TO", + "target_id": "AssistantFirstMessageIntegrationTest", + "target_ref": "[AssistantFirstMessageIntegrationTest]" + } + ], "schema_warnings": [ { "code": "tag_forbidden_by_complexity", @@ -120000,140 +86996,30 @@ "actual_complexity": 1, "contract_type": "Function" } - } - ], - "anchor_syntax": "def", - "body": "// [DEF:assistant_chat_contract_tests:Function]\n// @PURPOSE: Validate assistant chat component contract and locale integration without DOM runtime dependency.\n// @PRE: Component and locale files exist in expected paths.\n// @POST: Contract checks guarantee assistant UI anchors and i18n wiring remain intact.\ndescribe('AssistantChatPanel integration contract', () => {\n it('contains semantic anchors and UX contract tags', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('');\n expect(source).toContain('@COMPLEXITY: 5');\n expect(source).toContain('@UX_STATE: LoadingHistory');\n expect(source).toContain('@UX_STATE: Sending');\n expect(source).toContain('@UX_STATE: Error');\n expect(source).toContain('@UX_FEEDBACK: Started operation surfaces task_id');\n expect(source).toContain('@UX_RECOVERY: User can retry command');\n expect(source).toContain('');\n });\n\n it('keeps confirmation/task-tracking action hooks in place', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('if (action.type === \"confirm\" && message.confirmation_id)');\n expect(source).toContain('if (action.type === \"cancel\" && message.confirmation_id)');\n expect(source).toContain('if (action.type === \"open_task\" && action.target)');\n expect(source).toContain('openDrawerForTask(action.target)');\n expect(source).toContain('goto(\"/reports\")');\n });\n\n it('guards first-message optimistic flow from premature history overwrite', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('if (panelVisible && initialized && conversationId && !loading)');\n expect(source).toContain('function appendLocalUserMessage(text, targetConversationId = conversationId)');\n expect(source).toContain('conversation_id: targetConversationId || null');\n expect(source).toContain('historyLoadVersion += 1;');\n });\n\n it('uses i18n bindings for assistant UI labels', () => {\n const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');\n\n expect(source).toContain('$t.assistant?.title');\n expect(source).toContain('$t.assistant?.input_placeholder');\n expect(source).toContain('$t.assistant?.send');\n expect(source).toContain('$t.assistant?.states?.[message.state]');\n expect(source).toContain('$t.assistant?.open_task_drawer');\n });\n\n it('provides assistant locale keys in both en and ru dictionaries', () => {\n const en = readJson(EN_LOCALE_PATH);\n const ru = readJson(RU_LOCALE_PATH);\n\n expect(en.title).toBeTruthy();\n expect(en.send).toBeTruthy();\n expect(en.states.needs_confirmation).toBeTruthy();\n\n expect(ru.title).toBeTruthy();\n expect(ru.send).toBeTruthy();\n expect(ru.states.needs_confirmation).toBeTruthy();\n });\n});\n// [/DEF:assistant_chat_contract_tests:Function]\n" - }, - { - "contract_id": "AssistantClarificationIntegrationTest", - "contract_type": "Module", - "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_clarification.integration.test.js", - "start_line": 1, - "end_line": 208, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "LAYER": "UI Tests", - "PURPOSE": "Verify AssistantChatPanel hosts the resumable dataset-review clarification flow inside the assistant drawer and refreshes workspace session state after answering.", - "SEMANTICS": [ - "assistant", - "clarification", - "integration-test", - "dataset-review", - "mixed-initiative" - ] - }, - "relations": [ - { - "source_id": "AssistantClarificationIntegrationTest", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantChatPanel", - "target_ref": "[AssistantChatPanel]" - } - ], - "schema_warnings": [], - "anchor_syntax": "def", - "body": "// [DEF:AssistantClarificationIntegrationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: assistant, clarification, integration-test, dataset-review, mixed-initiative\n// @PURPOSE: Verify AssistantChatPanel hosts the resumable dataset-review clarification flow inside the assistant drawer and refreshes workspace session state after answering.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [AssistantChatPanel]\n\nimport { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@testing-library/svelte\";\nimport AssistantChatPanel from \"../AssistantChatPanel.svelte\";\nimport * as assistantApi from \"$lib/api/assistant\";\nimport { api, requestApi } from \"$lib/api.js\";\nimport { setSession as setDatasetReviewSession } from \"$lib/stores/datasetReviewSession.js\";\n\nvi.mock(\"$lib/api/assistant\", () => ({\n getAssistantHistory: vi.fn(() => Promise.resolve({ items: [], total: 0, has_next: false })),\n getAssistantConversations: vi.fn(() => Promise.resolve({ items: [], total: 0, has_next: false, active_total: 0, archived_total: 0 })),\n deleteAssistantConversation: vi.fn(),\n confirmAssistantOperation: vi.fn(),\n cancelAssistantOperation: vi.fn(),\n sendAssistantMessage: vi.fn(),\n}));\n\nvi.mock(\"$lib/api.js\", () => ({\n api: {\n getLlmStatus: vi.fn(() => Promise.resolve({ configured: true, reason: \"ok\" })),\n fetchApi: vi.fn(),\n },\n requestApi: vi.fn(),\n}));\n\nvi.mock(\"$lib/stores/datasetReviewSession.js\", () => ({\n setSession: vi.fn(),\n}));\n\nvi.mock(\"$lib/stores/taskDrawer.js\", () => ({\n openDrawerForTask: vi.fn(),\n}));\n\nconst { assistantState } = vi.hoisted(() => ({\n assistantState: (() => {\n let value = {\n isOpen: true,\n conversationId: \"conv-1\",\n datasetReviewSessionId: \"session-1\",\n seedMessage: \"\",\n focusTarget: null,\n };\n const subscribers = new Set();\n return {\n subscribe(fn) {\n subscribers.add(fn);\n fn(value);\n return () => subscribers.delete(fn);\n },\n update(updater) {\n value = updater(value);\n subscribers.forEach((fn) => fn(value));\n },\n };\n })(),\n}));\n\nvi.mock(\"$lib/stores/assistantChat\", () => ({\n assistantChatStore: { subscribe: assistantState.subscribe },\n closeAssistantChat: vi.fn(),\n setAssistantConversationId: vi.fn(),\n setAssistantSeedMessage: vi.fn(),\n setAssistantFocusTarget: vi.fn(),\n}));\n\nvi.mock(\"$app/navigation\", () => ({ goto: vi.fn() }));\nvi.mock(\"../../../../services/gitService.js\", () => ({ gitService: { getDiff: vi.fn() } }));\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: { error: \"Common error\" },\n assistant: {\n title: \"Assistant\",\n close: \"Close\",\n conversations: \"Conversations\",\n new: \"New\",\n active: \"Active\",\n archived: \"Archived\",\n more: \"More\",\n loading_history: \"Loading history\",\n loading_older: \"Loading older\",\n try_commands: \"Try commands\",\n sample_command_branch: \"branch\",\n sample_command_migration: \"migration\",\n sample_command_status: \"status\",\n you: \"You\",\n assistant: \"Assistant\",\n thinking: \"Thinking\",\n input_placeholder: \"Type command\",\n send: \"Send\",\n conversation: \"Conversation\",\n session_scope_title: \"Active review context\",\n session_scope_label: \"Session\",\n focus_target_label: \"Focus\",\n clear_focus_action: \"Clear focus\",\n states: { success: \"Success\" },\n open_task_drawer: \"Open task drawer\",\n },\n dataset_review: {\n clarification: {\n title: \"Clarification queue\",\n progress_label: \"Progress\",\n active_question_label: \"Active question\",\n why_it_matters_label: \"Why it matters\",\n current_guess_label: \"Current guess\",\n topic_label: \"Topic\",\n custom_answer_label: \"Custom answer\",\n answer_action: \"Answer\",\n custom_answer_action: \"Use custom answer\",\n skip_action: \"Skip\",\n expert_review_action: \"Expert review\",\n completed: \"No active clarification question remains.\",\n resume_action: \"Resume clarification\",\n feedback_label: \"Feedback\",\n feedback_prompt: \"Record whether the clarification result was useful.\",\n feedback_up_action: \"Thumbs up\",\n feedback_down_action: \"Thumbs down\",\n messages: {\n saving: \"Saving clarification answer...\",\n saved: \"Clarification answer saved.\",\n resumed: \"Clarification queue resumed.\",\n },\n },\n },\n dashboard: {},\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"AssistantChatPanel clarification integration\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n api.fetchApi.mockImplementation((endpoint) => {\n if (endpoint === \"/dataset-orchestration/sessions/session-1/clarification\") {\n return Promise.resolve({\n clarification_session: {\n clarification_session_id: \"clarification-1\",\n session_id: \"session-1\",\n status: \"active\",\n current_question_id: \"question-1\",\n resolved_count: 1,\n remaining_count: 2,\n },\n current_question: {\n question_id: \"question-1\",\n topic_ref: \"profile.summary\",\n question_text: \"Which customer label should be used?\",\n why_it_matters: \"This label is shown to reviewers.\",\n current_guess: \"Customer name\",\n options: [{ value: \"Customer name\", label: \"Customer name\", is_recommended: true }],\n },\n });\n }\n if (endpoint === \"/dataset-orchestration/sessions/session-1\") {\n return Promise.resolve({ session_id: \"session-1\", session_version: 4, version: 4, profile: { business_summary: \"Updated summary\" } });\n }\n return Promise.resolve(null);\n });\n requestApi.mockResolvedValue({\n clarification_state: {\n clarification_session: {\n clarification_session_id: \"clarification-1\",\n session_id: \"session-1\",\n status: \"active\",\n current_question_id: null,\n resolved_count: 2,\n remaining_count: 1,\n },\n current_question: null,\n },\n session: { session_id: \"session-1\", session_version: 4, version: 4 },\n });\n });\n\n it(\"renders clarification queue inside assistant and refreshes session after answering\", async () => {\n render(AssistantChatPanel);\n\n expect(await screen.findByText(\"Clarification queue\")).toBeTruthy();\n expect(screen.getByText(\"Which customer label should be used?\")).toBeTruthy();\n expect(screen.getByText(\"This label is shown to reviewers.\")).toBeTruthy();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Answer\" }));\n\n await waitFor(() => {\n expect(requestApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/clarification/answers\",\n \"POST\",\n expect.objectContaining({ question_id: \"question-1\", answer_kind: \"selected\", answer_value: \"Customer name\" }),\n );\n });\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledWith(\"/dataset-orchestration/sessions/session-1\");\n expect(setDatasetReviewSession).toHaveBeenCalled();\n });\n });\n});\n// [/DEF:AssistantClarificationIntegrationTest:Module]\n" - }, - { - "contract_id": "AssistantFirstMessageIntegrationTest", - "contract_type": "Module", - "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_first_message.integration.test.js", - "start_line": 1, - "end_line": 241, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Starting a new conversation must not trigger history reload that overwrites the first local user message.", - "LAYER": "UI Tests", - "PURPOSE": "Verify first optimistic user message stays visible while a new conversation request is pending.", - "SEMANTICS": [ - "assistant", - "integration-test", - "optimistic-message", - "conversation-race" - ] - }, - "relations": [ - { - "source_id": "AssistantFirstMessageIntegrationTest", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantChatPanel", - "target_ref": "[AssistantChatPanel]" - } - ], - "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" - } - } - ], - "anchor_syntax": "def", - "body": "// [DEF:AssistantFirstMessageIntegrationTest:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: assistant, integration-test, optimistic-message, conversation-race\n// @PURPOSE: Verify first optimistic user message stays visible while a new conversation request is pending.\n// @LAYER: UI Tests\n// @RELATION: DEPENDS_ON -> [AssistantChatPanel]\n// @INVARIANT: Starting a new conversation must not trigger history reload that overwrites the first local user message.\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { render, screen, fireEvent, waitFor } from '@testing-library/svelte';\nimport AssistantChatPanel from '../AssistantChatPanel.svelte';\nimport * as assistantApi from '$lib/api/assistant';\n\nconst { assistantState } = vi.hoisted(() => ({\n assistantState: (() => {\n let value = {\n isOpen: true,\n conversationId: null,\n datasetReviewSessionId: 'session-1',\n seedMessage: '',\n focusTarget: null,\n };\n const subscribers = new Set();\n return {\n subscribe(fn) {\n subscribers.add(fn);\n fn(value);\n return () => subscribers.delete(fn);\n },\n set(next) {\n value = next;\n subscribers.forEach((fn) => fn(value));\n },\n update(updater) {\n value = updater(value);\n subscribers.forEach((fn) => fn(value));\n },\n };\n })(),\n}));\n\nvi.mock('$lib/api/assistant', () => ({\n getAssistantHistory: vi.fn(() =>\n Promise.resolve({ items: [], total: 0, has_next: false }),\n ),\n getAssistantConversations: vi.fn(() =>\n Promise.resolve({\n items: [],\n total: 0,\n has_next: false,\n active_total: 0,\n archived_total: 0,\n }),\n ),\n deleteAssistantConversation: vi.fn(),\n confirmAssistantOperation: vi.fn(),\n cancelAssistantOperation: vi.fn(),\n sendAssistantMessage: vi.fn(),\n}));\n\nvi.mock('$lib/api', () => ({\n api: {\n getLlmStatus: vi.fn(() => Promise.resolve({ configured: true, reason: 'ok' })),\n fetchApi: vi.fn((endpoint) => {\n if (endpoint === '/dataset-orchestration/sessions/session-1/clarification') {\n return Promise.resolve(null);\n }\n if (endpoint === '/dataset-orchestration/sessions/session-1') {\n return Promise.resolve({ session_id: 'session-1', session_version: 2, version: 2 });\n }\n return Promise.resolve(null);\n }),\n },\n requestApi: vi.fn(),\n}));\n\nvi.mock('$lib/stores/assistantChat', () => ({\n assistantChatStore: {\n subscribe: assistantState.subscribe,\n },\n closeAssistantChat: vi.fn(),\n setAssistantConversationId: vi.fn((conversationId) => {\n assistantState.update((state) => ({ ...state, conversationId }));\n }),\n setAssistantSeedMessage: vi.fn((seedMessage) => {\n assistantState.update((state) => ({ ...state, seedMessage }));\n }),\n setAssistantFocusTarget: vi.fn((focusTarget) => {\n assistantState.update((state) => ({ ...state, focusTarget }));\n }),\n}));\n\nvi.mock('$lib/stores/taskDrawer.js', () => ({\n openDrawerForTask: vi.fn(),\n}));\n\nvi.mock('$app/navigation', () => ({\n goto: vi.fn(),\n}));\n\nvi.mock('../../../../services/gitService.js', () => ({\n gitService: {\n getDiff: vi.fn(),\n },\n}));\n\nvi.mock('$lib/i18n', () => ({\n t: {\n subscribe: (fn) => {\n fn({\n assistant: {\n title: 'Assistant',\n close: 'Close',\n conversations: 'Conversations',\n new: 'New',\n active: 'Active',\n archived: 'Archived',\n more: 'More',\n loading_history: 'Loading history',\n loading_older: 'Loading older',\n try_commands: 'Try commands',\n sample_command_branch: 'branch',\n sample_command_migration: 'migration',\n sample_command_status: 'status',\n you: 'You',\n assistant: 'Assistant',\n thinking: 'Thinking',\n input_placeholder: 'Type command',\n send: 'Send',\n conversation: 'Conversation',\n session_scope_title: 'Active review context',\n session_scope_label: 'Session',\n focus_target_label: 'Focus',\n clear_focus_action: 'Clear focus',\n states: {\n success: 'Success',\n failed: 'Failed',\n started: 'Started',\n },\n open_task_drawer: 'Open task drawer',\n },\n dashboard: {},\n });\n return () => {};\n },\n },\n}));\n\n// [DEF:assistant_first_message_tests:Function]\n// @RELATION: BINDS_TO -> [AssistantFirstMessageIntegrationTest]\n// @COMPLEXITY: 3\n// @PURPOSE: Guard optimistic first-message UX against history reload race in new conversation flow.\n// @PRE: Assistant panel renders with open state and mocked network dependencies.\n// @POST: First user message remains visible before pending send request resolves.\ndescribe('AssistantChatPanel first message flow', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n assistantState.set({\n isOpen: true,\n conversationId: null,\n datasetReviewSessionId: 'session-1',\n seedMessage: '',\n focusTarget: null,\n });\n });\n\n it('keeps the first optimistic user message visible during pending send in a new conversation', async () => {\n let resolveSend;\n assistantApi.sendAssistantMessage.mockImplementation(\n (payload) =>\n new Promise((resolve) => {\n resolveSend = () =>\n resolve({\n response_id: 'resp-1',\n conversation_id: payload.conversation_id,\n text: 'Started',\n state: 'success',\n created_at: new Date().toISOString(),\n actions: [],\n });\n }),\n );\n\n render(AssistantChatPanel);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n await fireEvent.click(screen.getByText('New'));\n\n const input = screen.getByPlaceholderText('Type command');\n await fireEvent.input(input, { target: { value: 'first prompt' } });\n await fireEvent.click(screen.getByText('Send'));\n\n expect(screen.getByText('first prompt')).toBeTruthy();\n expect(assistantApi.sendAssistantMessage).toHaveBeenCalledTimes(1);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n resolveSend();\n\n await waitFor(() => {\n expect(screen.getByText('Started')).toBeTruthy();\n });\n });\n\n it('passes dataset review session id with first assistant message', async () => {\n assistantApi.sendAssistantMessage.mockResolvedValue({\n response_id: 'resp-2',\n conversation_id: 'conv-2',\n text: 'Started',\n state: 'success',\n created_at: new Date().toISOString(),\n actions: [],\n });\n\n render(AssistantChatPanel);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n const input = screen.getByPlaceholderText('Type command');\n await fireEvent.input(input, { target: { value: 'review this mapping' } });\n await fireEvent.click(screen.getByText('Send'));\n\n await waitFor(() => {\n expect(assistantApi.sendAssistantMessage).toHaveBeenCalledWith(\n expect.objectContaining({\n dataset_review_session_id: 'session-1',\n message: 'review this mapping',\n }),\n );\n });\n });\n});\n// [/DEF:assistant_first_message_tests:Function]\n// [/DEF:AssistantFirstMessageIntegrationTest:Module]\n" - }, - { - "contract_id": "assistant_first_message_tests", - "contract_type": "Function", - "file_path": "frontend/src/lib/components/assistant/__tests__/assistant_first_message.integration.test.js", - "start_line": 149, - "end_line": 240, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "POST": "First user message remains visible before pending send request resolves.", - "PRE": "Assistant panel renders with open state and mocked network dependencies.", - "PURPOSE": "Guard optimistic first-message UX against history reload race in new conversation flow." - }, - "relations": [ - { - "source_id": "assistant_first_message_tests", - "relation_type": "BINDS_TO", - "target_id": "AssistantFirstMessageIntegrationTest", - "target_ref": "[AssistantFirstMessageIntegrationTest]" - } - ], - "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", + "tag": "RELATION", + "message": "@RELATION is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, + "actual_complexity": 1, "contract_type": "Function" } } ], - "anchor_syntax": "def", - "body": "// [DEF:assistant_first_message_tests:Function]\n// @RELATION: BINDS_TO -> [AssistantFirstMessageIntegrationTest]\n// @COMPLEXITY: 3\n// @PURPOSE: Guard optimistic first-message UX against history reload race in new conversation flow.\n// @PRE: Assistant panel renders with open state and mocked network dependencies.\n// @POST: First user message remains visible before pending send request resolves.\ndescribe('AssistantChatPanel first message flow', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n assistantState.set({\n isOpen: true,\n conversationId: null,\n datasetReviewSessionId: 'session-1',\n seedMessage: '',\n focusTarget: null,\n });\n });\n\n it('keeps the first optimistic user message visible during pending send in a new conversation', async () => {\n let resolveSend;\n assistantApi.sendAssistantMessage.mockImplementation(\n (payload) =>\n new Promise((resolve) => {\n resolveSend = () =>\n resolve({\n response_id: 'resp-1',\n conversation_id: payload.conversation_id,\n text: 'Started',\n state: 'success',\n created_at: new Date().toISOString(),\n actions: [],\n });\n }),\n );\n\n render(AssistantChatPanel);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n await fireEvent.click(screen.getByText('New'));\n\n const input = screen.getByPlaceholderText('Type command');\n await fireEvent.input(input, { target: { value: 'first prompt' } });\n await fireEvent.click(screen.getByText('Send'));\n\n expect(screen.getByText('first prompt')).toBeTruthy();\n expect(assistantApi.sendAssistantMessage).toHaveBeenCalledTimes(1);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n resolveSend();\n\n await waitFor(() => {\n expect(screen.getByText('Started')).toBeTruthy();\n });\n });\n\n it('passes dataset review session id with first assistant message', async () => {\n assistantApi.sendAssistantMessage.mockResolvedValue({\n response_id: 'resp-2',\n conversation_id: 'conv-2',\n text: 'Started',\n state: 'success',\n created_at: new Date().toISOString(),\n actions: [],\n });\n\n render(AssistantChatPanel);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n const input = screen.getByPlaceholderText('Type command');\n await fireEvent.input(input, { target: { value: 'review this mapping' } });\n await fireEvent.click(screen.getByText('Send'));\n\n await waitFor(() => {\n expect(assistantApi.sendAssistantMessage).toHaveBeenCalledWith(\n expect.objectContaining({\n dataset_review_session_id: 'session-1',\n message: 'review this mapping',\n }),\n );\n });\n });\n});\n// [/DEF:assistant_first_message_tests:Function]\n" + "anchor_syntax": "region", + "body": "// #region assistant_first_message_tests:Function [TYPE Function]\n// @RELATION: BINDS_TO -> [AssistantFirstMessageIntegrationTest]\n// @PURPOSE: Guard optimistic first-message UX against history reload race in new conversation flow.\n// @PRE: Assistant panel renders with open state and mocked network dependencies.\n// @POST: First user message remains visible before pending send request resolves.\ndescribe('AssistantChatPanel first message flow', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n assistantState.set({\n isOpen: true,\n conversationId: null,\n datasetReviewSessionId: 'session-1',\n seedMessage: '',\n focusTarget: null,\n });\n });\n\n it('keeps the first optimistic user message visible during pending send in a new conversation', async () => {\n let resolveSend;\n assistantApi.sendAssistantMessage.mockImplementation(\n (payload) =>\n new Promise((resolve) => {\n resolveSend = () =>\n resolve({\n response_id: 'resp-1',\n conversation_id: payload.conversation_id,\n text: 'Started',\n state: 'success',\n created_at: new Date().toISOString(),\n actions: [],\n });\n }),\n );\n\n render(AssistantChatPanel);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n await fireEvent.click(screen.getByText('New'));\n\n const input = screen.getByPlaceholderText('Type command');\n await fireEvent.input(input, { target: { value: 'first prompt' } });\n await fireEvent.click(screen.getByText('Send'));\n\n expect(screen.getByText('first prompt')).toBeTruthy();\n expect(assistantApi.sendAssistantMessage).toHaveBeenCalledTimes(1);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n resolveSend();\n\n await waitFor(() => {\n expect(screen.getByText('Started')).toBeTruthy();\n });\n });\n\n it('passes dataset review session id with first assistant message', async () => {\n assistantApi.sendAssistantMessage.mockResolvedValue({\n response_id: 'resp-2',\n conversation_id: 'conv-2',\n text: 'Started',\n state: 'success',\n created_at: new Date().toISOString(),\n actions: [],\n });\n\n render(AssistantChatPanel);\n\n await waitFor(() => {\n expect(assistantApi.getAssistantHistory).toHaveBeenCalledTimes(1);\n });\n\n const input = screen.getByPlaceholderText('Type command');\n await fireEvent.input(input, { target: { value: 'review this mapping' } });\n await fireEvent.click(screen.getByText('Send'));\n\n await waitFor(() => {\n expect(assistantApi.sendAssistantMessage).toHaveBeenCalledWith(\n expect.objectContaining({\n dataset_review_session_id: 'session-1',\n message: 'review this mapping',\n }),\n );\n });\n });\n});\n// #endregion assistant_first_message_tests:Function\n" }, { "contract_id": "CompiledSQLPreview", "contract_type": "Component", "file_path": "frontend/src/lib/components/dataset-review/CompiledSQLPreview.svelte", "start_line": 1, - "end_line": 328, + "end_line": 329, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input -> Dataset review preview payload { sessionId, preview, previewState }; Output -> onupdated({ preview, preview_state }) route-shell refresh payload and onjump({ target }) recovery navigation signal.", "LAYER": "UI", "POST": "Users can distinguish missing, pending, ready, stale, and error preview states and can trigger only Superset-backed preview generation.", @@ -120170,9 +87056,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C4", + "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -120192,9 +87078,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C4", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "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" } }, @@ -120215,9 +87119,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C4", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, "contract_type": "Component" } }, @@ -120258,19 +87171,19 @@ } } ], - "anchor_syntax": "def", - "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 {$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" + "anchor_syntax": "region", + "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", "contract_type": "Component", "file_path": "frontend/src/lib/components/dataset-review/ExecutionMappingReview.svelte", "start_line": 1, - "end_line": 647, + "end_line": 648, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input -> Dataset review execution payload { sessionId, mappings[], importedFilters[], templateVariables[] }; Output -> onupdated({ mapping | mappings, preview_state }) route-shell refresh payload.", "LAYER": "UI", "POST": "Users can review effective mapping values, approve warning-sensitive transformations, or manually override them while unresolved required-value blockers remain visible.", @@ -120307,9 +87220,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C4", + "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -120329,9 +87242,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C4", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "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" } }, @@ -120352,9 +87283,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C4", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, "contract_type": "Component" } }, @@ -120395,19 +87335,19 @@ } } ], - "anchor_syntax": "def", - "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?.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" + "anchor_syntax": "region", + "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", "contract_type": "Component", "file_path": "frontend/src/lib/components/dataset-review/LaunchConfirmationPanel.svelte", "start_line": 1, - "end_line": 391, + "end_line": 392, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input -> Dataset review launch payload { sessionId, session, findings[], mappings[], preview, previewState, latestRunContext }; Output -> onupdated({ launch_result, preview_state }) workspace refresh payload and onjump({ target }) remediation navigation signal.", "LAYER": "UI", "POST": "Users can see why launch is blocked or confirm a run-ready launch with explicit SQL Lab handoff evidence.", @@ -120444,9 +87384,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C4", + "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -120466,9 +87406,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C4", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "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" } }, @@ -120489,9 +87447,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C4", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, "contract_type": "Component" } }, @@ -120532,19 +87499,19 @@ } } ], - "anchor_syntax": "def", - "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?.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" + "anchor_syntax": "region", + "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", "contract_type": "Component", "file_path": "frontend/src/lib/components/dataset-review/SemanticLayerReview.svelte", "start_line": 1, - "end_line": 676, + "end_line": 677, "tier": "TIER_2", - "complexity": 4, + "complexity": 3, "metadata": { - "COMPLEXITY": "4", + "COMPLEXITY": 3, "DATA_CONTRACT": "Input -> Dataset review session detail { sessionId, fields[], semanticSources[] }; Output -> onupdated(updatedField | { fields: updatedFields }) refresh payload.", "LAYER": "UI", "POST": "Users can review the current semantic value, accept a candidate, apply manual override, and lock or unlock field state without violating backend provenance rules.", @@ -120581,9 +87548,9 @@ { "code": "tag_forbidden_by_complexity", "tag": "DATA_CONTRACT", - "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C4", + "message": "@DATA_CONTRACT is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, "contract_type": "Component" } }, @@ -120603,9 +87570,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C4", + "message": "@LAYER is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "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" } }, @@ -120626,9 +87611,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "SEMANTICS", - "message": "@SEMANTICS is forbidden for contract type 'Component' at C4", + "message": "@SEMANTICS is forbidden for contract type 'Component' at C3", "detail": { - "actual_complexity": 4, + "actual_complexity": 3, + "contract_type": "Component" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "SIDE_EFFECT", + "message": "@SIDE_EFFECT is forbidden for contract type 'Component' at C3", + "detail": { + "actual_complexity": 3, "contract_type": "Component" } }, @@ -120669,19 +87663,19 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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", "contract_type": "Component", "file_path": "frontend/src/lib/components/dataset-review/SourceIntakePanel.svelte", "start_line": 1, - "end_line": 348, + "end_line": 349, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Collect initial dataset source input through Superset link paste or dataset selection entry paths.", "SEMANTICS": [ @@ -120769,19 +87763,19 @@ } } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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", "contract_type": "Component", "file_path": "frontend/src/lib/components/dataset-review/ValidationFindingsPanel.svelte", "start_line": 1, - "end_line": 461, + "end_line": 462, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Present validation findings grouped by severity with explicit resolution and actionability signals.", "SEMANTICS": [ @@ -120869,19 +87863,18 @@ } } ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n
\n {$t.dataset_review?.findings?.next_action_label}\n
\n
\n {getRecommendedActionLabel(recommendedAction)}\n
\n
\n
\n\n {#if unresolvedBlockingCount > 0}\n
\n Обнаружены незакрытые launch-блокеры: {unresolvedBlockingCount}. Продолжать review можно, но запуск останется недоступен.\n
\n {/if}\n\n {#if totalFindings === 0}\n
\n {$t.dataset_review?.findings?.empty}\n
\n {:else}\n
\n
\n
\n

\n {$t.dataset_review?.findings?.blocking_title}\n

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

\n {$t.dataset_review?.findings?.blocking_empty}\n

\n {:else}\n
\n {#each blockingFindings as finding}\n \n
\n
\n
\n

\n {finding.title}\n

\n \n {finding.code}\n \n \n {getAreaLabel(finding.area)}\n \n
\n\n

\n {finding.message}\n

\n\n
\n \n {$t.dataset_review?.findings?.resolution_label}:\n {getResolutionLabel(finding.resolution_state)}\n \n\n {#if finding.caused_by_ref}\n \n {$t.dataset_review?.findings?.reference_label}:\n {finding.caused_by_ref}\n \n {/if}\n
\n\n {#if finding.resolution_note}\n

{finding.resolution_note}

\n {/if}\n
\n\n
\n askAiAboutFinding(finding)}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveFindingGuidance(finding)}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n jumpToFindingTarget(finding)}\n >\n {$t.dataset_review?.findings?.jump_action}\n \n
\n
\n \n {/each}\n
\n {/if}\n
\n\n
\n
\n

\n {$t.dataset_review?.findings?.warning_title}\n

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

\n {$t.dataset_review?.findings?.warning_empty}\n

\n {:else}\n
\n {#each warningFindings as finding}\n \n
\n
\n
\n

\n {finding.title}\n

\n \n {finding.code}\n \n \n {getAreaLabel(finding.area)}\n \n
\n\n

\n {finding.message}\n

\n\n
\n \n {$t.dataset_review?.findings?.resolution_label}:\n {getResolutionLabel(finding.resolution_state)}\n \n\n {#if finding.caused_by_ref}\n \n {$t.dataset_review?.findings?.reference_label}:\n {finding.caused_by_ref}\n \n {/if}\n
\n\n {#if finding.resolution_note}\n

{finding.resolution_note}

\n {/if}\n
\n\n
\n askAiAboutFinding(finding)}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveFindingGuidance(finding)}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n jumpToFindingTarget(finding)}\n >\n {$t.dataset_review?.findings?.jump_action}\n \n
\n
\n \n {/each}\n
\n {/if}\n
\n\n
\n
\n

\n {$t.dataset_review?.findings?.informational_title}\n

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

\n {$t.dataset_review?.findings?.informational_empty}\n

\n {:else}\n
\n {#each informationalFindings as finding}\n \n
\n
\n
\n

\n {finding.title}\n

\n \n {finding.code}\n \n \n {getAreaLabel(finding.area)}\n \n
\n\n

\n {finding.message}\n

\n\n
\n \n {$t.dataset_review?.findings?.resolution_label}:\n {getResolutionLabel(finding.resolution_state)}\n \n\n {#if finding.caused_by_ref}\n \n {$t.dataset_review?.findings?.reference_label}:\n {finding.caused_by_ref}\n \n {/if}\n
\n\n {#if finding.resolution_note}\n

{finding.resolution_note}

\n {/if}\n
\n\n
\n askAiAboutFinding(finding)}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveFindingGuidance(finding)}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n jumpToFindingTarget(finding)}\n >\n {$t.dataset_review?.findings?.jump_action}\n \n
\n
\n \n {/each}\n
\n {/if}\n
\n
\n {/if}\n
\n\n\n" + "anchor_syntax": "region", + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

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

\n

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

\n

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

\n
\n\n
\n
\n {$t.dataset_review?.findings?.next_action_label}\n
\n
\n {getRecommendedActionLabel(recommendedAction)}\n
\n
\n
\n\n {#if unresolvedBlockingCount > 0}\n
\n Обнаружены незакрытые launch-блокеры: {unresolvedBlockingCount}. Продолжать review можно, но запуск останется недоступен.\n
\n {/if}\n\n {#if totalFindings === 0}\n
\n {$t.dataset_review?.findings?.empty}\n
\n {:else}\n
\n
\n
\n

\n {$t.dataset_review?.findings?.blocking_title}\n

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

\n {$t.dataset_review?.findings?.blocking_empty}\n

\n {:else}\n
\n {#each blockingFindings as finding}\n \n
\n
\n
\n

\n {finding.title}\n

\n \n {finding.code}\n \n \n {getAreaLabel(finding.area)}\n \n
\n\n

\n {finding.message}\n

\n\n
\n \n {$t.dataset_review?.findings?.resolution_label}:\n {getResolutionLabel(finding.resolution_state)}\n \n\n {#if finding.caused_by_ref}\n \n {$t.dataset_review?.findings?.reference_label}:\n {finding.caused_by_ref}\n \n {/if}\n
\n\n {#if finding.resolution_note}\n

{finding.resolution_note}

\n {/if}\n
\n\n
\n askAiAboutFinding(finding)}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveFindingGuidance(finding)}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n jumpToFindingTarget(finding)}\n >\n {$t.dataset_review?.findings?.jump_action}\n \n
\n
\n \n {/each}\n
\n {/if}\n
\n\n
\n
\n

\n {$t.dataset_review?.findings?.warning_title}\n

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

\n {$t.dataset_review?.findings?.warning_empty}\n

\n {:else}\n
\n {#each warningFindings as finding}\n \n
\n
\n
\n

\n {finding.title}\n

\n \n {finding.code}\n \n \n {getAreaLabel(finding.area)}\n \n
\n\n

\n {finding.message}\n

\n\n
\n \n {$t.dataset_review?.findings?.resolution_label}:\n {getResolutionLabel(finding.resolution_state)}\n \n\n {#if finding.caused_by_ref}\n \n {$t.dataset_review?.findings?.reference_label}:\n {finding.caused_by_ref}\n \n {/if}\n
\n\n {#if finding.resolution_note}\n

{finding.resolution_note}

\n {/if}\n
\n\n
\n askAiAboutFinding(finding)}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveFindingGuidance(finding)}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n jumpToFindingTarget(finding)}\n >\n {$t.dataset_review?.findings?.jump_action}\n \n
\n
\n \n {/each}\n
\n {/if}\n
\n\n
\n
\n

\n {$t.dataset_review?.findings?.informational_title}\n

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

\n {$t.dataset_review?.findings?.informational_empty}\n

\n {:else}\n
\n {#each informationalFindings as finding}\n \n
\n
\n
\n

\n {finding.title}\n

\n \n {finding.code}\n \n \n {getAreaLabel(finding.area)}\n \n
\n\n

\n {finding.message}\n

\n\n
\n \n {$t.dataset_review?.findings?.resolution_label}:\n {getResolutionLabel(finding.resolution_state)}\n \n\n {#if finding.caused_by_ref}\n \n {$t.dataset_review?.findings?.reference_label}:\n {finding.caused_by_ref}\n \n {/if}\n
\n\n {#if finding.resolution_note}\n

{finding.resolution_note}

\n {/if}\n
\n\n
\n askAiAboutFinding(finding)}\n >\n {$t.dataset_review?.assistant?.ask_ai_action || \"✨ Ask AI\"}\n \n improveFindingGuidance(finding)}\n >\n {$t.dataset_review?.assistant?.improve_action || \"✨ Improve\"}\n \n jumpToFindingTarget(finding)}\n >\n {$t.dataset_review?.findings?.jump_action}\n \n
\n
\n \n {/each}\n
\n {/if}\n
\n
\n {/if}\n
\n\n\n" }, { - "contract_id": "SourceIntakePanelUxTests", - "contract_type": "Module", + "contract_id": "SourceIntakePanelUxTests:Module", + "contract_type": "Function", "file_path": "frontend/src/lib/components/dataset-review/__tests__/source_intake_panel.ux.test.js", "start_line": 4, - "end_line": 161, - "tier": "TIER_2", - "complexity": 3, + "end_line": 160, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify source intake entry paths, validation feedback, and submit payload behavior for US1.", "SEMANTICS": [ @@ -120899,7 +87892,7 @@ }, "relations": [ { - "source_id": "SourceIntakePanelUxTests", + "source_id": "SourceIntakePanelUxTests:Module", "relation_type": "DEPENDS_ON", "target_id": "SourceIntakePanel", "target_ref": "[SourceIntakePanel]" @@ -120908,73 +87901,64 @@ "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ - "Function", - "Block" + "Module", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C3", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" } }, { "code": "tag_not_for_contract_type", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is not allowed for contract type 'Module'", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ - "Function", - "Block" + "Module", + "Block", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is forbidden for contract type 'Module' at C3", + "tag": "SEMANTICS", + "message": "@SEMANTICS is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" } }, { "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not allowed for contract type 'Module'", + "message": "@UX_STATE is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ "Component" ] @@ -120983,26 +87967,34 @@ { "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 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "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" } } ], - "anchor_syntax": "def", - "body": "// [DEF:SourceIntakePanelUxTests:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, source-intake, ux-tests, validation, recovery\n// @PURPOSE: Verify source intake entry paths, validation feedback, and submit payload behavior for US1.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [SourceIntakePanel]\n// @UX_STATE: Idle -> Default intake renders both entry paths and waits for user input.\n// @UX_STATE: Validating -> Inline feedback confirms recognized links and local payload validation.\n// @UX_STATE: Rejected -> Invalid source input remains local and exposes recovery guidance.\n// @TEST_CONTRACT: SourceIntakePanelProps -> ObservableIntakeUX\n// @TEST_SCENARIO: invalid_superset_link_shows_rejected_state -> Invalid URL input keeps submit local and shows rejection feedback.\n// @TEST_SCENARIO: recognized_superset_link_submits_payload -> Recognized link path submits normalized payload.\n// @TEST_SCENARIO: dataset_selection_mode_changes_cta -> Dataset selection path switches CTA and payload source kind.\n// @TEST_EDGE: missing_environment -> Required environment guard blocks submit.\n// @TEST_EDGE: invalid_type -> Invalid Superset URL shows recovery hint.\n// @TEST_EDGE: external_fail -> Submit callback failure is rendered inline.\n// @TEST_INVARIANT: intake_contract_remains_observable -> VERIFIED_BY: [invalid_superset_link_shows_rejected_state, recognized_superset_link_submits_payload, dataset_selection_mode_changes_cta]\n\nimport { describe, it, expect, vi } from \"vitest\";\nimport { fireEvent, render, screen } from \"@testing-library/svelte\";\nimport SourceIntakePanel from \"../SourceIntakePanel.svelte\";\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n choose_environment: \"Choose environment\",\n error: \"Common error\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n },\n },\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"SourceIntakePanel UX Contract\", () => {\n const environments = [{ id: \"env-1\", name: \"DEV\" }];\n\n it(\"invalid_superset_link_shows_rejected_state\", async () => {\n const onsubmit = vi.fn();\n const { container } = render(SourceIntakePanel, {\n environments,\n onsubmit,\n });\n\n await fireEvent.change(screen.getByRole(\"combobox\"), {\n target: { value: \"env-1\" },\n });\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"not-a-url\" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).not.toHaveBeenCalled();\n expect(screen.getByText(\"Rejected\")).toBeDefined();\n expect(screen.getByText(\"Superset link must start with http\")).toBeDefined();\n });\n\n it(\"recognized_superset_link_submits_payload\", async () => {\n const onsubmit = vi.fn().mockResolvedValue(undefined);\n const { container } = render(SourceIntakePanel, {\n environments,\n selectedEnvironmentId: \"env-1\",\n acknowledgment: \"Recognized Superset link\",\n onsubmit,\n });\n\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"https://demo.local/superset/dashboard/42 \" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).toHaveBeenCalledWith({\n environment_id: \"env-1\",\n source_input: \"https://demo.local/superset/dashboard/42\",\n source_kind: \"superset_link\",\n });\n expect(screen.getByText(\"Recognized Superset link\")).toBeDefined();\n });\n\n it(\"dataset_selection_mode_changes_cta\", async () => {\n const onsubmit = vi.fn().mockResolvedValue(undefined);\n const { container } = render(SourceIntakePanel, {\n environments,\n onsubmit,\n });\n\n await fireEvent.click(\n screen.getByRole(\"button\", {\n name: \"Dataset selection Enter dataset ref\",\n }),\n );\n await fireEvent.change(screen.getByRole(\"combobox\"), {\n target: { value: \"env-1\" },\n });\n await fireEvent.input(screen.getByPlaceholderText(\"public.sales\"), {\n target: { value: \" public.sales \" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).toHaveBeenCalledWith({\n environment_id: \"env-1\",\n source_input: \"public.sales\",\n source_kind: \"dataset_selection\",\n });\n });\n\n it(\"external_fail_renders_inline_error\", async () => {\n const onsubmit = vi.fn().mockRejectedValue(new Error(\"Backend rejected source\"));\n const { container } = render(SourceIntakePanel, {\n environments,\n selectedEnvironmentId: \"env-1\",\n onsubmit,\n });\n\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"https://demo.local/dashboard/42\" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(screen.getByText(\"Backend rejected source\")).toBeDefined();\n });\n});\n// [/DEF:SourceIntakePanelUxTests:Module]\n" + "anchor_syntax": "region", + "body": "// #region SourceIntakePanelUxTests:Module [TYPE Function]\n// @SEMANTICS: dataset-review, source-intake, ux-tests, validation, recovery\n// @PURPOSE: Verify source intake entry paths, validation feedback, and submit payload behavior for US1.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [SourceIntakePanel]\n// @UX_STATE: Idle -> Default intake renders both entry paths and waits for user input.\n// @UX_STATE: Validating -> Inline feedback confirms recognized links and local payload validation.\n// @UX_STATE: Rejected -> Invalid source input remains local and exposes recovery guidance.\n// @TEST_CONTRACT: SourceIntakePanelProps -> ObservableIntakeUX\n// @TEST_SCENARIO: invalid_superset_link_shows_rejected_state -> Invalid URL input keeps submit local and shows rejection feedback.\n// @TEST_SCENARIO: recognized_superset_link_submits_payload -> Recognized link path submits normalized payload.\n// @TEST_SCENARIO: dataset_selection_mode_changes_cta -> Dataset selection path switches CTA and payload source kind.\n// @TEST_EDGE: missing_environment -> Required environment guard blocks submit.\n// @TEST_EDGE: invalid_type -> Invalid Superset URL shows recovery hint.\n// @TEST_EDGE: external_fail -> Submit callback failure is rendered inline.\n// @TEST_INVARIANT: intake_contract_remains_observable -> VERIFIED_BY: [invalid_superset_link_shows_rejected_state, recognized_superset_link_submits_payload, dataset_selection_mode_changes_cta]\n\nimport { describe, it, expect, vi } from \"vitest\";\nimport { fireEvent, render, screen } from \"@testing-library/svelte\";\nimport SourceIntakePanel from \"../SourceIntakePanel.svelte\";\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n choose_environment: \"Choose environment\",\n error: \"Common error\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n },\n },\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"SourceIntakePanel UX Contract\", () => {\n const environments = [{ id: \"env-1\", name: \"DEV\" }];\n\n it(\"invalid_superset_link_shows_rejected_state\", async () => {\n const onsubmit = vi.fn();\n const { container } = render(SourceIntakePanel, {\n environments,\n onsubmit,\n });\n\n await fireEvent.change(screen.getByRole(\"combobox\"), {\n target: { value: \"env-1\" },\n });\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"not-a-url\" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).not.toHaveBeenCalled();\n expect(screen.getByText(\"Rejected\")).toBeDefined();\n expect(screen.getByText(\"Superset link must start with http\")).toBeDefined();\n });\n\n it(\"recognized_superset_link_submits_payload\", async () => {\n const onsubmit = vi.fn().mockResolvedValue(undefined);\n const { container } = render(SourceIntakePanel, {\n environments,\n selectedEnvironmentId: \"env-1\",\n acknowledgment: \"Recognized Superset link\",\n onsubmit,\n });\n\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"https://demo.local/superset/dashboard/42 \" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).toHaveBeenCalledWith({\n environment_id: \"env-1\",\n source_input: \"https://demo.local/superset/dashboard/42\",\n source_kind: \"superset_link\",\n });\n expect(screen.getByText(\"Recognized Superset link\")).toBeDefined();\n });\n\n it(\"dataset_selection_mode_changes_cta\", async () => {\n const onsubmit = vi.fn().mockResolvedValue(undefined);\n const { container } = render(SourceIntakePanel, {\n environments,\n onsubmit,\n });\n\n await fireEvent.click(\n screen.getByRole(\"button\", {\n name: \"Dataset selection Enter dataset ref\",\n }),\n );\n await fireEvent.change(screen.getByRole(\"combobox\"), {\n target: { value: \"env-1\" },\n });\n await fireEvent.input(screen.getByPlaceholderText(\"public.sales\"), {\n target: { value: \" public.sales \" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).toHaveBeenCalledWith({\n environment_id: \"env-1\",\n source_input: \"public.sales\",\n source_kind: \"dataset_selection\",\n });\n });\n\n it(\"external_fail_renders_inline_error\", async () => {\n const onsubmit = vi.fn().mockRejectedValue(new Error(\"Backend rejected source\"));\n const { container } = render(SourceIntakePanel, {\n environments,\n selectedEnvironmentId: \"env-1\",\n onsubmit,\n });\n\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"https://demo.local/dashboard/42\" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(screen.getByText(\"Backend rejected source\")).toBeDefined();\n });\n});\n// #endregion SourceIntakePanelUxTests:Module\n" }, { - "contract_id": "DatasetReviewUs2WorkspaceUxTests", - "contract_type": "Module", + "contract_id": "DatasetReviewUs2WorkspaceUxTests:Module", + "contract_type": "Function", "file_path": "frontend/src/lib/components/dataset-review/__tests__/us2_semantic_workspace.ux.test.js", "start_line": 5, - "end_line": 311, - "tier": "TIER_2", - "complexity": 3, + "end_line": 310, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify US2 dataset review surfaces keep semantic review actionable, route clarification through AssistantChatPanel, and preserve explicit preview/mapping gates.", "SEMANTICS": [ @@ -121018,13 +88010,13 @@ }, "relations": [ { - "source_id": "DatasetReviewUs2WorkspaceUxTests", + "source_id": "DatasetReviewUs2WorkspaceUxTests:Module", "relation_type": "DEPENDS_ON", "target_id": "SemanticLayerReview", "target_ref": "[SemanticLayerReview]" }, { - "source_id": "DatasetReviewUs2WorkspaceUxTests", + "source_id": "DatasetReviewUs2WorkspaceUxTests:Module", "relation_type": "DEPENDS_ON", "target_id": "ExecutionMappingReview", "target_ref": "[ExecutionMappingReview]" @@ -121033,60 +88025,80 @@ "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is not allowed for contract type 'Module'", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ - "Function", - "Block" + "Module", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is forbidden for contract type 'Module' at C3", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" } }, { "code": "tag_not_for_contract_type", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ - "Function", - "Block" + "Module", + "Block", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", + "tag": "SEMANTICS", + "message": "@SEMANTICS is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "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" } } ], - "anchor_syntax": "def", - "body": "// [DEF:DatasetReviewUs2WorkspaceUxTests:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, semantics, assistant, workspace, ux-tests, mapping-review\n// @PURPOSE: Verify US2 dataset review surfaces keep semantic review actionable, route clarification through AssistantChatPanel, and preserve explicit preview/mapping gates.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [SemanticLayerReview]\n// @RELATION: DEPENDS_ON -> [ExecutionMappingReview]\n// @TEST_SCENARIO: semantic_review_surfaces_active_value_and_candidates -> Field-level semantic review shows provenance, candidates, and mutation callbacks.\n// @TEST_SCENARIO: mapping_review_marks_row_focused_and_requires_explicit_approval -> Mapping review keeps warning approvals explicit and highlightable.\n// @TEST_EDGE: external_fail -> Failed request surfaces inline recovery message.\n\nimport { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@testing-library/svelte\";\nimport SemanticLayerReview from \"../SemanticLayerReview.svelte\";\nimport ExecutionMappingReview from \"../ExecutionMappingReview.svelte\";\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n cancel: \"Cancel\",\n unknown: \"Unknown\",\n not_available: \"N/A\",\n },\n dataset_review: {\n assistant: {\n ask_ai_action: \"✨ Ask AI\",\n improve_action: \"✨ Improve\",\n },\n semantics: {\n eyebrow: \"Semantic layer\",\n title: \"Review semantic field values\",\n description: \"Review semantic candidates\",\n field_count_label: \"Fields in review\",\n empty: \"No semantic fields\",\n unknown_source: \"Unknown source\",\n active_value_label: \"Active semantic value\",\n provenance_label: \"Provenance\",\n confidence_label: \"Confidence\",\n confidence_unset: \"No confidence rank\",\n confidence_rank_label: \"Rank\",\n source_label: \"Source\",\n changed_by_label: \"Changed by\",\n locked_badge: \"Locked\",\n unlocked_badge: \"Unlocked\",\n conflict_badge: \"Conflict\",\n needs_review_badge: \"Needs review\",\n manual_verbose_name_label: \"Manual verbose name\",\n manual_description_label: \"Manual description\",\n manual_display_format_label: \"Manual display format\",\n manual_override_action: \"Manual override\",\n save_manual_action: \"Save manual value\",\n lock_action: \"Lock field\",\n unlock_action: \"Unlock field\",\n feedback_up_action: \"Thumbs up\",\n feedback_down_action: \"Thumbs down\",\n candidates_title: \"Candidate options\",\n candidates_empty: \"No candidates\",\n candidate_description_empty: \"No candidate description\",\n display_format_label: \"Display format\",\n score_label: \"Score\",\n apply_candidate_action: \"Apply\",\n apply_and_lock_action: \"Apply and lock\",\n empty_value: \"No value\",\n provenance: {\n unresolved: \"Unresolved\",\n dictionary_exact: \"Dictionary exact match\",\n manual_override: \"Manual override\",\n },\n candidate_status: {\n pending: \"Pending\",\n },\n messages: {\n saving: \"Saving semantic decision...\",\n save_failed: \"Failed to save semantic decision.\",\n candidate_applied: \"Candidate value applied.\",\n },\n },\n mapping: {\n eyebrow: \"Template mapping\",\n title: \"Review filter-to-template mappings\",\n description: \"Verify imported values\",\n state_label: \"Mapping state\",\n state: {\n Incomplete: \"Incomplete\",\n WarningApproval: \"Approval required\",\n Approved: \"Approved\",\n },\n pending_approvals_label: \"Pending approvals\",\n required_values_label: \"Missing required values\",\n empty: \"No mappings\",\n required_blockers_notice: \"{count} required values missing\",\n approval_notice: \"{count} approvals needed\",\n to_variable_label: \"To variable\",\n required_badge: \"Required\",\n approval_required_badge: \"Approval required\",\n approved_badge: \"Approved\",\n source_filter_label: \"Source filter\",\n source_value_label: \"Source value\",\n confidence_label: \"Confidence\",\n recovery_label: \"Recovery\",\n effective_value_label: \"Effective value\",\n method_label: \"Method\",\n approval_label: \"Approval\",\n warning_label: \"Warning\",\n transformation_note_label: \"Transformation note\",\n missing_required_value: \"Missing required value\",\n manual_value_label: \"Manual effective value\",\n manual_note_label: \"Manual note\",\n save_override_action: \"Save manual override\",\n manual_override_action: \"Manual override\",\n approve_action: \"Approve mapping\",\n approve_all_action: \"Approve all pending mappings\",\n approval_state: {\n pending: \"Pending\",\n approved: \"Approved\",\n },\n warning_level: {\n high: \"High\",\n },\n method: {\n heuristic_match: \"Heuristic match\",\n manual_override: \"Manual override\",\n },\n messages: {\n saving: \"Saving mapping decision...\",\n approval_saved: \"Mapping approval recorded.\",\n approval_failed: \"Failed to save mapping approval.\",\n override_saved: \"Manual mapping override saved. Preview should be refreshed.\",\n override_failed: \"Failed to save manual mapping override.\",\n required_value_missing: \"Provide a required value before saving the manual override.\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\nvi.mock(\"$lib/api/datasetReview.js\", () => ({\n requestDatasetReviewApi: vi.fn(),\n}));\n\ndescribe(\"Dataset review US2 workspace surfaces\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it(\"semantic_review_surfaces_active_value_and_candidates\", async () => {\n const { requestDatasetReviewApi } = await import(\"$lib/api/datasetReview.js\");\n requestDatasetReviewApi.mockResolvedValue({\n field_id: \"field-1\",\n field_name: \"customer_name\",\n verbose_name: \"Customer name\",\n description: \"Customer display name\",\n display_format: \"text\",\n provenance: \"dictionary_exact\",\n source_id: \"source-1\",\n confidence_rank: 1,\n is_locked: false,\n has_conflict: false,\n needs_review: false,\n last_changed_by: \"user\",\n candidates: [],\n session_version: 5,\n });\n\n const onupdated = vi.fn();\n\n render(SemanticLayerReview, {\n sessionId: \"session-1\",\n sessionVersion: 4,\n semanticSources: [\n { source_id: \"source-1\", display_name: \"Trusted dictionary\", source_version: \"v1\" },\n ],\n fields: [\n {\n field_id: \"field-1\",\n field_name: \"customer_name\",\n field_kind: \"dimension\",\n verbose_name: \"Customer name\",\n description: \"Resolved from trusted dictionary\",\n display_format: \"text\",\n provenance: \"dictionary_exact\",\n source_id: \"source-1\",\n confidence_rank: 1,\n is_locked: false,\n has_conflict: true,\n needs_review: true,\n last_changed_by: \"system\",\n candidates: [\n {\n candidate_id: \"candidate-1\",\n match_type: \"exact\",\n confidence_score: 0.98,\n proposed_verbose_name: \"Customer name\",\n proposed_description: \"Customer display name\",\n proposed_display_format: \"text\",\n status: \"pending\",\n },\n ],\n },\n ],\n focusTarget: { target: \"field:field-1\", source: \"test\" },\n onupdated,\n });\n\n expect(screen.getByText(\"Review semantic field values\")).toBeDefined();\n expect(screen.getByText(\"customer_name\")).toBeDefined();\n expect(document.querySelector('[data-focus-target=\"field:field-1\"]')).toBeTruthy();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Apply\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/fields/field-1/semantic\",\n \"PATCH\",\n { candidate_id: \"candidate-1\", lock_field: false },\n 4,\n );\n });\n\n expect(onupdated).toHaveBeenCalled();\n expect(screen.getByText(\"Candidate value applied.\")).toBeDefined();\n });\n\n it(\"mapping_review_marks_row_focused_and_requires_explicit_approval\", async () => {\n const { requestDatasetReviewApi } = await import(\"$lib/api/datasetReview.js\");\n requestDatasetReviewApi.mockResolvedValue({\n mapping_id: \"mapping-1\",\n filter_id: \"filter-1\",\n variable_id: \"variable-1\",\n approval_state: \"approved\",\n requires_explicit_approval: true,\n warning_level: \"high\",\n mapping_method: \"heuristic_match\",\n effective_value: \"EU\",\n session_version: 6,\n });\n\n const onupdated = vi.fn();\n const onaskai = vi.fn();\n\n render(ExecutionMappingReview, {\n sessionId: \"session-1\",\n sessionVersion: 5,\n mappings: [\n {\n mapping_id: \"mapping-1\",\n filter_id: \"filter-1\",\n variable_id: \"variable-1\",\n approval_state: \"pending\",\n requires_explicit_approval: true,\n warning_level: \"high\",\n mapping_method: \"heuristic_match\",\n transformation_note: \"Europe → EU\",\n effective_value: \"EU\",\n },\n ],\n importedFilters: [\n {\n filter_id: \"filter-1\",\n filter_name: \"region\",\n display_name: \"Region\",\n raw_value: \"Europe\",\n confidence_state: \"mostly_confirmed\",\n recovery_status: \"partial\",\n },\n ],\n templateVariables: [\n {\n variable_id: \"variable-1\",\n variable_name: \"region_code\",\n is_required: true,\n },\n ],\n focusTarget: { target: \"mapping:mapping-1\", source: \"test\" },\n onupdated,\n onaskai,\n });\n\n expect(document.querySelector('[data-focus-target=\"mapping:mapping-1\"]')).toBeTruthy();\n expect(screen.getByText(\"Approval required\")).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"✨ Ask AI\" }));\n expect(onaskai).toHaveBeenCalled();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Approve mapping\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/mappings/mapping-1/approve\",\n \"POST\",\n { approval_note: \"Europe → EU\" },\n 5,\n );\n });\n\n expect(onupdated).toHaveBeenCalledWith(\n expect.objectContaining({ preview_state: \"unchanged\" }),\n );\n });\n});\n// [/DEF:DatasetReviewUs2WorkspaceUxTests:Module]\n" + "anchor_syntax": "region", + "body": "// #region DatasetReviewUs2WorkspaceUxTests:Module [TYPE Function]\n// @SEMANTICS: dataset-review, semantics, assistant, workspace, ux-tests, mapping-review\n// @PURPOSE: Verify US2 dataset review surfaces keep semantic review actionable, route clarification through AssistantChatPanel, and preserve explicit preview/mapping gates.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [SemanticLayerReview]\n// @RELATION: DEPENDS_ON -> [ExecutionMappingReview]\n// @TEST_SCENARIO: semantic_review_surfaces_active_value_and_candidates -> Field-level semantic review shows provenance, candidates, and mutation callbacks.\n// @TEST_SCENARIO: mapping_review_marks_row_focused_and_requires_explicit_approval -> Mapping review keeps warning approvals explicit and highlightable.\n// @TEST_EDGE: external_fail -> Failed request surfaces inline recovery message.\n\nimport { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@testing-library/svelte\";\nimport SemanticLayerReview from \"../SemanticLayerReview.svelte\";\nimport ExecutionMappingReview from \"../ExecutionMappingReview.svelte\";\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n cancel: \"Cancel\",\n unknown: \"Unknown\",\n not_available: \"N/A\",\n },\n dataset_review: {\n assistant: {\n ask_ai_action: \"✨ Ask AI\",\n improve_action: \"✨ Improve\",\n },\n semantics: {\n eyebrow: \"Semantic layer\",\n title: \"Review semantic field values\",\n description: \"Review semantic candidates\",\n field_count_label: \"Fields in review\",\n empty: \"No semantic fields\",\n unknown_source: \"Unknown source\",\n active_value_label: \"Active semantic value\",\n provenance_label: \"Provenance\",\n confidence_label: \"Confidence\",\n confidence_unset: \"No confidence rank\",\n confidence_rank_label: \"Rank\",\n source_label: \"Source\",\n changed_by_label: \"Changed by\",\n locked_badge: \"Locked\",\n unlocked_badge: \"Unlocked\",\n conflict_badge: \"Conflict\",\n needs_review_badge: \"Needs review\",\n manual_verbose_name_label: \"Manual verbose name\",\n manual_description_label: \"Manual description\",\n manual_display_format_label: \"Manual display format\",\n manual_override_action: \"Manual override\",\n save_manual_action: \"Save manual value\",\n lock_action: \"Lock field\",\n unlock_action: \"Unlock field\",\n feedback_up_action: \"Thumbs up\",\n feedback_down_action: \"Thumbs down\",\n candidates_title: \"Candidate options\",\n candidates_empty: \"No candidates\",\n candidate_description_empty: \"No candidate description\",\n display_format_label: \"Display format\",\n score_label: \"Score\",\n apply_candidate_action: \"Apply\",\n apply_and_lock_action: \"Apply and lock\",\n empty_value: \"No value\",\n provenance: {\n unresolved: \"Unresolved\",\n dictionary_exact: \"Dictionary exact match\",\n manual_override: \"Manual override\",\n },\n candidate_status: {\n pending: \"Pending\",\n },\n messages: {\n saving: \"Saving semantic decision...\",\n save_failed: \"Failed to save semantic decision.\",\n candidate_applied: \"Candidate value applied.\",\n },\n },\n mapping: {\n eyebrow: \"Template mapping\",\n title: \"Review filter-to-template mappings\",\n description: \"Verify imported values\",\n state_label: \"Mapping state\",\n state: {\n Incomplete: \"Incomplete\",\n WarningApproval: \"Approval required\",\n Approved: \"Approved\",\n },\n pending_approvals_label: \"Pending approvals\",\n required_values_label: \"Missing required values\",\n empty: \"No mappings\",\n required_blockers_notice: \"{count} required values missing\",\n approval_notice: \"{count} approvals needed\",\n to_variable_label: \"To variable\",\n required_badge: \"Required\",\n approval_required_badge: \"Approval required\",\n approved_badge: \"Approved\",\n source_filter_label: \"Source filter\",\n source_value_label: \"Source value\",\n confidence_label: \"Confidence\",\n recovery_label: \"Recovery\",\n effective_value_label: \"Effective value\",\n method_label: \"Method\",\n approval_label: \"Approval\",\n warning_label: \"Warning\",\n transformation_note_label: \"Transformation note\",\n missing_required_value: \"Missing required value\",\n manual_value_label: \"Manual effective value\",\n manual_note_label: \"Manual note\",\n save_override_action: \"Save manual override\",\n manual_override_action: \"Manual override\",\n approve_action: \"Approve mapping\",\n approve_all_action: \"Approve all pending mappings\",\n approval_state: {\n pending: \"Pending\",\n approved: \"Approved\",\n },\n warning_level: {\n high: \"High\",\n },\n method: {\n heuristic_match: \"Heuristic match\",\n manual_override: \"Manual override\",\n },\n messages: {\n saving: \"Saving mapping decision...\",\n approval_saved: \"Mapping approval recorded.\",\n approval_failed: \"Failed to save mapping approval.\",\n override_saved: \"Manual mapping override saved. Preview should be refreshed.\",\n override_failed: \"Failed to save manual mapping override.\",\n required_value_missing: \"Provide a required value before saving the manual override.\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\nvi.mock(\"$lib/api/datasetReview.js\", () => ({\n requestDatasetReviewApi: vi.fn(),\n}));\n\ndescribe(\"Dataset review US2 workspace surfaces\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it(\"semantic_review_surfaces_active_value_and_candidates\", async () => {\n const { requestDatasetReviewApi } = await import(\"$lib/api/datasetReview.js\");\n requestDatasetReviewApi.mockResolvedValue({\n field_id: \"field-1\",\n field_name: \"customer_name\",\n verbose_name: \"Customer name\",\n description: \"Customer display name\",\n display_format: \"text\",\n provenance: \"dictionary_exact\",\n source_id: \"source-1\",\n confidence_rank: 1,\n is_locked: false,\n has_conflict: false,\n needs_review: false,\n last_changed_by: \"user\",\n candidates: [],\n session_version: 5,\n });\n\n const onupdated = vi.fn();\n\n render(SemanticLayerReview, {\n sessionId: \"session-1\",\n sessionVersion: 4,\n semanticSources: [\n { source_id: \"source-1\", display_name: \"Trusted dictionary\", source_version: \"v1\" },\n ],\n fields: [\n {\n field_id: \"field-1\",\n field_name: \"customer_name\",\n field_kind: \"dimension\",\n verbose_name: \"Customer name\",\n description: \"Resolved from trusted dictionary\",\n display_format: \"text\",\n provenance: \"dictionary_exact\",\n source_id: \"source-1\",\n confidence_rank: 1,\n is_locked: false,\n has_conflict: true,\n needs_review: true,\n last_changed_by: \"system\",\n candidates: [\n {\n candidate_id: \"candidate-1\",\n match_type: \"exact\",\n confidence_score: 0.98,\n proposed_verbose_name: \"Customer name\",\n proposed_description: \"Customer display name\",\n proposed_display_format: \"text\",\n status: \"pending\",\n },\n ],\n },\n ],\n focusTarget: { target: \"field:field-1\", source: \"test\" },\n onupdated,\n });\n\n expect(screen.getByText(\"Review semantic field values\")).toBeDefined();\n expect(screen.getByText(\"customer_name\")).toBeDefined();\n expect(document.querySelector('[data-focus-target=\"field:field-1\"]')).toBeTruthy();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Apply\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/fields/field-1/semantic\",\n \"PATCH\",\n { candidate_id: \"candidate-1\", lock_field: false },\n 4,\n );\n });\n\n expect(onupdated).toHaveBeenCalled();\n expect(screen.getByText(\"Candidate value applied.\")).toBeDefined();\n });\n\n it(\"mapping_review_marks_row_focused_and_requires_explicit_approval\", async () => {\n const { requestDatasetReviewApi } = await import(\"$lib/api/datasetReview.js\");\n requestDatasetReviewApi.mockResolvedValue({\n mapping_id: \"mapping-1\",\n filter_id: \"filter-1\",\n variable_id: \"variable-1\",\n approval_state: \"approved\",\n requires_explicit_approval: true,\n warning_level: \"high\",\n mapping_method: \"heuristic_match\",\n effective_value: \"EU\",\n session_version: 6,\n });\n\n const onupdated = vi.fn();\n const onaskai = vi.fn();\n\n render(ExecutionMappingReview, {\n sessionId: \"session-1\",\n sessionVersion: 5,\n mappings: [\n {\n mapping_id: \"mapping-1\",\n filter_id: \"filter-1\",\n variable_id: \"variable-1\",\n approval_state: \"pending\",\n requires_explicit_approval: true,\n warning_level: \"high\",\n mapping_method: \"heuristic_match\",\n transformation_note: \"Europe → EU\",\n effective_value: \"EU\",\n },\n ],\n importedFilters: [\n {\n filter_id: \"filter-1\",\n filter_name: \"region\",\n display_name: \"Region\",\n raw_value: \"Europe\",\n confidence_state: \"mostly_confirmed\",\n recovery_status: \"partial\",\n },\n ],\n templateVariables: [\n {\n variable_id: \"variable-1\",\n variable_name: \"region_code\",\n is_required: true,\n },\n ],\n focusTarget: { target: \"mapping:mapping-1\", source: \"test\" },\n onupdated,\n onaskai,\n });\n\n expect(document.querySelector('[data-focus-target=\"mapping:mapping-1\"]')).toBeTruthy();\n expect(screen.getByText(\"Approval required\")).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"✨ Ask AI\" }));\n expect(onaskai).toHaveBeenCalled();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Approve mapping\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/mappings/mapping-1/approve\",\n \"POST\",\n { approval_note: \"Europe → EU\" },\n 5,\n );\n });\n\n expect(onupdated).toHaveBeenCalledWith(\n expect.objectContaining({ preview_state: \"unchanged\" }),\n );\n });\n});\n// #endregion DatasetReviewUs2WorkspaceUxTests:Module\n" }, { - "contract_id": "DatasetReviewUs3UxTests", - "contract_type": "Module", + "contract_id": "DatasetReviewUs3UxTests:Module", + "contract_type": "Function", "file_path": "frontend/src/lib/components/dataset-review/__tests__/us3_execution_batch.ux.test.js", "start_line": 5, - "end_line": 507, - "tier": "TIER_2", - "complexity": 3, + "end_line": 506, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify US3 mapping review, Superset preview, and launch confirmation UX contracts.", "SEMANTICS": [ @@ -121105,19 +88117,19 @@ }, "relations": [ { - "source_id": "DatasetReviewUs3UxTests", + "source_id": "DatasetReviewUs3UxTests:Module", "relation_type": "DEPENDS_ON", "target_id": "ExecutionMappingReview", "target_ref": "[ExecutionMappingReview]" }, { - "source_id": "DatasetReviewUs3UxTests", + "source_id": "DatasetReviewUs3UxTests:Module", "relation_type": "DEPENDS_ON", "target_id": "CompiledSQLPreview", "target_ref": "[CompiledSQLPreview]" }, { - "source_id": "DatasetReviewUs3UxTests", + "source_id": "DatasetReviewUs3UxTests:Module", "relation_type": "DEPENDS_ON", "target_id": "LaunchConfirmationPanel", "target_ref": "[LaunchConfirmationPanel]" @@ -121126,73 +88138,64 @@ "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ - "Function", - "Block" + "Module", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C3", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" } }, { "code": "tag_not_for_contract_type", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is not allowed for contract type 'Module'", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ - "Function", - "Block" + "Module", + "Block", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is forbidden for contract type 'Module' at C3", + "tag": "SEMANTICS", + "message": "@SEMANTICS is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" } }, { "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not allowed for contract type 'Module'", + "message": "@UX_STATE is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ "Component" ] @@ -121201,26 +88204,34 @@ { "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 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "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" } } ], - "anchor_syntax": "def", - "body": "// [DEF:DatasetReviewUs3UxTests:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, execution, mapping, preview, launch, ux-tests\n// @PURPOSE: Verify US3 mapping review, Superset preview, and launch confirmation UX contracts.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [ExecutionMappingReview]\n// @RELATION: DEPENDS_ON -> [CompiledSQLPreview]\n// @RELATION: DEPENDS_ON -> [LaunchConfirmationPanel]\n// @UX_STATE: WarningApproval -> Mapping review requires explicit approval or manual override.\n// @UX_STATE: Ready -> Preview and launch panels expose reviewed Superset-backed execution context.\n// @UX_STATE: Blocked -> Launch panel lists blockers instead of allowing hidden bypass.\n// @TEST_CONTRACT: Us3ExecutionProps -> ObservableExecutionUx\n// @TEST_SCENARIO: mapping_review_approves_warning_sensitive_row -> Approval persists and keeps blockers visible until cleared.\n// @TEST_SCENARIO: preview_panel_requests_superset_compilation_and_renders_sql -> Preview stays explicitly Superset-derived.\n// @TEST_SCENARIO: launch_panel_blocks_then_submits_sql_lab_launch -> Launch lists gates first and shows audited handoff after success.\n// @TEST_EDGE: missing_field -> Missing preview or run context remains readable.\n// @TEST_EDGE: external_fail -> Failed preview request surfaces inline error.\n// @TEST_EDGE: invalid_type -> Mixed values stringify without crashing.\n// @TEST_INVARIANT: execution_gates_remain_visible -> VERIFIED_BY: [mapping_review_approves_warning_sensitive_row, preview_panel_requests_superset_compilation_and_renders_sql, launch_panel_blocks_then_submits_sql_lab_launch]\n\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/svelte\";\nimport { requestDatasetReviewApi } from \"$lib/api/datasetReview.js\";\nimport ExecutionMappingReview from \"../ExecutionMappingReview.svelte\";\nimport CompiledSQLPreview from \"../CompiledSQLPreview.svelte\";\nimport LaunchConfirmationPanel from \"../LaunchConfirmationPanel.svelte\";\n\nvi.mock(\"$lib/api/datasetReview.js\", () => ({\n requestDatasetReviewApi: vi.fn(),\n}));\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n cancel: \"Cancel\",\n not_available: \"N/A\",\n unknown: \"Unknown\",\n },\n dataset_review: {\n mapping: {\n eyebrow: \"Template mapping\",\n title: \"Review filter-to-template mappings\",\n description: \"Verify imported filter values, effective execution values, and warning-sensitive transformations before preview or launch.\",\n state_label: \"Mapping state\",\n state: {\n Incomplete: \"Incomplete\",\n WarningApproval: \"Approval required\",\n Approved: \"Approved\",\n },\n pending_approvals_label: \"Pending approvals\",\n required_values_label: \"Missing required values\",\n empty: \"No execution mappings are available yet.\",\n required_blockers_notice: \"{count} required values still need attention before preview or launch can proceed.\",\n approval_notice: \"{count} mapping transformations still need explicit approval.\",\n to_variable_label: \"To variable\",\n required_badge: \"Required\",\n approval_required_badge: \"Approval required\",\n approved_badge: \"Approved\",\n source_filter_label: \"Source filter\",\n source_value_label: \"Source value\",\n confidence_label: \"Confidence\",\n recovery_label: \"Recovery\",\n effective_value_label: \"Effective value\",\n method_label: \"Method\",\n approval_label: \"Approval\",\n warning_label: \"Warning\",\n transformation_note_label: \"Transformation note\",\n missing_required_value: \"This mapping still lacks a required effective value.\",\n manual_value_label: \"Manual effective value\",\n manual_note_label: \"Manual override note\",\n save_override_action: \"Save manual override\",\n manual_override_action: \"Manual override\",\n approve_action: \"Approve mapping\",\n approve_all_action: \"Approve all pending mappings\",\n approval_state: {\n pending: \"Pending\",\n approved: \"Approved\",\n rejected: \"Rejected\",\n not_required: \"Not required\",\n },\n warning_level: {\n none: \"None\",\n low: \"Low\",\n medium: \"Medium\",\n high: \"High\",\n },\n method: {\n direct_match: \"Direct match\",\n heuristic_match: \"Heuristic match\",\n semantic_match: \"Semantic match\",\n manual_override: \"Manual override\",\n },\n messages: {\n saving: \"Saving mapping decision...\",\n approval_saved: \"Mapping approval recorded.\",\n approval_failed: \"Failed to save mapping approval.\",\n override_saved: \"Manual mapping override saved. Preview should be refreshed.\",\n override_failed: \"Failed to save manual mapping override.\",\n required_value_missing: \"Provide a required value before saving the manual override.\",\n },\n },\n preview: {\n eyebrow: \"Compiled SQL preview\",\n title: \"Review Superset-compiled SQL\",\n description: \"Preview truth comes only from Superset. Regenerate the preview whenever mappings or required inputs change.\",\n state_label: \"Preview state\",\n state: {\n missing: \"Missing\",\n pending: \"Pending\",\n ready: \"Ready\",\n stale: \"Stale\",\n failed: \"Error\",\n },\n generate_action: \"Generate SQL preview\",\n generate_loading: \"Generating preview...\",\n missing_body: \"No Superset preview is available yet. Generate one before attempting launch.\",\n pending_body: \"Superset is compiling the current execution context. Launch stays blocked until preview completes.\",\n ready_body: \"This SQL preview was compiled by Superset for the current execution inputs.\",\n stale_body: \"Mappings or effective values changed after the last successful preview. Regenerate before launch.\",\n error_body: \"Superset could not compile the current execution context.\",\n compiler_label: \"Compiled source\",\n compiled_by_superset: \"Compiled by Superset\",\n compiled_source_unknown: \"Compilation source unavailable\",\n fingerprint_label: \"Preview fingerprint\",\n compiled_at_label: \"Compiled at\",\n sql_block_title: \"Superset SQL\",\n compiled_truth_note: \"Exact SQL returned by Superset\",\n go_to_mapping_action: \"Review mapping inputs\",\n review_inputs_action: \"Review changed inputs\",\n messages: {\n generated: \"Superset preview refreshed.\",\n generate_failed: \"Failed to generate Superset preview.\",\n },\n },\n launch: {\n eyebrow: \"Launch confirmation\",\n title: \"Confirm SQL Lab launch\",\n description: \"Launch remains blocked until preview truth, approvals, and readiness gates all match the reviewed execution context.\",\n state_label: \"Launch state\",\n state: {\n Blocked: \"Blocked\",\n Ready: \"Ready\",\n Submitted: \"Submitted\",\n },\n blocked_title: \"Launch blockers\",\n blocked_body: \"Resolve the items below before sending this dataset run to SQL Lab.\",\n resolve_action: \"Open related area\",\n dataset_ref_label: \"Dataset reference\",\n readiness_label: \"Readiness\",\n approved_mappings_label: \"Approved mappings\",\n preview_fingerprint_label: \"Preview fingerprint\",\n sql_lab_target_title: \"Launch target\",\n sql_lab_target_body: \"The canonical launch target is a Superset SQL Lab session using the reviewed preview and effective execution inputs.\",\n preview_status_label: \"Preview status\",\n compiled_by_label: \"Compiled by\",\n launch_action: \"Launch dataset\",\n launch_loading: \"Launching dataset...\",\n submitted_title: \"Launch submitted\",\n submitted_body: \"SQL Lab handoff and audited run context were recorded for this launch request.\",\n run_context_label: \"Run context\",\n sql_lab_session_label: \"SQL Lab session\",\n launch_status_label: \"Launch status\",\n preview_ref_label: \"Preview reference\",\n blockers: {\n blocking_finding: \"Blocking findings remain unresolved\",\n mapping_approval_required: \"Mapping approval is still required\",\n preview_missing: \"Superset preview is required before launch\",\n preview_pending: \"Preview generation is still in progress\",\n preview_stale: \"Preview no longer matches the current execution inputs\",\n preview_failed: \"Preview failed and launch remains blocked\",\n readiness_not_run_ready: \"Session is not yet in run-ready state\",\n preview_fingerprint_missing: \"Preview fingerprint is missing, so launch cannot be trusted\",\n },\n messages: {\n launch_started: \"Dataset launch request sent to SQL Lab.\",\n launch_failed: \"Failed to launch dataset in SQL Lab.\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"Dataset review US3 execution UX\", () => {\n beforeEach(() => {\n requestDatasetReviewApi.mockReset();\n });\n\n it(\"mapping_review_approves_warning_sensitive_row\", async () => {\n requestDatasetReviewApi.mockResolvedValueOnce({\n mapping_id: \"map-1\",\n filter_id: \"filter-1\",\n variable_id: \"var-1\",\n mapping_method: \"direct_match\",\n raw_input_value: \"DE\",\n effective_value: \"DE\",\n transformation_note: \"Trimmed imported value\",\n warning_level: \"medium\",\n requires_explicit_approval: true,\n approval_state: \"approved\",\n });\n\n const onupdated = vi.fn();\n\n render(ExecutionMappingReview, {\n sessionId: \"session-1\",\n mappings: [\n {\n mapping_id: \"map-1\",\n filter_id: \"filter-1\",\n variable_id: \"var-1\",\n mapping_method: \"direct_match\",\n raw_input_value: \"DE\",\n effective_value: \"DE\",\n transformation_note: \"Trimmed imported value\",\n warning_level: \"medium\",\n requires_explicit_approval: true,\n approval_state: \"pending\",\n },\n ],\n importedFilters: [\n {\n filter_id: \"filter-1\",\n filter_name: \"country\",\n display_name: \"Country\",\n raw_value: \"DE\",\n normalized_value: \"DE\",\n confidence_state: \"imported\",\n recovery_status: \"recovered\",\n },\n ],\n templateVariables: [\n {\n variable_id: \"var-1\",\n variable_name: \"country\",\n is_required: true,\n default_value: null,\n },\n ],\n onupdated,\n });\n\n expect(screen.getByText(\"Review filter-to-template mappings\")).toBeDefined();\n expect(screen.getByText(\"Approval required\")).toBeDefined();\n expect(screen.getByText(\"Country\")).toBeDefined();\n expect(screen.getByText(/Trimmed imported value/)).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Approve mapping\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/mappings/map-1/approve\",\n \"POST\",\n { approval_note: \"Trimmed imported value\" },\n null,\n );\n });\n\n expect(onupdated).toHaveBeenCalledWith({\n mapping: expect.objectContaining({\n mapping_id: \"map-1\",\n approval_state: \"approved\",\n }),\n preview_state: \"unchanged\",\n });\n expect(screen.getByText(\"Mapping approval recorded.\")).toBeDefined();\n });\n\n it(\"mapping_review_batch_approves_pending_rows\", async () => {\n requestDatasetReviewApi.mockResolvedValueOnce([\n {\n mapping_id: \"map-1\",\n filter_id: \"filter-1\",\n variable_id: \"var-1\",\n mapping_method: \"direct_match\",\n raw_input_value: \"DE\",\n effective_value: \"DE\",\n transformation_note: \"Trimmed imported value\",\n warning_level: \"medium\",\n requires_explicit_approval: true,\n approval_state: \"approved\",\n },\n ]);\n\n const onupdated = vi.fn();\n\n render(ExecutionMappingReview, {\n sessionId: \"session-1\",\n mappings: [\n {\n mapping_id: \"map-1\",\n filter_id: \"filter-1\",\n variable_id: \"var-1\",\n mapping_method: \"direct_match\",\n raw_input_value: \"DE\",\n effective_value: \"DE\",\n transformation_note: \"Trimmed imported value\",\n warning_level: \"medium\",\n requires_explicit_approval: true,\n approval_state: \"pending\",\n },\n ],\n importedFilters: [\n {\n filter_id: \"filter-1\",\n filter_name: \"country\",\n display_name: \"Country\",\n raw_value: \"DE\",\n normalized_value: \"DE\",\n confidence_state: \"imported\",\n recovery_status: \"recovered\",\n },\n ],\n templateVariables: [\n {\n variable_id: \"var-1\",\n variable_name: \"country\",\n is_required: true,\n default_value: null,\n },\n ],\n onupdated,\n });\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Approve all pending mappings\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/mappings/approve-batch\",\n \"POST\",\n { mapping_ids: [\"map-1\"] },\n null,\n );\n });\n\n expect(onupdated).toHaveBeenCalledWith({\n mappings: expect.any(Array),\n preview_state: \"unchanged\",\n });\n });\n\n it(\"preview_panel_requests_superset_compilation_and_renders_sql\", async () => {\n requestDatasetReviewApi.mockResolvedValueOnce({\n preview_id: \"preview-1\",\n session_id: \"session-1\",\n preview_status: \"ready\",\n compiled_sql: \"SELECT * FROM sales WHERE country = 'DE'\",\n preview_fingerprint: \"fingerprint-1\",\n compiled_by: \"superset\",\n compiled_at: \"2026-03-17T09:00:00Z\",\n });\n\n const onupdated = vi.fn();\n\n render(CompiledSQLPreview, {\n sessionId: \"session-1\",\n preview: null,\n previewState: \"missing\",\n onupdated,\n });\n\n expect(screen.getByText(\"Review Superset-compiled SQL\")).toBeDefined();\n expect(screen.getByText(\"No Superset preview is available yet. Generate one before attempting launch.\")).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Generate SQL preview\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/preview\",\n \"POST\",\n null,\n null,\n );\n });\n\n await waitFor(() => {\n expect(onupdated).toHaveBeenCalledWith({\n preview: expect.objectContaining({\n preview_id: \"preview-1\",\n compiled_by: \"superset\",\n }),\n preview_state: \"ready\",\n });\n });\n\n requestDatasetReviewApi.mockRejectedValueOnce(new Error(\"Superset compile failed\"));\n\n const onjump = vi.fn();\n render(CompiledSQLPreview, {\n sessionId: \"session-1\",\n preview: {\n preview_id: \"preview-err\",\n preview_status: \"failed\",\n preview_fingerprint: \"fingerprint-err\",\n compiled_by: \"superset\",\n error_details: \"Variable country is invalid\",\n },\n previewState: \"failed\",\n onjump,\n });\n\n expect(screen.getByText(\"Variable country is invalid\")).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Review mapping inputs\" }));\n expect(onjump).toHaveBeenCalledWith({ target: \"mapping\" });\n });\n\n it(\"launch_panel_blocks_then_submits_sql_lab_launch\", async () => {\n const onjump = vi.fn();\n const { rerender } = render(LaunchConfirmationPanel, {\n sessionId: \"session-1\",\n session: {\n dataset_ref: \"public.sales\",\n readiness_state: \"mapping_review_needed\",\n },\n findings: [\n {\n severity: \"blocking\",\n resolution_state: \"open\",\n title: \"Missing required value\",\n message: \"country is required\",\n code: \"REQ_COUNTRY\",\n },\n ],\n mappings: [\n {\n mapping_id: \"map-1\",\n requires_explicit_approval: true,\n approval_state: \"pending\",\n transformation_note: \"Europe -> EU\",\n },\n ],\n preview: null,\n previewState: \"missing\",\n onjump,\n });\n\n expect(screen.getByText(\"Launch blockers\")).toBeDefined();\n expect(screen.getByText(\"Blocking findings remain unresolved\")).toBeDefined();\n expect(screen.getByText(\"Mapping approval is still required\")).toBeDefined();\n expect(screen.getByText(\"Superset preview is required before launch\")).toBeDefined();\n\n await fireEvent.click(screen.getAllByRole(\"button\", { name: \"Open related area\" })[1]);\n expect(onjump).toHaveBeenCalledWith({ target: \"mapping\" });\n\n requestDatasetReviewApi.mockResolvedValueOnce({\n run_context: {\n run_context_id: \"run-1\",\n sql_lab_session_ref: \"sql-lab-77\",\n launch_status: \"started\",\n preview_id: \"preview-1\",\n },\n });\n\n await rerender({\n sessionId: \"session-1\",\n session: {\n dataset_ref: \"public.sales\",\n readiness_state: \"run_ready\",\n },\n findings: [],\n mappings: [\n {\n mapping_id: \"map-1\",\n requires_explicit_approval: true,\n approval_state: \"approved\",\n transformation_note: \"Europe -> EU\",\n },\n ],\n preview: {\n preview_id: \"preview-1\",\n preview_status: \"ready\",\n preview_fingerprint: \"fingerprint-1\",\n compiled_by: \"superset\",\n },\n previewState: \"ready\",\n onupdated: vi.fn(),\n onjump,\n });\n\n expect(screen.getByText(\"Confirm SQL Lab launch\")).toBeDefined();\n expect(screen.getByText(\"Launch state: Ready\")).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Launch dataset\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/launch\",\n \"POST\",\n null,\n null,\n );\n });\n\n expect(screen.getByText(\"Dataset launch request sent to SQL Lab.\")).toBeDefined();\n });\n});\n// [/DEF:DatasetReviewUs3UxTests:Module]\n" + "anchor_syntax": "region", + "body": "// #region DatasetReviewUs3UxTests:Module [TYPE Function]\n// @SEMANTICS: dataset-review, execution, mapping, preview, launch, ux-tests\n// @PURPOSE: Verify US3 mapping review, Superset preview, and launch confirmation UX contracts.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [ExecutionMappingReview]\n// @RELATION: DEPENDS_ON -> [CompiledSQLPreview]\n// @RELATION: DEPENDS_ON -> [LaunchConfirmationPanel]\n// @UX_STATE: WarningApproval -> Mapping review requires explicit approval or manual override.\n// @UX_STATE: Ready -> Preview and launch panels expose reviewed Superset-backed execution context.\n// @UX_STATE: Blocked -> Launch panel lists blockers instead of allowing hidden bypass.\n// @TEST_CONTRACT: Us3ExecutionProps -> ObservableExecutionUx\n// @TEST_SCENARIO: mapping_review_approves_warning_sensitive_row -> Approval persists and keeps blockers visible until cleared.\n// @TEST_SCENARIO: preview_panel_requests_superset_compilation_and_renders_sql -> Preview stays explicitly Superset-derived.\n// @TEST_SCENARIO: launch_panel_blocks_then_submits_sql_lab_launch -> Launch lists gates first and shows audited handoff after success.\n// @TEST_EDGE: missing_field -> Missing preview or run context remains readable.\n// @TEST_EDGE: external_fail -> Failed preview request surfaces inline error.\n// @TEST_EDGE: invalid_type -> Mixed values stringify without crashing.\n// @TEST_INVARIANT: execution_gates_remain_visible -> VERIFIED_BY: [mapping_review_approves_warning_sensitive_row, preview_panel_requests_superset_compilation_and_renders_sql, launch_panel_blocks_then_submits_sql_lab_launch]\n\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/svelte\";\nimport { requestDatasetReviewApi } from \"$lib/api/datasetReview.js\";\nimport ExecutionMappingReview from \"../ExecutionMappingReview.svelte\";\nimport CompiledSQLPreview from \"../CompiledSQLPreview.svelte\";\nimport LaunchConfirmationPanel from \"../LaunchConfirmationPanel.svelte\";\n\nvi.mock(\"$lib/api/datasetReview.js\", () => ({\n requestDatasetReviewApi: vi.fn(),\n}));\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n cancel: \"Cancel\",\n not_available: \"N/A\",\n unknown: \"Unknown\",\n },\n dataset_review: {\n mapping: {\n eyebrow: \"Template mapping\",\n title: \"Review filter-to-template mappings\",\n description: \"Verify imported filter values, effective execution values, and warning-sensitive transformations before preview or launch.\",\n state_label: \"Mapping state\",\n state: {\n Incomplete: \"Incomplete\",\n WarningApproval: \"Approval required\",\n Approved: \"Approved\",\n },\n pending_approvals_label: \"Pending approvals\",\n required_values_label: \"Missing required values\",\n empty: \"No execution mappings are available yet.\",\n required_blockers_notice: \"{count} required values still need attention before preview or launch can proceed.\",\n approval_notice: \"{count} mapping transformations still need explicit approval.\",\n to_variable_label: \"To variable\",\n required_badge: \"Required\",\n approval_required_badge: \"Approval required\",\n approved_badge: \"Approved\",\n source_filter_label: \"Source filter\",\n source_value_label: \"Source value\",\n confidence_label: \"Confidence\",\n recovery_label: \"Recovery\",\n effective_value_label: \"Effective value\",\n method_label: \"Method\",\n approval_label: \"Approval\",\n warning_label: \"Warning\",\n transformation_note_label: \"Transformation note\",\n missing_required_value: \"This mapping still lacks a required effective value.\",\n manual_value_label: \"Manual effective value\",\n manual_note_label: \"Manual override note\",\n save_override_action: \"Save manual override\",\n manual_override_action: \"Manual override\",\n approve_action: \"Approve mapping\",\n approve_all_action: \"Approve all pending mappings\",\n approval_state: {\n pending: \"Pending\",\n approved: \"Approved\",\n rejected: \"Rejected\",\n not_required: \"Not required\",\n },\n warning_level: {\n none: \"None\",\n low: \"Low\",\n medium: \"Medium\",\n high: \"High\",\n },\n method: {\n direct_match: \"Direct match\",\n heuristic_match: \"Heuristic match\",\n semantic_match: \"Semantic match\",\n manual_override: \"Manual override\",\n },\n messages: {\n saving: \"Saving mapping decision...\",\n approval_saved: \"Mapping approval recorded.\",\n approval_failed: \"Failed to save mapping approval.\",\n override_saved: \"Manual mapping override saved. Preview should be refreshed.\",\n override_failed: \"Failed to save manual mapping override.\",\n required_value_missing: \"Provide a required value before saving the manual override.\",\n },\n },\n preview: {\n eyebrow: \"Compiled SQL preview\",\n title: \"Review Superset-compiled SQL\",\n description: \"Preview truth comes only from Superset. Regenerate the preview whenever mappings or required inputs change.\",\n state_label: \"Preview state\",\n state: {\n missing: \"Missing\",\n pending: \"Pending\",\n ready: \"Ready\",\n stale: \"Stale\",\n failed: \"Error\",\n },\n generate_action: \"Generate SQL preview\",\n generate_loading: \"Generating preview...\",\n missing_body: \"No Superset preview is available yet. Generate one before attempting launch.\",\n pending_body: \"Superset is compiling the current execution context. Launch stays blocked until preview completes.\",\n ready_body: \"This SQL preview was compiled by Superset for the current execution inputs.\",\n stale_body: \"Mappings or effective values changed after the last successful preview. Regenerate before launch.\",\n error_body: \"Superset could not compile the current execution context.\",\n compiler_label: \"Compiled source\",\n compiled_by_superset: \"Compiled by Superset\",\n compiled_source_unknown: \"Compilation source unavailable\",\n fingerprint_label: \"Preview fingerprint\",\n compiled_at_label: \"Compiled at\",\n sql_block_title: \"Superset SQL\",\n compiled_truth_note: \"Exact SQL returned by Superset\",\n go_to_mapping_action: \"Review mapping inputs\",\n review_inputs_action: \"Review changed inputs\",\n messages: {\n generated: \"Superset preview refreshed.\",\n generate_failed: \"Failed to generate Superset preview.\",\n },\n },\n launch: {\n eyebrow: \"Launch confirmation\",\n title: \"Confirm SQL Lab launch\",\n description: \"Launch remains blocked until preview truth, approvals, and readiness gates all match the reviewed execution context.\",\n state_label: \"Launch state\",\n state: {\n Blocked: \"Blocked\",\n Ready: \"Ready\",\n Submitted: \"Submitted\",\n },\n blocked_title: \"Launch blockers\",\n blocked_body: \"Resolve the items below before sending this dataset run to SQL Lab.\",\n resolve_action: \"Open related area\",\n dataset_ref_label: \"Dataset reference\",\n readiness_label: \"Readiness\",\n approved_mappings_label: \"Approved mappings\",\n preview_fingerprint_label: \"Preview fingerprint\",\n sql_lab_target_title: \"Launch target\",\n sql_lab_target_body: \"The canonical launch target is a Superset SQL Lab session using the reviewed preview and effective execution inputs.\",\n preview_status_label: \"Preview status\",\n compiled_by_label: \"Compiled by\",\n launch_action: \"Launch dataset\",\n launch_loading: \"Launching dataset...\",\n submitted_title: \"Launch submitted\",\n submitted_body: \"SQL Lab handoff and audited run context were recorded for this launch request.\",\n run_context_label: \"Run context\",\n sql_lab_session_label: \"SQL Lab session\",\n launch_status_label: \"Launch status\",\n preview_ref_label: \"Preview reference\",\n blockers: {\n blocking_finding: \"Blocking findings remain unresolved\",\n mapping_approval_required: \"Mapping approval is still required\",\n preview_missing: \"Superset preview is required before launch\",\n preview_pending: \"Preview generation is still in progress\",\n preview_stale: \"Preview no longer matches the current execution inputs\",\n preview_failed: \"Preview failed and launch remains blocked\",\n readiness_not_run_ready: \"Session is not yet in run-ready state\",\n preview_fingerprint_missing: \"Preview fingerprint is missing, so launch cannot be trusted\",\n },\n messages: {\n launch_started: \"Dataset launch request sent to SQL Lab.\",\n launch_failed: \"Failed to launch dataset in SQL Lab.\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"Dataset review US3 execution UX\", () => {\n beforeEach(() => {\n requestDatasetReviewApi.mockReset();\n });\n\n it(\"mapping_review_approves_warning_sensitive_row\", async () => {\n requestDatasetReviewApi.mockResolvedValueOnce({\n mapping_id: \"map-1\",\n filter_id: \"filter-1\",\n variable_id: \"var-1\",\n mapping_method: \"direct_match\",\n raw_input_value: \"DE\",\n effective_value: \"DE\",\n transformation_note: \"Trimmed imported value\",\n warning_level: \"medium\",\n requires_explicit_approval: true,\n approval_state: \"approved\",\n });\n\n const onupdated = vi.fn();\n\n render(ExecutionMappingReview, {\n sessionId: \"session-1\",\n mappings: [\n {\n mapping_id: \"map-1\",\n filter_id: \"filter-1\",\n variable_id: \"var-1\",\n mapping_method: \"direct_match\",\n raw_input_value: \"DE\",\n effective_value: \"DE\",\n transformation_note: \"Trimmed imported value\",\n warning_level: \"medium\",\n requires_explicit_approval: true,\n approval_state: \"pending\",\n },\n ],\n importedFilters: [\n {\n filter_id: \"filter-1\",\n filter_name: \"country\",\n display_name: \"Country\",\n raw_value: \"DE\",\n normalized_value: \"DE\",\n confidence_state: \"imported\",\n recovery_status: \"recovered\",\n },\n ],\n templateVariables: [\n {\n variable_id: \"var-1\",\n variable_name: \"country\",\n is_required: true,\n default_value: null,\n },\n ],\n onupdated,\n });\n\n expect(screen.getByText(\"Review filter-to-template mappings\")).toBeDefined();\n expect(screen.getByText(\"Approval required\")).toBeDefined();\n expect(screen.getByText(\"Country\")).toBeDefined();\n expect(screen.getByText(/Trimmed imported value/)).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Approve mapping\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/mappings/map-1/approve\",\n \"POST\",\n { approval_note: \"Trimmed imported value\" },\n null,\n );\n });\n\n expect(onupdated).toHaveBeenCalledWith({\n mapping: expect.objectContaining({\n mapping_id: \"map-1\",\n approval_state: \"approved\",\n }),\n preview_state: \"unchanged\",\n });\n expect(screen.getByText(\"Mapping approval recorded.\")).toBeDefined();\n });\n\n it(\"mapping_review_batch_approves_pending_rows\", async () => {\n requestDatasetReviewApi.mockResolvedValueOnce([\n {\n mapping_id: \"map-1\",\n filter_id: \"filter-1\",\n variable_id: \"var-1\",\n mapping_method: \"direct_match\",\n raw_input_value: \"DE\",\n effective_value: \"DE\",\n transformation_note: \"Trimmed imported value\",\n warning_level: \"medium\",\n requires_explicit_approval: true,\n approval_state: \"approved\",\n },\n ]);\n\n const onupdated = vi.fn();\n\n render(ExecutionMappingReview, {\n sessionId: \"session-1\",\n mappings: [\n {\n mapping_id: \"map-1\",\n filter_id: \"filter-1\",\n variable_id: \"var-1\",\n mapping_method: \"direct_match\",\n raw_input_value: \"DE\",\n effective_value: \"DE\",\n transformation_note: \"Trimmed imported value\",\n warning_level: \"medium\",\n requires_explicit_approval: true,\n approval_state: \"pending\",\n },\n ],\n importedFilters: [\n {\n filter_id: \"filter-1\",\n filter_name: \"country\",\n display_name: \"Country\",\n raw_value: \"DE\",\n normalized_value: \"DE\",\n confidence_state: \"imported\",\n recovery_status: \"recovered\",\n },\n ],\n templateVariables: [\n {\n variable_id: \"var-1\",\n variable_name: \"country\",\n is_required: true,\n default_value: null,\n },\n ],\n onupdated,\n });\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Approve all pending mappings\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/mappings/approve-batch\",\n \"POST\",\n { mapping_ids: [\"map-1\"] },\n null,\n );\n });\n\n expect(onupdated).toHaveBeenCalledWith({\n mappings: expect.any(Array),\n preview_state: \"unchanged\",\n });\n });\n\n it(\"preview_panel_requests_superset_compilation_and_renders_sql\", async () => {\n requestDatasetReviewApi.mockResolvedValueOnce({\n preview_id: \"preview-1\",\n session_id: \"session-1\",\n preview_status: \"ready\",\n compiled_sql: \"SELECT * FROM sales WHERE country = 'DE'\",\n preview_fingerprint: \"fingerprint-1\",\n compiled_by: \"superset\",\n compiled_at: \"2026-03-17T09:00:00Z\",\n });\n\n const onupdated = vi.fn();\n\n render(CompiledSQLPreview, {\n sessionId: \"session-1\",\n preview: null,\n previewState: \"missing\",\n onupdated,\n });\n\n expect(screen.getByText(\"Review Superset-compiled SQL\")).toBeDefined();\n expect(screen.getByText(\"No Superset preview is available yet. Generate one before attempting launch.\")).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Generate SQL preview\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/preview\",\n \"POST\",\n null,\n null,\n );\n });\n\n await waitFor(() => {\n expect(onupdated).toHaveBeenCalledWith({\n preview: expect.objectContaining({\n preview_id: \"preview-1\",\n compiled_by: \"superset\",\n }),\n preview_state: \"ready\",\n });\n });\n\n requestDatasetReviewApi.mockRejectedValueOnce(new Error(\"Superset compile failed\"));\n\n const onjump = vi.fn();\n render(CompiledSQLPreview, {\n sessionId: \"session-1\",\n preview: {\n preview_id: \"preview-err\",\n preview_status: \"failed\",\n preview_fingerprint: \"fingerprint-err\",\n compiled_by: \"superset\",\n error_details: \"Variable country is invalid\",\n },\n previewState: \"failed\",\n onjump,\n });\n\n expect(screen.getByText(\"Variable country is invalid\")).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Review mapping inputs\" }));\n expect(onjump).toHaveBeenCalledWith({ target: \"mapping\" });\n });\n\n it(\"launch_panel_blocks_then_submits_sql_lab_launch\", async () => {\n const onjump = vi.fn();\n const { rerender } = render(LaunchConfirmationPanel, {\n sessionId: \"session-1\",\n session: {\n dataset_ref: \"public.sales\",\n readiness_state: \"mapping_review_needed\",\n },\n findings: [\n {\n severity: \"blocking\",\n resolution_state: \"open\",\n title: \"Missing required value\",\n message: \"country is required\",\n code: \"REQ_COUNTRY\",\n },\n ],\n mappings: [\n {\n mapping_id: \"map-1\",\n requires_explicit_approval: true,\n approval_state: \"pending\",\n transformation_note: \"Europe -> EU\",\n },\n ],\n preview: null,\n previewState: \"missing\",\n onjump,\n });\n\n expect(screen.getByText(\"Launch blockers\")).toBeDefined();\n expect(screen.getByText(\"Blocking findings remain unresolved\")).toBeDefined();\n expect(screen.getByText(\"Mapping approval is still required\")).toBeDefined();\n expect(screen.getByText(\"Superset preview is required before launch\")).toBeDefined();\n\n await fireEvent.click(screen.getAllByRole(\"button\", { name: \"Open related area\" })[1]);\n expect(onjump).toHaveBeenCalledWith({ target: \"mapping\" });\n\n requestDatasetReviewApi.mockResolvedValueOnce({\n run_context: {\n run_context_id: \"run-1\",\n sql_lab_session_ref: \"sql-lab-77\",\n launch_status: \"started\",\n preview_id: \"preview-1\",\n },\n });\n\n await rerender({\n sessionId: \"session-1\",\n session: {\n dataset_ref: \"public.sales\",\n readiness_state: \"run_ready\",\n },\n findings: [],\n mappings: [\n {\n mapping_id: \"map-1\",\n requires_explicit_approval: true,\n approval_state: \"approved\",\n transformation_note: \"Europe -> EU\",\n },\n ],\n preview: {\n preview_id: \"preview-1\",\n preview_status: \"ready\",\n preview_fingerprint: \"fingerprint-1\",\n compiled_by: \"superset\",\n },\n previewState: \"ready\",\n onupdated: vi.fn(),\n onjump,\n });\n\n expect(screen.getByText(\"Confirm SQL Lab launch\")).toBeDefined();\n expect(screen.getByText(\"Launch state: Ready\")).toBeDefined();\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Launch dataset\" }));\n\n await waitFor(() => {\n expect(requestDatasetReviewApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions/session-1/launch\",\n \"POST\",\n null,\n null,\n );\n });\n\n expect(screen.getByText(\"Dataset launch request sent to SQL Lab.\")).toBeDefined();\n });\n});\n// #endregion DatasetReviewUs3UxTests:Module\n" }, { - "contract_id": "ValidationFindingsPanelUxTests", - "contract_type": "Module", + "contract_id": "ValidationFindingsPanelUxTests:Module", + "contract_type": "Function", "file_path": "frontend/src/lib/components/dataset-review/__tests__/validation_findings_panel.ux.test.js", "start_line": 4, - "end_line": 141, - "tier": "TIER_2", - "complexity": 3, + "end_line": 140, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "LAYER": "UI", "PURPOSE": "Verify grouped findings visibility, empty state, and remediation jump behavior for US1.", "SEMANTICS": [ @@ -121238,7 +88249,7 @@ }, "relations": [ { - "source_id": "ValidationFindingsPanelUxTests", + "source_id": "ValidationFindingsPanelUxTests:Module", "relation_type": "DEPENDS_ON", "target_id": "ValidationFindingsPanel", "target_ref": "[ValidationFindingsPanel]" @@ -121247,73 +88258,64 @@ "schema_warnings": [ { "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Module'", + "tag": "LAYER", + "message": "@LAYER is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ - "Function", - "Block" + "Module", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Module' at C3", + "tag": "LAYER", + "message": "@LAYER is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, + "contract_type": "Function" } }, { "code": "tag_not_for_contract_type", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is not allowed for contract type 'Module'", + "tag": "SEMANTICS", + "message": "@SEMANTICS is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ - "Function", - "Block" + "Module", + "Block", + "Skill", + "Agent" ] } }, { "code": "tag_forbidden_by_complexity", - "tag": "TEST_EDGE", - "message": "@TEST_EDGE is forbidden for contract type 'Module' at C3", + "tag": "SEMANTICS", + "message": "@SEMANTICS is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is not allowed for contract type 'Module'", - "detail": { - "actual_type": "Module", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_SCENARIO", - "message": "@TEST_SCENARIO is forbidden for contract type 'Module' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "actual_complexity": 1, + "contract_type": "Function" } }, { "code": "tag_not_for_contract_type", "tag": "UX_STATE", - "message": "@UX_STATE is not allowed for contract type 'Module'", + "message": "@UX_STATE is not allowed for contract type 'Function'", "detail": { - "actual_type": "Module", + "actual_type": "Function", "allowed_types": [ "Component" ] @@ -121322,26 +88324,35 @@ { "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 'Function' at C1", "detail": { - "actual_complexity": 3, - "contract_type": "Module" + "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" } } ], - "anchor_syntax": "def", - "body": "// [DEF:ValidationFindingsPanelUxTests:Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, findings, severity, jump, ux-tests\n// @PURPOSE: Verify grouped findings visibility, empty state, and remediation jump behavior for US1.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [ValidationFindingsPanel]\n// @UX_STATE: Blocking -> Blocking findings dominate the panel and expose remediation jumps.\n// @UX_STATE: Warning -> Warning findings remain visible with explicit resolution state.\n// @UX_STATE: Informational -> Informational notes stay readable without competing with blockers.\n// @TEST_CONTRACT: FindingsPanelProps -> ObservableFindingsUX\n// @TEST_SCENARIO: blocking_warning_info_groups_render -> Findings render in the proper severity groups with counts.\n// @TEST_SCENARIO: jump_action_maps_area_to_workspace_target -> Jump action emits normalized remediation target.\n// @TEST_SCENARIO: empty_findings_show_success_state -> Empty list shows ready feedback.\n// @TEST_EDGE: missing_field -> Missing optional fields do not crash rendering.\n// @TEST_EDGE: invalid_type -> Unknown severity falls back to informational grouping.\n// @TEST_EDGE: external_fail -> Jump callback remains optional.\n// @TEST_INVARIANT: findings_groups_remain_actionable -> VERIFIED_BY: [blocking_warning_info_groups_render, jump_action_maps_area_to_workspace_target, empty_findings_show_success_state]\n\nimport { describe, it, expect, vi } from \"vitest\";\nimport { fireEvent, render, screen } from \"@testing-library/svelte\";\nimport ValidationFindingsPanel from \"../ValidationFindingsPanel.svelte\";\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n dataset_review: {\n findings: {\n eyebrow: \"Validation findings\",\n title: \"Findings\",\n description: \"Review validation results\",\n next_action_label: \"Next action\",\n empty: \"No findings\",\n blocking_title: \"Blocking\",\n blocking_empty: \"No blocking findings\",\n warning_title: \"Warnings\",\n warning_empty: \"No warning findings\",\n informational_title: \"Info\",\n informational_empty: \"No informational findings\",\n resolution_label: \"Resolution\",\n reference_label: \"Reference\",\n jump_action: \"Jump to area\",\n resolution: {\n open: \"Open\",\n approved: \"Approved\",\n },\n areas: {\n source_intake: \"Source intake\",\n dataset_profile: \"Dataset summary\",\n semantic_enrichment: \"Semantics\",\n },\n },\n workspace: {\n actions: {\n review_documentation: \"Review documentation\",\n import_from_superset: \"Import from Superset\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"ValidationFindingsPanel UX Contract\", () => {\n const findings = [\n {\n finding_id: \"f-1\",\n severity: \"blocking\",\n code: \"REQ_ENV\",\n area: \"source_intake\",\n title: \"Environment required\",\n message: \"Select environment\",\n resolution_state: \"open\",\n },\n {\n finding_id: \"f-2\",\n severity: \"warning\",\n code: \"PARTIAL_RECOVERY\",\n area: \"dataset_profile\",\n title: \"Partial recovery\",\n message: \"Some metadata needs review\",\n resolution_state: \"approved\",\n caused_by_ref: \"dashboard:42\",\n },\n {\n finding_id: \"f-3\",\n severity: \"unexpected\",\n code: \"INFO_NOTE\",\n area: \"semantic_enrichment\",\n title: \"Dictionary note\",\n message: \"Trusted source used\",\n resolution_state: \"open\",\n },\n ];\n\n it(\"blocking_warning_info_groups_render\", () => {\n render(ValidationFindingsPanel, {\n findings,\n recommendedAction: \"review_documentation\",\n });\n\n expect(screen.getByText(\"Blocking\")).toBeDefined();\n expect(screen.getByText(\"Warnings\")).toBeDefined();\n expect(screen.getByText(\"Info\")).toBeDefined();\n expect(screen.getByText(\"Environment required\")).toBeDefined();\n expect(screen.getByText(\"Partial recovery\")).toBeDefined();\n expect(screen.getByText(\"Dictionary note\")).toBeDefined();\n expect(screen.getByText(\"Review documentation\")).toBeDefined();\n });\n\n it(\"jump_action_maps_area_to_workspace_target\", async () => {\n const onjump = vi.fn();\n render(ValidationFindingsPanel, {\n findings: [findings[0]],\n recommendedAction: \"import_from_superset\",\n onjump,\n });\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Jump to area\" }));\n\n expect(onjump).toHaveBeenCalledWith({\n target: \"intake\",\n finding: findings[0],\n });\n });\n\n it(\"empty_findings_show_success_state\", () => {\n render(ValidationFindingsPanel, {\n findings: [],\n recommendedAction: \"review_documentation\",\n });\n\n expect(screen.getByText(\"No findings\")).toBeDefined();\n });\n});\n// [/DEF:ValidationFindingsPanelUxTests:Module]\n" + "anchor_syntax": "region", + "body": "// #region ValidationFindingsPanelUxTests:Module [TYPE Function]\n// @SEMANTICS: dataset-review, findings, severity, jump, ux-tests\n// @PURPOSE: Verify grouped findings visibility, empty state, and remediation jump behavior for US1.\n// @LAYER: UI\n// @RELATION: DEPENDS_ON -> [ValidationFindingsPanel]\n// @UX_STATE: Blocking -> Blocking findings dominate the panel and expose remediation jumps.\n// @UX_STATE: Warning -> Warning findings remain visible with explicit resolution state.\n// @UX_STATE: Informational -> Informational notes stay readable without competing with blockers.\n// @TEST_CONTRACT: FindingsPanelProps -> ObservableFindingsUX\n// @TEST_SCENARIO: blocking_warning_info_groups_render -> Findings render in the proper severity groups with counts.\n// @TEST_SCENARIO: jump_action_maps_area_to_workspace_target -> Jump action emits normalized remediation target.\n// @TEST_SCENARIO: empty_findings_show_success_state -> Empty list shows ready feedback.\n// @TEST_EDGE: missing_field -> Missing optional fields do not crash rendering.\n// @TEST_EDGE: invalid_type -> Unknown severity falls back to informational grouping.\n// @TEST_EDGE: external_fail -> Jump callback remains optional.\n// @TEST_INVARIANT: findings_groups_remain_actionable -> VERIFIED_BY: [blocking_warning_info_groups_render, jump_action_maps_area_to_workspace_target, empty_findings_show_success_state]\n\nimport { describe, it, expect, vi } from \"vitest\";\nimport { fireEvent, render, screen } from \"@testing-library/svelte\";\nimport ValidationFindingsPanel from \"../ValidationFindingsPanel.svelte\";\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n dataset_review: {\n findings: {\n eyebrow: \"Validation findings\",\n title: \"Findings\",\n description: \"Review validation results\",\n next_action_label: \"Next action\",\n empty: \"No findings\",\n blocking_title: \"Blocking\",\n blocking_empty: \"No blocking findings\",\n warning_title: \"Warnings\",\n warning_empty: \"No warning findings\",\n informational_title: \"Info\",\n informational_empty: \"No informational findings\",\n resolution_label: \"Resolution\",\n reference_label: \"Reference\",\n jump_action: \"Jump to area\",\n resolution: {\n open: \"Open\",\n approved: \"Approved\",\n },\n areas: {\n source_intake: \"Source intake\",\n dataset_profile: \"Dataset summary\",\n semantic_enrichment: \"Semantics\",\n },\n },\n workspace: {\n actions: {\n review_documentation: \"Review documentation\",\n import_from_superset: \"Import from Superset\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"ValidationFindingsPanel UX Contract\", () => {\n const findings = [\n {\n finding_id: \"f-1\",\n severity: \"blocking\",\n code: \"REQ_ENV\",\n area: \"source_intake\",\n title: \"Environment required\",\n message: \"Select environment\",\n resolution_state: \"open\",\n },\n {\n finding_id: \"f-2\",\n severity: \"warning\",\n code: \"PARTIAL_RECOVERY\",\n area: \"dataset_profile\",\n title: \"Partial recovery\",\n message: \"Some metadata needs review\",\n resolution_state: \"approved\",\n caused_by_ref: \"dashboard:42\",\n },\n {\n finding_id: \"f-3\",\n severity: \"unexpected\",\n code: \"INFO_NOTE\",\n area: \"semantic_enrichment\",\n title: \"Dictionary note\",\n message: \"Trusted source used\",\n resolution_state: \"open\",\n },\n ];\n\n it(\"blocking_warning_info_groups_render\", () => {\n render(ValidationFindingsPanel, {\n findings,\n recommendedAction: \"review_documentation\",\n });\n\n expect(screen.getByText(\"Blocking\")).toBeDefined();\n expect(screen.getByText(\"Warnings\")).toBeDefined();\n expect(screen.getByText(\"Info\")).toBeDefined();\n expect(screen.getByText(\"Environment required\")).toBeDefined();\n expect(screen.getByText(\"Partial recovery\")).toBeDefined();\n expect(screen.getByText(\"Dictionary note\")).toBeDefined();\n expect(screen.getByText(\"Review documentation\")).toBeDefined();\n });\n\n it(\"jump_action_maps_area_to_workspace_target\", async () => {\n const onjump = vi.fn();\n render(ValidationFindingsPanel, {\n findings: [findings[0]],\n recommendedAction: \"import_from_superset\",\n onjump,\n });\n\n await fireEvent.click(screen.getByRole(\"button\", { name: \"Jump to area\" }));\n\n expect(onjump).toHaveBeenCalledWith({\n target: \"intake\",\n finding: findings[0],\n });\n });\n\n it(\"empty_findings_show_success_state\", () => {\n render(ValidationFindingsPanel, {\n findings: [],\n recommendedAction: \"review_documentation\",\n });\n\n expect(screen.getByText(\"No findings\")).toBeDefined();\n });\n});\n// #endregion ValidationFindingsPanelUxTests:Module\n" }, { "contract_id": "HealthMatrix", "contract_type": "Component", "file_path": "frontend/src/lib/components/health/HealthMatrix.svelte", "start_line": 1, - "end_line": 83, + "end_line": 84, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "PURPOSE": "Visual grid summary representing dashboard health status counts.", "TYPE": "{{", @@ -121386,19 +88397,19 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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", "contract_type": "Component", "file_path": "frontend/src/lib/components/health/PolicyForm.svelte", "start_line": 1, - "end_line": 199, + "end_line": 200, "tier": "TIER_2", "complexity": 3, "metadata": { - "COMPLEXITY": "3", + "COMPLEXITY": 3, "LAYER": "UI", "POST": "Form submission forwards the current draft to onSave and cancel delegates dismissal to the parent callback without mutating external state directly.", "PRE": "Parent provides callable onSave/onCancel handlers and an environments collection that may be empty but remains array-like.", @@ -121464,327 +88475,18 @@ "detail": null } ], - "anchor_syntax": "def", - "body": "\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" + "anchor_syntax": "region", + "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": "Breadcrumbs", - "contract_type": "Component", - "file_path": "frontend/src/lib/components/layout/Breadcrumbs.svelte", - "start_line": 1, - "end_line": 189, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "COMPLEXITY": "3", - "INVARIANT": "Always shows current page path.", - "LAYER": "UI", - "PURPOSE": "Display page hierarchy navigation.", - "UX_FEEDBACK": "Hover on breadcrumb shows clickable state", - "UX_RECOVERY": "Click breadcrumb to navigate", - "UX_STATE": "Collapsed -> Intermediate segments collapse into an ellipsis when the path exceeds the visibility budget." - }, - "relations": [ - { - "source_id": "Breadcrumbs", - "relation_type": "BINDS_TO", - "target_id": "i18n", - "target_ref": "[i18n]" - }, - { - "source_id": "Breadcrumbs", - "relation_type": "DEPENDS_ON", - "target_id": "Icon", - "target_ref": "[Icon]" - } - ], - "schema_warnings": [ - { - "code": "tag_forbidden_by_complexity", - "tag": "INVARIANT", - "message": "@INVARIANT is forbidden for contract type 'Component' at C3", - "detail": { - "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": "LAYER", - "message": "@LAYER is forbidden for contract type 'Component' at C3", - "detail": { - "actual_complexity": 3, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n
\n {#each breadcrumbItems as item, index}\n
\n {#if item.isEllipsis}\n ...\n {:else}\n {@const meta = getCrumbMeta(item)}\n {#if item.isLast}\n \n \n \n \n {item.label}\n \n {:else}\n \n \n \n \n {item.label}\n \n {/if}\n {/if}\n
\n {#if index < breadcrumbItems.length - 1}\n \n \n \n {/if}\n {/each}\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": 354, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": "5", - "DATA_CONTRACT": "SidebarCategory[] -> RenderedNavigationTree", - "INVARIANT": "Always shows active category and item", - "LAYER": "UI", - "POST": "Active category and item remain aligned with the current route and mobile state is dismissible.", - "PRE": "Sidebar store and translated category definitions are available before navigation is rendered.", - "PURPOSE": "Persistent left sidebar with resource categories navigation", - "SEMANTICS": [ - "Navigation" - ], - "SIDE_EFFECT": "Persists sidebar state via store actions, refreshes health indicators, and updates window location on navigation.", - "TEST_CONTRACT": "Component_Sidebar ->", - "UX_FEEDBACK": "Active item highlighted with different background", - "UX_REACTIVITY": "Uses $derived for store-backed navigation state and $effect guards for route/category coherence.", - "UX_RECOVERY": "Click outside on mobile closes overlay", - "UX_STATE": "MobileOpen -> Overlay and sidebar stay visible on small screens until dismissed." - }, - "relations": [ - { - "source_id": "Sidebar", - "relation_type": "BINDS_TO", - "target_id": "sidebar", - "target_ref": "sidebar" - }, - { - "source_id": "Sidebar", - "relation_type": "BINDS_TO", - "target_id": "auth", - "target_ref": "auth" - }, - { - "source_id": "Sidebar", - "relation_type": "BINDS_TO", - "target_id": "health_store", - "target_ref": "health_store" - }, - { - "source_id": "Sidebar", - "relation_type": "DEPENDS_ON", - "target_id": "SidebarNavigation", - "target_ref": "SidebarNavigation" - }, - { - "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 C5", - "detail": { - "actual_complexity": 5, - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Component" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Component'", - "detail": { - "actual_type": "Component", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Component' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Component" - } - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n{#if isMobileOpen}\n e.key === \"Escape\" && handleOverlayClick()}\n role=\"presentation\"\n >\n{/if}\n\n\n\n \n \n {#if isExpanded}\n \n \n \n \n {translationState.current?.nav?.menu}\n \n {:else}\n M\n {/if}\n \n\n \n \n\n \n {#if isExpanded}\n
\n \n \n \n \n {translationState.current?.nav?.collapse}\n \n
\n {:else}\n
\n \n \n {translationState.current?.nav?.expand}\n \n
\n {/if}\n\n\n\n" - }, - { - "contract_id": "TaskDrawer", - "contract_type": "Component", - "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 1, - "end_line": 853, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": "5", - "DATA_CONTRACT": "TaskDrawerState -> RecentTaskList | ActiveTaskDetail", - "INVARIANT": "Drawer shows logs for active task or remains closed", - "LAYER": "UI", - "POST": "Drawer stays closed, shows the recent-task list, or renders a selected task detail without losing task context.", - "PRE": "Task drawer store and optional websocket task context are available before detail mode is requested.", - "PURPOSE": "Global task drawer for monitoring background operations", - "SEMANTICS": [ - "TaskLogViewer" - ], - "SIDE_EFFECT": "Opens websocket streams, loads recent tasks, and mutates drawer selection state.", - "TEST_CONTRACT": "Component_TaskDrawer ->", - "TEST_DATA": "llm_task_success_with_pass_result -> {\"activeTaskDetails\":{\"plugin_id\":\"llm_dashboard_validation\",\"status\":\"SUCCESS\",\"result\":{\"status\":\"PASS\"}}}", - "UX_FEEDBACK": "Back button returns to task list", - "UX_REACTIVITY": "Uses $state for local drawer UI and $derived or store subscriptions for active task synchronization.", - "UX_RECOVERY": "Back button shows task list when viewing task details", - "UX_STATE": "InputRequired -> Interactive form rendered in drawer" - }, - "relations": [ - { - "source_id": "TaskDrawer", - "relation_type": "BINDS_TO", - "target_id": "taskDrawer", - "target_ref": "taskDrawer" - } - ], - "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 C5", - "detail": { - "actual_complexity": 5, - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Component" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Component'", - "detail": { - "actual_type": "Component", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Component' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Component" - } - }, - { - "code": "unknown_tag", - "tag": "TEST_DATA", - "message": "@TEST_DATA is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n\n{#if isOpen}\n \n \n \n
\n {#if !activeTaskId && recentTasks.length > 0}\n \n \n \n {:else if activeTaskId}\n \n \n \n {/if}\n

\n {activeTaskId ? $t.tasks?.details_logs : $t.tasks?.recent}\n

\n {#if shortTaskId}\n {shortTaskId}…\n {/if}\n {#if taskStatus}\n \n {taskStatus}\n \n {/if}\n {#if derivedActiveTaskValidation}\n \n \n {derivedActiveTaskValidation.icon}\n \n {derivedActiveTaskValidation.label}\n \n {/if}\n
\n
\n \n {$t.nav?.reports}\n \n \n \n \n
\n \n\n \n
\n {#if activeTaskId}\n {#if derivedTaskSummary}\n \n
\n
\n
\n \n
\n

\n {$t.tasks?.summary_report || \"Summary report\"}\n

\n
\n \n {taskStatus}\n \n
\n {#if derivedTaskSummary.headline}\n

{derivedTaskSummary.headline}

\n {/if}\n {#if derivedTaskSummary.lines.length > 0}\n
    \n {#each derivedTaskSummary.lines as line}\n
  • {line}
  • \n {/each}\n
\n {/if}\n {#if derivedTaskSummary.warnings.length > 0}\n \n

\n {$t.tasks?.observability_warnings || \"Warnings\"}\n

\n
    \n {#each derivedTaskSummary.warnings as warning}\n
  • {warning}
  • \n {/each}\n
\n
\n {/if}\n
\n \n {#if derivedTaskSummary?.targetEnvName}\n {(\n $t.tasks?.open_dashboard_target || \"Open dashboard in {env}\"\n ).replace(\"{env}\", derivedTaskSummary.targetEnvName)}\n {:else}\n {$t.tasks?.open_dashboard_target_fallback || \"Open dashboard\"}\n {/if}\n \n \n {$t.tasks?.show_diff || \"Show diff\"}\n \n {#if activeTaskDetails?.plugin_id === \"llm_dashboard_validation\"}\n \n {$t.tasks?.open_llm_report || \"Open LLM report\"}\n \n {/if}\n
\n {#if showDiff}\n
\n \n {$t.tasks?.diff_preview || \"Diff preview\"}\n

\n {#if isDiffLoading}\n

\n {$t.git?.loading_diff || \"Loading diff...\"}\n

\n {:else if diffText}\n {diffText}\n {:else}\n

\n {$t.tasks?.no_diff_available || \"No diff available\"}\n

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

{$t.tasks?.loading}

\n \n {:else if recentTasks.length > 0}\n
\n
\n

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

\n {#if loadingTasks}\n
\n {/if}\n
\n
\n {#each recentTasks as task}\n {@const taskValidation = resolveLlmValidationStatus(task)}\n {@const profile = getReportTypeProfile(\n {\n \"llm_dashboard_validation\": \"llm_verification\",\n \"superset-backup\": \"backup\",\n \"superset-migration\": \"migration\",\n \"documentation\": \"documentation\",\n }[task.plugin_id] || task.plugin_id,\n )}\n selectTask(task)}\n >\n
\n \n {profile?.label\n ? typeof profile.label === 'function'\n ? profile.label()\n : profile.label\n : task.plugin_id || $t.common?.unknown}\n \n
\n {#if taskValidation}\n \n {taskValidation.label}\n \n {/if}\n \n {task.status || $t.common?.unknown}\n \n
\n
\n\n
\n

\n {task.params?.dashboard_id ||\n (task.plugin_id === 'superset-migration' ? $t.nav?.migration : task.plugin_id) ||\n $t.common?.not_available}\n

\n

\n {task.result?.summary || task.id}\n

\n
\n\n \n
\n \n {task.id?.substring(0, 8) || 'N/A'}\n
\n
\n \n {task.updated_at ? new Date(task.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : ''}\n
\n
\n \n {/each}\n
\n \n {:else}\n \n \n

{$t.tasks?.select_task}

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

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

\n \n \n \n" - }, - { - "contract_id": "disconnectWebSocket", + "contract_id": "disconnectWebSocket:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 435, - "end_line": 451, - "tier": "TIER_2", - "complexity": 3, + "start_line": 447, + "end_line": 462, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "POST": "ws is closed and set to null", "PRE": "ws may or may not be initialized", "PURPOSE": "Disconnects the active WebSocket connection", @@ -121792,7 +88494,7 @@ }, "relations": [ { - "source_id": "disconnectWebSocket", + "source_id": "disconnectWebSocket:Function", "relation_type": "USES", "target_id": "App", "target_ref": "App" @@ -121802,18 +88504,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, + "actual_complexity": 1, "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", + "message": "@PRE is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Function" } }, @@ -121831,9 +88542,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "UX_STATE", - "message": "@UX_STATE is forbidden for contract type 'Function' at C3", + "message": "@UX_STATE is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, + "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" } }, @@ -121856,15 +88576,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:disconnectWebSocket:Function]\n// @UX_STATE: Loading -> Default\n// @RELATION: USES -> App\n /**\n * @PURPOSE: Disconnects the active WebSocket connection\n * @PRE: ws may or may not be initialized\n * @POST: ws is closed and set to null\n * @COMPLEXITY: 3\n */\n function disconnectWebSocket() {\n console.log(\"[TaskDrawer][WebSocket][disconnectWebSocket:START]\");\n if (ws) {\n ws.close();\n ws = null;\n }\n }\n // [/DEF:disconnectWebSocket:Function]\n" + "anchor_syntax": "region", + "body": " // #region disconnectWebSocket:Function [TYPE Function]\n// @UX_STATE: Loading -> Default\n// @RELATION: USES -> App\n /**\n * @PURPOSE: Disconnects the active WebSocket connection\n * @PRE: ws may or may not be initialized\n * @POST: ws is closed and set to null\n */\n function disconnectWebSocket() {\n console.log(\"[TaskDrawer][WebSocket][disconnectWebSocket:START]\");\n if (ws) {\n ws.close();\n ws = null;\n }\n }\n // #endregion disconnectWebSocket:Function\n" }, { - "contract_id": "loadRecentTasks", + "contract_id": "loadRecentTasks:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 453, - "end_line": 476, + "start_line": 464, + "end_line": 487, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -121902,15 +88622,15 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:loadRecentTasks:Function]\n /**\n * @PURPOSE: Load recent tasks for list mode display\n * @PRE: User is on task drawer or api is ready.\n * @POST: recentTasks array populated with task list\n */\n async function loadRecentTasks() {\n loadingTasks = true;\n try {\n console.log(\"[TaskDrawer][API][loadRecentTasks:STARTED]\");\n // API returns List[Task] directly, not {tasks: [...]}\n const response = await api.getTasks();\n recentTasks = Array.isArray(response) ? response : response.tasks || [];\n console.log(\n `[TaskDrawer][API][loadRecentTasks:SUCCESS] loaded ${recentTasks.length} tasks`,\n );\n } catch (err) {\n console.error(\"[TaskDrawer][API][loadRecentTasks:FAILED]\", err);\n recentTasks = [];\n } finally {\n loadingTasks = false;\n }\n }\n // [/DEF:loadRecentTasks:Function]\n" + "anchor_syntax": "region", + "body": " // #region loadRecentTasks:Function [TYPE Function]\n /**\n * @PURPOSE: Load recent tasks for list mode display\n * @PRE: User is on task drawer or api is ready.\n * @POST: recentTasks array populated with task list\n */\n async function loadRecentTasks() {\n loadingTasks = true;\n try {\n console.log(\"[TaskDrawer][API][loadRecentTasks:STARTED]\");\n // API returns List[Task] directly, not {tasks: [...]}\n const response = await api.getTasks();\n recentTasks = Array.isArray(response) ? response : response.tasks || [];\n console.log(\n `[TaskDrawer][API][loadRecentTasks:SUCCESS] loaded ${recentTasks.length} tasks`,\n );\n } catch (err) {\n console.error(\"[TaskDrawer][API][loadRecentTasks:FAILED]\", err);\n recentTasks = [];\n } finally {\n loadingTasks = false;\n }\n }\n // #endregion loadRecentTasks:Function\n" }, { - "contract_id": "selectTask", + "contract_id": "selectTask:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 478, - "end_line": 491, + "start_line": 489, + "end_line": 502, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -121948,19 +88668,18 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:selectTask:Function]\n /**\n * @PURPOSE: Select a task from list to view details\n * @PRE: task is a valid task object\n * @POST: drawer state updated to show task details\n */\n function selectTask(task) {\n console.log(\"[TaskDrawer][UI][selectTask:START]\");\n taskDrawerStore.update((state) => ({\n ...state,\n activeTaskId: task.id,\n }));\n }\n // [/DEF:selectTask:Function]\n" + "anchor_syntax": "region", + "body": " // #region selectTask:Function [TYPE Function]\n /**\n * @PURPOSE: Select a task from list to view details\n * @PRE: task is a valid task object\n * @POST: drawer state updated to show task details\n */\n function selectTask(task) {\n console.log(\"[TaskDrawer][UI][selectTask:START]\");\n taskDrawerStore.update((state) => ({\n ...state,\n activeTaskId: task.id,\n }));\n }\n // #endregion selectTask:Function\n" }, { - "contract_id": "goBackToList", + "contract_id": "goBackToList:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 493, - "end_line": 512, - "tier": "TIER_2", - "complexity": 3, + "start_line": 504, + "end_line": 522, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "COMPLEXITY": "3", "POST": "Drawer switches to list view and reloads tasks", "PRE": "Drawer is open and activeTaskId is set", "PURPOSE": "Return to task list view from task details", @@ -121968,7 +88687,7 @@ }, "relations": [ { - "source_id": "goBackToList", + "source_id": "goBackToList:Function", "relation_type": "USES", "target_id": "App", "target_ref": "App" @@ -121978,18 +88697,27 @@ { "code": "tag_forbidden_by_complexity", "tag": "POST", - "message": "@POST is forbidden for contract type 'Function' at C3", + "message": "@POST is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, + "actual_complexity": 1, "contract_type": "Function" } }, { "code": "tag_forbidden_by_complexity", "tag": "PRE", - "message": "@PRE is forbidden for contract type 'Function' at C3", + "message": "@PRE is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, + "actual_complexity": 1, + "contract_type": "Function" + } + }, + { + "code": "tag_forbidden_by_complexity", + "tag": "PURPOSE", + "message": "@PURPOSE is forbidden for contract type 'Function' at C1", + "detail": { + "actual_complexity": 1, "contract_type": "Function" } }, @@ -122007,9 +88735,18 @@ { "code": "tag_forbidden_by_complexity", "tag": "UX_STATE", - "message": "@UX_STATE is forbidden for contract type 'Function' at C3", + "message": "@UX_STATE is forbidden for contract type 'Function' at C1", "detail": { - "actual_complexity": 3, + "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" } }, @@ -122032,168 +88769,18 @@ } } ], - "anchor_syntax": "def", - "body": " // [DEF:goBackToList:Function]\n// @UX_STATE: Loading -> Default\n// @RELATION: USES -> App\n /**\n * @PURPOSE: Return to task list view from task details\n * @PRE: Drawer is open and activeTaskId is set\n * @POST: Drawer switches to list view and reloads tasks\n * @COMPLEXITY: 3\n */\n function goBackToList() {\n console.log(\"[TaskDrawer][UI][goBackToList:START]\");\n taskDrawerStore.update((state) => ({\n ...state,\n activeTaskId: null,\n }));\n // Reload the task list\n loadRecentTasks();\n console.log(\"[TaskDrawer][UI][goBackToList:SUCCESS]\");\n }\n // [/DEF:goBackToList:Function]\n" + "anchor_syntax": "region", + "body": " // #region goBackToList:Function [TYPE Function]\n// @UX_STATE: Loading -> Default\n// @RELATION: USES -> App\n /**\n * @PURPOSE: Return to task list view from task details\n * @PRE: Drawer is open and activeTaskId is set\n * @POST: Drawer switches to list view and reloads tasks\n */\n function goBackToList() {\n console.log(\"[TaskDrawer][UI][goBackToList:START]\");\n taskDrawerStore.update((state) => ({\n ...state,\n activeTaskId: null,\n }));\n // Reload the task list\n loadRecentTasks();\n console.log(\"[TaskDrawer][UI][goBackToList:SUCCESS]\");\n }\n // #endregion goBackToList:Function\n" }, { - "contract_id": "TopNavbar", - "contract_type": "Component", - "file_path": "frontend/src/lib/components/layout/TopNavbar.svelte", - "start_line": 1, - "end_line": 581, - "tier": "TIER_3", - "complexity": 5, - "metadata": { - "COMPLEXITY": "5", - "DATA_CONTRACT": "SearchQuery -> SearchSection[] | ProfilePreferences -> TaskDrawerPreference", - "INVARIANT": "Always visible on non-login pages", - "LAYER": "UI", - "POST": "Search, environment selection, and global action controls remain synchronized with current app state.", - "PRE": "Auth, environment, and task stores are initialized before interactive navbar actions are used.", - "PURPOSE": "Unified top navigation bar with Logo, Search, Activity, and User menu", - "SEMANTICS": [ - "Navigation", - "UserSession" - ], - "SIDE_EFFECT": "Reads profile preferences, performs search API requests, and triggers drawer, auth, and assistant actions.", - "TEST_CONTRACT": "Component_TopNavbar ->", - "UX_FEEDBACK": "Activity badge shows count of running tasks", - "UX_REACTIVITY": "Uses $state for menu/search state and $derived projections from app stores.", - "UX_RECOVERY": "Click outside closes dropdowns", - "UX_STATE": "Searching -> Search dropdown stays open while async results are loading.", - "UX_TEST": "ActivityClick -> {click: activity button, expected: task drawer opens}" - }, - "relations": [ - { - "source_id": "TopNavbar", - "relation_type": "BINDS_TO", - "target_id": "activity", - "target_ref": "activity" - }, - { - "source_id": "TopNavbar", - "relation_type": "BINDS_TO", - "target_id": "auth", - "target_ref": "auth" - }, - { - "source_id": "TopNavbar", - "relation_type": "BINDS_TO", - "target_id": "environmentContext", - "target_ref": "environmentContext" - }, - { - "source_id": "TopNavbar", - "relation_type": "DISPATCHES", - "target_id": "taskDrawer", - "target_ref": "taskDrawer" - }, - { - "source_id": "TopNavbar", - "relation_type": "DISPATCHES", - "target_id": "assistantChat", - "target_ref": "assistantChat" - }, - { - "source_id": "TopNavbar", - "relation_type": "DEPENDS_ON", - "target_id": "Icon", - "target_ref": "Icon" - }, - { - "source_id": "TopNavbar", - "relation_type": "DEPENDS_ON", - "target_id": "LanguageSwitcher", - "target_ref": "LanguageSwitcher" - } - ], - "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 C5", - "detail": { - "actual_complexity": 5, - "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 C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Component" - } - }, - { - "code": "tag_not_for_contract_type", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is not allowed for contract type 'Component'", - "detail": { - "actual_type": "Component", - "allowed_types": [ - "Function", - "Block" - ] - } - }, - { - "code": "tag_forbidden_by_complexity", - "tag": "TEST_CONTRACT", - "message": "@TEST_CONTRACT is forbidden for contract type 'Component' at C5", - "detail": { - "actual_complexity": 5, - "contract_type": "Component" - } - }, - { - "code": "unknown_tag", - "tag": "UX_TEST", - "message": "@UX_TEST is not defined in axiom_config.yaml tags", - "detail": null - } - ], - "anchor_syntax": "def", - "body": "\n\n\n\n \n
\n \n \n \n \n\n \n \n \n \n \n {$t.common?.brand}\n \n
\n\n \n